package org.infinispan.persistence.async; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.infinispan.Cache; import org.infinispan.commons.CacheException; import org.infinispan.commons.util.CollectionFactory; import org.infinispan.configuration.cache.AsyncStoreConfiguration; import org.infinispan.configuration.cache.Configuration; import org.infinispan.factories.threads.DefaultThreadFactory; import org.infinispan.marshall.core.MarshalledEntry; import org.infinispan.persistence.modifications.Modification; import org.infinispan.persistence.modifications.Remove; import org.infinispan.persistence.modifications.Store; import org.infinispan.persistence.spi.CacheWriter; import org.infinispan.persistence.spi.InitializationContext; import org.infinispan.persistence.spi.PersistenceException; import org.infinispan.persistence.support.DelegatingCacheWriter; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import net.jcip.annotations.GuardedBy; /** * The AsyncCacheWriter is a delegating CacheStore that buffers changes and writes them asynchronously to * the underlying CacheStore. * <p/> * Read operations are done synchronously, taking into account the current state of buffered changes. * <p/> * There is no provision for exception handling for problems encountered with the underlying store * during a write operation, and the exception is just logged. * <p/> * When configuring the loader, use the following element: * <p/> * <code> <async enabled="true" /> </code> * <p/> * to define whether cache loader operations are to be asynchronous. If not specified, a cache loader operation is * assumed synchronous and this decorator is not applied. * <p/> * Write operations affecting same key are now coalesced so that only the final state is actually stored. * <p/> * * @author Manik Surtani * @author Galder ZamarreƱo * @author Sanne Grinovero * @author Karsten Blees * @author Mircea Markus * @since 4.0 */ public class AsyncCacheWriter extends DelegatingCacheWriter { private static final Log log = LogFactory.getLog(AsyncCacheWriter.class); private static final boolean trace = log.isTraceEnabled(); private ExecutorService executor; private Thread coordinator; private int concurrencyLevel; private String cacheName; private String nodeName; protected BufferLock stateLock; @GuardedBy("stateLock") protected final AtomicReference<State> state = new AtomicReference<>(); @GuardedBy("stateLock") private boolean stopped; protected AsyncStoreConfiguration asyncConfiguration; public AsyncCacheWriter(CacheWriter delegate) { super(delegate); } @Override public void init(InitializationContext ctx) { super.init(ctx); this.asyncConfiguration = ctx.getConfiguration().async(); Cache cache = ctx.getCache(); Configuration cacheCfg = cache != null ? cache.getCacheConfiguration() : null; concurrencyLevel = cacheCfg != null ? cacheCfg.locking().concurrencyLevel() : 16; cacheName = cache != null ? cache.getName() : null; nodeName = cache != null ? cache.getCacheManager().getCacheManagerConfiguration().transport().nodeName() : null; } @Override public void start() { log.debugf("Async cache loader starting %s", this); state.set(newState(false, null)); stopped = false; stateLock = new BufferLock(asyncConfiguration.modificationQueueSize()); // Create a thread pool with unbounded work queue, so that all work is accepted and eventually // executed. A bounded queue could throw RejectedExecutionException and thus lose data. int poolSize = asyncConfiguration.threadPoolSize(); DefaultThreadFactory processorThreadFactory = new DefaultThreadFactory(null, Thread.NORM_PRIORITY, DefaultThreadFactory.DEFAULT_PATTERN, nodeName, "AsyncStoreProcessor"); executor = new ThreadPoolExecutor(poolSize, poolSize, 120L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), processorThreadFactory); ((ThreadPoolExecutor) executor).allowCoreThreadTimeOut(true); DefaultThreadFactory coordinatorThreadFactory = new DefaultThreadFactory(null, Thread.NORM_PRIORITY, DefaultThreadFactory.DEFAULT_PATTERN, nodeName, "AsyncStoreCoordinator"); coordinator = coordinatorThreadFactory.newThread(new AsyncStoreCoordinator()); coordinator.start(); } @Override public void stop() { if (trace) log.tracef("Stop async store %s", this); stateLock.writeLock(0); stopped = true; stateLock.writeUnlock(); try { // It is safe to wait without timeout because the thread pool uses an unbounded work queue (i.e. // all work handed to the pool will be accepted and eventually executed) and AsyncStoreProcessors // decrement the workerThreads latch in a finally block (i.e. even if the back-end store throws // java.lang.Error). The coordinator thread can only block forever if the back-end's write() / // remove() methods block, but this is no different from PassivationManager.stop() being blocked // in a synchronous call to write() / remove(). coordinator.join(); // The coordinator thread waits for AsyncStoreProcessor threads to count down their latch (nearly // at the end). Thus the threads should have terminated or terminate instantly. executor.shutdown(); if (!executor.awaitTermination(1, TimeUnit.SECONDS)) log.errorAsyncStoreNotStopped(); } catch (InterruptedException e) { log.interruptedWaitingAsyncStorePush(e); Thread.currentThread().interrupt(); } } @Override public void write(MarshalledEntry entry) { put(new Store(entry.getKey(), entry), 1); } @Override public boolean delete(Object key) { put(new Remove(key), 1); return true; } protected void applyModificationsSync(List<Modification> mods) throws PersistenceException { for (Modification m : mods) { switch (m.getType()) { case STORE: actual.write(((Store) m).getStoredValue()); break; case REMOVE: actual.delete(((Remove) m).getKey()); break; default: throw new IllegalArgumentException("Unknown modification type " + m.getType()); } } } protected State newState(boolean clear, State next) { ConcurrentMap<Object, Modification> map = CollectionFactory.makeConcurrentMap(64, concurrencyLevel); return new State(clear, map, next); } void assertNotStopped() throws CacheException { if (stopped) throw new CacheException("AsyncCacheWriter stopped; no longer accepting more entries."); } private void put(Modification mod, int count) { stateLock.writeLock(count); try { if (trace) log.tracef("Queue modification: %s", mod); assertNotStopped(); state.get().put(mod); } finally { stateLock.writeUnlock(); } } public AtomicReference<State> getState() { return state; } protected void clearStore() { // No-op, not supported for async } private class AsyncStoreCoordinator implements Runnable { @Override public void run() { LogFactory.pushNDC(cacheName, trace); try { for (;;) { final State s, head, tail; final boolean shouldStop; stateLock.readLock(); try { s = state.get(); shouldStop = stopped; tail = s.next; assert tail == null || tail.next == null : "State chain longer than 3 entries!"; head = newState(false, s); state.set(head); } finally { stateLock.reset(0); stateLock.readUnlock(); } try { if (s.clear) { // clear() must be called synchronously, wait until background threads are done if (tail != null) tail.workerThreads.await(); clearStore(); } final List<Modification> mods = new ArrayList<>(s.modifications.size()); final List<Modification> deferredMods = new ArrayList<>(); if (tail != null && tail.workerThreads.getCount() > 0) { // sort out modifications that are still in use by tail's AsyncStoreProcessors for (Map.Entry<Object, Modification> e : s.modifications.entrySet()) { if (!tail.modifications.containsKey(e.getKey())) mods.add(e.getValue()); else deferredMods.add(e.getValue()); } } else { mods.addAll(s.modifications.values()); } // create AsyncStoreProcessors final List<AsyncStoreProcessor> procs = createProcessors(s, mods); final List<AsyncStoreProcessor> deferredProcs = createProcessors(s, deferredMods); s.workerThreads = new CountDownLatch(procs.size() + deferredProcs.size()); // schedule AsyncStoreProcessors that don't conflict with tail's processors for (AsyncStoreProcessor processor : procs) executor.execute(processor); // wait until background threads of previous round are done if (tail != null) { tail.workerThreads.await(); s.next = null; } // schedule remaining AsyncStoreProcessors for (AsyncStoreProcessor processor : deferredProcs) executor.execute(processor); // if this is the last state to process, wait for background threads, then quit if (shouldStop) { s.workerThreads.await(); return; } } catch (Exception e) { log.unexpectedErrorInAsyncStoreCoordinator(e); } } } finally { LogFactory.popNDC(trace); } } private List<AsyncStoreProcessor> createProcessors(State state, List<Modification> mods) { List<AsyncStoreProcessor> result = new ArrayList<>(); // distribute modifications evenly across worker threads int threads = Math.min(mods.size(), asyncConfiguration.threadPoolSize()); if (threads > 0) { // create background threads int start = 0; int quotient = mods.size() / threads; int remainder = mods.size() % threads; for (int i = 0; i < threads; i++) { int end = start + quotient + (i < remainder ? 1 : 0); result.add(new AsyncStoreProcessor(mods.subList(start, end), state)); start = end; } assert start == mods.size() : "Thread distribution is broken!"; } return result; } } private class AsyncStoreProcessor implements Runnable { private final List<Modification> modifications; private final State myState; AsyncStoreProcessor(List<Modification> modifications, State myState) { this.modifications = modifications; this.myState = myState; } @Override public void run() { try { // try 3 times to store the modifications retryWork(3); } finally { // decrement active worker threads and disconnect myState if this was the last one myState.workerThreads.countDown(); if (myState.workerThreads.getCount() == 0 && myState.next == null) for (State s = state.get(); s != null; s = s.next) if (s.next == myState) s.next = null; } } private void retryWork(int maxRetries) { for (int attempt = 0; attempt < maxRetries; attempt++) { if (attempt > 0 && log.isDebugEnabled()) log.debugf("Retrying due to previous failure. %s attempts left.", maxRetries - attempt); try { AsyncCacheWriter.this.applyModificationsSync(modifications); return; } catch (Exception e) { if (log.isDebugEnabled()) log.debug("Failed to process async modifications", e); } } log.unableToProcessAsyncModifications(maxRetries); } } }