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

com.elastisys.scale.cloudpool.commons.basepool.poolfetcher.impl.CachingPoolFetcher Maven / Gradle / Ivy

package com.elastisys.scale.cloudpool.commons.basepool.poolfetcher.impl;

import static com.elastisys.scale.cloudpool.commons.basepool.alerts.AlertTopics.POOL_FETCH;
import static java.lang.String.format;

import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.elastisys.scale.cloudpool.api.CloudPoolException;
import com.elastisys.scale.cloudpool.api.types.MachinePool;
import com.elastisys.scale.cloudpool.commons.basepool.StateStorage;
import com.elastisys.scale.cloudpool.commons.basepool.config.PoolFetchConfig;
import com.elastisys.scale.cloudpool.commons.basepool.poolfetcher.FetchOption;
import com.elastisys.scale.cloudpool.commons.basepool.poolfetcher.PoolFetcher;
import com.elastisys.scale.commons.json.persistence.PersistentState;
import com.elastisys.scale.commons.json.types.TimeInterval;
import com.elastisys.scale.commons.net.alerter.Alert;
import com.elastisys.scale.commons.net.alerter.AlertBuilder;
import com.elastisys.scale.commons.net.alerter.AlertSeverity;
import com.elastisys.scale.commons.net.alerter.Alerter;
import com.elastisys.scale.commons.util.time.UtcTime;
import com.google.common.eventbus.EventBus;
import com.google.common.util.concurrent.Atomics;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

/**
 * A {@link PoolFetcher} that caches {@link MachinePool}s retrieved by a wrapped
 * {@link PoolFetcher} for a configurable time (thereby also masking failures to
 * retrieve pool members from the backing cloud API).
 *
 */
public class CachingPoolFetcher implements PoolFetcher {
	private static final Logger LOG = LoggerFactory
			.getLogger(CachingPoolFetcher.class);

	/** Wrapped {@link PoolFetcher} to delegate actual fetching to. */
	private final PoolFetcher delegate;
	/** Controls fetch behavior. */
	private final PoolFetchConfig fetchConfig;
	/** The last pool fetch error. */
	private final AtomicReference lastFetchError;
	/**
	 * {@link CountDownLatch} that can be used to wait for the first fetch
	 * attempt to complete (successful or not). See {@link #awaitFirstFetch()}.
	 */
	private final CountDownLatch firstFetchComplete;

	/**
	 * {@link EventBus} used to post {@link Alert} events that are to be
	 * forwarded by configured {@link Alerter}s (if any).
	 */
	private final EventBus eventBus;

	private final PersistentState cachedMachinePool;
	/** {@link ExecutorService} handling execution of cache updates. */
	private final ScheduledExecutorService executorService;

	/**
	 * Creates a {@link CachingPoolFetcher} with a given {@link PoolFetcher}
	 * delegate and configuration. The first attempt to fetch the machine pool
	 * will be executed immediately and can be waited for using
	 * {@link #awaitFirstFetch()}.
	 *
	 * @param delegate
	 *            Wrapped {@link PoolFetcher} to delegate actual fetching to.
	 * @param fetchConfig
	 *            Controls fetch behavior.
	 */
	public CachingPoolFetcher(StateStorage stateStorage, PoolFetcher delegate,
			PoolFetchConfig fetchConfig, EventBus eventBus) {
		this.delegate = delegate;
		this.fetchConfig = fetchConfig;
		this.eventBus = eventBus;

		this.cachedMachinePool = new PersistentState(
				stateStorage.getCachedMachinePoolFile(), MachinePool.class);
		if (this.cachedMachinePool.get().isPresent()) {
			LOG.info("recovered cached machine pool: {}",
					this.cachedMachinePool.get().get());
		} else {
			LOG.info("no previously stored machine pool found.");
		}
		this.lastFetchError = Atomics.newReference();
		this.firstFetchComplete = new CountDownLatch(1);

		// start periodical cache update task
		this.executorService = createExecutor();
		startPeriodicalFetch(fetchConfig);

		LOG.debug("started {}", getClass().getSimpleName());
	}

	private void startPeriodicalFetch(PoolFetchConfig fetchConfig) {
		TimeInterval refreshInterval = fetchConfig.getRefreshInterval();
		this.executorService.scheduleWithFixedDelay(new PoolRefreshTask(this),
				0L, refreshInterval.getTime(), refreshInterval.getUnit());
	}

	private ScheduledExecutorService createExecutor() {
		ThreadFactory threadFactory = new ThreadFactoryBuilder().setDaemon(true)
				.setNameFormat("pool-fetcher-%d").build();
		return Executors.newSingleThreadScheduledExecutor(threadFactory);
	}

	/**
	 * Waits for the first pool fetch attempt to complete. The method returns
	 * when the first attempt has completed (successful or not).
	 *
	 * @throws InterruptedException
	 */
	public CachingPoolFetcher awaitFirstFetch() {
		try {
			this.firstFetchComplete.await();
			return this;
		} catch (InterruptedException e) {
			throw new RuntimeException(String.format(
					"interrupted while waiting for first pool fetch: %s",
					e.getMessage()), e);
		}
	}

	@Override
	public void close() {
		// stop periodical execution of cache update task
		try {
			LOG.debug("shutting down {} ...", getClass().getSimpleName());
			this.executorService.shutdown();
			this.executorService.awaitTermination(3, TimeUnit.SECONDS);
			this.executorService.shutdownNow();
		} catch (InterruptedException e) {
			LOG.warn("failed to shut down {}: {}", getClass().getSimpleName(),
					e.getMessage());
		}
	}

	@Override
	public MachinePool get(FetchOption... options) throws CloudPoolException {
		if (forceRefresh(options)) {
			refreshCache();
			return this.cachedMachinePool.get().get();
		}

		if (cacheEmpty()) {
			LOG.debug("no machine pool in cache yet. failing ...");
			poolUnreachableFailure();
		}

		if (reachabilityTimeoutExceeded(this.cachedMachinePool.get().get())) {
			// pool has not been reachable since reachabilityTimeout
			LOG.debug(
					"cached pool older than reachabilityTimeout. failing ...");
			reachabilityTimeoutFailure();
		}

		MachinePool cachedPool = this.cachedMachinePool.get().get();
		LOG.debug("responding with cached machine pool: {}", cachedPool);
		return cachedPool;
	}

	private void reachabilityTimeoutFailure() {
		throw new PoolReachabilityTimeoutException(String.format(
				"Could not serve a sufficiently up-to-date machine pool (%d %s). "
						+ "Cloud API presumably unreachable.",
				reachabilityTimeout().getTime(),
				reachabilityTimeout().getUnit().name().toLowerCase()));
	}

	private void poolUnreachableFailure() {
		Throwable lastError = this.lastFetchError.get();
		if (lastError != null) {
			throw new PoolUnreachableException(String.format(
					"Could not serve machine pool: no fetch attempt "
							+ "has been successful yet. Latest error: %s",
					lastError.getMessage()), lastError);
		} else {
			throw new PoolUnreachableException(
					"Could not serve machine pool: no fetch attempt has completed yet.");
		}
	}

	private boolean cacheEmpty() {
		return !this.cachedMachinePool.get().isPresent();
	}

	private TimeInterval reachabilityTimeout() {
		return this.fetchConfig.getReachabilityTimeout();
	}

	/**
	 * Determines if the cached {@link MachinePool} is too old. That is, returns
	 * true if the reachability timeout (or maximum fault masking
	 * time) has been exceeded.
	 *
	 * @param machinePool
	 * @return
	 */
	private boolean reachabilityTimeoutExceeded(MachinePool machinePool) {
		DateTime now = UtcTime.now();

		DateTime cacheTimestamp = machinePool.getTimestamp();
		long cacheAgeSeconds = new Duration(cacheTimestamp, now)
				.getStandardSeconds();

		TimeInterval reachabilityTimeout = reachabilityTimeout();
		long maxAgeSeconds = TimeUnit.SECONDS.convert(
				reachabilityTimeout.getTime(), reachabilityTimeout.getUnit());

		return cacheAgeSeconds >= maxAgeSeconds;
	}

	private boolean forceRefresh(FetchOption... options) {
		return options != null
				&& Arrays.asList(options).contains(FetchOption.FORCE_REFRESH);
	}

	/**
	 * Forces a refresh of the cached machine pool. In case of failure, an
	 * {@link Alert} is posted on the {@link EventBus} and a
	 * {@link CloudPoolException} is thrown.
	 *
	 * @throws CloudPoolException
	 */
	void refreshCache() throws CloudPoolException {
		LOG.debug("refreshing cached cloud pool ...");
		try {
			this.cachedMachinePool
					.update(this.delegate.get(FetchOption.FORCE_REFRESH));
		} catch (Throwable e) {
			this.lastFetchError.set(e);
			String message = format("machine pool refresh failed: %s",
					e.getMessage());
			Alert alert = AlertBuilder.create().topic(POOL_FETCH.name())
					.severity(AlertSeverity.WARN).message(message).build();
			this.eventBus.post(alert);
			LOG.warn(message, e);
			throw new CloudPoolException(message, e);
		} finally {
			this.firstFetchComplete.countDown();
		}
	}

	/** Task that, when executed, updates the machine pool cache. */
	public static class PoolRefreshTask implements Runnable {
		private final CachingPoolFetcher poolFetcher;

		public PoolRefreshTask(CachingPoolFetcher poolFetcher) {
			this.poolFetcher = poolFetcher;
		}

		@Override
		public void run() {
			try {
				this.poolFetcher.refreshCache();
			} catch (Exception e) {
				// just catch exception to prevent periodical execution from
				// aborting
			}
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy