package com.indeed.status.core; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.indeed.util.core.time.WallClock; import com.indeed.util.varexport.Export; import com.indeed.util.varexport.VarExporter; import org.apache.log4j.Logger; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.PreDestroy; import java.lang.Thread.UncaughtExceptionHandler; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor.AbortPolicy; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static com.google.common.base.Preconditions.checkNotNull; /** * The {@link AbstractDependencyManager} is a singleton clearinghouse responsible for knowing * all dependencies of the system and their current availability. */ abstract public class AbstractDependencyManager implements StatusUpdateProducer, StatusUpdateListener/*,Terminable todo(cameron)*/ { private static final int DEFAULT_PING_PERIOD = 30 * 1000; // 30 seconds private static final AtomicInteger DEFAULT_THREAD_POOL_COUNT = new AtomicInteger(1); private static final AtomicInteger MANAGEMENT_THREAD_POOL_COUNT = new AtomicInteger(1); @Nonnull private final Logger log; @Nullable private final String appName; /// Timer for managing scheduled executions @Nonnull private final ScheduledExecutorService executor; /// Thread pool for running dependency checks @Nonnull private final ThreadPoolExecutor threadPool; /// Container for checking all dependencies @Nonnull private final DependencyChecker checker; /// Delegate for handling event propagation private final StatusUpdateDelegate updateHandler = new StatusUpdateDelegate(); /// Collection of all dependencies governed by this manager. The keys of this map are the unique /// String identifiers of each dependency. The values are the immutable objects representing the /// canonical view of each dependency. This map does <em>not</em> indicate the current status /// of dependencies, but rather the set of dependencies that are registered with the system. @Nonnull private final ConcurrentMap<String, Dependency> dependencies = Maps.newConcurrentMap(); /// Collection of all dependency pingers that have been created to monitor the health of a dependency. /// Once a dependency is removed from the manager, its associated pinger is cancelled. @Nonnull private final ConcurrentMap<String, ScheduledFuture<?>> dependencyPingers = Maps.newConcurrentMap(); private long pingPeriod = DEFAULT_PING_PERIOD; public static class Qualifiers { protected Qualifiers () { throw new UnsupportedOperationException("ResultType is a constants class."); } public static final String LIVE = "live"; public static final String BACKGROUND = "bkgd"; // NOTE: Varying slightly from the "background" string used by client projects // just so we don't trip somebody up. } // TODO Some day, all of this will be replaced with a builder. // At the time when we do that, we'll need to work with the dependency manager extensions present in the // unit tests, since those are the only reasonable extensions of the checker. They can probably be refactored // to push the custom behavior up into the test case or down into the dependency. public AbstractDependencyManager() { this(null, null, newDefaultThreadPool()); } public AbstractDependencyManager(final String appName) { this(appName, null, newDefaultThreadPool()); } public AbstractDependencyManager (final String appName, final Logger logger) { this(appName, logger, newDefaultThreadPool()); } public AbstractDependencyManager( final String appName, final Logger logger, @Nonnull final SystemReporter systemReporter ) { this(appName, logger, newDefaultThreadPool(), systemReporter); } public AbstractDependencyManager( final String appName, final Logger logger, @Nonnull final SystemReporter systemReporter, final boolean throttleDependencyChecks ) { this(appName, logger, newDefaultThreadPool(), systemReporter, throttleDependencyChecks); } public AbstractDependencyManager (final Logger logger) { this(null, logger, newDefaultThreadPool()); } public AbstractDependencyManager( @Nullable final String appName, @Nullable final Logger logger, @Nonnull final DependencyChecker checker ) { this(appName, logger, newDefaultThreadPool(), checker); } /** * @deprecated Use {@link #AbstractDependencyManager(String, Logger, ThreadPoolExecutor, WallClock)} instead. */ @Deprecated public AbstractDependencyManager( @Nullable final String appName, @Nullable final Logger logger, @Nonnull final ThreadPoolExecutor threadPool ) { this(appName, logger, threadPool, new SystemReporter()); } public AbstractDependencyManager( @Nullable final String appName, @Nullable final Logger logger, @Nonnull final ThreadPoolExecutor threadPool, @Nonnull final WallClock wallClock ) { this(appName, logger, threadPool, new SystemReporter(wallClock)); } public AbstractDependencyManager( @Nullable final String appName, @Nullable final Logger logger, @Nonnull final ThreadPoolExecutor threadPool, @Nonnull final SystemReporter systemReporter ) { this( appName, logger, threadPool, systemReporter, false); } public AbstractDependencyManager( @Nullable final String appName, @Nullable final Logger logger, @Nonnull final ThreadPoolExecutor threadPool, @Nonnull final SystemReporter systemReporter, final boolean throttleDependencyChecks ) { this( appName, logger, threadPool, DependencyChecker.newBuilder() .setExecutorService(threadPool) .setLogger(logger) .setSystemReporter(systemReporter) .setThrottle(throttleDependencyChecks) .build()); } public AbstractDependencyManager( @Nullable final String appName, @Nullable final Logger logger, @Nonnull final ThreadPoolExecutor threadPool, @Nonnull final DependencyChecker checker ) { this.appName = Strings.isNullOrEmpty(appName) ? getAppName() : appName; this.log = null == logger ? Logger.getLogger(getClass()) : logger; this.executor = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder() .setNameFormat("dependency-management-" + MANAGEMENT_THREAD_POOL_COUNT.getAndIncrement() + "-thread-%d") .setDaemon(true) .setUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { log.error("Uncaught throwable in thread " + t.getName() + "/" + t.getId(), e); } }) .build() ); this.threadPool = threadPool; this.checker = checker; VarExporter.forNamespace(getClass().getSimpleName()).includeInGlobal().export(this, ""); } @Nullable public String getAppName() { return appName; } @Nonnull protected WallClock getWallClock() { return this.checker.getWallClock(); } static ThreadPoolExecutor newDefaultThreadPool() { final ThreadPoolExecutor result = new ThreadPoolExecutor( // Bound the pool. Most foreground dependency managers should be called only very rarely, so // keep a minimal core pool around and only grow it on demand. 1, 16, // Threads will be kept alive in an idle state for a minute or two. After that, they may be // garbage-collected, so that we're keeping a larger thread pool only during weird periods of // congestion. (Note: the background manager will typically keep all threads pretty active, since it's // repeatedly launching new pingers. The live manager will spin them up and down based on traffic to // the rather uncommonly used /healthcheck/live uri). 30, TimeUnit.SECONDS, // Use a blocking queue just to keep track of checks when the world is going wrong. This is mostly useful // when we're adding a bunch of checks at the same time, such as during a live healthcheck. Might as well // keep this pretty small, because any nontrivial wait to execute is going to blow up a timeout anyway. new SynchronousQueue<Runnable>(), // Name your threads. new ThreadFactoryBuilder() .setNameFormat("dependency-default-" + DEFAULT_THREAD_POOL_COUNT.getAndIncrement() + "-checker-%d") .setDaemon(true) .setUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { Logger.getLogger(AbstractDependencyManager.class) .error("Uncaught throwable in thread " + t.getName() + "/" + t.getId(), e); } }) .build(), // Explicitly restating the default policy here, because healthchecks should Just Not Work if there // are insufficient resources to support them. Given the smallish queue above, this means that // we're going to end up throwing exceptions if we get too blocked up somehow. new AbortPolicy()); result.prestartAllCoreThreads(); return result; } @SuppressWarnings("UnusedDeclaration") public Collection<String> getDependencyIds() { return Collections.unmodifiableCollection(dependencies.keySet()); } @Nonnull public CheckResultSet evaluate() { return evaluate(getDependencies()); } @Nullable public CheckResult evaluate(@Nonnull final String id) { final Dependency dependency = checkNotNull(dependencies.get(id), "Missing dependency '%s'", id); return evaluate(Collections.singleton(dependency)).get(id); } @Nonnull private CheckResultSet evaluate(Collection<Dependency> dependencies) { final CheckResultSet result = checker.evaluate(dependencies); result.setAppName(appName); return result; } /** * Launches a background pinger over the given dependency. The periodicity of the * check is controlled by the dependency manager object. * * @param dependency */ public void launchPinger(final Dependency dependency) { final DependencyPinger pinger = newPingerFor(dependency); // Add a listener so that objects that want to listen for updates to ANY dependency // can do so. Note that this is done ONLY for background-pinger type dependency // checks, because it makes less sense to monitor checks that are evaluated // unpredictably. pinger.addListener(updateHandler); // Note: we can assume the id is unique and the dependency is not duplicated because of the check // in AbstractDependencyManager#addDependency dependencyPingers.computeIfAbsent(dependency.getId(), dependencyId -> executor.scheduleWithFixedDelay(pinger, 0, pinger.getPingPeriod(), TimeUnit.MILLISECONDS)); addDependency(pinger); } protected DependencyPinger newPingerFor (final Dependency dependency) { final DependencyPinger pinger; final long dependencyPingPeriod = dependency.getPingPeriod(); if (dependencyPingPeriod <= 0 || dependencyPingPeriod == AbstractDependency.DEFAULT_PING_PERIOD) { log.info("Creating pinger with ping period " + pingPeriod); pinger = new DependencyPinger(dependency, pingPeriod, checker); } else { log.info("Creating pinger with ping period " + dependency.getPingPeriod()); pinger = new DependencyPinger(dependency, checker); } return pinger; } public Dependency getDependency (final String id) { return dependencies.get(id); } public void addDependency(final Dependency dependency) { final Dependency dependencyToAdd; if (checker.getThrottle() && !(dependency instanceof DependencyPinger)) { dependencyToAdd = new ThrottledDependencyWrapper(dependency); } else { dependencyToAdd = dependency; } final Dependency existing = dependencies.putIfAbsent(dependencyToAdd.getId(), dependencyToAdd); Preconditions.checkState( null == existing, "Can't have two dependencies with the same ID [%s]. Check your setup.", dependencyToAdd.getId()); // Direct this through the update-handler so that we don't inadvertently alert ourselves that we added a dependency updateHandler.onAdded(dependencyToAdd); } public Dependency removeDependency(final String id) { final ScheduledFuture<?> pinger = dependencyPingers.remove(id); // Cancel all future pings for this dependency, interrupting any current pings if (pinger != null) { pinger.cancel(true); } return dependencies.remove(id); } public Collection<Dependency> getDependencies() { return Collections.unmodifiableCollection(dependencies.values()); } @Override public void onChanged (@Nonnull final Dependency source, @Nullable final CheckResult original, @Nonnull final CheckResult updated) { updateHandler.onChanged(source, original, updated); } @Override public void onAdded(@Nonnull final Dependency dependency) { updateHandler.onAdded(dependency); } @Override public void onChecked(@Nonnull final Dependency source, @Nonnull final CheckResult result) { updateHandler.onChecked(source, result); } @Override public void clear () { updateHandler.clear(); } @Override public void addListener (final StatusUpdateListener listener) { updateHandler.addListener(listener); } @Override public Iterator<StatusUpdateListener> listeners() { return updateHandler.listeners(); } public void setPingPeriod(final long pingPeriod) { this.pingPeriod = pingPeriod; } /*@Override todo(cameron)*/ @PreDestroy public void shutdown () { this.checker.shutdown(); this.executor.shutdownNow(); } @Export (name="active-threads") public int getActiveDependencyThreads() { return threadPool.getActiveCount(); } @Export(name="core-pool-size") public int getCorePoolSize() { return threadPool.getCorePoolSize(); } @Export(name="queue-size") public int getQueueSize() { final BlockingQueue<Runnable> queue = threadPool.getQueue(); return null == queue ? 0 : queue.size(); } }