com.bazaarvoice.ostrich.pool.MultiThreadedClientServiceCache Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ostrich-core Show documentation
Show all versions of ostrich-core Show documentation
Core classes that form Ostrich
package com.bazaarvoice.ostrich.pool;
import com.bazaarvoice.ostrich.MultiThreadedServiceFactory;
import com.bazaarvoice.ostrich.ServiceEndPoint;
import com.bazaarvoice.ostrich.ServiceFactory;
import com.bazaarvoice.ostrich.metrics.Metrics;
import com.codahale.metrics.Counter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static com.bazaarvoice.ostrich.pool.ServiceCacheBuilder.buildDefaultExecutor;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* A ServiceCache for "heavy weight" client instances, i.e. ones that are already thread safe.
* Therefore unlike {@link com.bazaarvoice.ostrich.pool.SingleThreadedClientServiceCache}, we
* can just map EndPoints to a single shared instance of a "heavy weight" client.
*
* This applies to third party client libraries for connecting to generic or specialized
* services, i.e. {@code HttpClient}, {@code JestClient}, {@code ElasticSearchClient} etc.
*
* If your client library is multi-thread safe, this ServiceCache should provide better
* performance than the {@link com.bazaarvoice.ostrich.pool.SingleThreadedClientServiceCache}.
*
* @param the Service type
*/
public class MultiThreadedClientServiceCache implements ServiceCache {
private static final Logger LOG = LoggerFactory.getLogger(MultiThreadedClientServiceCache.class);
// Grace period during which a new service instance will not be replaced by subsequent registrations
// of the same end point.
private static final long DUP_REGISTRATION_WINDOW_MILLIS = SECONDS.toMillis(1);
private static final int DEFAULT_CLEANUP_DELAY_SECONDS = 15;
private static final int DEFAULT_EVICTION_DELAY_SECONDS = (int) MINUTES.toSeconds(3);
/**
* We want to be able to perform more than 300 checkOuts and checkIns per second.
* Thus we would like those methods to use an un-synchronized / non-blocking Map implementation.
*
* In order to do that we use a Copy-On-Write approach where all modifications
* (new EndPoint, EndPoint goes away, etc) will create a new Map instance. This is acceptable as
* EndPoint addition / removal events are rare compared to checkIns and checkOuts.
*
* Because we are changing the pointer that is "_instancesPerEndpoint", we have to use the volatile
* keyword to force any thread that accesses the map to have the latest reference to it. There is a
* performance penalty associated with volatile, but it is better than synchronization.
*/
private volatile Map> _instancesPerEndpoint = Maps.newHashMap();
private volatile boolean _isClosed;
private final Metrics.InstanceMetrics _metrics;
private final Timer _registerTimer;
private final Timer _evictionTimer;
private final Counter _serviceCounter;
private final ServiceFactory _serviceFactory;
private final Future> _cleanupFuture;
private final ScheduledExecutorService _cleanupExecutor;
private final long _evictionDelayInMilliSeconds;
/**
* ServiceHandle that also tracks eviction and freshness status
*/
private class HeavyServiceHandle extends ServiceHandle {
private final long _sellByDate;
private volatile long _expireAfterDate = Long.MAX_VALUE;
public HeavyServiceHandle(T service, ServiceEndPoint endPoint) {
super(service, endPoint);
_sellByDate = System.currentTimeMillis() + DUP_REGISTRATION_WINDOW_MILLIS;
}
public boolean isOld() {
return System.currentTimeMillis() > _sellByDate;
}
public boolean hasBeenFlaggedForEviction() {
return _expireAfterDate != Long.MAX_VALUE;
}
public boolean timeToEvict() {
return _expireAfterDate != Long.MAX_VALUE && System.currentTimeMillis() > _expireAfterDate;
}
public void flagAsEvicted() {
if (_expireAfterDate == Long.MAX_VALUE) {
// Not synchronized - evictionDelay is long enough that thread contention here shouldn't matter
_expireAfterDate = System.currentTimeMillis() + _evictionDelayInMilliSeconds;
}
}
}
/**
* Builds a {@code MultiThreadedClientServiceCache} with a default executor and cleanup delay.
* Used by the builder.
*
* @param serviceFactory The service factory for creating service handles
* @param metricRegistry The metric registry for reporting metrics
*/
MultiThreadedClientServiceCache(MultiThreadedServiceFactory serviceFactory, MetricRegistry metricRegistry) {
this(serviceFactory, buildDefaultExecutor(), DEFAULT_EVICTION_DELAY_SECONDS, DEFAULT_CLEANUP_DELAY_SECONDS, metricRegistry);
}
/**
* Builds a {@code MultiThreadedClientServiceCache} with configurable eviction and cleanUp delays.
*
* @param serviceFactory The service factory for creating service handles
* @param executor The executor for creating the eviction list (cache) cleanup thread
* @param evictionDelayInSeconds how long to keep evicted handles around
* @param cleanUpDelayInSeconds how long to wait before scheduled cleanup
* @param metricRegistry The metric registry for reporting metrics
*/
@VisibleForTesting
MultiThreadedClientServiceCache(MultiThreadedServiceFactory serviceFactory, ScheduledExecutorService executor,
int evictionDelayInSeconds, int cleanUpDelayInSeconds, MetricRegistry metricRegistry) {
checkNotNull(serviceFactory);
checkNotNull(metricRegistry);
checkArgument(evictionDelayInSeconds >= 0);
checkArgument(cleanUpDelayInSeconds >= 0);
_serviceFactory = serviceFactory;
_cleanupExecutor = executor;
_evictionDelayInMilliSeconds = SECONDS.toMillis(evictionDelayInSeconds);
_isClosed = false;
_metrics = Metrics.forInstance(metricRegistry, this, serviceFactory.getServiceName());
_registerTimer = _metrics.timer("register-time");
_evictionTimer = _metrics.timer("eviction-time");
_serviceCounter = _metrics.counter("service-counter");
_cleanupFuture = _cleanupExecutor.scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
List> handlesToDelete = new LinkedList<>();
// Don't synchronize on just 'this', as 'this' is a anonymous inner Runnable class.
// Instead synchronize on our parent class instance of MultiThreadedClientServiceCache so that
// this code will be synchronized with evict() and doRegister().
synchronized (MultiThreadedClientServiceCache.this) {
Map> sourceCopy = Maps.newHashMap(_instancesPerEndpoint);
// Purge evicted instances from the copy-on-write map
for (HeavyServiceHandle handle : _instancesPerEndpoint.values()) {
if (handle.timeToEvict()) {
handlesToDelete.add(sourceCopy.remove(handle.getEndPoint()));
}
}
_instancesPerEndpoint = sourceCopy;
}
// Outside the synchronization loop, schedule the ServiceHandles for deletion
for (HeavyServiceHandle handle : handlesToDelete) {
destroyService(handle);
}
_serviceCounter.dec(handlesToDelete.size());
}
},
// In our unit tests we want to set the cleanup timeout to "zero", but executorService
// does not like that, so we set it to at least a millisecond.
SECONDS.toMillis(cleanUpDelayInSeconds) + 1,
SECONDS.toMillis(cleanUpDelayInSeconds) + 1,
TimeUnit.MILLISECONDS);
}
/**
* Mimics the behavior of a cache check in, actually a NO-OP.
*
* Since the {@code MultiThreadedClientServiceCache} does not have multiple service handles for an endPoint
*
* @param handle The service handle that is being checked in.
* @throws NullPointerException if the handle is null
*/
@Override
public void checkIn(ServiceHandle handle) throws Exception {
checkNotNull(handle);
}
/**
* Given an ServiceEndpoint return a ServiceHandle.
*
* If a ServiceHandle does not exist for the given ServiceEndpoint, this method will
* synchronously create one and return it.
*
* @param endPoint The end point to retrieve the instance of service handle for
* @return the service handle
* @throws IllegalStateException if the cache is closed
* @throws NullPointerException if endPoint is null
*/
@Override
public ServiceHandle checkOut(ServiceEndPoint endPoint) throws Exception {
checkNotNull(endPoint);
checkState(!_isClosed, "cache is closed");
ServiceHandle serviceHandle = _instancesPerEndpoint.get(endPoint);
if (serviceHandle == null) {
// This is the non-ideal state, as we now have to call a synchronized
// method to create the ServiceHandle and update the copy-on-write map.
//
// Note this can/will happen when new Endpoints are discovered due to the
// inherent race conditions in ServicePool and HostDiscovery.
return doRegister(endPoint);
}
// Note we are not checking if the serviceHandle has been flagged for Eviction, as
// there are race conditions between checkOut() and ServiceCache.evict().
return serviceHandle;
}
/**
* Private registration method that is used by checkout() and register().
*
* @param endPoint the end point
* @return the service handle
*/
private ServiceHandle doRegister(ServiceEndPoint endPoint) {
checkNotNull(endPoint);
ServiceHandle toDelete = null;
ServiceHandle toReturn;
synchronized (this) {
HeavyServiceHandle existingServiceHandle = _instancesPerEndpoint.get(endPoint);
if (existingServiceHandle == null || existingServiceHandle.hasBeenFlaggedForEviction() || existingServiceHandle.isOld()) {
// If there was not an existingServiceHandle, then make a new one.
//
// If existingServiceHandle.hasBeenFlaggedForEviction() is true, that means this EndPoint
// has been "evicted" for being "bad" but has recovered before the Eviction timeout
// process has gotten around to cleaning up this serviceHandle. In this case, we assume
// the "safest" thing to do is to create a new Client for that EndPoint, in case the
// problem was with the "old" client.
//
// If the existingServiceHandle is "new" don't create a new client object due to the
// race condition in HostDiscovery and ServicePool, which can cause a checkOut() to
// occur before its associated ServiceCache.register().
// Thus we want have a short period of time where duplicate "checkouts" and a register
// will not thrash the system creating a series of Client instances.
//
// _serviceFactory.create(endPoint) is a potentially expensive operation, memory, file handles, etc.
// hence we really only want to do it when we have to, preferably via the out-of-band
// ServiceCache.register() method, instead of the high traffic checkOut method.
S service = _serviceFactory.create(endPoint);
_serviceCounter.inc();
HeavyServiceHandle newServiceHandle = new HeavyServiceHandle<>(service, endPoint);
// add the newServiceHandle to a new copy of the _instancesPerEndpoint map
Map> sourceCopy = Maps.newHashMap(_instancesPerEndpoint);
sourceCopy.put(endPoint, newServiceHandle);
_instancesPerEndpoint = sourceCopy;
toDelete = existingServiceHandle;
toReturn = newServiceHandle;
} else {
// The existingServiceHandle was not null, not evicted, and not old, thus we did not recreate it.
toReturn = existingServiceHandle;
}
}
// Destroy instances outside the synchronization, with the idea being it may be an expensive operation
if (toDelete != null) {
destroyService(toDelete);
_serviceCounter.dec();
}
return toReturn;
}
@Override
public void register(ServiceEndPoint endPoint) {
checkNotNull(endPoint);
Timer.Context context = _registerTimer.time();
try {
doRegister(endPoint);
} catch (Exception ex) {
LOG.error("Error registering service handle", ex);
} finally {
context.stop();
}
}
@Override
public synchronized void evict(ServiceEndPoint endPoint) {
checkNotNull(endPoint);
Timer.Context context = _evictionTimer.time();
// This method is synchronized, as even though we are not swapping the copy-on-write map,
// we are still modifying its contents a bit.
HeavyServiceHandle serviceHandle = _instancesPerEndpoint.get(endPoint);
if (serviceHandle != null) {
serviceHandle.flagAsEvicted();
}
context.stop();
}
@Override
public synchronized void close() {
_isClosed = true;
for (ServiceHandle serviceHandle : _instancesPerEndpoint.values()) {
_serviceFactory.destroy(serviceHandle.getEndPoint(), serviceHandle.getService());
}
_instancesPerEndpoint = Maps.newHashMap();
_cleanupFuture.cancel(false);
_cleanupExecutor.shutdownNow();
_metrics.close();
}
/**
* As these clients are multi threaded single instance, there's always one available
*
* @param endPoint to find idle instance count
* @return 1 if endPoint is registered, 0 otherwise
*/
@Override
public int getNumIdleInstances(ServiceEndPoint endPoint) {
checkNotNull(endPoint);
return _instancesPerEndpoint.containsKey(endPoint) ? 1 : 0;
}
/**
* This does not track if an instance is actively being used, however given its
* singleton nature but it is safe to assume it is always being used
*
* @param endPoint to find active instance count
* @return 1 if endPoint is registered, 0 otherwise
*/
@Override
public int getNumActiveInstances(ServiceEndPoint endPoint) {
checkNotNull(endPoint);
return _instancesPerEndpoint.containsKey(endPoint) ? 1 : 0;
}
/**
* Destroys a service handle quietly, swallows any exception occurred
*
* @param serviceHandle to destroy
*/
private void destroyService(ServiceHandle serviceHandle) {
try {
_serviceFactory.destroy(serviceHandle.getEndPoint(), serviceHandle.getService());
} catch (Exception e) {
// this should not happen, but if it does, swallow the exception and log it
LOG.warn("Error destroying serviceHandle", e);
}
}
}