/* * Hibernate, Relational Persistence for Idiomatic Java * * License: GNU Lesser General Public License (LGPL), version 2.1 or later. * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.cache.infinispan.access; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import org.hibernate.cache.infinispan.util.CacheCommandInitializer; import org.hibernate.cache.infinispan.util.EndInvalidationCommand; import org.hibernate.cache.infinispan.util.InfinispanMessageLogger; import org.infinispan.commands.VisitableCommand; import org.infinispan.commands.tx.CommitCommand; import org.infinispan.commands.tx.PrepareCommand; import org.infinispan.commands.tx.RollbackCommand; import org.infinispan.commands.write.PutKeyValueCommand; import org.infinispan.commands.write.RemoveCommand; import org.infinispan.commands.write.WriteCommand; import org.infinispan.container.DataContainer; import org.infinispan.context.Flag; import org.infinispan.context.InvocationContext; import org.infinispan.context.impl.TxInvocationContext; import org.infinispan.factories.annotations.Inject; import org.infinispan.factories.annotations.Start; import org.infinispan.interceptors.base.BaseRpcInterceptor; import org.infinispan.remoting.inboundhandler.DeliverOrder; import org.infinispan.remoting.rpc.ResponseMode; import org.infinispan.remoting.rpc.RpcManager; import org.infinispan.remoting.rpc.RpcOptions; import org.infinispan.remoting.transport.Address; import org.infinispan.statetransfer.StateTransferManager; import org.infinispan.transaction.xa.GlobalTransaction; /** * Intercepts transactions in Infinispan, calling {@link PutFromLoadValidator#beginInvalidatingKey(Object, Object)} * before locks are acquired (and the entry is invalidated) and sends {@link EndInvalidationCommand} to release * invalidation throught {@link PutFromLoadValidator#endInvalidatingKey(Object, Object)} after the transaction * is committed. * * @author Radim Vansa <rvansa@redhat.com> */ class TxPutFromLoadInterceptor extends BaseRpcInterceptor { private final static InfinispanMessageLogger log = InfinispanMessageLogger.Provider.getLog(TxPutFromLoadInterceptor.class); private PutFromLoadValidator putFromLoadValidator; private final String cacheName; private RpcManager rpcManager; private CacheCommandInitializer cacheCommandInitializer; private DataContainer dataContainer; private StateTransferManager stateTransferManager; private RpcOptions asyncUnordered; public TxPutFromLoadInterceptor(PutFromLoadValidator putFromLoadValidator, String cacheName) { this.putFromLoadValidator = putFromLoadValidator; this.cacheName = cacheName; } @Inject public void injectDependencies(RpcManager rpcManager, CacheCommandInitializer cacheCommandInitializer, DataContainer dataContainer, StateTransferManager stateTransferManager) { this.rpcManager = rpcManager; this.cacheCommandInitializer = cacheCommandInitializer; this.dataContainer = dataContainer; this.stateTransferManager = stateTransferManager; } @Start public void start() { asyncUnordered = rpcManager.getRpcOptionsBuilder(ResponseMode.ASYNCHRONOUS, DeliverOrder.NONE).build(); } private void beginInvalidating(InvocationContext ctx, Object key) { TxInvocationContext txCtx = (TxInvocationContext) ctx; // make sure that the command is registered in the transaction txCtx.addAffectedKey(key); GlobalTransaction globalTransaction = txCtx.getGlobalTransaction(); if (!putFromLoadValidator.beginInvalidatingKey(globalTransaction, key)) { log.failedInvalidatePendingPut(key, cacheName); } } @Override public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable { if (!command.hasFlag(Flag.PUT_FOR_EXTERNAL_READ)) { beginInvalidating(ctx, command.getKey()); } return invokeNextInterceptor(ctx, command); } @Override public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable { beginInvalidating(ctx, command.getKey()); return invokeNextInterceptor(ctx, command); } // We need to intercept PrepareCommand, not InvalidateCommand since the interception takes // place before EntryWrappingInterceptor and the PrepareCommand is multiplexed into InvalidateCommands // as part of EntryWrappingInterceptor @Override public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable { if (ctx.isOriginLocal()) { // We can't wait to commit phase to remove the entry locally (invalidations are processed in 1pc // on remote nodes, so only local case matters here). The problem is that while the entry is locked // reads still can take place and we can read outdated collection after reading updated entity // owning this collection from DB; when this happens, the version lock on entity cannot protect // us against concurrent modification of the collection. Therefore, we need to remove the entry // here (even without lock!) and let possible update happen in commit phase. for (WriteCommand wc : command.getModifications()) { for (Object key : wc.getAffectedKeys()) { dataContainer.remove(key); } } } else { for (WriteCommand wc : command.getModifications()) { Set<Object> keys = wc.getAffectedKeys(); if (log.isTraceEnabled()) { log.tracef("Invalidating keys %s with lock owner %s", keys, ctx.getLockOwner()); } for (Object key : keys ) { putFromLoadValidator.beginInvalidatingKey(ctx.getLockOwner(), key); } } } return invokeNextInterceptor(ctx, command); } @Override public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable { if (log.isTraceEnabled()) { log.tracef( "Commit command received, end invalidation" ); } return endInvalidationAndInvokeNextInterceptor(ctx, command); } @Override public Object visitRollbackCommand(TxInvocationContext ctx, RollbackCommand command) throws Throwable { if (log.isTraceEnabled()) { log.tracef( "Rollback command received, end invalidation" ); } return endInvalidationAndInvokeNextInterceptor(ctx, command); } protected Object endInvalidationAndInvokeNextInterceptor(TxInvocationContext<?> ctx, VisitableCommand command) throws Throwable { try { if (ctx.isOriginLocal()) { // We cannot use directly ctx.getAffectedKeys() and that includes keys from local-only operations. // During evictAll inside transaction this would cause unnecessary invalidate command if (!ctx.getModifications().isEmpty()) { Object[] keys = ctx.getModifications().stream() .flatMap(mod -> mod.getAffectedKeys().stream()).distinct().toArray(); if (log.isTraceEnabled()) { log.tracef( "Sending end invalidation for keys %s asynchronously, modifications are %s", Arrays.toString(keys), ctx.getCacheTransaction().getModifications()); } GlobalTransaction globalTransaction = ctx.getGlobalTransaction(); EndInvalidationCommand commitCommand = cacheCommandInitializer.buildEndInvalidationCommand( cacheName, keys, globalTransaction); List<Address> members = stateTransferManager.getCacheTopology().getMembers(); rpcManager.invokeRemotely(members, commitCommand, asyncUnordered); // If the transaction is not successful, *RegionAccessStrategy would not be called, therefore // we have to end invalidation from here manually (in successful case as well) for (Object key : keys) { putFromLoadValidator.endInvalidatingKey(globalTransaction, key); } } } } finally { return invokeNextInterceptor(ctx, command); } } }