All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hazelcast.map.impl.operation.EntryOperation Maven / Gradle / Ivy

There is a newer version: 4.5.4
Show newest version
/*
 * Copyright (c) 2008-2018, 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.map.impl.operation;

import com.hazelcast.concurrent.lock.LockWaitNotifyKey;
import com.hazelcast.core.EntryEventType;
import com.hazelcast.core.HazelcastException;
import com.hazelcast.core.ManagedContext;
import com.hazelcast.core.Offloadable;
import com.hazelcast.core.ReadOnly;
import com.hazelcast.map.EntryBackupProcessor;
import com.hazelcast.map.EntryProcessor;
import com.hazelcast.map.impl.MapDataSerializerHook;
import com.hazelcast.nio.ObjectDataInput;
import com.hazelcast.nio.ObjectDataOutput;
import com.hazelcast.nio.serialization.Data;
import com.hazelcast.spi.BackupAwareOperation;
import com.hazelcast.spi.BlockingOperation;
import com.hazelcast.spi.CallStatus;
import com.hazelcast.spi.ExecutionService;
import com.hazelcast.spi.Operation;
import com.hazelcast.spi.OperationAccessor;
import com.hazelcast.spi.OperationResponseHandler;
import com.hazelcast.spi.WaitNotifyKey;
import com.hazelcast.spi.exception.RetryableHazelcastException;
import com.hazelcast.spi.exception.WrongTargetException;
import com.hazelcast.spi.impl.MutatingOperation;
import com.hazelcast.spi.impl.operationservice.impl.OperationServiceImpl;
import com.hazelcast.spi.impl.operationservice.impl.responses.CallTimeoutResponse;
import com.hazelcast.spi.serialization.SerializationService;
import com.hazelcast.util.Clock;
import com.hazelcast.util.UuidUtil;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import static com.hazelcast.config.InMemoryFormat.OBJECT;
import static com.hazelcast.core.Offloadable.NO_OFFLOADING;
import static com.hazelcast.map.impl.operation.EntryOperator.operator;
import static com.hazelcast.spi.CallStatus.DONE_RESPONSE;
import static com.hazelcast.spi.CallStatus.OFFLOADED;
import static com.hazelcast.spi.CallStatus.WAIT;
import static com.hazelcast.spi.ExecutionService.OFFLOADABLE_EXECUTOR;
import static com.hazelcast.spi.InvocationBuilder.DEFAULT_TRY_PAUSE_MILLIS;
import static com.hazelcast.util.ExceptionUtil.sneakyThrow;

/**
 * Contains implementation of the off-loadable contract for EntryProcessor execution on a single key.
 * 

* ### Overview *

* Implementation of the the Offloadable contract for the EntryProcessor execution on a single key. *

* Allows off-loading the processing unit implementing this interface to the specified or default Executor. * Currently supported in: *

* IMap.executeOnKey(Object, EntryProcessor) * IMap.submitToKey(Object, EntryProcessor) * IMap.submitToKey(Object, EntryProcessor, ExecutionCallback) *

* ### Offloadable (for reading & writing) *

* If the EntryProcessor implements the Offloadable interface the processing will be offloaded to the given * ExecutorService allowing unblocking the partition-thread. The key will be locked for the time-span of the processing * in order to not generate a write-conflict. *

* If the EntryProcessor implements Offloadable the invocation scenario looks as follows: * - EntryOperation fetches the entry and locks the given key on partition-thread * - Then the processing is offloaded to the given executor * - When the processing finishes * if there is a change to the entry, a EntryOffloadableSetUnlockOperation is spawned * which sets the new value and unlocks the given key on partition-thread * if there is no change to the entry, a UnlockOperation is spawned, which just unlocks the kiven key * on partition thread *

* There will not be a conflict on a write due to the pessimistic locking of the key. * The threading looks as follows: *

* 1. partition-thread (fetch & lock) * 2. execution-thread (process) * 3. partition-thread (set & unlock, or just unlock if no changes) *

* ### Offloadable (for reading only) *

* If the EntryProcessor implements the Offloadable and ReadOnly interfaces the processing will be offloaded to the * givenExecutorService allowing unblocking the partition-thread. Since the EntryProcessor is not supposed to do any * changes to the Entry the key will NOT be locked for the time-span of the processing. *

* If the EntryProcessor implements Offloadable and ReadOnly the invocation scenario looks as follows: * - EntryOperation fetches the entry and DOES NOT lock the given key on partition-thread * - Then the processing is offloaded to the given executor * - When the processing finishes * if there is a change to the entry -> exception is thrown * if there is no change to the entry -> the result is returned to the user from the executor-thread. *

* In the read-only case the threading looks as follows: *

* 1. partition-thread (fetch) * 2. execution-thread (process) *

* ### Primary partition - main actors *

* - EntryOperation * - EntryOffloadableSetUnlockOperation *

* ### Backup partitions *

* Offloading will not be applied to backup partitions. It is possible to initialize the EntryBackupProcessor * with some input provided by the EntryProcessor in the EntryProcessor.getBackupProcessor() method. * The input allows providing context to the EntryBackupProcessor - for example the "delta" * so that the EntryBackupProcessor does not have to calculate the "delta" but it may just apply it. *

* ### Locking *

* The locking takes place only locally. If a partition locked by an off-loaded task gets migrated, the lock will not * be migrated. In this situation the off-loaded task "relying" on the lock will fail on the unlock operation, since it * will notice that there is no such a lock and therefore the processing for the key will get retried. * The reason behind is that the off-loadable backup-processing does not use locking there cannot be any transfer of * off-loadable locks from the primary replica to backup replicas. *

* GOTCHA: This operation LOADS missing keys from map-store, in contrast with PartitionWideEntryOperation. */ @SuppressWarnings("checkstyle:methodcount") public class EntryOperation extends KeyBasedMapOperation implements BackupAwareOperation, BlockingOperation, MutatingOperation { private static final int SET_UNLOCK_FAST_RETRY_LIMIT = 10; private EntryProcessor entryProcessor; private transient boolean offloading; // EntryOperation private transient Object response; // EntryOffloadableOperation private transient boolean readOnly; private transient int setUnlockRetryCount; private transient long begin; private transient OperationServiceImpl ops; private transient ExecutionService exs; public EntryOperation() { } public EntryOperation(String name, Data dataKey, EntryProcessor entryProcessor) { super(name, dataKey); this.entryProcessor = entryProcessor; } @Override public void innerBeforeRun() throws Exception { super.innerBeforeRun(); this.ops = (OperationServiceImpl) getNodeEngine().getOperationService(); this.exs = getNodeEngine().getExecutionService(); this.begin = Clock.currentTimeMillis(); this.readOnly = entryProcessor instanceof ReadOnly; final SerializationService serializationService = getNodeEngine().getSerializationService(); final ManagedContext managedContext = serializationService.getManagedContext(); managedContext.initialize(entryProcessor); } @Override public CallStatus call() { if (shouldWait()) { return WAIT; } if (offloading) { runOffloaded(); return OFFLOADED; } else { runVanilla(); return DONE_RESPONSE; } } public void runOffloaded() { if (!(entryProcessor instanceof Offloadable)) { throw new HazelcastException("EntryProcessor is expected to implement Offloadable for this operation"); } if (readOnly && entryProcessor.getBackupProcessor() != null) { throw new HazelcastException("EntryProcessor.getBackupProcessor() should return null if ReadOnly implemented"); } boolean shouldCloneForOffloading = OBJECT.equals(mapContainer.getMapConfig().getInMemoryFormat()); Object oldValue = recordStore.get(dataKey, false); Object clonedOldValue = shouldCloneForOffloading ? toData(oldValue) : oldValue; String executorName = ((Offloadable) entryProcessor).getExecutorName(); executorName = executorName.equals(Offloadable.OFFLOADABLE_EXECUTOR) ? OFFLOADABLE_EXECUTOR : executorName; if (readOnly) { runOffloadedReadOnlyEntryProcessor(clonedOldValue, executorName); } else { runOffloadedModifyingEntryProcessor(clonedOldValue, executorName); } } @SuppressWarnings("unchecked") private void runOffloadedReadOnlyEntryProcessor(final Object oldValue, String executorName) { ops.onStartAsyncOperation(this); getNodeEngine().getExecutionService().execute(executorName, new Runnable() { @Override public void run() { try { Data result = operator(EntryOperation.this, entryProcessor) .operateOnKeyValue(dataKey, oldValue).getResult(); getOperationResponseHandler().sendResponse(EntryOperation.this, result); } catch (Throwable t) { getOperationResponseHandler().sendResponse(EntryOperation.this, t); } finally { ops.onCompletionAsyncOperation(EntryOperation.this); } } }); } @SuppressWarnings("unchecked") private void runOffloadedModifyingEntryProcessor(final Object oldValue, String executorName) { final OperationServiceImpl ops = (OperationServiceImpl) getNodeEngine().getOperationService(); // callerId is random since the local locks are NOT re-entrant // using a randomID every time prevents from re-entering the already acquired lock final String finalCaller = UuidUtil.newUnsecureUuidString(); final Data finalDataKey = dataKey; final long finalThreadId = threadId; final long finalCallId = getCallId(); final long finalBegin = begin; // The off-loading uses local locks, since the locking is used only on primary-replica. // The locks are not supposed to be migrated on partition migration or partition promotion & downgrade. lock(finalDataKey, finalCaller, finalThreadId, finalCallId); try { ops.onStartAsyncOperation(this); getNodeEngine().getExecutionService().execute(executorName, new Runnable() { @Override public void run() { try { EntryOperator entryOperator = operator(EntryOperation.this, entryProcessor) .operateOnKeyValue(dataKey, oldValue); Data result = entryOperator.getResult(); EntryEventType modificationType = entryOperator.getEventType(); if (modificationType != null) { Data newValue = toData(entryOperator.getNewValue()); updateAndUnlock(toData(oldValue), newValue, modificationType, finalCaller, finalThreadId, result, finalBegin); } else { unlockOnly(result, finalCaller, finalThreadId, finalBegin); } } catch (Throwable t) { getLogger().severe("Unexpected error on Offloadable execution", t); unlockOnly(t, finalCaller, finalThreadId, finalBegin); } } }); } catch (Throwable t) { try { unlock(finalDataKey, finalCaller, finalThreadId, finalCallId, t); sneakyThrow(t); } finally { ops.onCompletionAsyncOperation(this); } } } private Data toData(Object obj) { return mapServiceContext.toData(obj); } private void lock(Data finalDataKey, String finalCaller, long finalThreadId, long finalCallId) { boolean locked = recordStore.localLock(finalDataKey, finalCaller, finalThreadId, finalCallId, -1); if (!locked) { // should not happen since it's a lock-awaiting operation and we are on a partition-thread, but just to make sure throw new IllegalStateException( String.format("Could not obtain a lock by the caller=%s and threadId=%d", finalCaller, threadId)); } } private void unlock(Data finalDataKey, String finalCaller, long finalThreadId, long finalCallId, Throwable cause) { boolean unlocked = recordStore.unlock(finalDataKey, finalCaller, finalThreadId, finalCallId); if (!unlocked) { throw new IllegalStateException( String.format("Could not unlock by the caller=%s and threadId=%d", finalCaller, threadId), cause); } } @SuppressWarnings({"unchecked", "checkstyle:methodlength"}) private void updateAndUnlock(Data previousValue, Data newValue, EntryEventType modificationType, String caller, long threadId, final Object result, long now) { EntryOffloadableSetUnlockOperation updateOperation = new EntryOffloadableSetUnlockOperation(name, modificationType, dataKey, previousValue, newValue, caller, threadId, now, entryProcessor.getBackupProcessor()); updateOperation.setPartitionId(getPartitionId()); updateOperation.setReplicaIndex(0); updateOperation.setNodeEngine(getNodeEngine()); updateOperation.setCallerUuid(getCallerUuid()); OperationAccessor.setCallerAddress(updateOperation, getCallerAddress()); @SuppressWarnings("checkstyle:anoninnerlength") OperationResponseHandler setUnlockResponseHandler = new OperationResponseHandler() { @Override public void sendResponse(Operation op, Object response) { if (isRetryable(response) || isTimeout(response)) { retry(op); } else { handleResponse(response); } } private void retry(final Operation op) { setUnlockRetryCount++; if (isFastRetryLimitReached()) { exs.schedule(new Runnable() { @Override public void run() { ops.execute(op); } }, DEFAULT_TRY_PAUSE_MILLIS, TimeUnit.MILLISECONDS); } else { ops.execute(op); } } private void handleResponse(Object response) { if (response instanceof Throwable) { Throwable t = (Throwable) response; try { // EntryOffloadableLockMismatchException is a marker send from the EntryOffloadableSetUnlockOperation // meaning that the whole invocation of the EntryOffloadableOperation should be retried if (t instanceof EntryOffloadableLockMismatchException) { t = new RetryableHazelcastException(t.getMessage(), t); } getOperationResponseHandler().sendResponse(EntryOperation.this, t); } finally { ops.onCompletionAsyncOperation(EntryOperation.this); } } else { try { getOperationResponseHandler().sendResponse(EntryOperation.this, result); } finally { ops.onCompletionAsyncOperation(EntryOperation.this); } } } }; updateOperation.setOperationResponseHandler(setUnlockResponseHandler); ops.execute(updateOperation); } private boolean isRetryable(Object response) { return response instanceof RetryableHazelcastException && !(response instanceof WrongTargetException); } private boolean isTimeout(Object response) { return response instanceof CallTimeoutResponse; } private boolean isFastRetryLimitReached() { return setUnlockRetryCount > SET_UNLOCK_FAST_RETRY_LIMIT; } private void unlockOnly(final Object result, String caller, long threadId, long now) { updateAndUnlock(null, null, null, caller, threadId, result, now); } private void runVanilla() { response = operator(this, entryProcessor) .operateOnKey(dataKey) .doPostOperateOps() .getResult(); } @Override public WaitNotifyKey getWaitKey() { return new LockWaitNotifyKey(getServiceNamespace(), dataKey); } @Override public boolean shouldWait() { // optimisation for ReadOnly processors -> they will not wait for the lock if (entryProcessor instanceof ReadOnly) { offloading = isOffloadingRequested(entryProcessor); return false; } // mutating offloading -> only if key not locked, since it uses locking too (but on reentrant one) if (!recordStore.isLocked(dataKey) && isOffloadingRequested(entryProcessor)) { offloading = true; return false; } //at this point we cannot offload. the entry is locked or the EP does not support offloading //if the entry is locked by us then we can still run the EP on the partition thread offloading = false; return !recordStore.canAcquireLock(dataKey, getCallerUuid(), getThreadId()); } private boolean isOffloadingRequested(EntryProcessor entryProcessor) { if (entryProcessor instanceof Offloadable) { String executorName = ((Offloadable) entryProcessor).getExecutorName(); if (!executorName.equals(NO_OFFLOADING)) { return true; } } return false; } @Override public void onWaitExpire() { sendResponse(null); } @Override public Object getResponse() { return response; } @Override public Operation getBackupOperation() { EntryBackupProcessor backupProcessor = entryProcessor.getBackupProcessor(); return backupProcessor != null ? new EntryBackupOperation(name, dataKey, backupProcessor) : null; } @Override public boolean shouldBackup() { return mapContainer.getTotalBackupCount() > 0 && entryProcessor.getBackupProcessor() != null; } @Override public int getAsyncBackupCount() { return mapContainer.getAsyncBackupCount(); } @Override public int getSyncBackupCount() { return mapContainer.getBackupCount(); } @Override protected void readInternal(ObjectDataInput in) throws IOException { super.readInternal(in); entryProcessor = in.readObject(); } @Override protected void writeInternal(ObjectDataOutput out) throws IOException { super.writeInternal(out); out.writeObject(entryProcessor); } @Override public int getId() { return MapDataSerializerHook.ENTRY_OPERATION; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy