com.hedera.node.app.grpc.impl.MethodBase Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of app Show documentation
Show all versions of app Show documentation
Hedera Application - Implementation
/*
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hedera.node.app.grpc.impl;
import static java.util.Objects.requireNonNull;
import com.hedera.node.app.Hedera;
import com.hedera.pbj.runtime.io.buffer.BufferedData;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import com.swirlds.common.metrics.SpeedometerMetric;
import com.swirlds.metrics.api.Counter;
import com.swirlds.metrics.api.Metrics;
import edu.umd.cs.findbugs.annotations.NonNull;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.ServerCalls;
import io.grpc.stub.StreamObserver;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* An instance of either {@link TransactionMethod} or {@link QueryMethod} is created per transaction
* type and query type.
*/
public abstract class MethodBase implements ServerCalls.UnaryMethod {
private static final Logger logger = LogManager.getLogger(MethodBase.class);
// To be set by configuration. See Issue #4294
private static final int MAX_MESSAGE_SIZE = Hedera.MAX_SIGNED_TXN_SIZE;
// To be set by configuration. See Issue #4294. Originally this was intended to be the same max size as
// a transaction, but some files and other responses are much larger. So we had to set this larger.
private static final int MAX_RESPONSE_SIZE = 1024 * 1024 * 2;
// Constants for metric names and descriptions
private static final String COUNTER_HANDLED_NAME_TPL = "%sHdl";
private static final String COUNTER_HANDLED_DESC_TPL = "number of %s handled";
private static final String COUNTER_RECEIVED_NAME_TPL = "%sRcv";
private static final String COUNTER_RECEIVED_DESC_TPL = "number of %s received";
private static final String COUNTER_FAILED_NAME_TPL = "%sFail";
private static final String COUNTER_FAILED_DESC_TPL = "number of %s failed";
private static final String SPEEDOMETER_HANDLED_NAME_TPL = "%sHdl_per_sec";
private static final String SPEEDOMETER_HANDLED_DESC_TPL = "number of %s handled per second";
private static final String SPEEDOMETER_RECEIVED_NAME_TPL = "%sRcv_per_sec";
private static final String SPEEDOMETER_RECEIVED_DESC_TPL = "number of %s received per second";
/**
* Per-thread shared {@link BufferedData} for responses. We store these in a thread local, because we do
* not have control over the thread pool used by the underlying gRPC server.
*/
@SuppressWarnings(
"java:S5164") // looks like a false positive ("ThreadLocal" variables should be cleaned up when no longer
// used), but these threads are long-lived and the lifetime of the thread local is the same as
// the application
private static final ThreadLocal BUFFER_THREAD_LOCAL =
ThreadLocal.withInitial(() -> BufferedData.allocate(MAX_RESPONSE_SIZE));
/** The name of the service associated with this method. */
protected final String serviceName;
/** The name of the method. */
protected final String methodName;
/** A metric for the number of times this method has been invoked */
private final Counter callsReceivedCounter;
/** A metric for the number of times this method successfully handled an invocation */
private final Counter callsHandledCounter;
/** A metric for the number of times this method failed to handle an invocation */
private final Counter callsFailedCounter;
/** A metric for the calls per second successfully this method was invoked */
private final SpeedometerMetric callsReceivedSpeedometer;
/** A metric for the calls per second successfully handled by this method */
private final SpeedometerMetric callsHandledSpeedometer;
/**
* Create a new instance.
*
* @param serviceName a non-null reference to the service name
* @param methodName a non-null reference to the method name
*/
MethodBase(@NonNull final String serviceName, @NonNull final String methodName, @NonNull final Metrics metrics) {
this.serviceName = requireNonNull(serviceName);
this.methodName = requireNonNull(methodName);
this.callsHandledCounter = counter(metrics, COUNTER_HANDLED_NAME_TPL, COUNTER_HANDLED_DESC_TPL);
this.callsReceivedCounter = counter(metrics, COUNTER_RECEIVED_NAME_TPL, COUNTER_RECEIVED_DESC_TPL);
this.callsFailedCounter = counter(metrics, COUNTER_FAILED_NAME_TPL, COUNTER_FAILED_DESC_TPL);
this.callsHandledSpeedometer = speedometer(metrics, SPEEDOMETER_HANDLED_NAME_TPL, SPEEDOMETER_HANDLED_DESC_TPL);
this.callsReceivedSpeedometer =
speedometer(metrics, SPEEDOMETER_RECEIVED_NAME_TPL, SPEEDOMETER_RECEIVED_DESC_TPL);
}
@Override
public void invoke(
@NonNull final BufferedData requestBuffer, @NonNull final StreamObserver responseObserver) {
// Track the number of times this method has been called
callsReceivedCounter.increment();
callsReceivedSpeedometer.cycle();
// Fail-fast if the request is too large (Note that the request buffer is sized to allow exactly
// 1 more byte than MAX_MESSAGE_SIZE, so we can detect this case).
if (requestBuffer.length() > MAX_MESSAGE_SIZE) {
callsFailedCounter.increment();
final var exception = new RuntimeException("More than " + MAX_MESSAGE_SIZE + " received");
responseObserver.onError(exception);
return;
}
try {
// Prepare the response buffer
final var responseBuffer = BUFFER_THREAD_LOCAL.get();
responseBuffer.reset();
// Convert the request BufferedData to a Bytes instance without copying the bytes
final var requestBytes = requestBuffer.getBytes(0, requestBuffer.length());
// Call the workflow
handle(requestBytes, responseBuffer);
// Respond to the client
responseBuffer.flip();
responseObserver.onNext(responseBuffer);
responseObserver.onCompleted();
// Track the number of times we successfully handled a call
callsHandledCounter.increment();
callsHandledSpeedometer.cycle();
} catch (final Exception e) {
// Track the number of times we failed to handle a call
if (!(e instanceof StatusRuntimeException)) {
logger.error("Unexpected exception while handling a GRPC message", e);
}
callsFailedCounter.increment();
responseObserver.onError(e);
}
}
/**
* Called to handle the method invocation. Implementations should only throw a {@link RuntimeException}
* if a gRPC ERROR is to be returned.
*
* @param requestBuffer The {@link Bytes} containing the protobuf bytes for the request
* @param responseBuffer A {@link BufferedData} into which the response protobuf bytes may be written
*/
protected abstract void handle(@NonNull final Bytes requestBuffer, @NonNull final BufferedData responseBuffer);
/**
* Helper method for creating a {@link Counter} metric.
*
* @param metrics The {@link Metrics} object to use to create the counter.
* @param nameTemplate A template to use for generating the metric name
* @param descriptionTemplate A template to use for generating the metric description
* @return The metric
*/
protected final @NonNull Counter counter(
@NonNull final Metrics metrics,
@NonNull final String nameTemplate,
@NonNull final String descriptionTemplate) {
final String baseName = calculateBaseName();
final var name = String.format(nameTemplate, baseName);
final var desc = String.format(descriptionTemplate, baseName);
return metrics.getOrCreate(new Counter.Config("app", name).withDescription(desc));
}
/**
* Helper method for creating a {@link SpeedometerMetric} metric.
*
* @param metrics The {@link Metrics} object to use to create the speedometer.
* @param nameTemplate A template to use for generating the metric name
* @param descriptionTemplate A template to use for generating the metric description
* @return The metric
*/
protected final @NonNull SpeedometerMetric speedometer(
@NonNull final Metrics metrics,
@NonNull final String nameTemplate,
@NonNull final String descriptionTemplate) {
final String baseName = calculateBaseName();
final var name = String.format(nameTemplate, baseName);
final var desc = String.format(descriptionTemplate, baseName);
return metrics.getOrCreate(new SpeedometerMetric.Config("app", name).withDescription(desc));
}
private String calculateBaseName() {
return serviceName.substring("proto.".length()).replace('.', ':') + ":" + methodName;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy