/*
* Copyright (c) 2008-2017, Hazelcast, Inc. 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hazelcast.spi.impl.operationservice.impl.operations;
import com.hazelcast.client.impl.operations.OperationFactoryWrapper;
import com.hazelcast.nio.ObjectDataInput;
import com.hazelcast.nio.ObjectDataOutput;
import com.hazelcast.nio.serialization.IdentifiedDataSerializable;
import com.hazelcast.spi.NodeEngine;
import com.hazelcast.spi.Operation;
import com.hazelcast.spi.OperationAccessor;
import com.hazelcast.spi.OperationFactory;
import com.hazelcast.spi.OperationResponseHandler;
import com.hazelcast.spi.OperationService;
import com.hazelcast.spi.impl.SpiDataSerializerHook;
import com.hazelcast.spi.impl.operationservice.impl.OperationServiceImpl;
import com.hazelcast.spi.impl.operationservice.impl.responses.ErrorResponse;
import com.hazelcast.spi.impl.operationservice.impl.responses.NormalResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
import static com.hazelcast.spi.impl.operationservice.impl.operations.PartitionAwareFactoryAccessor.extractPartitionAware;
import static com.hazelcast.util.CollectionUtil.toIntArray;
/**
* Executes Operations on one or more partitions.
* <p>
* THe execution used to be synchronous; so the thread executing this PartitionIteratingOperation would block till all
* the child requests are done. In 3.8 this is made asynchronous so that the thread isn't consumed and available for
* other tasks. On each partition operation an {@link OperationResponseHandler} is set, that sends the result to the
* caller when all responses have completed.
*/
public final class PartitionIteratingOperation extends Operation implements IdentifiedDataSerializable {
private static final Object NULL = new Object() {
@Override
public String toString() {
return "null";
}
};
private OperationFactory operationFactory;
private int[] partitions;
public PartitionIteratingOperation() {
}
public PartitionIteratingOperation(OperationFactory operationFactory, List<Integer> partitions) {
this.operationFactory = operationFactory;
this.partitions = toIntArray(partitions);
}
public OperationFactory getOperationFactory() {
return operationFactory;
}
@Override
public boolean returnsResponse() {
// since this call is non blocking, we don't have a response. The response is send when the actual operations complete.
return false;
}
@Override
public void run() throws Exception {
getOperationServiceImpl().onStartAsyncOperation(this);
PartitionAwareOperationFactory partitionAwareFactory = extractPartitionAware(operationFactory);
if (partitionAwareFactory != null) {
executePartitionAwareOperations(partitionAwareFactory);
} else {
executeOperations();
}
}
@Override
public void onExecutionFailure(Throwable cause) {
// in case of an error, we need to de-register to prevent leaks.
getOperationServiceImpl().onCompletionAsyncOperation(this);
// we also send a response so that the caller doesn't wait indefinitely.
sendResponse(new ErrorResponse(cause, getCallId(), isUrgent()));
getLogger().severe(cause);
}
private void executeOperations() {
NodeEngine nodeEngine = getNodeEngine();
OperationResponseHandler responseHandler = new OperationResponseHandlerImpl(partitions);
OperationService operationService = nodeEngine.getOperationService();
Object service = getServiceName() == null ? null : getService();
for (int partitionId : partitions) {
Operation operation = operationFactory.createOperation()
.setNodeEngine(nodeEngine)
.setPartitionId(partitionId)
.setReplicaIndex(getReplicaIndex())
.setOperationResponseHandler(responseHandler)
.setServiceName(getServiceName())
.setService(service)
.setCallerUuid(extractCallerUuid());
OperationAccessor.setCallerAddress(operation, getCallerAddress());
operationService.execute(operation);
}
}
private void executePartitionAwareOperations(PartitionAwareOperationFactory givenFactory) {
PartitionAwareOperationFactory factory = givenFactory.createFactoryOnRunner(getNodeEngine());
NodeEngine nodeEngine = getNodeEngine();
int[] operationFactoryPartitions = factory.getPartitions();
partitions = operationFactoryPartitions == null ? partitions : operationFactoryPartitions;
OperationResponseHandler responseHandler = new OperationResponseHandlerImpl(partitions);
OperationService operationService = nodeEngine.getOperationService();
Object service = getServiceName() == null ? null : getService();
for (int partitionId : partitions) {
Operation op = factory.createPartitionOperation(partitionId)
.setNodeEngine(nodeEngine)
.setPartitionId(partitionId)
.setReplicaIndex(getReplicaIndex())
.setOperationResponseHandler(responseHandler)
.setServiceName(getServiceName())
.setService(service)
.setCallerUuid(extractCallerUuid());
OperationAccessor.setCallerAddress(op, getCallerAddress());
operationService.execute(op);
}
}
private OperationServiceImpl getOperationServiceImpl() {
return (OperationServiceImpl) getNodeEngine().getOperationService();
}
private String extractCallerUuid() {
// Clients callerUUID can be set already. See OperationFactoryWrapper usage.
if (operationFactory instanceof OperationFactoryWrapper) {
return ((OperationFactoryWrapper) operationFactory).getUuid();
}
// Members UUID
return getCallerUuid();
}
@Override
protected void toString(StringBuilder sb) {
super.toString(sb);
sb.append(", operationFactory=").append(operationFactory);
}
private class OperationResponseHandlerImpl implements OperationResponseHandler {
// an array with the partitionCount as length, so we can quickly do a lookup for a given partitionId.
// it will store all the 'sub' responses.
private final AtomicReferenceArray<Object> responseArray = new AtomicReferenceArray<Object>(
getNodeEngine().getPartitionService().getPartitionCount());
// contains the number of pending operations. If it hits zero, all responses have been received.
private final AtomicInteger pendingOperations;
private final int[] partitions;
OperationResponseHandlerImpl(int[] partitions) {
this.partitions = partitions;
this.pendingOperations = new AtomicInteger(partitions.length);
}
@Override
public void sendResponse(Operation op, Object response) {
if (response instanceof NormalResponse) {
response = ((NormalResponse) response).getValue();
} else if (response == null) {
response = NULL;
}
// we try to set the response in the responseArray
if (!responseArray.compareAndSet(op.getPartitionId(), null, response)) {
// duplicate response; should not happen. We can only log it since this method is being executed on some
// general purpose executor.
getLogger().warning("Duplicate response for " + op + " second response [" + response + "]"
+ "first response [" + responseArray.get(op.getPartitionId()) + "]");
return;
}
// if it is the last response we are waiting for, we can send the final response to the caller.
if (pendingOperations.decrementAndGet() == 0) {
getOperationServiceImpl().onCompletionAsyncOperation(PartitionIteratingOperation.this);
sendResponse();
}
}
private void sendResponse() {
Object[] results = new Object[partitions.length];
for (int k = 0; k < partitions.length; k++) {
int partitionId = partitions[k];
Object response = responseArray.get(partitionId);
results[k] = response == NULL ? null : response;
}
PartitionIteratingOperation.this.sendResponse(new PartitionResponse(partitions, results));
}
}
@Override
public int getFactoryId() {
return SpiDataSerializerHook.F_ID;
}
@Override
public int getId() {
return SpiDataSerializerHook.PARTITION_ITERATOR;
}
@Override
protected void writeInternal(ObjectDataOutput out) throws IOException {
super.writeInternal(out);
out.writeObject(operationFactory);
out.writeIntArray(partitions);
}
@Override
protected void readInternal(ObjectDataInput in) throws IOException {
super.readInternal(in);
operationFactory = in.readObject();
partitions = in.readIntArray();
}
// implements IdentifiedDataSerializable to speed up serialization of arrays
public static final class PartitionResponse implements IdentifiedDataSerializable {
private int[] partitions;
private Object[] results;
public PartitionResponse() {
}
PartitionResponse(int[] partitions, Object[] results) {
this.partitions = partitions;
this.results = results;
}
public void addResults(Map<Integer, Object> partitionResults) {
if (results == null) {
return;
}
for (int i = 0; i < results.length; i++) {
partitionResults.put(partitions[i], results[i]);
}
}
@Override
public int getFactoryId() {
return SpiDataSerializerHook.F_ID;
}
@Override
public int getId() {
return SpiDataSerializerHook.PARTITION_RESPONSE;
}
@Override
public void writeData(ObjectDataOutput out) throws IOException {
out.writeIntArray(partitions);
int resultLength = results != null ? results.length : 0;
out.writeInt(resultLength);
if (resultLength > 0) {
for (Object result : results) {
out.writeObject(result);
}
}
}
@Override
public void readData(ObjectDataInput in) throws IOException {
partitions = in.readIntArray();
int resultLength = in.readInt();
if (resultLength > 0) {
results = new Object[resultLength];
for (int i = 0; i < resultLength; i++) {
results[i] = in.readObject();
}
}
}
}
}