package com.netflix.discovery.shared.resolver;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.netflix.discovery.TimedSupervisorTask;
import com.netflix.servo.annotations.DataSourceType;
import com.netflix.servo.annotations.Monitor;
import com.netflix.servo.monitor.Monitors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static com.netflix.discovery.EurekaClientNames.METRIC_RESOLVER_PREFIX;
/**
* An async resolver that keeps a cached version of the endpoint list value for gets, and updates this cache
* periodically in a different thread.
*
* @author David Liu
*/
public class AsyncResolver<T extends EurekaEndpoint> implements ClosableResolver<T> {
private static final Logger logger = LoggerFactory.getLogger(AsyncResolver.class);
// Note that warm up is best effort. If the resolver is accessed by multiple threads pre warmup,
// only the first thread will block for the warmup (up to the configurable timeout).
private final AtomicBoolean warmedUp = new AtomicBoolean(false);
private final AtomicBoolean scheduled = new AtomicBoolean(false);
private final String name; // a name for metric purposes
private final ClusterResolver<T> delegate;
private final ScheduledExecutorService executorService;
private final ThreadPoolExecutor threadPoolExecutor;
private final TimedSupervisorTask backgroundTask;
private final AtomicReference<List<T>> resultsRef;
private final int refreshIntervalMs;
private final int warmUpTimeoutMs;
// Metric timestamp, tracking last time when data were effectively changed.
private volatile long lastLoadTimestamp = -1;
/**
* Create an async resolver with an empty initial value. When this resolver is called for the first time,
* an initial warm up will be executed before scheduling the periodic update task.
*/
public AsyncResolver(String name,
ClusterResolver<T> delegate,
int executorThreadPoolSize,
int refreshIntervalMs,
int warmUpTimeoutMs) {
this(
name,
delegate,
Collections.<T>emptyList(),
executorThreadPoolSize,
refreshIntervalMs,
warmUpTimeoutMs
);
}
/**
* Create an async resolver with a preset initial value. WHen this resolver is called for the first time,
* there will be no warm up and the initial value will be returned. The periodic update task will not be
* scheduled until after the first time getClusterEndpoints call.
*/
public AsyncResolver(String name,
ClusterResolver<T> delegate,
List<T> initialValues,
int executorThreadPoolSize,
int refreshIntervalMs) {
this(
name,
delegate,
initialValues,
executorThreadPoolSize,
refreshIntervalMs,
0
);
warmedUp.set(true);
}
/**
* @param delegate the delegate resolver to async resolve from
* @param initialValue the initial value to use
* @param executorThreadPoolSize the max number of threads for the threadpool
* @param refreshIntervalMs the async refresh interval
* @param warmUpTimeoutMs the time to wait for the initial warm up
*/
AsyncResolver(String name,
ClusterResolver<T> delegate,
List<T> initialValue,
int executorThreadPoolSize,
int refreshIntervalMs,
int warmUpTimeoutMs) {
this.name = name;
this.delegate = delegate;
this.refreshIntervalMs = refreshIntervalMs;
this.warmUpTimeoutMs = warmUpTimeoutMs;
this.executorService = Executors.newScheduledThreadPool(1,
new ThreadFactoryBuilder()
.setNameFormat("AsyncResolver-" + name + "-%d")
.setDaemon(true)
.build());
this.threadPoolExecutor = new ThreadPoolExecutor(
1, executorThreadPoolSize, 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), // use direct handoff
new ThreadFactoryBuilder()
.setNameFormat("AsyncResolver-" + name + "-executor-%d")
.setDaemon(true)
.build()
);
this.backgroundTask = new TimedSupervisorTask(
this.getClass().getSimpleName(),
executorService,
threadPoolExecutor,
refreshIntervalMs,
TimeUnit.MILLISECONDS,
5,
updateTask
);
this.resultsRef = new AtomicReference<>(initialValue);
Monitors.registerObject(name, this);
}
@Override
public void shutdown() {
if(Monitors.isObjectRegistered(name, this)) {
Monitors.unregisterObject(name, this);
}
executorService.shutdownNow();
threadPoolExecutor.shutdownNow();
backgroundTask.cancel();
}
@Override
public String getRegion() {
return delegate.getRegion();
}
@Override
public List<T> getClusterEndpoints() {
long delay = refreshIntervalMs;
if (warmedUp.compareAndSet(false, true)) {
if (!doWarmUp()) {
delay = 0;
}
}
if (scheduled.compareAndSet(false, true)) {
scheduleTask(delay);
}
return resultsRef.get();
}
/* visible for testing */ boolean doWarmUp() {
Future future = null;
try {
future = threadPoolExecutor.submit(updateTask);
future.get(warmUpTimeoutMs, TimeUnit.MILLISECONDS); // block until done or timeout
return true;
} catch (Exception e) {
logger.warn("Best effort warm up failed", e);
} finally {
if (future != null) {
future.cancel(true);
}
}
return false;
}
/* visible for testing */ void scheduleTask(long delay) {
executorService.schedule(
backgroundTask, delay, TimeUnit.MILLISECONDS);
}
@Monitor(name = METRIC_RESOLVER_PREFIX + "lastLoadTimestamp",
description = "How much time has passed from last successful async load", type = DataSourceType.GAUGE)
public long getLastLoadTimestamp() {
return lastLoadTimestamp < 0 ? 0 : System.currentTimeMillis() - lastLoadTimestamp;
}
@Monitor(name = METRIC_RESOLVER_PREFIX + "endpointsSize",
description = "How many records are the in the endpoints ref", type = DataSourceType.GAUGE)
public long getEndpointsSize() {
return resultsRef.get().size(); // return directly from the ref and not the method so as to not trigger warming
}
private final Runnable updateTask = new Runnable() {
@Override
public void run() {
try {
List<T> newList = delegate.getClusterEndpoints();
if (newList != null) {
resultsRef.getAndSet(newList);
lastLoadTimestamp = System.currentTimeMillis();
} else {
logger.warn("Delegate returned null list of cluster endpoints");
}
logger.debug("Resolved to {}", newList);
} catch (Exception e) {
logger.warn("Failed to retrieve cluster endpoints from the delegate", e);
}
}
};
}