com.bazaarvoice.ostrich.pool.ServicePoolBuilder Maven / Gradle / Ivy
Show all versions of ostrich-core Show documentation
package com.bazaarvoice.ostrich.pool;
import com.bazaarvoice.ostrich.HostDiscovery;
import com.bazaarvoice.ostrich.HostDiscoverySource;
import com.bazaarvoice.ostrich.LoadBalanceAlgorithm;
import com.bazaarvoice.ostrich.RetryPolicy;
import com.bazaarvoice.ostrich.ServiceFactory;
import com.bazaarvoice.ostrich.healthcheck.ExponentialBackoffHealthCheckRetryDelay;
import com.bazaarvoice.ostrich.healthcheck.HealthCheckRetryDelay;
import com.bazaarvoice.ostrich.loadbalance.RandomAlgorithm;
import com.bazaarvoice.ostrich.partition.IdentityPartitionFilter;
import com.bazaarvoice.ostrich.partition.PartitionFilter;
import com.bazaarvoice.ostrich.partition.PartitionKey;
import com.codahale.metrics.MetricRegistry;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.base.Ticker;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.io.IOException;
import java.util.List;
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 static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
public class ServicePoolBuilder {
private static final int DEFAULT_NUM_HEALTH_CHECK_THREADS = 1;
private static final HealthCheckRetryDelay DEFAULT_HEALTH_CHECK_RETRY_POLICY = new ExponentialBackoffHealthCheckRetryDelay(100, 10_000, TimeUnit.MILLISECONDS);
private final Class _serviceType;
private final List _hostDiscoverySources = Lists.newArrayList();
private boolean _closeHostDiscovery;
private ServiceFactory _serviceFactory;
private String _serviceName;
private ScheduledExecutorService _healthCheckExecutor;
private ServiceCachingPolicy _cachingPolicy;
private PartitionFilter _partitionFilter = new IdentityPartitionFilter();
private PartitionContextSupplier _partitionContextSupplier = new EmptyPartitionContextSupplier();
private LoadBalanceAlgorithm _loadBalanceAlgorithm = new RandomAlgorithm();
private MetricRegistry _metrics;
private ExecutorService _asyncExecutor;
private HealthCheckRetryDelay _healthCheckRetryDelay = DEFAULT_HEALTH_CHECK_RETRY_POLICY;
public static ServicePoolBuilder create(Class serviceType) {
return new ServicePoolBuilder<>(serviceType);
}
private ServicePoolBuilder(Class serviceType) {
_serviceType = checkNotNull(serviceType);
}
/**
* Adds a {@link HostDiscoverySource} instance to the builder. Multiple instances of {@code HostDiscoverySource}
* may be specified. The service pool will query the sources in the order they were registered and use the first
* non-null {@link HostDiscovery} returned for the service name provided by the
* {@link ServiceFactory#getServiceName()} method of the factory configured by {@link #withServiceFactory}.
*
* Note that using this method will cause the ServicePoolBuilder to call
* {@link HostDiscoverySource#forService(String serviceName)} when {@link #build()} is called and pass the returned
* {@link HostDiscovery} to the new {@code ServicePool}. Subsequently calling {@link ServicePool#close()} will in
* turn call {@link HostDiscovery#close()} on the passed instance.
*
* @param hostDiscoverySource a host discovery source to use to find the {@link HostDiscovery} when constructing
* the {@link ServicePool}
* @return this
*/
public ServicePoolBuilder withHostDiscoverySource(HostDiscoverySource hostDiscoverySource) {
checkNotNull(hostDiscoverySource);
return withHostDiscovery(hostDiscoverySource, true);
}
/**
* Adds a {@link HostDiscovery} instance to the builder. The service pool will use this {@code HostDiscovery}
* instance unless a preceding {@link HostDiscoverySource} provides a non-null instance of {@code HostDiscovery} for
* a given service name.
*
* Once this method is called, any subsequent calls to host discovery-related methods on this builder instance are
* ignored (because this non-null discovery will always be returned).
*
* Note that callers of this method are responsible for calling {@link HostDiscovery#close} on the passed instance.
*
* @param hostDiscovery the host discovery instance to use in the built {@link ServicePool}
* @return this
*/
public ServicePoolBuilder withHostDiscovery(final HostDiscovery hostDiscovery) {
checkNotNull(hostDiscovery);
HostDiscoverySource hostDiscoverySource = new HostDiscoverySource() {
@Override
public HostDiscovery forService(String serviceName) {
return hostDiscovery;
}
};
return withHostDiscovery(hostDiscoverySource, false);
}
private ServicePoolBuilder withHostDiscovery(HostDiscoverySource source, boolean close) {
_hostDiscoverySources.add(close
? new ClosingHostDiscoverySource(source)
: source);
return this;
}
/**
* Adds a {@code ServiceFactory} instance to the builder. The {@code ServiceFactory#configure} method will be
* called at this time to allow the {@code ServiceFactory} to set service pool settings on the builder.
*
* @param serviceFactory the ServiceFactory to use
* @return this
*/
public ServicePoolBuilder withServiceFactory(ServiceFactory serviceFactory) {
checkNotNull(serviceFactory);
checkArgument(!Strings.isNullOrEmpty(serviceFactory.getServiceName()), "Service name must be configured");
_serviceFactory = serviceFactory;
_serviceName = serviceFactory.getServiceName();
_serviceFactory.configure(this);
return this;
}
/**
* Adds a {@code ScheduledExecutorService} instance to the builder for use in executing health checks.
*
* Adding an executor is optional. If one isn't specified then one will be created and used automatically.
*
* @param executor The {@code ScheduledExecutorService} to use
* @return this
*/
public ServicePoolBuilder withHealthCheckExecutor(ScheduledExecutorService executor) {
_healthCheckExecutor = checkNotNull(executor);
return this;
}
/**
* Adds an {@code ExecutorService} instance to the builder for use in executing asynchronous requests. The executor
* is not used unless an asynchronous pool is built with the {@link #buildAsync} method.
*
* Adding an executor is optional. If one isn't specified then one will be created and used automatically when
* {@code buildAsync} is called.
*
* @param executor The {@code ExecutorService} to use
* @return this
*/
public ServicePoolBuilder withAsyncExecutor(ExecutorService executor) {
_asyncExecutor = checkNotNull(executor);
return this;
}
/**
* Enables caching of service instances in the built {@link ServicePool}.
*
* Specifying a caching policy is optional. If one isn't specified then a default one that doesn't cache service
* instances will be created and used automatically.
*
* @param policy The {@link ServiceCachingPolicy} to use
* @return this
*/
public ServicePoolBuilder withCachingPolicy(ServiceCachingPolicy policy) {
_cachingPolicy = checkNotNull(policy);
return this;
}
/**
* Uses the specified partition filter on every service pool operation to narrow down the set of end points that
* may be used to service a particular request.
*
* @param partitionFilter The {@link PartitionFilter} to use
* @return this
*/
public ServicePoolBuilder withPartitionFilter(PartitionFilter partitionFilter) {
_partitionFilter = checkNotNull(partitionFilter);
return this;
}
/**
* Makes the built proxy generate partition context based on the {@link PartitionKey} annotation
* on method arguments in class {@code S}.
*
* If {@code S} is not annotated, or annotated differently than desired, consider using
* {@link #withPartitionContextAnnotationsFrom(Class)} instead.
*
* NOTE: This is only useful if building a proxy with {@link #buildProxy(com.bazaarvoice.ostrich.RetryPolicy)}. If
* partition context is necessary with a normal service pool, then can be provided directly by calling
* {@link com.bazaarvoice.ostrich.ServicePool#execute(com.bazaarvoice.ostrich.PartitionContext,
* com.bazaarvoice.ostrich.RetryPolicy, com.bazaarvoice.ostrich.ServiceCallback)}.
*
* @return this
*/
public ServicePoolBuilder withPartitionContextAnnotations() {
return withPartitionContextAnnotationsFrom(_serviceType);
}
/**
* Uses {@link PartitionKey} annotations from the specified class to generate partition context in the built proxy.
*
* NOTE: This is only useful if building a proxy with {@link #buildProxy(com.bazaarvoice.ostrich.RetryPolicy)}. If
* partition context is necessary with a normal service pool, then can be provided directly by calling
* {@link com.bazaarvoice.ostrich.ServicePool#execute(com.bazaarvoice.ostrich.PartitionContext,
* com.bazaarvoice.ostrich.RetryPolicy, com.bazaarvoice.ostrich.ServiceCallback)}.
*
* @param annotatedServiceClass A service class with {@link PartitionKey} annotations.
* @return this
*/
public ServicePoolBuilder withPartitionContextAnnotationsFrom(Class extends S> annotatedServiceClass) {
checkNotNull(annotatedServiceClass);
_partitionContextSupplier = new AnnotationPartitionContextSupplier(_serviceType, annotatedServiceClass);
return this;
}
/**
* Sets the {@code LoadBalanceAlgorithm} that should be used for this service.
*
* @param algorithm A load balance algorithm to choose between available end points for the service.
* @return this
*/
public ServicePoolBuilder withLoadBalanceAlgorithm(LoadBalanceAlgorithm algorithm) {
_loadBalanceAlgorithm = checkNotNull(algorithm);
return this;
}
/** Sets the {@code MetricRegistry} that should be used for this service.
*
* @param metrics The metric registry to use.
* @return this
*/
public ServicePoolBuilder withMetricRegistry(MetricRegistry metrics) {
_metrics = checkNotNull(metrics);
return this;
}
/**
* Sets the {@code HealthCheckRetryPolicy} that should be used for this service.
*
* @param healthCheckRetryDelay retry policy to use
* @return this
*/
public ServicePoolBuilder withHealthCheckRetryPolicy(HealthCheckRetryDelay healthCheckRetryDelay) {
_healthCheckRetryDelay = checkNotNull(healthCheckRetryDelay);
return this;
}
/**
* Builds a {@code com.bazaarvoice.ostrich.ServicePool}.
*
* @return The {@code com.bazaarvoice.ostrich.ServicePool} that was constructed.
*/
public com.bazaarvoice.ostrich.ServicePool build() {
return buildInternal();
}
/**
* Builds a {@code com.bazaarvoice.ostrich.AsyncServicePool}.
*
* @return The {@code com.bazaarvoice.ostrich.AsyncServicePool} that was constructed.
*/
public com.bazaarvoice.ostrich.AsyncServicePool buildAsync() {
ServicePool pool = buildInternal();
boolean shutdownAsyncExecutorOnClose = (_asyncExecutor == null);
if (_asyncExecutor == null) {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat(_serviceName + "-AsyncExecutorThread-%d")
.setDaemon(true)
.build();
_asyncExecutor = Executors.newCachedThreadPool(threadFactory);
}
return new AsyncServicePool<>(Ticker.systemTicker(), pool, true, _asyncExecutor,
shutdownAsyncExecutorOnClose, _metrics);
}
/**
* Builds a dynamic proxy that wraps a {@code ServicePool} and implements the service interface directly. This is
* appropriate for stateless services where it's sensible for the same retry policy to apply to every method.
*
* It is the caller's responsibility to shutdown the service pool when they're done with it by casting the proxy
* to {@link java.io.Closeable} and calling the {@link java.io.Closeable#close()} method.
*
* @param retryPolicy The retry policy to apply for every service call.
* @return The dynamic proxy instance that implements the service interface {@code S} and the
* {@link java.io.Closeable} interface.
*/
public S buildProxy(RetryPolicy retryPolicy) {
return ServicePoolProxy.create(_serviceType, retryPolicy, build(), _partitionContextSupplier, true);
}
@VisibleForTesting
ServicePool buildInternal() {
checkNotNull(_serviceFactory);
checkNotNull(_metrics);
HostDiscovery hostDiscovery = findHostDiscovery(_serviceName);
boolean shutdownHealthCheckExecutorOnClose = (_healthCheckExecutor == null);
try {
if (_cachingPolicy == null) {
_cachingPolicy = ServiceCachingPolicyBuilder.NO_CACHING;
}
if (_healthCheckExecutor == null) {
_healthCheckExecutor = Executors.newScheduledThreadPool(DEFAULT_NUM_HEALTH_CHECK_THREADS,
new ThreadFactoryBuilder()
.setNameFormat(_serviceName + "-HealthCheckThread-%d")
.setDaemon(true)
.build());
}
ServicePool servicePool = new ServicePool<>(Ticker.systemTicker(), hostDiscovery, _closeHostDiscovery,
_serviceFactory, _cachingPolicy, _partitionFilter, _loadBalanceAlgorithm, _healthCheckExecutor,
shutdownHealthCheckExecutorOnClose, _healthCheckRetryDelay, _metrics);
_closeHostDiscovery = false;
return servicePool;
} catch (Throwable t) {
if (shutdownHealthCheckExecutorOnClose && _healthCheckExecutor != null) {
_healthCheckExecutor.shutdownNow();
_healthCheckExecutor = null;
}
try {
if (_closeHostDiscovery) {
hostDiscovery.close();
}
} catch (IOException e) {
// NOP
} finally {
_closeHostDiscovery = false;
}
throw Throwables.propagate(t);
}
}
private HostDiscovery findHostDiscovery(String serviceName) {
for (HostDiscoverySource source : _hostDiscoverySources) {
HostDiscovery hostDiscovery = source.forService(serviceName);
if (hostDiscovery != null) {
return hostDiscovery;
}
}
throw new IllegalStateException(format("No HostDiscovery found for service: %s", serviceName));
}
private class ClosingHostDiscoverySource implements HostDiscoverySource {
private HostDiscoverySource _wrappedSource;
public ClosingHostDiscoverySource(HostDiscoverySource wrappedSource) {
_wrappedSource = wrappedSource;
}
@Override
public HostDiscovery forService(String serviceName) {
HostDiscovery hostDiscovery = _wrappedSource.forService(serviceName);
if (hostDiscovery != null) {
_closeHostDiscovery = true;
}
return hostDiscovery;
}
}
}