package com.netflix.governator.internal; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.inject.Binding; import com.google.inject.Key; import com.google.inject.Provider; import com.google.inject.Scope; import com.google.inject.Scopes; import com.google.inject.spi.BindingScopingVisitor; import com.google.inject.util.Providers; import com.netflix.governator.LifecycleAction; import com.netflix.governator.ManagedInstanceAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * Monitors managed instances and invokes cleanup actions when they become unreferenced * * @author tcellucci * */ public class PreDestroyMonitor implements AutoCloseable { private static Logger LOGGER = LoggerFactory.getLogger(PreDestroyMonitor.class); private static class ScopeCleanupMarker { static final Key<ScopeCleanupMarker> MARKER_KEY = Key.get(ScopeCleanupMarker.class); // simple id uses identity equality private final Object id = new Object(); private final ScopeCleanupAction cleanupAction; public ScopeCleanupMarker(ReferenceQueue<ScopeCleanupMarker> markerReferenceQueue) { this.cleanupAction = new ScopeCleanupAction(this, markerReferenceQueue); } Object getId() { return id; } public ScopeCleanupAction getCleanupAction() { return cleanupAction; } } static final class ScopeCleaner implements Provider<ScopeCleanupMarker> { ConcurrentMap<Object, ScopeCleanupAction> scopedCleanupActions = new ConcurrentHashMap<>(BinaryConstant.I14_16384); ReferenceQueue<ScopeCleanupMarker> markerReferenceQueue = new ReferenceQueue<>(); final ExecutorService reqQueueExecutor = Executors.newSingleThreadExecutor( new ThreadFactoryBuilder().setDaemon(true).setNameFormat("predestroy-monitor-%d").build()); final AtomicBoolean running = new AtomicBoolean(true); final ScopeCleanupMarker singletonMarker = get(); { this.reqQueueExecutor.submit(new ScopedCleanupWorker()); } @Override public ScopeCleanupMarker get() { ScopeCleanupMarker marker = new ScopeCleanupMarker(markerReferenceQueue); scopedCleanupActions.put(marker.getId(), marker.getCleanupAction()); return marker; } public boolean isRunning() { return running.get(); } public boolean close() throws Exception { boolean rv = running.compareAndSet(true, false); if (rv) { reqQueueExecutor.shutdown(); // executor to stop // process any remaining scoped cleanup actions List<ScopeCleanupAction> values = new ArrayList<>(scopedCleanupActions.values()); scopedCleanupActions.clear(); Collections.sort(values); for (Callable<Void> actions : values) { actions.call(); } // make sure executor thread really ended if (!reqQueueExecutor.awaitTermination(90, TimeUnit.SECONDS)) { LOGGER.error("internal executor still active; shutting down now"); reqQueueExecutor.shutdownNow(); } markerReferenceQueue = null; } return rv; } /** * Processes unreferenced markers from the referenceQueue, until the 'running' flag is false or interrupted * */ final class ScopedCleanupWorker implements Runnable { public void run() { try { while (running.get()) { Reference<? extends ScopeCleanupMarker> ref = markerReferenceQueue.remove(1000); if (ref != null && ref instanceof ScopeCleanupAction) { Object markerKey = ((ScopeCleanupAction) ref).getId(); ScopeCleanupAction cleanupAction = scopedCleanupActions.remove(markerKey); if (cleanupAction != null) { cleanupAction.call(); } } } LOGGER.info("PreDestroyMonitor.ScopedCleanupWorker is exiting"); } catch (InterruptedException e) { LOGGER.info("PreDestroyMonitor.ScopedCleanupWorker is exiting due to thread interrupt"); Thread.currentThread().interrupt(); // clear interrupted status } } } } private Deque<Callable<Void>> cleanupActions = new ConcurrentLinkedDeque<>(); private ScopeCleaner scopeCleaner = new ScopeCleaner(); Map<Class<? extends Annotation>, Scope> scopeBindings; public PreDestroyMonitor(Map<Class<? extends Annotation>, Scope> scopeBindings) { this.scopeBindings = new HashMap<>(scopeBindings); } public <T> boolean register(T destroyableInstance, Binding<T> binding, Iterable<LifecycleAction> action) { return scopeCleaner.isRunning() ? binding.acceptScopingVisitor( new ManagedInstanceScopingVisitor(destroyableInstance, binding.getSource(), action)) : false; } /* * compatibility-mode - scope is assumed to be eager singleton */ public <T> boolean register(T destroyableInstance, Object context, Iterable<LifecycleAction> action) { return scopeCleaner.isRunning() ? new ManagedInstanceScopingVisitor(destroyableInstance, context, action).visitEagerSingleton() : false; } /** * allows late-binding of scopes to PreDestroyMonitor, useful if more than one Injector contributes scope bindings * * @param bindings * additional annotation-to-scope bindings to add */ public void addScopeBindings(Map<Class<? extends Annotation>, Scope> bindings) { if (scopeCleaner.isRunning()) { scopeBindings.putAll(bindings); } } /** * final cleanup of managed instances if any */ @Override public void close() throws Exception { if (scopeCleaner.close()) { // executor thread to exit processing loop LOGGER.info("closing PreDestroyMonitor..."); for (Callable<Void> action : cleanupActions) { action.call(); } cleanupActions.clear(); scopeBindings.clear(); scopeBindings = Collections.emptyMap(); } else { LOGGER.warn("PreDestroyMonitor.close() invoked but instance is not running"); } } /** * visits bindingScope of managed instance to set up an appropriate strategy for cleanup, adding actions to either * the scopedCleanupActions map or cleanupActions list. Returns true if cleanup actions were added, false if no * cleanup strategy was selected. * */ private final class ManagedInstanceScopingVisitor implements BindingScopingVisitor<Boolean> { private final Object injectee; private final Object context; private final Iterable<LifecycleAction> lifecycleActions; private ManagedInstanceScopingVisitor(Object injectee, Object context, Iterable<LifecycleAction> lifecycleActions) { this.injectee = injectee; this.context = context; this.lifecycleActions = lifecycleActions; } /* * handle eager singletons same as singletons for cleanup purposes. * */ @Override public Boolean visitEagerSingleton() { return visitScope(Scopes.SINGLETON); } /* * use ScopeCleanupMarker dereferencing strategy to detect scope closure, add new entry to scopedCleanupActions * map * */ @Override public Boolean visitScope(Scope scope) { final Provider<ScopeCleanupMarker> scopedMarkerProvider; if (scope.equals(Scopes.SINGLETON) || (scope instanceof AbstractScope && ((AbstractScope)scope).isSingletonScope())) { scopedMarkerProvider = Providers.of(scopeCleaner.singletonMarker); } else { scopedMarkerProvider = scope.scope(ScopeCleanupMarker.MARKER_KEY, scopeCleaner); } ScopeCleanupMarker marker = scopedMarkerProvider.get(); marker.getCleanupAction().add(scopedMarkerProvider, new ManagedInstanceAction(injectee, lifecycleActions)); return true; } /* * lookup Scope by annotation, then delegate to visitScope() * */ @Override public Boolean visitScopeAnnotation(final Class<? extends Annotation> scopeAnnotation) { Scope scope = scopeBindings.get(scopeAnnotation); boolean rv; if (scope != null) { rv = visitScope(scope); } else { LOGGER.warn("no scope binding found for annotation " + scopeAnnotation.getName()); rv = false; } return rv; } /* * add a soft-reference ManagedInstanceAction to cleanupActions deque. Cleanup triggered only at injector * shutdown if referent has not yet been collected. * */ @Override public Boolean visitNoScoping() { cleanupActions.addFirst( new ManagedInstanceAction(new SoftReference<Object>(injectee), context, lifecycleActions)); return true; } } /** * Runnable that weakly references a scopeCleanupMarker and strongly references a list of delegate runnables. When * the marker is unreferenced, delegates will be invoked in the reverse order of addition. */ private static final class ScopeCleanupAction extends WeakReference<ScopeCleanupMarker> implements Callable<Void>, Comparable<ScopeCleanupAction> { private volatile static long instanceCounter = 0; private final Object id; private final long ordinal; private Deque<Object[]> delegates = new ConcurrentLinkedDeque<>(); private final AtomicBoolean complete = new AtomicBoolean(false); public ScopeCleanupAction(ScopeCleanupMarker marker, ReferenceQueue<ScopeCleanupMarker> refQueue) { super(marker, refQueue); this.id = marker.getId(); this.ordinal = instanceCounter++; } public Object getId() { return id; } public void add(Provider<ScopeCleanupMarker> scopeProvider, Callable<Void> action) { if (!complete.get()) { delegates.addFirst(new Object[] { action, scopeProvider }); // add first } } @SuppressWarnings("unchecked") @Override public Void call() { if (complete.compareAndSet(false, true) && delegates != null) { for (Object[] r : delegates) { try { ((Callable<Void>) r[0]).call(); } catch (Exception e) { LOGGER.error("PreDestroy call failed for " + r, e); } } delegates.clear(); clear(); } return null; } @Override public int compareTo(ScopeCleanupAction o) { return Long.compare(ordinal, o.ordinal); } } }