/**
* 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.admin;
import com.codahale.metrics.Histogram;
import com.github.ambry.clustermap.ClusterMap;
import com.github.ambry.commons.ByteBufferReadableStreamChannel;
import com.github.ambry.config.AdminConfig;
import com.github.ambry.messageformat.BlobInfo;
import com.github.ambry.protocol.GetOption;
import com.github.ambry.rest.BlobStorageService;
import com.github.ambry.rest.IdConverter;
import com.github.ambry.rest.IdConverterFactory;
import com.github.ambry.rest.ResponseStatus;
import com.github.ambry.rest.RestMethod;
import com.github.ambry.rest.RestRequest;
import com.github.ambry.rest.RestRequestMetrics;
import com.github.ambry.rest.RestResponseChannel;
import com.github.ambry.rest.RestResponseHandler;
import com.github.ambry.rest.RestServiceErrorCode;
import com.github.ambry.rest.RestServiceException;
import com.github.ambry.rest.RestUtils;
import com.github.ambry.rest.SecurityService;
import com.github.ambry.rest.SecurityServiceFactory;
import com.github.ambry.router.Callback;
import com.github.ambry.router.GetBlobOptions;
import com.github.ambry.router.GetBlobOptionsBuilder;
import com.github.ambry.router.GetBlobResult;
import com.github.ambry.router.ReadableStreamChannel;
import com.github.ambry.router.Router;
import com.github.ambry.router.RouterException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.GregorianCalendar;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is an Admin specific implementation of {@link BlobStorageService}.
* <p/>
* All the operations that need to be performed by the Admin are supported here.
*/
class AdminBlobStorageService implements BlobStorageService {
protected static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
protected final AdminMetrics adminMetrics;
private static final String OPERATION_TYPE_INBOUND_ID_CONVERSION = "Inbound Id Conversion";
private static final String OPERATION_TYPE_GET_RESPONSE_SECURITY = "GET Response Security";
private static final String OPERATION_TYPE_HEAD_RESPONSE_SECURITY = "HEAD Response Security";
private static final String OPERATION_TYPE_GET = "GET";
private static final String OPERATION_TYPE_HEAD = "HEAD";
private static final String OPERATION_TYPE_DELETE = "DELETE";
private final RestResponseHandler responseHandler;
private final Router router;
private final IdConverterFactory idConverterFactory;
private final SecurityServiceFactory securityServiceFactory;
private final AdminConfig adminConfig;
private final GetReplicasHandler getReplicasHandler;
private final Logger logger = LoggerFactory.getLogger(getClass());
private IdConverter idConverter = null;
private SecurityService securityService = null;
private boolean isUp = false;
/**
* Create a new instance of AdminBlobStorageService by supplying it with config, metrics, cluster map, a
* response handler controller and a router.
* @param adminConfig the {@link AdminConfig} with configuration parameters.
* @param adminMetrics the metrics instance to use in the form of {@link AdminMetrics}.
* @param clusterMap the {@link ClusterMap} that contains information about the cluster.
* @param responseHandler the {@link RestResponseHandler} that can be used to submit responses that need to be sent
* out.
* @param router the {@link Router} instance to use to perform blob operations.
* @param idConverterFactory the {@link IdConverterFactory} to use to get an {@link IdConverter}.
* @param securityServiceFactory the {@link SecurityServiceFactory} to use to get an {@link SecurityService}.
*/
public AdminBlobStorageService(AdminConfig adminConfig, AdminMetrics adminMetrics, ClusterMap clusterMap,
RestResponseHandler responseHandler, Router router, IdConverterFactory idConverterFactory,
SecurityServiceFactory securityServiceFactory) {
this.adminConfig = adminConfig;
this.adminMetrics = adminMetrics;
this.responseHandler = responseHandler;
this.router = router;
this.idConverterFactory = idConverterFactory;
this.securityServiceFactory = securityServiceFactory;
getReplicasHandler = new GetReplicasHandler(adminMetrics, clusterMap);
logger.trace("Instantiated AdminBlobStorageService");
}
@Override
public void start() throws InstantiationException {
long startupBeginTime = System.currentTimeMillis();
idConverter = idConverterFactory.getIdConverter();
securityService = securityServiceFactory.getSecurityService();
isUp = true;
logger.info("AdminBlobStorageService has started");
adminMetrics.blobStorageServiceStartupTimeInMs.update(System.currentTimeMillis() - startupBeginTime);
}
@Override
public void shutdown() {
long shutdownBeginTime = System.currentTimeMillis();
isUp = false;
try {
if (securityService != null) {
securityService.close();
securityService = null;
}
if (idConverter != null) {
idConverter.close();
idConverter = null;
}
logger.info("AdminBlobStorageService shutdown complete");
} catch (IOException e) {
logger.error("Downstream service close failed", e);
} finally {
adminMetrics.blobStorageServiceShutdownTimeInMs.update(System.currentTimeMillis() - shutdownBeginTime);
}
}
@Override
public void handleGet(RestRequest restRequest, RestResponseChannel restResponseChannel) {
long processingStartTime = System.currentTimeMillis();
long preProcessingTime = 0;
handlePrechecks(restRequest, restResponseChannel);
try {
logger.trace("Handling GET request - {}", restRequest.getUri());
checkAvailable();
RestUtils.SubResource subresource = RestUtils.getBlobSubResource(restRequest);
RestRequestMetrics requestMetrics =
restRequest.getSSLSession() != null ? adminMetrics.getBlobSSLMetrics : adminMetrics.getBlobMetrics;
GetCallback routerCallback = new GetCallback(restRequest, restResponseChannel, subresource);
SecurityProcessRequestCallback securityCallback =
new SecurityProcessRequestCallback(restRequest, restResponseChannel, routerCallback);
if (subresource != null) {
logger.trace("Sub-resource requested: {}", subresource);
switch (subresource) {
case BlobInfo:
requestMetrics = restRequest.getSSLSession() != null ? adminMetrics.getBlobInfoSSLMetrics
: adminMetrics.getBlobInfoMetrics;
break;
case UserMetadata:
requestMetrics = restRequest.getSSLSession() != null ? adminMetrics.getUserMetadataSSLMetrics
: adminMetrics.getUserMetadataMetrics;
break;
case Replicas:
requestMetrics = restRequest.getSSLSession() != null ? adminMetrics.getReplicasSSLMetrics
: adminMetrics.getReplicasMetrics;
securityCallback = new SecurityProcessRequestCallback(restRequest, restResponseChannel);
break;
}
}
restRequest.getMetricsTracker().injectMetrics(requestMetrics);
preProcessingTime = System.currentTimeMillis() - processingStartTime;
securityService.processRequest(restRequest, securityCallback);
} catch (Exception e) {
submitResponse(restRequest, restResponseChannel, null, e);
} finally {
adminMetrics.getPreProcessingTimeInMs.update(preProcessingTime);
}
}
/**
* {@inheritDoc}
* <p/>
* POST is not supported by {@link AdminBlobStorageService}.
* @param restRequest the {@link RestRequest} that needs to be handled.
* @param restResponseChannel the {@link RestResponseChannel} over which response to {@code restRequest} can be sent.
*/
@Override
public void handlePost(RestRequest restRequest, RestResponseChannel restResponseChannel) {
handlePrechecks(restRequest, restResponseChannel);
RestRequestMetrics requestMetrics =
restRequest.getSSLSession() != null ? adminMetrics.postBlobSSLMetrics : adminMetrics.postBlobMetrics;
restRequest.getMetricsTracker().injectMetrics(requestMetrics);
Exception exception =
isUp ? new RestServiceException("POST is not supported", RestServiceErrorCode.UnsupportedHttpMethod)
: new RestServiceException("AdminBlobStorageService unavailable", RestServiceErrorCode.ServiceUnavailable);
submitResponse(restRequest, restResponseChannel, null, exception);
}
/**
* {@inheritDoc}
* <p/>
* PUT is not supported by {@link AdminBlobStorageService}.
* @param restRequest the {@link RestRequest} that needs to be handled.
* @param restResponseChannel the {@link RestResponseChannel} over which response to {@code restRequest} can be sent.
*/
@Override
public void handlePut(RestRequest restRequest, RestResponseChannel restResponseChannel) {
handlePrechecks(restRequest, restResponseChannel);
Exception exception =
isUp ? new RestServiceException("PUT is not supported", RestServiceErrorCode.UnsupportedHttpMethod)
: new RestServiceException("AdminBlobStorageService unavailable", RestServiceErrorCode.ServiceUnavailable);
submitResponse(restRequest, restResponseChannel, null, exception);
}
@Override
public void handleDelete(RestRequest restRequest, RestResponseChannel restResponseChannel) {
long processingStartTime = System.currentTimeMillis();
long preProcessingTime = 0;
handlePrechecks(restRequest, restResponseChannel);
RestRequestMetrics requestMetrics =
restRequest.getSSLSession() != null ? adminMetrics.deleteBlobSSLMetrics : adminMetrics.deleteBlobMetrics;
restRequest.getMetricsTracker().injectMetrics(requestMetrics);
try {
logger.trace("Handling DELETE request - {}", restRequest.getUri());
checkAvailable();
DeleteCallback routerCallback = new DeleteCallback(restRequest, restResponseChannel);
preProcessingTime = System.currentTimeMillis() - processingStartTime;
SecurityProcessRequestCallback securityCallback =
new SecurityProcessRequestCallback(restRequest, restResponseChannel, routerCallback);
securityService.processRequest(restRequest, securityCallback);
} catch (Exception e) {
submitResponse(restRequest, restResponseChannel, null, e);
} finally {
adminMetrics.deletePreProcessingTimeInMs.update(preProcessingTime);
}
}
@Override
public void handleHead(RestRequest restRequest, RestResponseChannel restResponseChannel) {
long processingStartTime = System.currentTimeMillis();
long preProcessingTime = 0;
handlePrechecks(restRequest, restResponseChannel);
RestRequestMetrics requestMetrics =
restRequest.getSSLSession() != null ? adminMetrics.headBlobSSLMetrics : adminMetrics.headBlobMetrics;
restRequest.getMetricsTracker().injectMetrics(requestMetrics);
try {
logger.trace("Handling HEAD request - {}", restRequest.getUri());
checkAvailable();
HeadCallback routerCallback = new HeadCallback(restRequest, restResponseChannel);
preProcessingTime = System.currentTimeMillis() - processingStartTime;
SecurityProcessRequestCallback securityCallback =
new SecurityProcessRequestCallback(restRequest, restResponseChannel, routerCallback);
securityService.processRequest(restRequest, securityCallback);
} catch (Exception e) {
submitResponse(restRequest, restResponseChannel, null, e);
} finally {
adminMetrics.headPreProcessingTimeInMs.update(preProcessingTime);
}
}
/**
* Submits the response and {@code responseBody} (and any {@code exception})for the {@code restRequest} to the
* {@code responseHandler}.
* @param restRequest the {@link RestRequest} for which a response is ready.
* @param restResponseChannel the {@link RestResponseChannel} over which the response can be sent.
* @param responseBody the body of the response in the form of a {@link ReadableStreamChannel}.
* @param exception any {@link Exception} that occurred during the handling of {@code restRequest}.
*/
protected void submitResponse(RestRequest restRequest, RestResponseChannel restResponseChannel,
ReadableStreamChannel responseBody, Exception exception) {
try {
if (exception != null && exception instanceof RouterException) {
exception = new RestServiceException(exception,
RestServiceErrorCode.getRestServiceErrorCode(((RouterException) exception).getErrorCode()));
}
responseHandler.handleResponse(restRequest, restResponseChannel, responseBody, exception);
} catch (Exception e) {
adminMetrics.responseSubmissionError.inc();
if (exception != null) {
logger.error("Error submitting response to response handler", e);
} else {
exception = e;
}
logger.error("Handling of request {} failed", restRequest.getUri(), exception);
restResponseChannel.onResponseComplete(exception);
if (responseBody != null) {
try {
responseBody.close();
} catch (IOException ioe) {
adminMetrics.resourceReleaseError.inc();
logger.error("Error closing ReadableStreamChannel", e);
}
}
}
}
/**
* Checks for bad arguments or states.
* @param restRequest the {@link RestRequest} to use. Cannot be null.
* @param restResponseChannel the {@link RestResponseChannel} to use. Cannot be null.
*/
private void handlePrechecks(RestRequest restRequest, RestResponseChannel restResponseChannel) {
if (restRequest == null || restResponseChannel == null) {
StringBuilder errorMessage = new StringBuilder("Null arg(s) received -");
if (restRequest == null) {
errorMessage.append(" [RestRequest] ");
}
if (restResponseChannel == null) {
errorMessage.append(" [RestResponseChannel] ");
}
throw new IllegalArgumentException(errorMessage.toString());
}
}
/**
* Checks if {@link AdminBlobStorageService} is available to serve requests.
* @throws RestServiceException if {@link AdminBlobStorageService} is not available to serve requests.
*/
private void checkAvailable() throws RestServiceException {
if (!isUp) {
throw new RestServiceException("AdminBlobStorageService unavailable", RestServiceErrorCode.ServiceUnavailable);
}
}
/**
* Callback for {@link IdConverter} that is used when inbound IDs are converted.
*/
private class InboundIdConverterCallback implements Callback<String> {
private final RestRequest restRequest;
private final RestResponseChannel restResponseChannel;
private final CallbackTracker callbackTracker;
private GetCallback getCallback = null;
private HeadCallback headCallback = null;
private DeleteCallback deleteCallback = null;
InboundIdConverterCallback(RestRequest restRequest, RestResponseChannel restResponseChannel, GetCallback callback) {
this(restRequest, restResponseChannel);
getCallback = callback;
}
InboundIdConverterCallback(RestRequest restRequest, RestResponseChannel restResponseChannel,
HeadCallback callback) {
this(restRequest, restResponseChannel);
headCallback = callback;
}
InboundIdConverterCallback(RestRequest restRequest, RestResponseChannel restResponseChannel,
DeleteCallback callback) {
this(restRequest, restResponseChannel);
deleteCallback = callback;
}
InboundIdConverterCallback(RestRequest restRequest, RestResponseChannel restResponseChannel) {
this.restRequest = restRequest;
this.restResponseChannel = restResponseChannel;
callbackTracker = new CallbackTracker(restRequest, OPERATION_TYPE_INBOUND_ID_CONVERSION,
adminMetrics.inboundIdConversionTimeInMs, adminMetrics.inboundIdConversionCallbackProcessingTimeInMs);
callbackTracker.markOperationStart();
}
/**
* Forwards request to the {@link Router} once ID conversion is complete.
* </p>
* In the case of some sub-resources (e.g., {@link RestUtils.SubResource#Replicas}), the request is not forwarded to
* the {@link Router}.
* @param result The converted ID. This would be non null when the request executed successfully
* @param exception The exception that was reported on execution of the request
* @throws IllegalStateException if both {@code result} and {@code exception} are null.
*/
@Override
public void onCompletion(String result, Exception exception) {
callbackTracker.markOperationEnd();
ReadableStreamChannel response = null;
if (result == null && exception == null) {
throw new IllegalStateException("Both result and exception cannot be null");
} else if (exception == null) {
try {
RestMethod restMethod = restRequest.getRestMethod();
logger.trace("Forwarding {} of {} to the router", restMethod, result);
switch (restMethod) {
case GET:
RestUtils.SubResource subresource = RestUtils.getBlobSubResource(restRequest);
GetOption getOption = RestUtils.getGetOption(restRequest);
if (subresource == null || subresource.equals(RestUtils.SubResource.BlobInfo) || subresource.equals(
RestUtils.SubResource.UserMetadata)) {
getCallback.markStartTime();
GetBlobOptions.OperationType getOperationType =
subresource != null ? GetBlobOptions.OperationType.BlobInfo : GetBlobOptions.OperationType.All;
router.getBlob(result,
new GetBlobOptionsBuilder().operationType(getOperationType).getOption(getOption).build(),
getCallback);
} else {
switch (subresource) {
case Replicas:
response = getReplicasHandler.getReplicas(result, restResponseChannel);
break;
}
}
break;
case HEAD:
getOption = RestUtils.getGetOption(restRequest);
headCallback.markStartTime();
router.getBlob(result, new GetBlobOptionsBuilder().operationType(GetBlobOptions.OperationType.BlobInfo)
.getOption(getOption)
.build(), headCallback);
break;
case DELETE:
deleteCallback.markStartTime();
router.deleteBlob(result, RestUtils.getServiceId(restRequest), deleteCallback);
break;
default:
exception = new IllegalStateException("Unrecognized RestMethod: " + restMethod);
}
} catch (Exception e) {
exception = e;
}
}
if (response != null || exception != null) {
submitResponse(restRequest, restResponseChannel, response, exception);
}
callbackTracker.markCallbackProcessingEnd();
}
}
/**
* Callback for {@link SecurityService#processRequest(RestRequest, Callback)}.
*/
private class SecurityProcessRequestCallback implements Callback<Void> {
private static final String PROCESS_GET = "GET Request Security";
private static final String PROCESS_HEAD = "HEAD Request Security";
private static final String PROCESS_DELETE = "DELETE Request Security";
private final RestRequest restRequest;
private final RestResponseChannel restResponseChannel;
private final CallbackTracker callbackTracker;
private final String receivedId;
private InboundIdConverterCallback idConverterCallback;
SecurityProcessRequestCallback(RestRequest restRequest, RestResponseChannel restResponseChannel,
GetCallback callback) {
this(restRequest, restResponseChannel, PROCESS_GET, adminMetrics.getSecurityRequestTimeInMs,
adminMetrics.getSecurityRequestCallbackProcessingTimeInMs);
idConverterCallback = new InboundIdConverterCallback(restRequest, restResponseChannel, callback);
}
SecurityProcessRequestCallback(RestRequest restRequest, RestResponseChannel restResponseChannel,
HeadCallback callback) {
this(restRequest, restResponseChannel, PROCESS_HEAD, adminMetrics.headSecurityRequestTimeInMs,
adminMetrics.headSecurityRequestCallbackProcessingTimeInMs);
idConverterCallback = new InboundIdConverterCallback(restRequest, restResponseChannel, callback);
}
SecurityProcessRequestCallback(RestRequest restRequest, RestResponseChannel restResponseChannel,
DeleteCallback callback) {
this(restRequest, restResponseChannel, PROCESS_DELETE, adminMetrics.deleteSecurityRequestTimeInMs,
adminMetrics.deleteSecurityRequestCallbackProcessingTimeInMs);
idConverterCallback = new InboundIdConverterCallback(restRequest, restResponseChannel, callback);
}
SecurityProcessRequestCallback(RestRequest restRequest, RestResponseChannel restResponseChannel) {
this(restRequest, restResponseChannel, PROCESS_GET, adminMetrics.deleteSecurityRequestTimeInMs,
adminMetrics.deleteSecurityRequestCallbackProcessingTimeInMs);
idConverterCallback = new InboundIdConverterCallback(restRequest, restResponseChannel);
}
private SecurityProcessRequestCallback(RestRequest restRequest, RestResponseChannel restResponseChannel,
String operationType, Histogram operationTimeTracker, Histogram callbackProcessingTimeTracker) {
this.restRequest = restRequest;
this.restResponseChannel = restResponseChannel;
receivedId = RestUtils.getOperationOrBlobIdFromUri(restRequest, RestUtils.getBlobSubResource(restRequest),
adminConfig.adminPathPrefixesToRemove);
callbackTracker =
new CallbackTracker(restRequest, operationType, operationTimeTracker, callbackProcessingTimeTracker);
callbackTracker.markOperationStart();
}
/**
* Handles request once it has been vetted by the {@link SecurityService}.
* In case of exception, response is immediately submitted to the {@link RestResponseHandler}.
* In case of GET, HEAD and DELETE, ID conversion is triggered.
* @param result The result of the request. This would be non null when the request executed successfully
* @param exception The exception that was reported on execution of the request
*/
@Override
public void onCompletion(Void result, Exception exception) {
callbackTracker.markOperationEnd();
if (exception == null) {
try {
idConverter.convert(restRequest, receivedId, idConverterCallback);
} catch (Exception e) {
exception = e;
}
}
if (exception != null) {
submitResponse(restRequest, restResponseChannel, null, exception);
}
callbackTracker.markCallbackProcessingEnd();
}
}
/**
* Tracks metrics and logs progress of operations that accept callbacks.
*/
private class CallbackTracker {
private long operationStartTime = 0;
private long processingStartTime = 0;
private final String operationType;
private final Histogram operationTimeTracker;
private final Histogram callbackProcessingTimeTracker;
private final String blobId;
/**
* Create a CallbackTracker that tracks a particular operation.
* @param restRequest the {@link RestRequest} for the operation.
* @param operationType the type of operation.
* @param operationTimeTracker the {@link Histogram} of the time taken by the operation.
* @param callbackProcessingTimeTracker the {@link Histogram} of the time taken by the callback of the operation.
*/
CallbackTracker(RestRequest restRequest, String operationType, Histogram operationTimeTracker,
Histogram callbackProcessingTimeTracker) {
this.operationType = operationType;
this.operationTimeTracker = operationTimeTracker;
this.callbackProcessingTimeTracker = callbackProcessingTimeTracker;
blobId = RestUtils.getOperationOrBlobIdFromUri(restRequest, RestUtils.getBlobSubResource(restRequest),
adminConfig.adminPathPrefixesToRemove);
}
/**
* Marks that the operation being tracked has started.
*/
void markOperationStart() {
logger.trace("{} started for {}", operationType, blobId);
operationStartTime = System.currentTimeMillis();
}
/**
* Marks that the operation being tracked has ended and callback processing has started.
*/
void markOperationEnd() {
logger.trace("{} finished for {}", operationType, blobId);
processingStartTime = System.currentTimeMillis();
long operationTime = processingStartTime - operationStartTime;
operationTimeTracker.update(operationTime);
}
/**
* Marks that the callback processing has ended.
*/
void markCallbackProcessingEnd() {
logger.trace("Callback for {} of {} finished", operationType, blobId);
long processingTime = System.currentTimeMillis() - processingStartTime;
callbackProcessingTimeTracker.update(processingTime);
}
}
/**
* Callback for GET operations. Updates headers and submits the response body if there is no security exception.
*/
private class GetCallback implements Callback<GetBlobResult> {
private final RestRequest restRequest;
private final RestResponseChannel restResponseChannel;
private final RestUtils.SubResource subResource;
private final CallbackTracker callbackTracker;
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* Create a GET callback.
* @param restRequest the {@link RestRequest} for whose response this is a callback.
* @param restResponseChannel the {@link RestResponseChannel} to set headers on.
* @param subResource the sub-resource requested.
*/
GetCallback(RestRequest restRequest, RestResponseChannel restResponseChannel, RestUtils.SubResource subResource) {
this.restRequest = restRequest;
this.restResponseChannel = restResponseChannel;
this.subResource = subResource;
callbackTracker = new CallbackTracker(restRequest, OPERATION_TYPE_GET, adminMetrics.getTimeInMs,
adminMetrics.getCallbackProcessingTimeInMs);
}
/**
* If the request is not for a sub resource, makes a GET call to the router. If the request is for a sub resource,
* responds immediately. If there was no {@code routerResult} or if there was an exception, bails out.
* Submits the GET response to {@link RestResponseHandler} so that it can be sent (or the exception handled).
* @param routerResult The result of the request i.e a {@link GetBlobResult} object with the properties of the blob
* (and a channel for blob data, if the request did not have a subresource) that is going to be
* returned if no exception occured. This is non null if the request executed successfully.
* @param routerException The exception that was reported on execution of the request (if any).
*/
@Override
public void onCompletion(final GetBlobResult routerResult, Exception routerException) {
callbackTracker.markOperationEnd();
if (routerResult == null && routerException == null) {
throw new IllegalStateException("Both response and exception are null");
}
try {
if (routerException == null) {
final CallbackTracker securityCallbackTracker =
new CallbackTracker(restRequest, OPERATION_TYPE_GET_RESPONSE_SECURITY,
adminMetrics.getSecurityResponseTimeInMs, adminMetrics.getSecurityResponseCallbackProcessingTimeInMs);
securityCallbackTracker.markOperationStart();
securityService.processResponse(restRequest, restResponseChannel, routerResult.getBlobInfo(),
new Callback<Void>() {
@Override
public void onCompletion(Void securityResult, Exception securityException) {
securityCallbackTracker.markOperationEnd();
ReadableStreamChannel response = null;
boolean blobNotModified = restResponseChannel.getStatus() == ResponseStatus.NotModified;
try {
if (securityException == null) {
if (subResource != null) {
BlobInfo blobInfo = routerResult.getBlobInfo();
Map<String, String> userMetadata = RestUtils.buildUserMetadata(blobInfo.getUserMetadata());
if (userMetadata == null) {
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_TYPE, "application/octet-stream");
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH,
blobInfo.getUserMetadata().length);
response = new ByteBufferReadableStreamChannel(ByteBuffer.wrap(blobInfo.getUserMetadata()));
} else {
setUserMetadataHeaders(userMetadata, restResponseChannel);
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, 0);
response = new ByteBufferReadableStreamChannel(AdminBlobStorageService.EMPTY_BUFFER);
}
} else if (!blobNotModified) {
response = routerResult.getBlobDataChannel();
} else {
// If the blob was not modified, we need to close the channel, as it will not be submitted to
// the RestResponseHandler
routerResult.getBlobDataChannel().close();
}
}
} catch (Exception e) {
adminMetrics.getSecurityResponseCallbackProcessingError.inc();
securityException = e;
} finally {
submitResponse(restRequest, restResponseChannel, response, securityException);
securityCallbackTracker.markCallbackProcessingEnd();
}
}
});
}
} catch (Exception e) {
adminMetrics.getCallbackProcessingError.inc();
routerException = e;
} finally {
if (routerException != null) {
submitResponse(restRequest, restResponseChannel,
routerResult != null ? routerResult.getBlobDataChannel() : null, routerException);
}
callbackTracker.markCallbackProcessingEnd();
}
}
/**
* Marks the start time of the operation.
*/
void markStartTime() {
callbackTracker.markOperationStart();
}
/**
* Sets the user metadata in the headers of the response.
* @param userMetadata the user metadata that need to be set in the headers.
* @param restResponseChannel the {@link RestResponseChannel} that is used for sending the response.
* @throws RestServiceException if there are any problems setting the header.
*/
private void setUserMetadataHeaders(Map<String, String> userMetadata, RestResponseChannel restResponseChannel)
throws RestServiceException {
for (Map.Entry<String, String> entry : userMetadata.entrySet()) {
restResponseChannel.setHeader(entry.getKey(), entry.getValue());
}
}
}
/**
* Callback for DELETE operations. Sends an ACCEPTED response to the client if operation is successful. Submits
* response either to handle exceptions or to clean up after a response.
*/
private class DeleteCallback implements Callback<Void> {
private final RestRequest restRequest;
private final RestResponseChannel restResponseChannel;
private final CallbackTracker callbackTracker;
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* Create a DELETE callback.
* @param restRequest the {@link RestRequest} for whose response this is a callback.
* @param restResponseChannel the {@link RestResponseChannel} over which response to {@code restRequest} can be
* sent.
*/
DeleteCallback(RestRequest restRequest, RestResponseChannel restResponseChannel) {
this.restRequest = restRequest;
this.restResponseChannel = restResponseChannel;
callbackTracker = new CallbackTracker(restRequest, OPERATION_TYPE_DELETE, adminMetrics.deleteTimeInMs,
adminMetrics.deleteCallbackProcessingTimeInMs);
}
/**
* If there was no exception, updates the header with the acceptance of the request. Submits the response either for
* exception handling or for cleanup.
* @param routerResult The result of the request. This is always null.
* @param routerException The exception that was reported on execution of the request (if any).
*/
@Override
public void onCompletion(Void routerResult, Exception routerException) {
callbackTracker.markOperationEnd();
try {
if (routerException == null) {
restResponseChannel.setHeader(RestUtils.Headers.DATE, new GregorianCalendar().getTime());
restResponseChannel.setStatus(ResponseStatus.Accepted);
restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, 0);
}
} catch (Exception e) {
adminMetrics.deleteCallbackProcessingError.inc();
routerException = e;
} finally {
submitResponse(restRequest, restResponseChannel, null, routerException);
callbackTracker.markCallbackProcessingEnd();
}
}
/**
* Marks the start time of the operation.
*/
void markStartTime() {
callbackTracker.markOperationStart();
}
}
/**
* Callback for HEAD operations. Sends the headers to the client if operation is successful. Submits response either
* to handle exceptions or to clean up after a response.
*/
private class HeadCallback implements Callback<GetBlobResult> {
private final RestRequest restRequest;
private final RestResponseChannel restResponseChannel;
private final CallbackTracker callbackTracker;
/**
* Create a HEAD callback.
* @param restRequest the {@link RestRequest} for whose response this is a callback.
* @param restResponseChannel the {@link RestResponseChannel} over which response to {@code restRequest} can be
* sent.
*/
HeadCallback(RestRequest restRequest, RestResponseChannel restResponseChannel) {
this.restRequest = restRequest;
this.restResponseChannel = restResponseChannel;
callbackTracker = new CallbackTracker(restRequest, OPERATION_TYPE_HEAD, adminMetrics.headTimeInMs,
adminMetrics.headCallbackProcessingTimeInMs);
}
/**
* If there was no exception, updates the header with the properties. Exceptions, if any, will be handled upon
* submission.
* @param routerResult The result of the request, which includes a {@link BlobInfo} object with the properties of
* the blob. This is non null if the request executed successfully.
* @param routerException The exception that was reported on execution of the request (if any).
*/
@Override
public void onCompletion(GetBlobResult routerResult, Exception routerException) {
callbackTracker.markOperationEnd();
if (routerResult == null && routerException == null) {
throw new IllegalStateException("Both response and exception are null");
}
try {
if (routerException == null) {
final CallbackTracker securityCallbackTracker =
new CallbackTracker(restRequest, OPERATION_TYPE_HEAD_RESPONSE_SECURITY,
adminMetrics.headSecurityResponseTimeInMs,
adminMetrics.headSecurityResponseCallbackProcessingTimeInMs);
securityCallbackTracker.markOperationStart();
securityService.processResponse(restRequest, restResponseChannel, routerResult.getBlobInfo(),
new Callback<Void>() {
@Override
public void onCompletion(Void securityResult, Exception securityException) {
callbackTracker.markOperationEnd();
submitResponse(restRequest, restResponseChannel, null, securityException);
callbackTracker.markCallbackProcessingEnd();
}
});
}
} catch (Exception e) {
adminMetrics.headCallbackProcessingError.inc();
routerException = e;
} finally {
if (routerException != null) {
submitResponse(restRequest, restResponseChannel, null, routerException);
}
callbackTracker.markCallbackProcessingEnd();
}
}
/**
* Marks the start time of the operation.
*/
void markStartTime() {
callbackTracker.markOperationStart();
}
}
}