
com.hedera.node.app.grpc.impl.MethodBase Maven / Gradle / Ivy
/*
* 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