/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* 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.
*/
package com.github.ambry.rest;
import com.codahale.metrics.MetricRegistry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/**
* Construct to support end-to-end metrics tracking based on request type. Usually accompanies a single
* {@link RestRequest} i.e. there is a one-to-one mapping b/w a {@link RestRequest} and a RestRequestMetricsTracker
* instance.
* <p/>
* A brief description of how the tracker works :-
* 1. When an object of type {@link RestRequest} (request) is instantiated, it is also expected to be associated with a
* unique instance of RestRequestMetricsTracker (tracker). This unique instance is returned on every call to
* {@link RestRequest#getMetricsTracker()}. Therefore there is one-to-one mapping between an instance of
* {@link RestRequest} and an instance of RestRequestMetricsTracker.
* 2. When the tracker is instantiated, the type of the request is usually not available. Therefore, the
* {@link RestRequestMetrics} (metrics) object associated with the tracker tracks "unknown" requests.
* 3. As the request passes through the NIO framework and the scaling framework, metrics associated with these layers
* are tracked and stored but not updated in the metrics object.
* 4. When the request reaches the {@link BlobStorageService}, it is usually identified as a specific type and a custom
* metrics object that tracks that specific type of request is "injected" into the tracker associated with the
* request.
* 5. When the response for the request is complete and the request is "closed", the metrics that are stored are
* updated in the metrics object (injected or default).
*/
public class RestRequestMetricsTracker {
protected static final String DEFAULT_REQUEST_TYPE = "Unknown";
private static RestRequestMetrics defaultMetrics;
/**
* NIO related metrics tracker instance.
*/
public final NioMetricsTracker nioMetricsTracker = new NioMetricsTracker();
/**
* Scaling related metrics tracker instance.
*/
public final ScalingMetricsTracker scalingMetricsTracker = new ScalingMetricsTracker();
private final AtomicBoolean metricsRecorded = new AtomicBoolean(false);
private RestRequestMetrics metrics = defaultMetrics;
private boolean failed = false;
/**
* Tracker for updating NIO related metrics.
* </p>
* These are usually updated in classes implemented by the {@link NioServer} framework (e.g. Implementations of
* {@link RestRequest}, {@link RestResponseChannel} or any other classes that form a part of the framework).
*/
public static class NioMetricsTracker {
private final AtomicLong requestProcessingTimeInMs = new AtomicLong(0);
private final AtomicLong responseProcessingTimeInMs = new AtomicLong(0);
private long requestReceivedTime = 0;
private long roundTripTimeInMs = 0;
/**
* Adds to the time taken to process the request at the NIO layer.
* @param delta the time taken in ms to do the current piece of processing at the NIO layer for the request.
* @return the total time taken in ms to process the request at the NIO layer, including the current piece, at this
* moment.
*/
public long addToRequestProcessingTime(long delta) {
return requestProcessingTimeInMs.addAndGet(delta);
}
/**
* Adds to the time taken to process the response at the NIO layer.
* @param delta the time taken in ms to do the current piece of processing at the NIO layer for the response.
* @return the total time taken in ms to process the response at the NIO layer, including the current piece, at this
* moment.
*/
public long addToResponseProcessingTime(long delta) {
return responseProcessingTimeInMs.addAndGet(delta);
}
/**
* Marks the time at which the request was received.
*/
public void markRequestReceived() {
requestReceivedTime = System.currentTimeMillis();
}
/**
* Marks the time at which request was completed so that request RTT can be calculated.
*/
public void markRequestCompleted() {
if (requestReceivedTime == 0) {
throw new IllegalStateException("Request was marked completed without being marked received");
}
roundTripTimeInMs = System.currentTimeMillis() - requestReceivedTime;
}
}
/**
* Helper for updating scaling related metrics. These metrics are updated in the classes that provide scaling
* capabilities when transferring control from {@link NioServer} to {@link BlobStorageService}.
*/
public static class ScalingMetricsTracker {
private final AtomicLong requestProcessingTimeInMs = new AtomicLong(0);
private final AtomicLong requestProcessingWaitTimeInMs = new AtomicLong(0);
private final AtomicLong responseProcessingTimeInMs = new AtomicLong(0);
private final AtomicLong responseProcessingWaitTimeInMs = new AtomicLong(0);
private long requestReceivedTime = 0;
private long roundTripTimeInMs = 0;
/**
* Adds to the time taken to process a request at the scaling layer.
* @param delta the time taken in ms to do the current piece of processing at the scaling layer for the request.
* @return the total time taken in ms to process this request at the scaling layer, including the current piece, at
* this moment.
*/
public long addToRequestProcessingTime(long delta) {
return requestProcessingTimeInMs.addAndGet(delta);
}
/**
* Adds to the scaling layer processing wait time for a request.
* @param delta the time in ms a request has spent waiting to be processed at the scaling layer.
* @return the total time in ms this request has spent waiting to be processed at the scaling layer at this moment.
*/
public long addToRequestProcessingWaitTime(long delta) {
return requestProcessingWaitTimeInMs.addAndGet(delta);
}
/**
* Adds to the time taken to process a response at the scaling layer.
* @param delta the time taken in ms to do the current piece of processing at the scaling layer for the response.
* @return the total time taken in ms to process the response at the scaling layer, including the current piece, at
* this moment.
*/
public long addToResponseProcessingTime(long delta) {
return responseProcessingTimeInMs.addAndGet(delta);
}
/**
* Adds to the scaling layer processing wait time of a response.
* @param delta the time in ms a response has spent waiting to be processed at the scaling layer.
* @return the total time in ms this response has spent waiting to be processed at the scaling layer at this moment.
*/
public long addToResponseProcessingWaitTime(long delta) {
return responseProcessingWaitTimeInMs.addAndGet(delta);
}
/**
* Marks the time at which the request was received.
*/
public void markRequestReceived() {
requestReceivedTime = System.currentTimeMillis();
}
/**
* Marks the time at which request was completed so that request RTT can be calculated.
*/
public void markRequestCompleted() {
if (requestReceivedTime == 0) {
throw new IllegalStateException("Request was marked completed without being marked received");
}
roundTripTimeInMs = System.currentTimeMillis() - requestReceivedTime;
}
}
/**
* Marks that the request is failed so that metrics can be tracked.
*/
public void markFailure() {
failed = true;
}
/**
* Injects a {@link RestRequestMetrics} that can be used to track the metrics of the {@link RestRequest} that this
* instance of RestRequestMetricsTracker is attached to.
* @param restRequestMetrics the {@link RestRequestMetrics} instance to use to track the metrics of the
* {@link RestRequest} that this instance of RestRequestMetricsTracker is attached to.
*/
public void injectMetrics(RestRequestMetrics restRequestMetrics) {
if (restRequestMetrics != null) {
metrics = restRequestMetrics;
} else {
throw new IllegalArgumentException("RestRequestMetrics provided cannot be null");
}
}
/**
* Records the metrics.
* </p>
* This method is expected to called when the {@link RestRequest}, that this instance of {@link RestRequestMetricsTracker} is
* attached to, finishes.
*/
public void recordMetrics() {
if (metrics != null) {
if (metricsRecorded.compareAndSet(false, true)) {
metrics.operationRate.mark();
metrics.nioRequestProcessingTimeInMs.update(nioMetricsTracker.requestProcessingTimeInMs.get());
metrics.nioResponseProcessingTimeInMs.update(nioMetricsTracker.responseProcessingTimeInMs.get());
metrics.nioRoundTripTimeInMs.update(nioMetricsTracker.roundTripTimeInMs);
metrics.scRequestProcessingTimeInMs.update(scalingMetricsTracker.requestProcessingTimeInMs.get());
metrics.scRequestProcessingWaitTimeInMs.update(scalingMetricsTracker.requestProcessingWaitTimeInMs.get());
metrics.scResponseProcessingTimeInMs.update(scalingMetricsTracker.responseProcessingTimeInMs.get());
metrics.scResponseProcessingWaitTimeInMs.update(scalingMetricsTracker.responseProcessingWaitTimeInMs.get());
metrics.scRoundTripTimeInMs.update(scalingMetricsTracker.roundTripTimeInMs);
if (failed) {
metrics.operationError.inc();
}
}
} else {
throw new IllegalStateException("Could not record metrics because there is no metrics tracker");
}
}
/**
* Creates a default {@link RestRequestMetrics} in case {@link #injectMetrics(RestRequestMetrics)} is never
* called on an instance of {@link RestRequestMetricsTracker}.
* @param metricRegistry the {@link MetricRegistry} to use to register the created metrics.
*/
public static void setDefaults(MetricRegistry metricRegistry) {
defaultMetrics = new RestRequestMetrics(RestRequestMetricsTracker.class, DEFAULT_REQUEST_TYPE, metricRegistry);
}
}