All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.indeed.status.core.DependencyPinger Maven / Gradle / Ivy

package com.indeed.status.core;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.indeed.util.core.LongRecentEventsCounter;
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 java.lang.Thread.UncaughtExceptionHandler;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * The {@link DependencyPinger} is a modification of the HCv2 ServicePinger class that keeps the
 *  idea of consecutive failures and slowly downgrading status, but uses abstractions from the HCv3
 *  system.
 *
 * @author matts Cloned and modified from ServicePinger.java
 */
@VisibleForTesting /* Do not use in production code outside webapp-common */
public class DependencyPinger implements Dependency, StatusUpdateProducer, Runnable {
    private static final Logger log = Logger.getLogger(DependencyPinger.class);

    /// The ping period respected by this dependency. This will be equal to the particular ping period for the
    ///  underlying dependency or the specified override, depending on the constructor used.
    private final long pingPeriod;
    @SuppressWarnings ({"FieldCanBeLocal"})
    private final int consecutiveFailureThreshold = 3;
    private final AtomicInteger consecutiveFailures = new AtomicInteger();
    private final AtomicLong totalSuccesses = new AtomicLong();
    private final AtomicLong totalFailures = new AtomicLong();
    private final LongRecentEventsCounter failuresOverTime = new LongRecentEventsCounter(LongRecentEventsCounter.MINUTE_TICKER, 60);
    private final StatusUpdateDelegate updateHandler = new StatusUpdateDelegate();
    @Nullable
    private volatile CheckResult lastResult = null;
    private transient Throwable lastThrown = null;
    private transient long lastDuration = 0L;
    private transient long lastExecuted = 0L;
    private transient long lastKnownGood = 0L;

    @Nonnull
    private final DependencyChecker checker;
    @Nonnull
    private final Dependency dependency;

    /**
     * @deprecated Use {@link #DependencyPinger(Dependency, SystemReporter)} instead.
     */
    public DependencyPinger(@Nonnull final Dependency dependency) {
        this(dependency, new SystemReporter());
    }

    DependencyPinger(
            @Nonnull final Dependency dependency,
            @Nonnull final SystemReporter systemReporter
    ) {
        this(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder()
                .setNameFormat("dependency-pinger-%d")
                .setDaemon(true)
                .setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
                    @Override
                    public void uncaughtException(final Thread t, final Throwable e) {
                        log.error("Uncaught throwable in thread " + t.getName() + "/" + t.getId(), e);
                    }
                })
                .build()
        ), dependency, systemReporter);
    }

    DependencyPinger(
            @Nonnull final Dependency dependency,
            @Nonnull final DependencyChecker checker
    ) {
        this(dependency, dependency.getPingPeriod(), checker);
    }

    DependencyPinger(
            @Nonnull final ExecutorService executorService,
            @Nonnull final Dependency dependency,
            @Nonnull final SystemReporter systemReporter
    ) {
        this(executorService, dependency, dependency.getPingPeriod(), systemReporter);
    }

    DependencyPinger (
            @Nonnull final ExecutorService executorService,
            @Nonnull final Dependency dependency,
            final long pingPeriod,
            @Nonnull final SystemReporter systemReporter
    ) {
        this(dependency, pingPeriod, DependencyChecker.newBuilder()
                .setExecutorService(executorService)
                .setLogger(log)
                .setSystemReporter(systemReporter)
                .build());
    }

    DependencyPinger (
            @Nonnull final Dependency dependency,
            final long pingPeriod,
            @Nonnull final DependencyChecker dependencyChecker
    ) {
        this.checker = dependencyChecker;
        this.dependency = dependency;
        this.pingPeriod = pingPeriod;

        VarExporter.forNamespace(DependencyPinger.class.getSimpleName() + "-" + this.dependency.getId()).includeInGlobal().export(this, "");
    }

    /**
     * The method intended to be executed by a worker thread; no result is returned directly here, it is simply
     *  cached for use by systems.
     *
     */
    @Override
    public void run () {
        // The result of a live execution
        CheckResult currentResult;

        synchronized(this) {
            try {
                lastExecuted = checker.getWallClock().currentTimeMillis();
                currentResult = checker.evaluate(dependency);

                if (null != currentResult && currentResult.getStatus() == CheckStatus.OK) {
                    currentResult = handleSuccess(currentResult);

                } else {
                    // Replace the result of the evaluation depending on the number of consecutive
                    //  failures, etc.
                    currentResult = handleFailure(currentResult, null);

                }

            } catch (final Throwable t) {
                currentResult = handleFailure(null, t);
            }

            notifyListeners(currentResult);
            lastResult = currentResult;
        }
    }

    /**
     * The method, derived from {@link Dependency} intended to be used by elements that check the status of the
     *  background task.
     */
    @Override
    @Nonnull
    public CheckResult call() {
        if (null == lastResult) {
            synchronized (this) {
                if (null == lastResult) {
                    run();
                }

                return checkNotNull(lastResult, "No result available after inline execution");
            }
        }

        // this will not be null, because lastResult goes from null -> notnull and never back to null
        //noinspection ConstantConditions
        return lastResult;
    }

    private void notifyListeners(@Nonnull final CheckResult currentResult) {
        updateHandler.onChecked(this, currentResult);

        // calling getStatus is safe because lastResult goes from null -> notnull and never back to null
        //noinspection ConstantConditions
        if (null == lastResult || lastResult.getStatus() != currentResult.getStatus()) {
            updateHandler.onChanged(this, lastResult, currentResult);
        }
    }

    private CheckResult handleSuccess(@Nonnull final CheckResult reportedResult) {
        if (consecutiveFailures.get() != 0) {
            consecutiveFailures.set(0);
        }
        totalSuccesses.incrementAndGet();
        lastDuration = checker.getWallClock().currentTimeMillis() - lastExecuted;
        lastKnownGood = lastExecuted;
        lastThrown = null;

        return CheckResult.newBuilder(this, reportedResult)
                .setTimestamp(lastExecuted)
                .setDuration(lastDuration)
                .setLastKnownGoodTimestamp(lastKnownGood)
                .setThrowable(null)
                .build();
    }

    private CheckResult handleFailure(@Nullable final CheckResult reportedResult, @Nullable final Throwable t) {
        consecutiveFailures.incrementAndGet();
        totalFailures.incrementAndGet();
        synchronized (failuresOverTime) {
            failuresOverTime.increment();
        }
        lastDuration = checker.getWallClock().currentTimeMillis() - lastExecuted;
        //noinspection ThrowableResultOfMethodCallIgnored
        lastThrown = null == reportedResult ? t  : (null == t ? reportedResult.getThrowable() : t);

        // The worst possible status this failure can generate. If the caller is using a full-fledged dependency rather than
        // an up/down exception-thrower, then we don't want to report an OUTAGE when it's simply been a MINOR degradation
        // for a while.
        final CheckStatus worstPossibleStatus = null == reportedResult ? CheckStatus.OUTAGE : reportedResult.getStatus();
        return newFailureNotice(worstPossibleStatus, reportedResult);
    }

    @Nonnull
    private CheckResult newFailureNotice(@Nonnull final CheckStatus outageStatus, @Nullable final CheckResult failedResult) {
        final CheckResult result;

        if (consecutiveFailures.get() < consecutiveFailureThreshold && totalSuccesses.get() > 0) {
            if (log.isDebugEnabled()) {
                log.debug ( "Most recent call to '" + dependency.getId() + "' was a failure, but recent pings succeeded. Noting impairment." );
            }

            // The most recent execution was a failure, but prior executions succeeded. Flag the
            //  target system as impaired on the assumption that it will respond properly on the
            //  next execution.
            result = CheckResult.newBuilder(this, CheckStatus.MINOR, getFailureMessage(failedResult, lastThrown))
                    .setTimestamp(lastExecuted)
                    .setDuration(lastDuration)
                    .setLastKnownGoodTimestamp(lastKnownGood)
                    .setPeriod(pingPeriod)
                    .setThrowable(lastThrown)
                    .build();

        } else {
            if (log.isDebugEnabled()) {
                log.debug ( "No recent pings to '" + getId() + "' have succeeded. Noting unavailability." );
            }

            // Use whatever the reported status was.
            result = CheckResult.newBuilder(this, outageStatus, getFailureMessage(failedResult, lastThrown))
                    .setTimestamp(lastExecuted)
                    .setDuration(lastDuration)
                    .setLastKnownGoodTimestamp(lastKnownGood)
                    .setPeriod(pingPeriod)
                    .setThrowable(lastThrown)
                    .build();
        }

        return result;
    }

    @Nonnull
    private static String getFailureMessage(@Nullable final CheckResult result, @Nullable final Throwable thrown) {
        if (null != thrown) {
            final String thrownMessage = thrown.getMessage();
            if (!Strings.isNullOrEmpty(thrownMessage)) {
                return thrownMessage;
            }
        }

        if (null != result) {
            final String resultMessage = result.getErrorMessage();
            if (!Strings.isNullOrEmpty(resultMessage)) {
                return resultMessage;
            }
        }

        // Fallback of fallbacks
        return "Timed out";
    }

    @Nonnull
    @Override
    public String toString() {
        return "background pinger for " + dependency;
    }

    @Export(name = "total-failures")
    public long getTotalFailures() {
        return totalFailures.get();
    }

    @Export(name = "total-successes")
    public long getTotalSuccesses() {
        return totalSuccesses.get();
    }

    @Export(name = "failures", doc = "Rolling count of recent failures")
    public String getFailures() {
        synchronized (failuresOverTime) {
            return failuresOverTime.toString();
        }
    }

    @Export(name = "consecutive-failures", doc = "Number of times failed consecutively")
    public int getConsecutiveFailures() { return consecutiveFailures.get(); }

    @Override
    public long getPingPeriod () {
        return pingPeriod;
    }

    @Override
    public String getId () {
        return dependency.getId();
    }

    @Override
    public String getDescription () {
        return dependency.getDescription();
    }

    @Override
    public String getDocumentationUrl () {
        return dependency.getDocumentationUrl();
    }

    @Override
    public long getTimeout () {
        return dependency.getTimeout();
    }

    @Override
    public Urgency getUrgency () {
        return dependency.getUrgency();
    }

    @Override
    public DependencyType getType () {
        return dependency.getType();
    }

    @Override
    public String getServicePool () {
        return dependency.getServicePool();
    }

    @Override
    public void clear () {
        updateHandler.clear();
    }

    @Override
    public void addListener (final StatusUpdateListener listener) {
        updateHandler.addListener(listener);
    }

    @Override
    public Iterator listeners() {
        return updateHandler.listeners();
    }

    // Access to the pinged dependency
    @Nonnull
    public Dependency getDependency() {
        return dependency;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy