package org.infinispan.interceptors.impl;
import static org.infinispan.persistence.PersistenceUtil.internalMetadata;
import static org.infinispan.persistence.manager.PersistenceManager.AccessMode.BOTH;
import static org.infinispan.persistence.manager.PersistenceManager.AccessMode.PRIVATE;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import javax.transaction.InvalidTransactionException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import org.infinispan.atomic.impl.AtomicHashMap;
import org.infinispan.commands.AbstractVisitor;
import org.infinispan.commands.FlagAffectedCommand;
import org.infinispan.commands.VisitableCommand;
import org.infinispan.commands.functional.FunctionalCommand;
import org.infinispan.commands.functional.ReadWriteKeyCommand;
import org.infinispan.commands.functional.ReadWriteKeyValueCommand;
import org.infinispan.commands.functional.ReadWriteManyCommand;
import org.infinispan.commands.functional.ReadWriteManyEntriesCommand;
import org.infinispan.commands.functional.WriteOnlyKeyCommand;
import org.infinispan.commands.functional.WriteOnlyKeyValueCommand;
import org.infinispan.commands.functional.WriteOnlyManyCommand;
import org.infinispan.commands.functional.WriteOnlyManyEntriesCommand;
import org.infinispan.commands.tx.CommitCommand;
import org.infinispan.commands.tx.PrepareCommand;
import org.infinispan.commands.write.ApplyDeltaCommand;
import org.infinispan.commands.write.ClearCommand;
import org.infinispan.commands.write.DataWriteCommand;
import org.infinispan.commands.write.PutKeyValueCommand;
import org.infinispan.commands.write.PutMapCommand;
import org.infinispan.commands.write.RemoveCommand;
import org.infinispan.commands.write.ReplaceCommand;
import org.infinispan.commands.write.WriteCommand;
import org.infinispan.commons.api.functional.Param;
import org.infinispan.commons.api.functional.Param.PersistenceMode;
import org.infinispan.commons.marshall.StreamingMarshaller;
import org.infinispan.configuration.cache.PersistenceConfiguration;
import org.infinispan.container.InternalEntryFactory;
import org.infinispan.container.entries.CacheEntry;
import org.infinispan.container.entries.DeltaAwareCacheEntry;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.container.entries.InternalCacheValue;
import org.infinispan.container.versioning.EntryVersion;
import org.infinispan.container.versioning.EntryVersionsMap;
import org.infinispan.context.InvocationContext;
import org.infinispan.context.impl.FlagBitSets;
import org.infinispan.context.impl.TxInvocationContext;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.factories.annotations.Start;
import org.infinispan.jmx.annotations.MBean;
import org.infinispan.jmx.annotations.ManagedAttribute;
import org.infinispan.jmx.annotations.ManagedOperation;
import org.infinispan.jmx.annotations.MeasurementType;
import org.infinispan.marshall.core.MarshalledEntryImpl;
import org.infinispan.metadata.EmbeddedMetadata;
import org.infinispan.metadata.Metadata;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.transaction.xa.GlobalTransaction;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
/**
* Writes modifications back to the store on the way out: stores modifications back through the CacheLoader, either
* after each method call (no TXs), or at TX commit.
*
* Only used for LOCAL and INVALIDATION caches.
*
* @author Bela Ban
* @author Dan Berindei
* @author Mircea Markus
* @since 9.0
*/
@MBean(objectName = "CacheStore", description = "Component that handles storing of entries to a CacheStore from memory.")
public class CacheWriterInterceptor extends JmxStatsCommandInterceptor {
private final boolean trace = getLog().isTraceEnabled();
PersistenceConfiguration loaderConfig = null;
final AtomicLong cacheStores = new AtomicLong(0);
protected PersistenceManager persistenceManager;
private InternalEntryFactory entryFactory;
private TransactionManager transactionManager;
private StreamingMarshaller marshaller;
private static final Log log = LogFactory.getLog(CacheWriterInterceptor.class);
protected Log getLog() {
return log;
}
@Inject
protected void init(PersistenceManager pm, InternalEntryFactory entryFactory, TransactionManager transactionManager,
StreamingMarshaller marshaller) {
this.persistenceManager = pm;
this.entryFactory = entryFactory;
this.transactionManager = transactionManager;
this.marshaller = marshaller;
}
@Start(priority = 15)
protected void start() {
this.setStatisticsEnabled(cacheConfiguration.jmxStatistics().enabled());
loaderConfig = cacheConfiguration.persistence();
}
@Override
public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable {
commitCommand(ctx);
return invokeNext(ctx, command);
}
@Override
public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable {
if (command.isOnePhaseCommit()) {
commitCommand(ctx);
}
return invokeNext(ctx, command);
}
protected void commitCommand(TxInvocationContext ctx) throws Throwable {
if (!ctx.getCacheTransaction().getAllModifications().isEmpty()) {
// this is a commit call.
GlobalTransaction tx = ctx.getGlobalTransaction();
if (trace) getLog().tracef("Calling loader.commit() for transaction %s", tx);
Transaction xaTx = null;
try {
xaTx = suspendRunningTx(ctx);
store(ctx);
} finally {
resumeRunningTx(xaTx);
}
} else {
if (trace) getLog().trace("Commit called with no modifications; ignoring.");
}
}
private void resumeRunningTx(Transaction xaTx) throws InvalidTransactionException, SystemException {
if (transactionManager != null && xaTx != null) {
transactionManager.resume(xaTx);
}
}
private Transaction suspendRunningTx(TxInvocationContext ctx) throws SystemException {
Transaction xaTx = null;
if (transactionManager != null) {
xaTx = transactionManager.suspend();
if (xaTx != null && !ctx.isOriginLocal())
throw new IllegalStateException("It is only possible to be in the context of an JRA transaction in the local node.");
}
return xaTx;
}
@Override
public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable {
return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> {
RemoveCommand removeCommand = (RemoveCommand) rCommand;
if (!isStoreEnabled(removeCommand) || rCtx.isInTxScope() || !removeCommand.isSuccessful()) return;
if (!isProperWriter(rCtx, removeCommand, removeCommand.getKey())) return;
Object key = removeCommand.getKey();
boolean resp = persistenceManager.deleteFromAllStores(key, BOTH);
if (trace)
getLog().tracef("Removed entry under key %s and got response %s from CacheStore", key, resp);
});
}
@Override
public Object visitClearCommand(InvocationContext ctx, ClearCommand command) throws Throwable {
if (isStoreEnabled(command) && !ctx.isInTxScope())
persistenceManager.clearAllStores(ctx.isOriginLocal() ? BOTH : PRIVATE);
return invokeNext(ctx, command);
}
@Override
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> {
PutKeyValueCommand putKeyValueCommand = (PutKeyValueCommand) rCommand;
if (!isStoreEnabled(putKeyValueCommand) || rCtx.isInTxScope() || !putKeyValueCommand.isSuccessful())
return;
if (!isProperWriter(rCtx, putKeyValueCommand, putKeyValueCommand.getKey()))
return;
Object key = putKeyValueCommand.getKey();
storeEntry(rCtx, key, putKeyValueCommand);
if (getStatisticsEnabled())
cacheStores.incrementAndGet();
});
}
@Override
public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable {
return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> {
ReplaceCommand replaceCommand = (ReplaceCommand) rCommand;
if (!isStoreEnabled(replaceCommand) || rCtx.isInTxScope() || !replaceCommand.isSuccessful())
return;
if (!isProperWriter(rCtx, replaceCommand, replaceCommand.getKey()))
return;
Object key = replaceCommand.getKey();
storeEntry(rCtx, key, replaceCommand);
if (getStatisticsEnabled())
cacheStores.incrementAndGet();
});
}
@Override
public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable {
return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> {
PutMapCommand putMapCommand = (PutMapCommand) rCommand;
if (!isStoreEnabled(putMapCommand) || rCtx.isInTxScope())
return;
Map<Object, Object> map = putMapCommand.getMap();
for (Object key : map.keySet()) {
if (isProperWriter(rCtx, putMapCommand, key)) {
storeEntry(rCtx, key, putMapCommand);
}
}
if (getStatisticsEnabled())
cacheStores.getAndAdd(map.size());
});
}
@Override
public Object visitReadWriteKeyCommand(InvocationContext ctx, ReadWriteKeyCommand command)
throws Throwable {
return visitWriteCommand(ctx, command);
}
@Override
public Object visitReadWriteKeyValueCommand(InvocationContext ctx, ReadWriteKeyValueCommand command)
throws Throwable {
return visitWriteCommand(ctx, command);
}
@Override
public Object visitWriteOnlyKeyCommand(InvocationContext ctx, WriteOnlyKeyCommand command)
throws Throwable {
return visitWriteCommand(ctx, command);
}
@Override
public Object visitWriteOnlyKeyValueCommand(InvocationContext ctx, WriteOnlyKeyValueCommand command)
throws Throwable {
return visitWriteCommand(ctx, command);
}
private <T extends DataWriteCommand & FunctionalCommand> Object visitWriteCommand(InvocationContext ctx,
VisitableCommand command) throws Throwable {
return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> {
T dataWriteCommand = (T) rCommand;
if (!isStoreEnabled(dataWriteCommand) || rCtx.isInTxScope() || !dataWriteCommand.isSuccessful())
return;
if (!isProperWriter(rCtx, dataWriteCommand, dataWriteCommand.getKey()))
return;
Param<PersistenceMode> persistMode = dataWriteCommand.getParams().get(PersistenceMode.ID);
switch (persistMode.get()) {
case PERSIST:
Object key = dataWriteCommand.getKey();
CacheEntry entry = rCtx.lookupEntry(key);
if (entry != null) {
if (entry.isRemoved()) {
boolean resp = persistenceManager.deleteFromAllStores(key, BOTH);
if (trace)
getLog().tracef("Removed entry under key %s and got response %s from CacheStore", key,
resp);
} else if (entry.isChanged()) {
storeEntry(rCtx, key, dataWriteCommand);
}
}
log.trace("Skipping cache store since entry was not found in context");
break;
case SKIP:
log.trace("Skipping cache store since persistence mode parameter is SKIP");
}
});
}
@Override
public Object visitWriteOnlyManyCommand(InvocationContext ctx, WriteOnlyManyCommand command)
throws Throwable {
return visitWriteManyCommand(ctx, command);
}
@Override
public Object visitWriteOnlyManyEntriesCommand(InvocationContext ctx, WriteOnlyManyEntriesCommand command)
throws Throwable {
return visitWriteManyCommand(ctx, command);
}
@Override
public Object visitReadWriteManyCommand(InvocationContext ctx, ReadWriteManyCommand command)
throws Throwable {
return visitWriteManyCommand(ctx, command);
}
@Override
public Object visitReadWriteManyEntriesCommand(InvocationContext ctx, ReadWriteManyEntriesCommand command)
throws Throwable {
return visitWriteManyCommand(ctx, command);
}
private <T extends WriteCommand & FunctionalCommand> Object visitWriteManyCommand(InvocationContext ctx,
WriteCommand command) throws Throwable {
return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> {
T manyEntriesCommand = (T) rCommand;
if (!isStoreEnabled(manyEntriesCommand) || rCtx.isInTxScope())
return;
Param<PersistenceMode> persistMode = manyEntriesCommand.getParams().get(PersistenceMode.ID);
switch (persistMode.get()) {
case PERSIST:
int storedCount = 0;
for (Object key : ((WriteCommand) rCommand).getAffectedKeys()) {
CacheEntry entry = rCtx.lookupEntry(key);
if (entry != null) {
if (entry.isRemoved()) {
boolean resp = persistenceManager.deleteFromAllStores(key, BOTH);
if (trace) getLog().tracef("Removed entry under key %s and got response %s from CacheStore", key, resp);
} else {
if (entry.isChanged() && isProperWriter(rCtx, manyEntriesCommand, key)) {
storeEntry(rCtx, key, manyEntriesCommand);
storedCount++;
}
}
}
}
if (getStatisticsEnabled())
cacheStores.getAndAdd(storedCount);
break;
case SKIP:
log.trace("Skipping cache store since persistence mode parameter is SKIP");
}
});
}
protected final void store(TxInvocationContext ctx) throws Throwable {
List<WriteCommand> modifications = ctx.getCacheTransaction().getAllModifications();
if (modifications.isEmpty()) {
if (trace) getLog().trace("Transaction has not logged any modifications!");
return;
}
if (trace) getLog().tracef("Cache loader modification list: %s", modifications);
Updater modsBuilder = new Updater(getStatisticsEnabled());
for (WriteCommand cacheCommand : modifications) {
if (isStoreEnabled(cacheCommand)) {
cacheCommand.acceptVisitor(ctx, modsBuilder);
}
}
if (getStatisticsEnabled() && modsBuilder.putCount > 0) {
cacheStores.getAndAdd(modsBuilder.putCount);
}
}
protected boolean isStoreEnabled(FlagAffectedCommand command) {
if (command.hasAnyFlag(FlagBitSets.SKIP_CACHE_STORE)) {
log.trace("Skipping cache store since the call contain a skip cache store flag");
return false;
}
return true;
}
protected boolean isProperWriter(InvocationContext ctx, FlagAffectedCommand command, Object key) {
return true;
}
public class Updater extends AbstractVisitor {
protected final boolean generateStatistics;
int putCount;
public Updater(boolean generateStatistics) {
this.generateStatistics = generateStatistics;
}
@Override
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
return visitSingleStore(ctx, command, command.getKey());
}
@Override
public Object visitApplyDeltaCommand(InvocationContext ctx, ApplyDeltaCommand command) throws Throwable {
if (isProperWriter(ctx, command, command.getKey())) {
if (generateStatistics) putCount++;
CacheEntry entry = ctx.lookupEntry(command.getKey());
// If the value is null, there is a subsequent remove operation in the transaction and we can ignore
// this modification.
if (entry.getValue() == null) {
return null;
}
InternalCacheEntry ice;
if (entry instanceof InternalCacheEntry) {
ice = (InternalCacheEntry) entry;
} else if (entry instanceof DeltaAwareCacheEntry) {
AtomicHashMap<?,?> uncommittedChanges = ((DeltaAwareCacheEntry) entry).getUncommittedChages();
ice = entryFactory.create(entry.getKey(), uncommittedChanges, entry.getMetadata(), entry.getLifespan(), entry.getMaxIdle());
} else {
ice = entryFactory.create(entry);
}
MarshalledEntryImpl marshalledEntry = new MarshalledEntryImpl(ice.getKey(), ice.getValue(), internalMetadata(ice), marshaller);
persistenceManager.writeToAllNonTxStores(marshalledEntry, skipSharedStores(ctx, command.getKey(), command) ? PRIVATE : BOTH);
}
return null;
}
@Override
public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable {
return visitSingleStore(ctx, command, command.getKey());
}
@Override
public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable {
Map<Object, Object> map = command.getMap();
for (Object key : map.keySet())
visitSingleStore(ctx, command, key);
return null;
}
@Override
public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable {
Object key = command.getKey();
if (isProperWriter(ctx, command, key)) {
persistenceManager.deleteFromAllStores(key, skipSharedStores(ctx, key, command) ? PRIVATE : BOTH);
}
return null;
}
@Override
public Object visitClearCommand(InvocationContext ctx, ClearCommand command) throws Throwable {
persistenceManager.clearAllStores(ctx.isOriginLocal() ? PRIVATE : BOTH);
return null;
}
protected Object visitSingleStore(InvocationContext ctx, FlagAffectedCommand command, Object key) throws Throwable {
if (isProperWriter(ctx, command, key)) {
if (generateStatistics) putCount++;
InternalCacheValue sv = entryFactory.getValueFromCtxOrCreateNew(key, ctx);
// TODO: we should merge all modifications and write each key only once
// If the value in context is null, the transaction contains a subsequent remove,
// so we'll ignore this modification
if (sv.getValue() != null) {
MarshalledEntryImpl me = new MarshalledEntryImpl(key, sv.getValue(), internalMetadata(sv), marshaller);
persistenceManager.writeToAllNonTxStores(me, skipSharedStores(ctx, key, command) ? PRIVATE : BOTH);
}
}
return null;
}
}
@Override
@ManagedOperation(
description = "Resets statistics gathered by this component",
displayName = "Reset statistics"
)
public void resetStatistics() {
cacheStores.set(0);
}
@ManagedAttribute(
description = "Number of writes to the store",
displayName = "Number of writes to the store",
measurementType = MeasurementType.TRENDSUP
)
public long getWritesToTheStores() {
return cacheStores.get();
}
void storeEntry(InvocationContext ctx, Object key, FlagAffectedCommand command) {
InternalCacheValue sv = getStoredValue(key, ctx);
persistenceManager.writeToAllNonTxStores(new MarshalledEntryImpl(key, sv.getValue(), internalMetadata(sv), marshaller),
skipSharedStores(ctx, key, command) ? PRIVATE : BOTH, command.getFlagsBitSet());
if (trace) getLog().tracef("Stored entry %s under key %s", sv, key);
}
protected boolean skipSharedStores(InvocationContext ctx, Object key, FlagAffectedCommand command) {
return !ctx.isOriginLocal() ||
command.hasAnyFlag(FlagBitSets.SKIP_SHARED_CACHE_STORE);
}
InternalCacheValue getStoredValue(Object key, InvocationContext ctx) {
CacheEntry entry = ctx.lookupEntry(key);
if (entry instanceof InternalCacheEntry) {
return ((InternalCacheEntry) entry).toInternalCacheValue();
} else {
if (ctx.isInTxScope()) {
EntryVersionsMap updatedVersions =
((TxInvocationContext) ctx).getCacheTransaction().getUpdatedEntryVersions();
if (updatedVersions != null) {
EntryVersion version = updatedVersions.get(entry.getKey());
if (version != null) {
Metadata metadata = entry.getMetadata();
if (metadata == null) {
// If no metadata passed, assumed embedded metadata
metadata = new EmbeddedMetadata.Builder().lifespan(entry.getLifespan()).maxIdle(entry.getMaxIdle())
.version(version).build();
return entryFactory.create(entry.getKey(), entry.getValue(), metadata).toInternalCacheValue();
} else {
metadata = metadata.builder().version(version).build();
return entryFactory.create(entry.getKey(), entry.getValue(), metadata).toInternalCacheValue();
}
}
}
}
return entryFactory.create(entry).toInternalCacheValue();
}
}
}