package co.codewizards.cloudstore.core.concurrent; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.config.Config; import co.codewizards.cloudstore.core.config.ConfigImpl; import co.codewizards.cloudstore.core.util.AssertUtil; public class DeferrableExecutor { private static final Logger logger = LoggerFactory.getLogger(DeferrableExecutor.class); /** * The {@code key} for the timeout used with {@link Config#getPropertyAsInt(String, int)}. * <p> * The configuration can be overridden by a system property - see {@link Config#SYSTEM_PROPERTY_PREFIX}. */ public static final String CONFIG_KEY_TIMEOUT = "deferrableExecutor.timeout"; //$NON-NLS-1$ private static final int DEFAULT_TIMEOUT = 60 * 1000; /** * The {@code key} for the expiry period used with {@link Config#getPropertyAsInt(String, int)}. * <p> * The configuration can be overridden by a system property - see {@link Config#SYSTEM_PROPERTY_PREFIX}. */ public static final String CONFIG_KEY_EXPIRY_PERIOD = "deferrableExecutor.expiryPeriod"; //$NON-NLS-1$ private static final int DEFAULT_EXPIRY_PERIOD = 60 * 60 * 1000; private final Map<String, WeakReference<String>> canonicalCallIdentifierMap = new WeakHashMap<String, WeakReference<String>>(); private final Map<String, Future<?>> callIdentifier2Future = Collections.synchronizedMap(new HashMap<String, Future<?>>()); private final Map<String, Date> callIdentifier2DoneDate = Collections.synchronizedMap(new WeakHashMap<String, Date>()); private final ExecutorService executorService = Executors.newCachedThreadPool(); private final Timer cleanUpExpiredEntriesTimer = new Timer("cleanUpExpiredEntriesTimer", true); private TimerTask cleanUpExpiredEntriesTimerTask; private int lastExpiryPeriod; private DeferrableExecutor() { } private static final class RunnableWithProgressExecutorHolder { private static final DeferrableExecutor instance = new DeferrableExecutor(); } public static DeferrableExecutor getInstance() { return RunnableWithProgressExecutorHolder.instance; } // TODO maybe we should make it possible to pass the timeout from the client, because // the client knows its socket's read-timeout. @SuppressWarnings("unchecked") public <V> V call(String callIdentifier, final CallableProvider<V> callableProvider) throws DeferredCompletionException, ExecutionException { AssertUtil.assertNotNull(callIdentifier, "callIdentifier"); AssertUtil.assertNotNull(callableProvider, "callableProvider"); final int timeout = ConfigImpl.getInstance().getPropertyAsPositiveOrZeroInt(CONFIG_KEY_TIMEOUT, DEFAULT_TIMEOUT); cleanUpExpiredEntries(); callIdentifier = canonicalizeCallIdentifier(callIdentifier); synchronized (callIdentifier) { Future<?> future = callIdentifier2Future.get(callIdentifier); if (future == null) { Callable<V> callable = callableProvider.getCallable(); future = executorService.submit(new CallableWrapper<V>(callIdentifier, callable)); callIdentifier2Future.put(callIdentifier, future); } Object result; try { result = future.get(timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { throw new DeferredCompletionException(e); } catch (TimeoutException e) { throw new DeferredCompletionException(e); } catch (java.util.concurrent.ExecutionException e) { callIdentifier2Future.remove(callIdentifier); // remove in case of failure (keep while still running) throw new ExecutionException(e); } callIdentifier2Future.remove(callIdentifier); // remove in case of successful completion (keep while still running) return (V) result; } } private class CallableWrapper<V> implements Callable<V> { private final String identifier; private final Callable<V> delegate; public CallableWrapper(String identifier, Callable<V> delegate) { this.identifier = AssertUtil.assertNotNull(identifier, "identifier"); this.delegate = AssertUtil.assertNotNull(delegate, "delegate"); } @Override public V call() throws Exception { try { return delegate.call(); } finally { callIdentifier2DoneDate.put(identifier, new Date()); } } } private String canonicalizeCallIdentifier(String callIdentifier) { synchronized (canonicalCallIdentifierMap) { WeakReference<String> ref = canonicalCallIdentifierMap.get(callIdentifier); String ci = ref == null ? null : ref.get(); if (ci == null) { ci = callIdentifier; canonicalCallIdentifierMap.put(ci, new WeakReference<String>(ci)); } return ci; } } private void cleanUpExpiredEntries() { rescheduleExpiredEntriesTimerTaskIfExpiryPeriodChanged(); List<String> expiredCallIdentifiers = new LinkedList<String>(); Date expireDoneBeforeDate = new Date(System.currentTimeMillis() - getExpiryPeriod()); synchronized (callIdentifier2DoneDate) { for (Map.Entry<String, Date> me : callIdentifier2DoneDate.entrySet()) { if (me.getValue().before(expireDoneBeforeDate)) expiredCallIdentifiers.add(me.getKey()); } } for (String callIdentifier : expiredCallIdentifiers) { synchronized (callIdentifier) { callIdentifier2Future.remove(callIdentifier); callIdentifier2DoneDate.remove(callIdentifier); } } } private void rescheduleExpiredEntriesTimerTaskIfExpiryPeriodChanged() { synchronized (cleanUpExpiredEntriesTimer) { final int expiryPeriod = getExpiryPeriod(); if (cleanUpExpiredEntriesTimerTask == null || lastExpiryPeriod != expiryPeriod) { if (cleanUpExpiredEntriesTimerTask != null) cleanUpExpiredEntriesTimerTask.cancel(); scheduleExpiredEntriesTimerTask(); } } } private void scheduleExpiredEntriesTimerTask() { synchronized (cleanUpExpiredEntriesTimer) { final int expiryPeriod = getExpiryPeriod(); lastExpiryPeriod = expiryPeriod; cleanUpExpiredEntriesTimerTask = new TimerTask() { @Override public void run() { cleanUpExpiredEntries(); } }; cleanUpExpiredEntriesTimer.schedule(cleanUpExpiredEntriesTimerTask, expiryPeriod / 2, expiryPeriod / 2); } } private int getExpiryPeriod() { return ConfigImpl.getInstance().getPropertyAsPositiveOrZeroInt(CONFIG_KEY_EXPIRY_PERIOD, DEFAULT_EXPIRY_PERIOD); } }