/** * 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.router; import com.github.ambry.clustermap.ClusterMap; import com.github.ambry.clustermap.ReplicaId; import com.github.ambry.commons.BlobId; import com.github.ambry.commons.ResponseHandler; import com.github.ambry.config.RouterConfig; import com.github.ambry.network.NetworkClientErrorCode; import com.github.ambry.network.RequestInfo; import com.github.ambry.network.ResponseInfo; import com.github.ambry.notification.NotificationSystem; import com.github.ambry.protocol.DeleteRequest; import com.github.ambry.protocol.DeleteResponse; import com.github.ambry.protocol.RequestOrResponse; import com.github.ambry.utils.ByteBufferInputStream; import com.github.ambry.utils.Time; import java.io.DataInputStream; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Handles {@link DeleteOperation}. A {@code DeleteManager} keeps track of all the delete * operations that are assigned to it, and manages their states and life cycles. */ class DeleteManager { private final Set<DeleteOperation> deleteOperations; private final HashMap<Integer, DeleteOperation> correlationIdToDeleteOperation; private final NotificationSystem notificationSystem; private final Time time; private final ResponseHandler responseHandler; private final NonBlockingRouterMetrics routerMetrics; private final ClusterMap clusterMap; private final RouterConfig routerConfig; private final RouterCallback routerCallback; private static final Logger logger = LoggerFactory.getLogger(DeleteManager.class); /** * Used by a {@link DeleteOperation} to associate a {@code CorrelationId} to a {@link DeleteOperation}. */ private class DeleteRequestRegistrationCallbackImpl implements RequestRegistrationCallback<DeleteOperation> { private List<RequestInfo> requestListToFill; @Override public void registerRequestToSend(DeleteOperation deleteOperation, RequestInfo requestInfo) { requestListToFill.add(requestInfo); correlationIdToDeleteOperation.put(((RequestOrResponse) requestInfo.getRequest()).getCorrelationId(), deleteOperation); } } private final DeleteRequestRegistrationCallbackImpl requestRegistrationCallback = new DeleteRequestRegistrationCallbackImpl(); /** * Creates a DeleteManager. * @param clusterMap The {@link ClusterMap} of the cluster. * @param responseHandler The {@link ResponseHandler} used to notify failures for failure detection. * @param notificationSystem The {@link NotificationSystem} used for notifying blob deletions. * @param routerConfig The {@link RouterConfig} containing the configs for the DeleteManager. * @param routerMetrics The {@link NonBlockingRouterMetrics} to be used for reporting metrics. * @param routerCallback The {@link RouterCallback} to use for callbacks to the router. * @param time The {@link Time} instance to use. */ DeleteManager(ClusterMap clusterMap, ResponseHandler responseHandler, NotificationSystem notificationSystem, RouterConfig routerConfig, NonBlockingRouterMetrics routerMetrics, RouterCallback routerCallback, Time time) { this.clusterMap = clusterMap; this.responseHandler = responseHandler; this.notificationSystem = notificationSystem; this.routerConfig = routerConfig; this.routerMetrics = routerMetrics; this.routerCallback = routerCallback; this.time = time; deleteOperations = Collections.newSetFromMap(new ConcurrentHashMap<DeleteOperation, Boolean>()); correlationIdToDeleteOperation = new HashMap<Integer, DeleteOperation>(); } /** * Submits a {@link DeleteOperation} to this {@code DeleteManager}. * @param blobIdString The blobId string to be deleted. * @param serviceId The service ID of the service deleting the blob. This can be null if unknown. * @param futureResult The {@link FutureResult} that will contain the result eventually and exception if any. * @param callback The {@link Callback} that will be called on completion of the request. */ void submitDeleteBlobOperation(String blobIdString, String serviceId, FutureResult<Void> futureResult, Callback<Void> callback) { try { BlobId blobId = RouterUtils.getBlobIdFromString(blobIdString, clusterMap); DeleteOperation deleteOperation = new DeleteOperation(routerConfig, routerMetrics, responseHandler, blobId, serviceId, callback, time, futureResult); deleteOperations.add(deleteOperation); } catch (RouterException e) { routerMetrics.operationDequeuingRate.mark(); routerMetrics.onDeleteBlobError(e); NonBlockingRouter.completeOperation(futureResult, callback, null, e); } } /** * Polls all delete operations and populates a list of {@link RequestInfo} to be sent to data nodes in order to * complete delete operations. * @param requestListToFill list to be filled with the requests created. */ public void poll(List<RequestInfo> requestListToFill) { long startTime = time.milliseconds(); requestRegistrationCallback.requestListToFill = requestListToFill; for (DeleteOperation op : deleteOperations) { boolean exceptionEncountered = false; try { op.poll(requestRegistrationCallback); } catch (Exception e) { exceptionEncountered = true; op.setOperationException(new RouterException("Delete poll encountered unexpected error", e, RouterErrorCode.UnexpectedInternalError)); } if (exceptionEncountered || op.isOperationComplete()) { if (deleteOperations.remove(op)) { // In order to ensure that an operation is completed only once, call onComplete() only at the place where the // operation actually gets removed from the set of operations. See comment within close(). onComplete(op); } } } routerMetrics.deleteManagerPollTimeMs.update(time.milliseconds() - startTime); } /** * Handles responses received for each of the {@link DeleteOperation} within this delete manager. * @param responseInfo the {@link ResponseInfo} containing the response. */ void handleResponse(ResponseInfo responseInfo) { long startTime = time.milliseconds(); DeleteResponse deleteReponse = extractDeleteResponseAndNotifyResponseHandler(responseInfo); RouterRequestInfo routerRequestInfo = (RouterRequestInfo) responseInfo.getRequestInfo(); int correlationId = ((DeleteRequest) routerRequestInfo.getRequest()).getCorrelationId(); DeleteOperation deleteOperation = correlationIdToDeleteOperation.remove(correlationId); // If it is still an active operation, hand over the response. Otherwise, ignore. if (deleteOperations.contains(deleteOperation)) { boolean exceptionEncountered = false; try { deleteOperation.handleResponse(responseInfo, deleteReponse); } catch (Exception e) { exceptionEncountered = true; deleteOperation.setOperationException( new RouterException("Delete handleResponse encountered unexpected error", e, RouterErrorCode.UnexpectedInternalError)); } if (exceptionEncountered || deleteOperation.isOperationComplete()) { if (deleteOperations.remove(deleteOperation)) { onComplete(deleteOperation); } } routerMetrics.deleteManagerHandleResponseTimeMs.update(time.milliseconds() - startTime); } else { routerMetrics.ignoredResponseCount.inc(); } } /** * Extract the {@link DeleteResponse} from the given {@link ResponseInfo} * @param responseInfo the {@link ResponseInfo} from which the {@link DeleteResponse} is to be extracted. * @return the extracted {@link DeleteResponse} if there is one; null otherwise. */ private DeleteResponse extractDeleteResponseAndNotifyResponseHandler(ResponseInfo responseInfo) { DeleteResponse deleteResponse = null; ReplicaId replicaId = ((RouterRequestInfo) responseInfo.getRequestInfo()).getReplicaId(); NetworkClientErrorCode networkClientErrorCode = responseInfo.getError(); if (networkClientErrorCode == null) { try { deleteResponse = DeleteResponse.readFrom(new DataInputStream(new ByteBufferInputStream(responseInfo.getResponse()))); responseHandler.onEvent(replicaId, deleteResponse.getError()); } catch (Exception e) { // Ignore. There is no value in notifying the response handler. logger.error("Response deserialization received unexpected error", e); routerMetrics.responseDeserializationErrorCount.inc(); } } else { responseHandler.onEvent(replicaId, networkClientErrorCode); } return deleteResponse; } /** * Called when the delete operation is completed. The {@code DeleteManager} also finishes the delete operation * by performing the callback and notification. * @param op The {@link DeleteOperation} that has completed. */ void onComplete(DeleteOperation op) { Exception e = op.getOperationException(); if (e == null) { notificationSystem.onBlobDeleted(op.getBlobId().getID(), op.getServiceId()); } else { routerMetrics.onDeleteBlobError(e); } routerMetrics.operationDequeuingRate.mark(); routerMetrics.deleteBlobOperationLatencyMs.update(time.milliseconds() - op.getSubmissionTimeMs()); NonBlockingRouter.completeOperation(op.getFutureResult(), op.getCallback(), op.getOperationResult(), op.getOperationException()); } /** * Closes the {@code DeleteManager}. A {@code DeleteManager} can be closed for only once. Any further close action * will have no effect. */ void close() { for (DeleteOperation op : deleteOperations) { // There is a rare scenario where the operation gets removed from this set and gets completed concurrently by // the RequestResponseHandler thread when it is in poll() or handleResponse(). In order to avoid the completion // from happening twice, complete it here only if the remove was successful. if (deleteOperations.remove(op)) { Exception e = new RouterException("Aborted operation because Router is closed.", RouterErrorCode.RouterClosed); routerMetrics.operationDequeuingRate.mark(); routerMetrics.operationAbortCount.inc(); routerMetrics.onDeleteBlobError(e); NonBlockingRouter.completeOperation(op.getFutureResult(), op.getCallback(), null, e); } } } }