/* * Copyright: Almende B.V. (2014), Rotterdam, The Netherlands * License: The Apache Software License, Version 2.0 */ package com.almende.util.callback; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.ReentrantLock; /** * Store to hold a map with callbacks in progress. * The Store handles timeouts on the callbacks. * * @param <T> * the generic type */ public class AsyncCallbackStore<T> { private final Map<Object, CallbackHandler> store = new ConcurrentHashMap<Object, CallbackHandler>( 5); private final TimeoutHandler head = new TimeoutHandler( null); private final Thread scanner; private boolean started = false; private TimeoutHandler tail = head; private int growsize = 10; private final static int MAXGROWSIZE = 20000; /** timeout in milliseconds */ private long timeout = 30000; /** * Instantiates a new async callback store. * * @param id * the id */ public AsyncCallbackStore(String id) { scanner = new Thread(new Runnable() { public void run() { while (true) { TimeoutHandler handler = tail; while (handler != null) { handler.checkTimeout(); handler = handler.prev; } try { Thread.sleep(1000); } catch (InterruptedException e) {} } } }, "AsyncCBScanner:" + id); } private void startScanner() { if (!started) { synchronized (scanner) { if (!started) { scanner.start(); started = true; } } } } private void grow() { synchronized (head) { if (growsize <= MAXGROWSIZE / 2) { growsize *= 2; } for (int i = 0; i < growsize; i++) { tail = new TimeoutHandler(tail); } } } private void put(final CallbackHandler handler) { TimeoutHandler spot = head; while (!spot.put(handler)) { if (spot == tail) { grow(); } spot = spot.next; } } class TimeoutHandler { private TimeoutHandler next = null; private TimeoutHandler prev = null; private CallbackHandler handler; private ReentrantLock lock = new ReentrantLock(); public TimeoutHandler(final TimeoutHandler prev) { if (prev != null) { this.prev = prev; prev.next = this; } } public boolean put(final CallbackHandler handler) { if (this.handler == null) { if (lock.tryLock()) { if (this.handler == null) { this.handler = handler; handler.parent = this; lock.unlock(); return true; } lock.unlock(); } } return false; } public void checkTimeout() { if (this.handler != null) { lock.lock(); if (this.handler != null && this.handler.callback != null && this.handler.timeout <= System.currentTimeMillis()) { this.handler.callback.onFailure(new TimeoutException( "Timeout occurred for callback with id '" + this.handler.id + "': " + this.handler.description)); this.handler = null; } lock.unlock(); } } public void forget() { lock.lock(); this.handler = null; lock.unlock(); } public CallbackHandler get() { return this.handler; } } /** * Place a callback in the store.. * The callback must be pulled from the store again within the * timeout. If not, the callback.onFailure will be called with a * TimeoutException as argument, and the callback will be deleted from the * store. * The method will throw an exception when a callback with the same id * is already in the store. * * @param id * the id * @param description * the description * @param callback * the callback */ public void put(final Object id, final String description, final AsyncCallback<T> callback) { startScanner(); if (store.containsKey(id)) { throw new IllegalStateException("Callback with id '" + id + "' already in queue"); } final CallbackHandler handler = new CallbackHandler(); handler.callback = callback; handler.id = id; handler.description = description; handler.timeout = System.currentTimeMillis() + timeout; put(handler); store.put(id, handler); } /** * Get a callback from the Store. The callback can be pulled from the * store only once. If no callback is found with given id, null will * be returned. * * @param id * the id * @return the async callback */ public AsyncCallback<T> get(final Object id) { final CallbackHandler handler = store.remove(id); if (handler != null) { handler.parent.forget(); return handler.callback; } return null; } /** * Remove all callbacks from the queue. */ public synchronized void clear() { store.clear(); } /** * Helper class to store a callback and its timeout task. */ private class CallbackHandler { private Object id; private String description; private AsyncCallback<T> callback; private long timeout; private TimeoutHandler parent; } /** * Gets the default callback timeout. * * @return the default timeout */ public int getTimeout() { return (int) (timeout / 1000); } /** * Sets the default callback timeout. * * @param timeout * the new timeout */ public void setTimeout(int timeout) { this.timeout = timeout * 1000; } }