@Start(priority = 9) // Start before cache loader manager @SuppressWarnings("unused") private void start() { final int concurrencyLevel = configuration.locking().concurrencyLevel(); localTransactions = ConcurrentMapFactory.makeConcurrentMap(concurrencyLevel, 0.75f, concurrencyLevel); globalToLocalTransactions = ConcurrentMapFactory.makeConcurrentMap(concurrencyLevel, 0.75f, concurrencyLevel); if (configuration.clustering().cacheMode().isClustered()) { minTopologyRecalculationLock = new ReentrantLock(); // Only initialize this if we are clustered. remoteTransactions = ConcurrentMapFactory.makeConcurrentMap(concurrencyLevel, 0.75f, concurrencyLevel); cleanupService.start(cacheName, rpcManager, configuration); notifier.addListener(cleanupService); notifier.addListener(this); clustered = true; } }
@Start private void start() { final int concurrencyLevel = configuration.locking().concurrencyLevel(); localTransactions = ConcurrentMapFactory.makeConcurrentMap(concurrencyLevel, 0.75f, concurrencyLevel); if (configuration.clustering().cacheMode().isClustered()) { minViewRecalculationLock = new ReentrantLock(); // Only initialize this if we are clustered. remoteTransactions = ConcurrentMapFactory.makeConcurrentMap(concurrencyLevel, 0.75f, concurrencyLevel); cleanupService.start(cacheName, rpcManager, invoker); cm.addListener(cleanupService); cm.addListener(this); notifier.addListener(cleanupService); minTxViewId = rpcManager.getTransport().getViewId(); currentViewId = minTxViewId; log.debugf("Min view id set to %s", minTxViewId); clustered = true; } }
public L1ManagerImpl() { requestors = ConcurrentMapFactory.makeConcurrentMap(); }
/** * An encapsulation of a JGroups transport. JGroups transports can be configured using a variety of * methods, usually by passing in one of the following properties: * * <ul> * <li><tt>configurationString</tt> - a JGroups configuration String * <li><tt>configurationXml</tt> - JGroups configuration XML as a String * <li><tt>configurationFile</tt> - String pointing to a JGroups XML configuration file * <li><tt>channelLookup</tt> - Fully qualified class name of a {@link * org.infinispan.remoting.transport.jgroups.JGroupsChannelLookup} instance * </ul> * * These are normally passed in as Properties in {@link * org.infinispan.config.GlobalConfiguration#setTransportProperties(java.util.Properties)} or in the * Infinispan XML configuration file. * * @author Manik Surtani * @author Galder Zamarreño * @since 4.0 */ public class JGroupsTransport extends AbstractTransport implements MembershipListener { public static final String CONFIGURATION_STRING = "configurationString"; public static final String CONFIGURATION_XML = "configurationXml"; public static final String CONFIGURATION_FILE = "configurationFile"; public static final String CHANNEL_LOOKUP = "channelLookup"; protected static final String DEFAULT_JGROUPS_CONFIGURATION_FILE = "jgroups-udp.xml"; static final Log log = LogFactory.getLog(JGroupsTransport.class); static final boolean trace = log.isTraceEnabled(); final ConcurrentMap<String, StateTransferMonitor> stateTransfersInProgress = ConcurrentMapFactory.makeConcurrentMap(); protected boolean startChannel = true, stopChannel = true; private CommandAwareRpcDispatcher dispatcher; protected TypedProperties props; protected InboundInvocationHandler inboundInvocationHandler; protected StreamingMarshaller marshaller; protected ExecutorService asyncExecutor; protected CacheManagerNotifier notifier; private boolean globalStatsEnabled; private MBeanServer mbeanServer; private String domain; protected Channel channel; protected Address address; protected Address physicalAddress; // these members are not valid until we have received the first view on a second thread // and channelConnectedLatch is signaled protected volatile List<Address> members = null; protected volatile Address coordinator = null; protected volatile boolean isCoordinator = false; protected CountDownLatch channelConnectedLatch = new CountDownLatch(1); /** * This form is used when the transport is created by an external source and passed in to the * GlobalConfiguration. * * @param channel created and running channel to use */ public JGroupsTransport(Channel channel) { this.channel = channel; if (channel == null) throw new IllegalArgumentException("Cannot deal with a null channel!"); if (channel.isConnected()) throw new IllegalArgumentException("Channel passed in cannot already be connected!"); } public JGroupsTransport() {} @Override public Log getLog() { return log; } // ------------------------------------------------------------------------------------------------------------------ // Lifecycle and setup stuff // ------------------------------------------------------------------------------------------------------------------ @Override public void initialize( @ComponentName(GLOBAL_MARSHALLER) StreamingMarshaller marshaller, ExecutorService asyncExecutor, InboundInvocationHandler inboundInvocationHandler, CacheManagerNotifier notifier) { this.marshaller = marshaller; this.asyncExecutor = asyncExecutor; this.inboundInvocationHandler = inboundInvocationHandler; this.notifier = notifier; } @Override public void start() { props = TypedProperties.toTypedProperties(configuration.getTransportProperties()); if (log.isInfoEnabled()) log.startingJGroupsChannel(); initChannelAndRPCDispatcher(); startJGroupsChannelIfNeeded(); waitForChannelToConnect(); } protected void startJGroupsChannelIfNeeded() { if (startChannel) { String clusterName = configuration.getClusterName(); try { channel.connect(clusterName); } catch (Exception e) { throw new CacheException("Unable to start JGroups Channel", e); } try { // Normally this would be done by CacheManagerJmxRegistration but // the channel is not started when the cache manager starts but // when first cache starts, so it's safer to do it here. globalStatsEnabled = configuration.isExposeGlobalJmxStatistics(); if (globalStatsEnabled) { String groupName = String.format("type=channel,cluster=%s", ObjectName.quote(clusterName)); mbeanServer = JmxUtil.lookupMBeanServer(configuration); domain = JmxUtil.buildJmxDomain(configuration, mbeanServer, groupName); JmxConfigurator.registerChannel( (JChannel) channel, mbeanServer, domain, clusterName, true); } } catch (Exception e) { throw new CacheException("Channel connected, but unable to register MBeans", e); } } address = fromJGroupsAddress(channel.getAddress()); if (!startChannel) { // the channel was already started externally, we need to initialize our member list viewAccepted(channel.getView()); } if (log.isInfoEnabled()) log.localAndPhysicalAddress(getAddress(), getPhysicalAddresses()); } @Override public int getViewId() { if (channel == null) throw new CacheException("The cache has been stopped and invocations are not allowed!"); View view = channel.getView(); if (view == null) return -1; return (int) view.getVid().getId(); } @Override public void stop() { try { if (stopChannel && channel != null && channel.isOpen()) { log.disconnectAndCloseJGroups(); // Unregistering before disconnecting/closing because // after that the cluster name is null if (globalStatsEnabled) { JmxConfigurator.unregisterChannel( (JChannel) channel, mbeanServer, domain, channel.getClusterName()); } channel.disconnect(); channel.close(); } } catch (Exception toLog) { log.problemClosingChannel(toLog); } channel = null; if (dispatcher != null) { log.stoppingRpcDispatcher(); dispatcher.stop(); } members = Collections.emptyList(); coordinator = null; isCoordinator = false; dispatcher = null; } protected void initChannel() { if (channel == null) { buildChannel(); String transportNodeName = configuration.getTransportNodeName(); if (transportNodeName != null && transportNodeName.length() > 0) { long range = Short.MAX_VALUE * 2; long randomInRange = (long) ((Math.random() * range) % range) + 1; transportNodeName = transportNodeName + "-" + randomInRange; channel.setName(transportNodeName); } } // Channel.LOCAL *must* be set to false so we don't see our own messages - otherwise // invalidations targeted at remote instances will be received by self. channel.setDiscardOwnMessages(true); // if we have a TopologyAwareConsistentHash, we need to set our own address generator in // JGroups if (configuration.hasTopologyInfo()) { // We can do this only if the channel hasn't been started already if (startChannel) { ((JChannel) channel) .setAddressGenerator( new AddressGenerator() { @Override public org.jgroups.Address generateAddress() { return TopologyUUID.randomUUID( channel.getName(), configuration.getSiteId(), configuration.getRackId(), configuration.getMachineId()); } }); } else { if (channel.getAddress() instanceof TopologyUUID) { TopologyUUID topologyAddress = (TopologyUUID) channel.getAddress(); if (!configuration.getSiteId().equals(topologyAddress.getSiteId()) || !configuration.getRackId().equals(topologyAddress.getRackId()) || !configuration.getMachineId().equals(topologyAddress.getMachineId())) { throw new CacheException( "Topology information does not match the one set by the provided JGroups channel"); } } else { throw new CacheException("JGroups address does not contain topology coordinates"); } } } } private void initChannelAndRPCDispatcher() throws CacheException { initChannel(); dispatcher = new CommandAwareRpcDispatcher(channel, this, asyncExecutor, inboundInvocationHandler); MarshallerAdapter adapter = new MarshallerAdapter(marshaller); dispatcher.setRequestMarshaller(adapter); dispatcher.setResponseMarshaller(adapter); dispatcher.start(); } // This is per CM, so the CL in use should be the CM CL private void buildChannel() { // in order of preference - we first look for an external JGroups file, then a set of XML // properties, and // finally the legacy JGroups String properties. String cfg; if (props != null) { if (props.containsKey(CHANNEL_LOOKUP)) { String channelLookupClassName = props.getProperty(CHANNEL_LOOKUP); try { JGroupsChannelLookup lookup = (JGroupsChannelLookup) Util.getInstance(channelLookupClassName, configuration.getClassLoader()); channel = lookup.getJGroupsChannel(props); startChannel = lookup.shouldStartAndConnect(); stopChannel = lookup.shouldStopAndDisconnect(); } catch (ClassCastException e) { log.wrongTypeForJGroupsChannelLookup(channelLookupClassName, e); throw new CacheException(e); } catch (Exception e) { log.errorInstantiatingJGroupsChannelLookup(channelLookupClassName, e); throw new CacheException(e); } if (configuration.isStrictPeerToPeer() && !startChannel) { log.warnStrictPeerToPeerWithInjectedChannel(); } } if (channel == null && props.containsKey(CONFIGURATION_FILE)) { cfg = props.getProperty(CONFIGURATION_FILE); try { channel = new JChannel( FileLookupFactory.newInstance() .lookupFileLocation(cfg, configuration.getClassLoader())); } catch (Exception e) { log.errorCreatingChannelFromConfigFile(cfg); throw new CacheException(e); } } if (channel == null && props.containsKey(CONFIGURATION_XML)) { cfg = props.getProperty(CONFIGURATION_XML); try { channel = new JChannel(XmlConfigHelper.stringToElement(cfg)); } catch (Exception e) { log.errorCreatingChannelFromXML(cfg); throw new CacheException(e); } } if (channel == null && props.containsKey(CONFIGURATION_STRING)) { cfg = props.getProperty(CONFIGURATION_STRING); try { channel = new JChannel(cfg); } catch (Exception e) { log.errorCreatingChannelFromConfigString(cfg); throw new CacheException(e); } } } if (channel == null) { log.unableToUseJGroupsPropertiesProvided(props); try { channel = new JChannel( FileLookupFactory.newInstance() .lookupFileLocation( DEFAULT_JGROUPS_CONFIGURATION_FILE, configuration.getClassLoader())); } catch (Exception e) { throw new CacheException("Unable to start JGroups channel", e); } } } // ------------------------------------------------------------------------------------------------------------------ // querying cluster status // ------------------------------------------------------------------------------------------------------------------ @Override public boolean isCoordinator() { return isCoordinator; } @Override public Address getCoordinator() { return coordinator; } public void waitForChannelToConnect() { if (channel == null) return; log.debug("Waiting on view being accepted"); try { channelConnectedLatch.await(); } catch (InterruptedException e) { log.interruptedWaitingForCoordinator(e); } } @Override public List<Address> getMembers() { return members != null ? members : Collections.<Address>emptyList(); } @Override public boolean isMulticastCapable() { return channel.getProtocolStack().getTransport().supportsMulticasting(); } @Override public Address getAddress() { if (address == null && channel != null) { address = fromJGroupsAddress(channel.getAddress()); } return address; } @Override public List<Address> getPhysicalAddresses() { if (physicalAddress == null && channel != null) { org.jgroups.Address addr = (org.jgroups.Address) channel.down(new Event(Event.GET_PHYSICAL_ADDRESS, channel.getAddress())); if (addr == null) { return Collections.emptyList(); } physicalAddress = new JGroupsAddress(addr); } return Collections.singletonList(physicalAddress); } // ------------------------------------------------------------------------------------------------------------------ // outbound RPC // ------------------------------------------------------------------------------------------------------------------ @Override public Map<Address, Response> invokeRemotely( Collection<Address> recipients, ReplicableCommand rpcCommand, ResponseMode mode, long timeout, boolean usePriorityQueue, ResponseFilter responseFilter, boolean supportReplay) throws Exception { if (recipients != null && recipients.isEmpty()) { // don't send if dest list is empty log.trace("Destination list is empty: no need to send message"); return Collections.emptyMap(); } if (trace) log.tracef( "dests=%s, command=%s, mode=%s, timeout=%s", recipients, rpcCommand, mode, timeout); if (mode.isSynchronous() && recipients != null && !getMembers().containsAll(recipients)) { if (mode == ResponseMode.SYNCHRONOUS) throw new SuspectException( "One or more nodes have left the cluster while replicating command " + rpcCommand); else { // SYNCHRONOUS_IGNORE_LEAVERS || WAIT_FOR_VALID_RESPONSE recipients = new ArrayList<Address>(recipients); recipients.retainAll(getMembers()); } } boolean asyncMarshalling = mode == ResponseMode.ASYNCHRONOUS; if (!usePriorityQueue && (ResponseMode.SYNCHRONOUS == mode || ResponseMode.SYNCHRONOUS_IGNORE_LEAVERS == mode)) usePriorityQueue = true; boolean broadcast = recipients == null || recipients.size() == members.size(); RspList<Object> rsps = dispatcher.invokeRemoteCommands( toJGroupsAddressList(recipients), rpcCommand, toJGroupsMode(mode), timeout, recipients != null, usePriorityQueue, toJGroupsFilter(responseFilter), supportReplay, asyncMarshalling, broadcast); if (mode.isAsynchronous()) return Collections.emptyMap(); // async case // short-circuit no-return-value calls. if (rsps == null) return Collections.emptyMap(); Map<Address, Response> retval = new HashMap<Address, Response>(rsps.size()); boolean ignoreLeavers = mode == ResponseMode.SYNCHRONOUS_IGNORE_LEAVERS || mode == ResponseMode.WAIT_FOR_VALID_RESPONSE; boolean noValidResponses = true; for (Rsp<Object> rsp : rsps.values()) { noValidResponses &= parseResponseAndAddToResponseList( rsp.getValue(), rsp.getException(), retval, rsp.wasSuspected(), rsp.wasReceived(), fromJGroupsAddress(rsp.getSender()), responseFilter != null, ignoreLeavers); } if (noValidResponses) throw new TimeoutException("Timed out waiting for valid responses!"); return retval; } private static org.jgroups.blocks.ResponseMode toJGroupsMode(ResponseMode mode) { switch (mode) { case ASYNCHRONOUS: case ASYNCHRONOUS_WITH_SYNC_MARSHALLING: return org.jgroups.blocks.ResponseMode.GET_NONE; case SYNCHRONOUS: case SYNCHRONOUS_IGNORE_LEAVERS: return org.jgroups.blocks.ResponseMode.GET_ALL; case WAIT_FOR_VALID_RESPONSE: return org.jgroups.blocks.ResponseMode.GET_FIRST; } throw new CacheException("Unknown response mode " + mode); } private RspFilter toJGroupsFilter(ResponseFilter responseFilter) { return responseFilter == null ? null : new JGroupsResponseFilterAdapter(responseFilter); } // ------------------------------------------------------------------------------------------------------------------ // Implementations of JGroups interfaces // ------------------------------------------------------------------------------------------------------------------ private interface Notify { void emitNotification(List<Address> oldMembers, View newView); } private class NotifyViewChange implements Notify { @Override public void emitNotification(List<Address> oldMembers, View newView) { notifier.notifyViewChange(members, oldMembers, getAddress(), (int) newView.getVid().getId()); } } private class NotifyMerge implements Notify { @Override public void emitNotification(List<Address> oldMembers, View newView) { MergeView mv = (MergeView) newView; final Address address = getAddress(); final int viewId = (int) newView.getVid().getId(); notifier.notifyMerge(members, oldMembers, address, viewId, getSubgroups(mv.getSubgroups())); } private List<List<Address>> getSubgroups(List<View> subviews) { List<List<Address>> l = new ArrayList<List<Address>>(subviews.size()); for (View v : subviews) l.add(fromJGroupsAddressList(v.getMembers())); return l; } } @Override public void viewAccepted(View newView) { log.debugf("New view accepted: %s", newView); List<org.jgroups.Address> newMembers = newView.getMembers(); if (newMembers == null || newMembers.isEmpty()) { log.debugf("Received null or empty member list from JGroups channel: " + newView); return; } List<Address> oldMembers = members; // we need a defensive copy anyway members = fromJGroupsAddressList(newMembers); // Now that we have a view, figure out if we are the isCoordinator coordinator = fromJGroupsAddress(newView.getCreator()); isCoordinator = coordinator != null && coordinator.equals(getAddress()); // Wake up any threads that are waiting to know about who the isCoordinator is // do it before the notifications, so if a listener throws an exception we can still start channelConnectedLatch.countDown(); // now notify listeners - *after* updating the isCoordinator. - JBCACHE-662 boolean hasNotifier = notifier != null; if (hasNotifier) { Notify n; if (newView instanceof MergeView) { if (log.isInfoEnabled()) log.receivedMergedView(newView); n = new NotifyMerge(); } else { if (log.isInfoEnabled()) log.receivedClusterView(newView); n = new NotifyViewChange(); } n.emitNotification(oldMembers, newView); } } @Override public void suspect(org.jgroups.Address suspected_mbr) { // no-op } @Override public void block() { // no-op since ISPN-83 has been resolved } @Override public void unblock() { // no-op since ISPN-83 has been resolved } // ------------------------------------------------------------------------------------------------------------------ // Helpers to convert between Address types // ------------------------------------------------------------------------------------------------------------------ protected static org.jgroups.Address toJGroupsAddress(Address a) { return ((JGroupsAddress) a).address; } static Address fromJGroupsAddress(org.jgroups.Address addr) { if (addr instanceof TopologyUUID) return new JGroupsTopologyAwareAddress((TopologyUUID) addr); else return new JGroupsAddress(addr); } private static List<org.jgroups.Address> toJGroupsAddressList(Collection<Address> list) { if (list == null) return null; if (list.isEmpty()) return Collections.emptyList(); List<org.jgroups.Address> retval = new LinkedList<org.jgroups.Address>(); for (Address a : list) retval.add(toJGroupsAddress(a)); return retval; } private static List<Address> fromJGroupsAddressList(List<org.jgroups.Address> list) { if (list == null || list.isEmpty()) return Collections.emptyList(); List<Address> retval = new ArrayList<Address>(list.size()); for (org.jgroups.Address a : list) retval.add(fromJGroupsAddress(a)); return Collections.unmodifiableList(retval); } // mainly for unit testing public CommandAwareRpcDispatcher getCommandAwareRpcDispatcher() { return dispatcher; } public Channel getChannel() { return channel; } }
/** * This interceptor will be created when the System Property "infinispan.query.indexLocalOnly" is * "false" * * <p>This type of interceptor will allow the indexing of data even when it comes from other caches * within a cluster. * * <p>However, if the a cache would not be putting the data locally, the interceptor will not index * it. * * @author Navin Surtani * @author Sanne Grinovero <*****@*****.**> (C) 2011 Red Hat Inc. * @author Marko Luksa * @since 4.0 */ public class QueryInterceptor extends CommandInterceptor { private final SearchFactoryIntegrator searchFactory; private final ConcurrentMap<Class<?>, Boolean> knownClasses = ConcurrentMapFactory.makeConcurrentMap(); private final Lock mutating = new ReentrantLock(); private final KeyTransformationHandler keyTransformationHandler = new KeyTransformationHandler(); protected TransactionManager transactionManager; protected TransactionSynchronizationRegistry transactionSynchronizationRegistry; protected ExecutorService asyncExecutor; private static final Log log = LogFactory.getLog(QueryInterceptor.class, Log.class); @Override protected Log getLog() { return log; } public QueryInterceptor(SearchFactoryIntegrator searchFactory) { this.searchFactory = searchFactory; } @Inject public void injectDependencies( @ComponentName(KnownComponentNames.ASYNC_TRANSPORT_EXECUTOR) ExecutorService e) { this.asyncExecutor = e; } protected boolean shouldModifyIndexes(InvocationContext ctx) { return !ctx.hasFlag(Flag.SKIP_INDEXING); } /** * Use this executor for Async operations * * @return */ public ExecutorService getAsyncExecutor() { return asyncExecutor; } @Override public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable { // This method will get the put() calls on the cache and then send them into Lucene once it's // successful. // do the actual put first. Object toReturn = invokeNextInterceptor(ctx, command); if (shouldModifyIndexes(ctx)) { // First making a check to see if the key is already in the cache or not. If it isn't we can // add the key no problem, // otherwise we need to be updating the indexes as opposed to simply adding to the indexes. getLog().debug("Infinispan Query indexing is triggered"); Object key = command.getKey(); Object value = extractValue(command.getValue()); if (updateKnownTypesIfNeeded(value)) { // This means that the entry is just modified so we need to update the indexes and not add // to them. updateIndexes(value, extractValue(key)); } else { if (updateKnownTypesIfNeeded(toReturn)) { removeFromIndexes(toReturn, extractValue(command.getKey())); } } } return toReturn; } @Override public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable { // remove the object out of the cache first. Object valueRemoved = invokeNextInterceptor(ctx, command); if (command.isSuccessful() && !command.isNonExistent() && shouldModifyIndexes(ctx)) { Object value = extractValue(valueRemoved); if (updateKnownTypesIfNeeded(value)) { removeFromIndexes(value, extractValue(command.getKey())); } } return valueRemoved; } @Override public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable { Object valueReplaced = invokeNextInterceptor(ctx, command); if (valueReplaced != null && command.isSuccessful() && shouldModifyIndexes(ctx)) { Object[] parameters = command.getParameters(); Object p1 = extractValue(parameters[1]); Object p2 = extractValue(parameters[2]); boolean originalIsIndexed = updateKnownTypesIfNeeded(p1); boolean newValueIsIndexed = updateKnownTypesIfNeeded(p2); Object key = extractValue(command.getKey()); if (p1 != null && originalIsIndexed) { removeFromIndexes(p1, key); } if (newValueIsIndexed) { updateIndexes(p2, key); } } return valueReplaced; } @Override public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable { Object mapPut = invokeNextInterceptor(ctx, command); if (shouldModifyIndexes(ctx)) { Map<Object, Object> dataMap = command.getMap(); // Loop through all the keys and put those key, value pairings into lucene. for (Map.Entry entry : dataMap.entrySet()) { Object value = extractValue(entry.getValue()); if (updateKnownTypesIfNeeded(value)) { updateIndexes(value, extractValue(entry.getKey())); } } } return mapPut; } @Override public Object visitClearCommand(InvocationContext ctx, ClearCommand command) throws Throwable { // This method is called when somebody calls a cache.clear() and we will need to wipe everything // in the indexes. Object returnValue = invokeNextInterceptor(ctx, command); if (shouldModifyIndexes(ctx)) { if (getLog().isTraceEnabled()) getLog().trace("shouldModifyIndexes() is true and we can clear the indexes"); for (Class c : this.knownClasses.keySet()) { EntityIndexBinder binder = this.searchFactory.getIndexBindingForEntity(c); if (binder != null) { // check as not all known classes are indexed searchFactory .getWorker() .performWork( new Work<Object>(c, (Serializable) null, WorkType.PURGE_ALL), new TransactionalEventTransactionContext( transactionManager, transactionSynchronizationRegistry)); } } } return returnValue; } // Method that will be called when data needs to be removed from Lucene. protected void removeFromIndexes(Object value, Object key) { // The key here is the String representation of the key that is stored in the cache. // The key is going to be the documentID for Lucene. // The object parameter is the actual value that needs to be removed from lucene. if (value == null) throw new NullPointerException("Cannot handle a null value!"); TransactionContext transactionContext = new TransactionalEventTransactionContext( transactionManager, transactionSynchronizationRegistry); searchFactory .getWorker() .performWork( new Work<Object>(value, keyToString(key), WorkType.DELETE), transactionContext); } protected void updateIndexes(Object value, Object key) { // The key here is the String representation of the key that is stored in the cache. // The key is going to be the documentID for Lucene. // The object parameter is the actual value that needs to be removed from lucene. if (value == null) throw new NullPointerException("Cannot handle a null value!"); TransactionContext transactionContext = new TransactionalEventTransactionContext( transactionManager, transactionSynchronizationRegistry); searchFactory .getWorker() .performWork( new Work<Object>(value, keyToString(key), WorkType.UPDATE), transactionContext); } private Object extractValue(Object wrappedValue) { if (wrappedValue instanceof MarshalledValue) return ((MarshalledValue) wrappedValue).get(); else return wrappedValue; } public void enableClasses(Class<?>[] classes) { if (classes == null || classes.length == 0) { return; } enableClassesIncrementally(classes, false); } private void enableClassesIncrementally(Class<?>[] classes, boolean locked) { ArrayList<Class<?>> toAdd = null; for (Class<?> type : classes) { if (!knownClasses.containsKey(type)) { if (toAdd == null) toAdd = new ArrayList<Class<?>>(classes.length); toAdd.add(type); } } if (toAdd == null) { return; } if (locked) { Set<Class<?>> existingClasses = knownClasses.keySet(); int index = existingClasses.size(); Class[] all = existingClasses.toArray(new Class[existingClasses.size() + toAdd.size()]); for (Class<?> toAddClass : toAdd) { all[index++] = toAddClass; } searchFactory.addClasses(all); for (Class<?> type : toAdd) { if (searchFactory.getIndexBindingForEntity(type) != null) { knownClasses.put(type, Boolean.TRUE); } else { knownClasses.put(type, Boolean.FALSE); } } } else { mutating.lock(); try { enableClassesIncrementally(classes, true); } finally { mutating.unlock(); } } } private boolean updateKnownTypesIfNeeded(Object value) { if (value != null) { Class<?> potentialNewType = value.getClass(); if (!this.knownClasses.containsKey(potentialNewType)) { mutating.lock(); try { enableClassesIncrementally(new Class[] {potentialNewType}, true); } finally { mutating.unlock(); } } return this.knownClasses.get(potentialNewType).booleanValue(); } else { return false; } } public void registerKeyTransformer( Class<?> keyClass, Class<? extends Transformer> transformerClass) { keyTransformationHandler.registerTransformer(keyClass, transformerClass); } private String keyToString(Object key) { return keyTransformationHandler.keyToString(key); } public KeyTransformationHandler getKeyTransformationHandler() { return keyTransformationHandler; } }
/** * Repository for {@link RemoteTransaction} and {@link * org.infinispan.transaction.xa.TransactionXaAdapter}s (locally originated transactions). * * @author [email protected] * @author Galder Zamarreño * @since 4.0 */ @Listener public class TransactionTable { public static final int CACHE_STOPPED_TOPOLOGY_ID = -1; private static final Log log = LogFactory.getLog(TransactionTable.class); private ConcurrentMap<Transaction, LocalTransaction> localTransactions; private ConcurrentMap<GlobalTransaction, LocalTransaction> globalToLocalTransactions; private ConcurrentMap<GlobalTransaction, RemoteTransaction> remoteTransactions; private final StaleTransactionCleanupService cleanupService = new StaleTransactionCleanupService(this); protected Configuration configuration; protected InvocationContextContainer icc; protected TransactionCoordinator txCoordinator; protected TransactionFactory txFactory; protected RpcManager rpcManager; protected CommandsFactory commandsFactory; private InterceptorChain invoker; private CacheNotifier notifier; private TransactionSynchronizationRegistry transactionSynchronizationRegistry; protected ClusteringDependentLogic clusteringLogic; protected boolean clustered = false; private Lock minTopologyRecalculationLock; private final ConcurrentMap<GlobalTransaction, Long> completedTransactions = ConcurrentMapFactory.makeConcurrentMap(); /** * minTxTopologyId is the minimum topology ID across all ongoing local and remote transactions. */ private volatile int minTxTopologyId = CACHE_STOPPED_TOPOLOGY_ID; private volatile int currentTopologyId = CACHE_STOPPED_TOPOLOGY_ID; private volatile boolean useStrictTopologyIdComparison = true; private String cacheName; @Inject public void initialize( RpcManager rpcManager, Configuration configuration, InvocationContextContainer icc, InterceptorChain invoker, CacheNotifier notifier, TransactionFactory gtf, TransactionCoordinator txCoordinator, TransactionSynchronizationRegistry transactionSynchronizationRegistry, CommandsFactory commandsFactory, ClusteringDependentLogic clusteringDependentLogic, Cache cache) { this.rpcManager = rpcManager; this.configuration = configuration; this.icc = icc; this.invoker = invoker; this.notifier = notifier; this.txFactory = gtf; this.txCoordinator = txCoordinator; this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; this.commandsFactory = commandsFactory; this.clusteringLogic = clusteringDependentLogic; this.cacheName = cache.getName(); } @Start(priority = 9) // Start before cache loader manager @SuppressWarnings("unused") private void start() { final int concurrencyLevel = configuration.locking().concurrencyLevel(); localTransactions = ConcurrentMapFactory.makeConcurrentMap(concurrencyLevel, 0.75f, concurrencyLevel); globalToLocalTransactions = ConcurrentMapFactory.makeConcurrentMap(concurrencyLevel, 0.75f, concurrencyLevel); if (configuration.clustering().cacheMode().isClustered()) { minTopologyRecalculationLock = new ReentrantLock(); // Only initialize this if we are clustered. remoteTransactions = ConcurrentMapFactory.makeConcurrentMap(concurrencyLevel, 0.75f, concurrencyLevel); cleanupService.start(cacheName, rpcManager, configuration); notifier.addListener(cleanupService); notifier.addListener(this); clustered = true; } } @Stop @SuppressWarnings("unused") private void stop() { if (clustered) { notifier.removeListener(cleanupService); cleanupService.stop(); notifier.removeListener(this); currentTopologyId = CACHE_STOPPED_TOPOLOGY_ID; // indicate that the cache has stopped } shutDownGracefully(); } public Set<Object> getLockedKeysForRemoteTransaction(GlobalTransaction gtx) { RemoteTransaction transaction = remoteTransactions.get(gtx); if (transaction == null) return InfinispanCollections.emptySet(); return transaction.getLockedKeys(); } public void remoteTransactionPrepared(GlobalTransaction gtx) { // do nothing } public void localTransactionPrepared(LocalTransaction localTransaction) { // nothing, only used by recovery } public void enlist(Transaction transaction, LocalTransaction localTransaction) { if (!localTransaction.isEnlisted()) { SynchronizationAdapter sync = new SynchronizationAdapter( localTransaction, txCoordinator, commandsFactory, rpcManager, this, clusteringLogic, configuration); if (transactionSynchronizationRegistry != null) { try { transactionSynchronizationRegistry.registerInterposedSynchronization(sync); } catch (Exception e) { log.failedSynchronizationRegistration(e); throw new CacheException(e); } } else { try { transaction.registerSynchronization(sync); } catch (Exception e) { log.failedSynchronizationRegistration(e); throw new CacheException(e); } } ((SyncLocalTransaction) localTransaction).setEnlisted(true); } } public void failureCompletingTransaction(Transaction tx) { final LocalTransaction localTransaction = localTransactions.get(tx); if (localTransaction != null) { removeLocalTransaction(localTransaction); } } /** * Returns true if the given transaction is already registered with the transaction table. * * @param tx if null false is returned */ public boolean containsLocalTx(Transaction tx) { return tx != null && localTransactions.containsKey(tx); } public int getMinTopologyId() { return minTxTopologyId; } /** * Indicates if topology id comparisons should be strict if one wants to compare topology ids in * oder to tell if a transaction was started in an older topology than a second transaction. This * flag is true most of the time except when the current topology did not increase its id (it's * not caused by a rebalance). * * @return true if strict topology id comparisons should be used, false otherwise */ public boolean useStrictTopologyIdComparison() { return useStrictTopologyIdComparison; } protected void updateStateOnNodesLeaving(Collection<Address> leavers) { Set<GlobalTransaction> toKill = new HashSet<GlobalTransaction>(); for (GlobalTransaction gt : remoteTransactions.keySet()) { if (leavers.contains(gt.getAddress())) toKill.add(gt); } if (toKill.isEmpty()) log.tracef( "No global transactions pertain to originator(s) %s who have left the cluster.", leavers); else log.tracef( "%s global transactions pertain to leavers list %s and need to be killed", toKill.size(), leavers); for (GlobalTransaction gtx : toKill) { log.tracef("Killing remote transaction originating on leaver %s", gtx); RollbackCommand rc = new RollbackCommand(cacheName, gtx); rc.init(invoker, icc, TransactionTable.this); try { rc.perform(null); log.tracef("Rollback of transaction %s complete.", gtx); } catch (Throwable e) { log.unableToRollbackGlobalTx(gtx, e); } } log.trace("Completed cleaning transactions originating on leavers"); } /** * Returns the {@link RemoteTransaction} associated with the supplied transaction id. Returns null * if no such association exists. */ public RemoteTransaction getRemoteTransaction(GlobalTransaction txId) { return remoteTransactions.get(txId); } public void remoteTransactionRollback(GlobalTransaction gtx) { final RemoteTransaction remove = removeRemoteTransaction(gtx); log.tracef("Removed local transaction %s? %b", gtx, remove); } /** * Creates and register a {@link RemoteTransaction}. Returns the created transaction. * * @throws IllegalStateException if an attempt to create a {@link RemoteTransaction} for an * already registered id is made. */ public RemoteTransaction createRemoteTransaction( GlobalTransaction globalTx, WriteCommand[] modifications) { return createRemoteTransaction(globalTx, modifications, currentTopologyId); } /** * Creates and register a {@link RemoteTransaction}. Returns the created transaction. * * @throws IllegalStateException if an attempt to create a {@link RemoteTransaction} for an * already registered id is made. */ public RemoteTransaction createRemoteTransaction( GlobalTransaction globalTx, WriteCommand[] modifications, int topologyId) { RemoteTransaction remoteTransaction = modifications == null ? txFactory.newRemoteTransaction(globalTx, topologyId) : txFactory.newRemoteTransaction(modifications, globalTx, topologyId); registerRemoteTransaction(globalTx, remoteTransaction); return remoteTransaction; } private void registerRemoteTransaction(GlobalTransaction gtx, RemoteTransaction rtx) { RemoteTransaction transaction = remoteTransactions.put(gtx, rtx); if (transaction != null) { log.remoteTxAlreadyRegistered(); throw new IllegalStateException( "A remote transaction with the given id was already registered!!!"); } log.tracef("Created and registered remote transaction %s", rtx); if (rtx.getTopologyId() < minTxTopologyId) { log.tracef( "Changing minimum topology ID from %d to %d", minTxTopologyId, rtx.getTopologyId()); minTxTopologyId = rtx.getTopologyId(); } } /** * Returns the {@link org.infinispan.transaction.xa.TransactionXaAdapter} corresponding to the * supplied transaction. If none exists, will be created first. */ public LocalTransaction getOrCreateLocalTransaction( Transaction transaction, TxInvocationContext ctx) { LocalTransaction current = localTransactions.get(transaction); if (current == null) { Address localAddress = rpcManager != null ? rpcManager.getTransport().getAddress() : null; GlobalTransaction tx = txFactory.newGlobalTransaction(localAddress, false); current = txFactory.newLocalTransaction( transaction, tx, ctx.isImplicitTransaction(), currentTopologyId); log.tracef("Created a new local transaction: %s", current); localTransactions.put(transaction, current); globalToLocalTransactions.put(current.getGlobalTransaction(), current); notifier.notifyTransactionRegistered(tx, ctx); } return current; } /** * Removes the {@link org.infinispan.transaction.xa.TransactionXaAdapter} corresponding to the * given tx. Returns true if such an tx exists. */ public boolean removeLocalTransaction(LocalTransaction localTransaction) { return localTransaction != null && (removeLocalTransactionInternal(localTransaction.getTransaction()) != null); } protected final LocalTransaction removeLocalTransactionInternal(Transaction tx) { LocalTransaction localTx = localTransactions.get(tx); if (localTx != null) { globalToLocalTransactions.remove(localTx.getGlobalTransaction()); localTransactions.remove(tx); releaseResources(localTx); } return localTx; } private void releaseResources(CacheTransaction cacheTransaction) { if (cacheTransaction != null) { if (clustered) { recalculateMinTopologyIdIfNeeded(cacheTransaction); } log.tracef("Removed %s from transaction table.", cacheTransaction); cacheTransaction.notifyOnTransactionFinished(); } } /** Removes the {@link RemoteTransaction} corresponding to the given tx. */ public void remoteTransactionCommitted(GlobalTransaction gtx) { if (Configurations.isSecondPhaseAsync(configuration)) { removeRemoteTransaction(gtx); } } public final RemoteTransaction removeRemoteTransaction(GlobalTransaction txId) { RemoteTransaction removed = remoteTransactions.remove(txId); log.tracef("Removed remote transaction %s ? %s", txId, removed); releaseResources(removed); return removed; } public int getRemoteTxCount() { return remoteTransactions.size(); } public int getLocalTxCount() { return localTransactions.size(); } /** * Looks up a LocalTransaction given a GlobalTransaction. * * @param txId the global transaction identifier * @return the LocalTransaction or null if not found */ public LocalTransaction getLocalTransaction(GlobalTransaction txId) { return globalToLocalTransactions.get(txId); } public LocalTransaction getLocalTransaction(Transaction tx) { return localTransactions.get(tx); } public boolean containRemoteTx(GlobalTransaction globalTransaction) { return remoteTransactions.containsKey(globalTransaction); } public Collection<RemoteTransaction> getRemoteTransactions() { return remoteTransactions.values(); } public Collection<LocalTransaction> getLocalTransactions() { return localTransactions.values(); } protected final void recalculateMinTopologyIdIfNeeded(CacheTransaction removedTransaction) { if (removedTransaction == null) throw new IllegalArgumentException("Transaction cannot be null!"); if (currentTopologyId != CACHE_STOPPED_TOPOLOGY_ID) { // Assume that we only get here if we are clustered. int removedTransactionTopologyId = removedTransaction.getTopologyId(); if (removedTransactionTopologyId < minTxTopologyId) { log.tracef( "A transaction has a topology ID (%s) that is smaller than the smallest transaction topology ID (%s) this node knows about! This can happen if a concurrent thread recalculates the minimum topology ID after the current transaction has been removed from the transaction table.", removedTransactionTopologyId, minTxTopologyId); } else if (removedTransactionTopologyId == minTxTopologyId && removedTransactionTopologyId < currentTopologyId) { // We should only need to re-calculate the minimum topology ID if the transaction being // completed // has the same ID as the smallest known transaction ID, to check what the new smallest is, // and this is // not the current topology ID. calculateMinTopologyId(removedTransactionTopologyId); } } } @TopologyChanged @SuppressWarnings("unused") public void onTopologyChange(TopologyChangedEvent<?, ?> tce) { // don't do anything if this cache is not clustered if (clustered) { if (tce.isPre()) { useStrictTopologyIdComparison = tce.getNewTopologyId() != currentTopologyId; currentTopologyId = tce.getNewTopologyId(); } else { log.debugf("Topology changed, recalculating minTopologyId"); calculateMinTopologyId(-1); } } } /** * This method calculates the minimum topology ID known by the current node. This method is only * used in a clustered cache, and only invoked when either a topology change is detected, or a * transaction whose topology ID is not the same as the current topology ID. * * <p>This method is guarded by minTopologyRecalculationLock to prevent concurrent updates to the * minimum topology ID field. * * @param idOfRemovedTransaction the topology ID associated with the transaction that triggered * this recalculation, or -1 if triggered by a topology change event. */ @GuardedBy("minTopologyRecalculationLock") private void calculateMinTopologyId(int idOfRemovedTransaction) { minTopologyRecalculationLock.lock(); try { // We should only need to re-calculate the minimum topology ID if the transaction being // completed // has the same ID as the smallest known transaction ID, to check what the new smallest is. // We do this check // again here, since this is now within a synchronized method. if (idOfRemovedTransaction == -1 || (idOfRemovedTransaction == minTxTopologyId && idOfRemovedTransaction < currentTopologyId)) { int minTopologyIdFound = currentTopologyId; for (CacheTransaction ct : localTransactions.values()) { int topologyId = ct.getTopologyId(); if (topologyId < minTopologyIdFound) minTopologyIdFound = topologyId; } for (CacheTransaction ct : remoteTransactions.values()) { int topologyId = ct.getTopologyId(); if (topologyId < minTopologyIdFound) minTopologyIdFound = topologyId; } if (minTopologyIdFound != minTxTopologyId) { log.tracef( "Changing minimum topology ID from %s to %s", minTxTopologyId, minTopologyIdFound); minTxTopologyId = minTopologyIdFound; } else { log.tracef("Minimum topology ID still is %s; nothing to change", minTopologyIdFound); } } } finally { minTopologyRecalculationLock.unlock(); } } private boolean areTxsOnGoing() { return !localTransactions.isEmpty() || (remoteTransactions != null && !remoteTransactions.isEmpty()); } private void shutDownGracefully() { if (log.isDebugEnabled()) log.debugf( "Wait for on-going transactions to finish for %s.", Util.prettyPrintTime( configuration.transaction().cacheStopTimeout(), TimeUnit.MILLISECONDS)); long failTime = currentMillisFromNanotime() + configuration.transaction().cacheStopTimeout(); boolean txsOnGoing = areTxsOnGoing(); while (txsOnGoing && currentMillisFromNanotime() < failTime) { try { Thread.sleep(30); txsOnGoing = areTxsOnGoing(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); if (clustered) { log.debugf( "Interrupted waiting for on-going transactions to finish. %s local transactions and %s remote transactions", localTransactions.size(), remoteTransactions.size()); } else { log.debugf( "Interrupted waiting for %s on-going transactions to finish.", localTransactions.size()); } } } if (txsOnGoing) { log.unfinishedTransactionsRemain( localTransactions == null ? 0 : localTransactions.size(), remoteTransactions == null ? 0 : remoteTransactions.size()); } else { log.debug("All transactions terminated"); } } /** * With the current state transfer implementation it is possible for a transaction to be prepared * several times on a remote node. This might cause leaks, e.g. if the transaction is prepared, * committed and prepared again. Once marked as completed (because of commit or rollback) any * further prepare received on that transaction are discarded. */ public void markTransactionCompleted(GlobalTransaction globalTx) { completedTransactions.put(globalTx, System.nanoTime()); } /** @see #markTransactionCompleted(org.infinispan.transaction.xa.GlobalTransaction) */ public boolean isTransactionCompleted(GlobalTransaction gtx) { return completedTransactions.containsKey(gtx); } public void cleanupCompletedTransactions() { if (!completedTransactions.isEmpty()) { try { log.tracef( "About to cleanup completed transaction. Initial size is %d", completedTransactions.size()); // this iterator is weekly consistent and will never throw ConcurrentModificationException Iterator<Map.Entry<GlobalTransaction, Long>> iterator = completedTransactions.entrySet().iterator(); long timeout = configuration.transaction().completedTxTimeout(); int removedEntries = 0; long beginning = System.nanoTime(); while (iterator.hasNext()) { Map.Entry<GlobalTransaction, Long> e = iterator.next(); long ageNanos = System.nanoTime() - e.getValue(); if (TimeUnit.NANOSECONDS.toMillis(ageNanos) >= timeout) { iterator.remove(); removedEntries++; } } long duration = System.nanoTime() - beginning; log.tracef( "Finished cleaning up completed transactions. %d transactions were removed, total duration was %d millis, " + "current number of completed transactions is %d", removedEntries, TimeUnit.NANOSECONDS.toMillis(duration), completedTransactions.size()); } catch (Exception e) { log.errorf(e, "Failed to cleanup completed transactions: %s", e.getMessage()); } } } }