xref: /aosp_15_r20/external/grpc-grpc-java/services/src/main/java/io/grpc/services/CallMetricRecorder.java (revision e07d83d3ffcef9ecfc9f7f475418ec639ff0e5fe)
1 /*
2  * Copyright 2019 The gRPC Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.grpc.services;
18 
19 import com.google.common.annotations.VisibleForTesting;
20 import com.google.errorprone.annotations.InlineMe;
21 import io.grpc.Context;
22 import io.grpc.ExperimentalApi;
23 import java.util.Collections;
24 import java.util.Map;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.concurrent.atomic.AtomicReference;
27 import javax.annotation.concurrent.ThreadSafe;
28 
29 /**
30  * Utility to record call metrics for load-balancing. One instance per call.
31  */
32 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/6012")
33 @ThreadSafe
34 public final class CallMetricRecorder {
35   private static final CallMetricRecorder NOOP = new CallMetricRecorder().disable();
36 
37   static final Context.Key<CallMetricRecorder> CONTEXT_KEY =
38       Context.key("io.grpc.services.CallMetricRecorder");
39 
40   private final AtomicReference<ConcurrentHashMap<String, Double>> utilizationMetrics =
41       new AtomicReference<>();
42   private final AtomicReference<ConcurrentHashMap<String, Double>> requestCostMetrics =
43       new AtomicReference<>();
44   private double cpuUtilizationMetric = 0;
45   private double applicationUtilizationMetric = 0;
46   private double memoryUtilizationMetric = 0;
47   private double qps = 0;
48   private double eps = 0;
49   private volatile boolean disabled;
50 
51   /**
52    * Returns the call metric recorder attached to the current {@link Context}.  If there is none,
53    * returns a no-op recorder.
54    *
55    * <p><strong>IMPORTANT:</strong>It returns the recorder specifically for the current RPC call.
56    * <b>DO NOT</b> save the returned object or share it between different RPC calls.
57    *
58    * <p><strong>IMPORTANT:</strong>It must be called under the {@link Context} under which the RPC
59    * handler was called.  If it is called from a different thread, the Context must be propagated to
60    * the same thread, e.g., with {@link Context#wrap(Runnable)}.
61    *
62    * @since 1.23.0
63    */
getCurrent()64   public static CallMetricRecorder getCurrent() {
65     CallMetricRecorder recorder = CONTEXT_KEY.get();
66     return recorder != null ? recorder : NOOP;
67   }
68 
69   /**
70    * Records a call metric measurement for utilization in the range [0, 1]. Values outside the valid
71    * range are ignored. If RPC has already finished, this method is no-op.
72    *
73    * <p>A latter record will overwrite its former name-sakes.
74    *
75    * @return this recorder object
76    * @since 1.23.0
77    */
recordUtilizationMetric(String name, double value)78   public CallMetricRecorder recordUtilizationMetric(String name, double value) {
79     if (disabled || !MetricRecorderHelper.isUtilizationValid(value)) {
80       return this;
81     }
82     if (utilizationMetrics.get() == null) {
83       // The chance of race of creation of the map should be very small, so it should be fine
84       // to create these maps that might be discarded.
85       utilizationMetrics.compareAndSet(null, new ConcurrentHashMap<String, Double>());
86     }
87     utilizationMetrics.get().put(name, value);
88     return this;
89   }
90 
91   /**
92    * Records a call metric measurement for request cost.
93    * If RPC has already finished, this method is no-op.
94    *
95    * <p>A latter record will overwrite its former name-sakes.
96    *
97    * @return this recorder object
98    * @since 1.47.0
99    * @deprecated use {@link #recordRequestCostMetric} instead.
100    *     This method will be removed in the future.
101    */
102   @Deprecated
103   @InlineMe(replacement = "this.recordRequestCostMetric(name, value)")
recordCallMetric(String name, double value)104   public CallMetricRecorder recordCallMetric(String name, double value) {
105     return recordRequestCostMetric(name, value);
106   }
107 
108   /**
109    * Records a call metric measurement for request cost.
110    * If RPC has already finished, this method is no-op.
111    *
112    * <p>A latter record will overwrite its former name-sakes.
113    *
114    * @return this recorder object
115    * @since 1.48.1
116    */
recordRequestCostMetric(String name, double value)117   public CallMetricRecorder recordRequestCostMetric(String name, double value) {
118     if (disabled) {
119       return this;
120     }
121     if (requestCostMetrics.get() == null) {
122       // The chance of race of creation of the map should be very small, so it should be fine
123       // to create these maps that might be discarded.
124       requestCostMetrics.compareAndSet(null, new ConcurrentHashMap<String, Double>());
125     }
126     requestCostMetrics.get().put(name, value);
127     return this;
128   }
129 
130   /**
131    * Records a call metric measurement for CPU utilization in the range [0, inf). Values outside the
132    * valid range are ignored. If RPC has already finished, this method is no-op.
133    *
134    * <p>A latter record will overwrite its former name-sakes.
135    *
136    * @return this recorder object
137    * @since 1.47.0
138    */
recordCpuUtilizationMetric(double value)139   public CallMetricRecorder recordCpuUtilizationMetric(double value) {
140     if (disabled || !MetricRecorderHelper.isCpuOrApplicationUtilizationValid(value)) {
141       return this;
142     }
143     cpuUtilizationMetric = value;
144     return this;
145   }
146 
147   /**
148    * Records a call metric measurement for application specific utilization in the range [0, inf).
149    * Values outside the valid range are ignored. If RPC has already finished, this method is no-op.
150    *
151    * <p>A latter record will overwrite its former name-sakes.
152    *
153    * @return this recorder object
154    */
recordApplicationUtilizationMetric(double value)155   public CallMetricRecorder recordApplicationUtilizationMetric(double value) {
156     if (disabled || !MetricRecorderHelper.isCpuOrApplicationUtilizationValid(value)) {
157       return this;
158     }
159     applicationUtilizationMetric = value;
160     return this;
161   }
162 
163   /**
164    * Records a call metric measurement for memory utilization in the range [0, 1]. Values outside
165    * the valid range are ignored. If RPC has already finished, this method is no-op.
166    *
167    * <p>A latter record will overwrite its former name-sakes.
168    *
169    * @return this recorder object
170    * @since 1.47.0
171    */
recordMemoryUtilizationMetric(double value)172   public CallMetricRecorder recordMemoryUtilizationMetric(double value) {
173     if (disabled || !MetricRecorderHelper.isUtilizationValid(value)) {
174       return this;
175     }
176     memoryUtilizationMetric = value;
177     return this;
178   }
179 
180   /**
181    * Records a call metric measurement for queries per second (qps) in the range [0, inf). Values
182    * outside the valid range are ignored. If RPC has already finished, this method is no-op.
183    *
184    * <p>A latter record will overwrite its former name-sakes.
185    *
186    * @return this recorder object
187    * @since 1.54.0
188    */
recordQpsMetric(double value)189   public CallMetricRecorder recordQpsMetric(double value) {
190     if (disabled || !MetricRecorderHelper.isRateValid(value)) {
191       return this;
192     }
193     qps = value;
194     return this;
195   }
196 
197   /**
198    * Records a call metric measurement for errors per second (eps) in the range [0, inf). Values
199    * outside the valid range are ignored. If RPC has already finished, this method is no-op.
200    *
201    * <p>A latter record will overwrite its former name-sakes.
202    *
203    * @return this recorder object
204    */
recordEpsMetric(double value)205   public CallMetricRecorder recordEpsMetric(double value) {
206     if (disabled || !MetricRecorderHelper.isRateValid(value)) {
207       return this;
208     }
209     eps = value;
210     return this;
211   }
212 
213   /**
214    * Returns all request cost metric values. No more metric values will be recorded after this
215    * method is called. Calling this method multiple times returns the same collection of metric
216    * values.
217    *
218    * @return a map containing all saved metric name-value pairs.
219    */
finalizeAndDump()220   Map<String, Double> finalizeAndDump() {
221     disabled = true;
222     Map<String, Double> savedMetrics = requestCostMetrics.get();
223     if (savedMetrics == null) {
224       return Collections.emptyMap();
225     }
226     return Collections.unmodifiableMap(savedMetrics);
227   }
228 
229   /**
230    * Returns all save metric values. No more metric values will be recorded after this method is
231    * called. Calling this method multiple times returns the same collection of metric values.
232    *
233    * @return a per-request ORCA reports containing all saved metrics.
234    */
finalizeAndDump2()235   MetricReport finalizeAndDump2() {
236     Map<String, Double> savedRequestCostMetrics = finalizeAndDump();
237     Map<String, Double> savedUtilizationMetrics = utilizationMetrics.get();
238     if (savedUtilizationMetrics == null) {
239       savedUtilizationMetrics = Collections.emptyMap();
240     }
241     return new MetricReport(cpuUtilizationMetric, applicationUtilizationMetric,
242         memoryUtilizationMetric, qps, eps, Collections.unmodifiableMap(savedRequestCostMetrics),
243         Collections.unmodifiableMap(savedUtilizationMetrics)
244     );
245   }
246 
247   @VisibleForTesting
isDisabled()248   boolean isDisabled() {
249     return disabled;
250   }
251 
252   /**
253    * Turn this recorder into a no-op one.
254    */
disable()255   private CallMetricRecorder disable() {
256     disabled = true;
257     return this;
258   }
259 }
260