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