package org.infinispan.interceptors.distribution;
import static org.infinispan.transaction.impl.WriteSkewHelper.readVersionsFromResponse;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.infinispan.commands.tx.PrepareCommand;
import org.infinispan.container.entries.CacheEntry;
import org.infinispan.container.versioning.EntryVersion;
import org.infinispan.container.versioning.InequalVersionComparisonResult;
import org.infinispan.container.versioning.VersionGenerator;
import org.infinispan.context.InvocationContext;
import org.infinispan.context.impl.TxInvocationContext;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.metadata.Metadata;
import org.infinispan.remoting.responses.Response;
import org.infinispan.remoting.transport.Address;
import org.infinispan.transaction.impl.AbstractCacheTransaction;
import org.infinispan.transaction.impl.LocalTransaction;
import org.infinispan.transaction.xa.CacheTransaction;
import org.infinispan.util.concurrent.CompletableFutures;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
/**
* A version of the {@link TxDistributionInterceptor} that adds logic to handling prepares when entries are
* versioned.
*
* @author Manik Surtani
* @author Dan Berindei
*/
public class VersionedDistributionInterceptor extends TxDistributionInterceptor {
private static final Log log = LogFactory.getLog(VersionedDistributionInterceptor.class);
private VersionGenerator versionGenerator;
@Inject
public void init(VersionGenerator versionGenerator) {
this.versionGenerator = versionGenerator;
}
@Override
protected Log getLog() {
return log;
}
@Override
protected void wrapRemoteEntry(InvocationContext ctx, Object key, CacheEntry ice, boolean isWrite) {
if (ctx.isInTxScope()) {
AbstractCacheTransaction cacheTransaction = ((TxInvocationContext) ctx).getCacheTransaction();
EntryVersion seenVersion = cacheTransaction.getVersionsRead().get(key);
if (seenVersion != null) {
EntryVersion newVersion = null;
if (ice != null && ice.getMetadata() != null) {
newVersion = ice.getMetadata().version();
}
if (newVersion == null) {
throw new IllegalStateException("Wrapping entry without version");
}
if (seenVersion.compareTo(newVersion) != InequalVersionComparisonResult.EQUAL) {
if (ctx.lookupEntry(key) == null) {
// We have read the entry using functional command on remote node and now we want
// the full entry, but we cannot provide the same version as the already read one.
throw log.writeSkewOnRead(key, key, seenVersion, newVersion);
} else {
// We have retrieved remote entry despite being already wrapped: that can happen
// for GetKeysInGroupCommand which does not know what entries will it fetch.
// We can safely ignore the newly fetched value.
return;
}
}
}
}
super.wrapRemoteEntry(ctx, key, ice, isWrite);
}
@Override
protected Object wrapFunctionalResultOnNonOriginOnReturn(Object rv, CacheEntry entry) {
Metadata metadata = entry.getMetadata();
EntryVersion version = metadata == null || metadata.version() == null ?
versionGenerator.nonExistingVersion() : metadata.version();
return new VersionedResult(rv, version);
}
@Override
protected Object wrapFunctionalManyResultOnNonOrigin(InvocationContext ctx, Collection<?> keys, Object[] values) {
// note: this relies on the fact that keys are already ordered on remote node
EntryVersion[] versions = new EntryVersion[keys.size()];
int i = 0;
for (Object key : keys) {
CacheEntry entry = ctx.lookupEntry(key);
Metadata metadata = entry.getMetadata();
versions[i++] = metadata == null || metadata.version() == null ?
versionGenerator.nonExistingVersion() : metadata.version();
}
return new VersionedResults(values, versions);
}
@Override
protected Object[] unwrapFunctionalManyResultOnOrigin(InvocationContext ctx, List<Object> keys, Object responseValue) {
if (responseValue instanceof VersionedResults) {
VersionedResults vrs = (VersionedResults) responseValue;
if (ctx.isInTxScope()) {
AbstractCacheTransaction tx = ((TxInvocationContext) ctx).getCacheTransaction();
for (int i = 0; i < vrs.versions.length; ++i) {
checkAndAddReadVersion(tx, keys.get(i), vrs.versions[i]);
}
}
return vrs.values;
} else {
return null;
}
}
@Override
protected Object unwrapFunctionalResultOnOrigin(InvocationContext ctx, Object key, Object responseValue) {
VersionedResult vr = (VersionedResult) responseValue;
// As an optimization, read-only single-key commands are executed in SingleKeyNonTxInvocationContext
if (ctx.isInTxScope()) {
AbstractCacheTransaction tx = ((TxInvocationContext) ctx).getCacheTransaction();
checkAndAddReadVersion(tx, key, vr.version);
}
return vr.result;
}
private void checkAndAddReadVersion(AbstractCacheTransaction tx, Object key, EntryVersion version) {
EntryVersion lastVersion = tx.getLookedUpRemoteVersion(key);
// TODO: should we check the write skew configuration here?
// TODO: version seen or looked up remote version?
if (lastVersion != null && lastVersion.compareTo(version) != InequalVersionComparisonResult.EQUAL) {
throw log.writeSkewOnRead(key, key, lastVersion, version);
}
EntryVersion lastVersionSeen = tx.getVersionsRead().get(key);
if (lastVersionSeen != null && lastVersionSeen.compareTo(version) != InequalVersionComparisonResult.EQUAL) {
throw log.writeSkewOnRead(key, key, lastVersionSeen, version);
}
tx.addVersionRead(key, version);
}
@Override
protected CompletableFuture<Object> prepareOnAffectedNodes(TxInvocationContext<?> ctx, PrepareCommand command, Collection<Address> recipients) {
// Perform the RPC
CompletableFuture<Map<Address, Response>>
remoteInvocation = rpcManager.invokeRemotelyAsync(recipients, command, createRpcOptions());
return remoteInvocation.handle((responses, t) -> {
transactionRemotelyPrepared(ctx);
CompletableFutures.rethrowException(t);
checkTxCommandResponses(responses, command, (TxInvocationContext<LocalTransaction>) ctx, recipients);
// Now store newly generated versions from lock owners for use during the commit phase.
CacheTransaction ct = ctx.getCacheTransaction();
for (Response r : responses.values()) readVersionsFromResponse(r, ct);
return null;
});
}
}