/* * $Id$ * * Copyright (c) 2007-2008 by Joel Uckelman * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.tools.opcache; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.jdesktop.swingworker.SwingWorker; import VASSAL.tools.ErrorDialog; import VASSAL.tools.concurrent.ConcurrentSoftHashMap; /** * A memory-sensitive cache for {@link Op}s and their results. * * @since 3.1.0 * @author Joel Uckelman */ public class OpCache { /** * A cache key for <code>OpCache</code>. */ public static class Key<V> { public final Op<V> op; public final int version; public final List<Key<?>> deps = new ArrayList<Key<?>>(); private final int hash; /** * Creates a new key for the given <code>Op</code> and version. * * @param op the <code>Op</code> for which this is a key * @param version the current version of this key */ public Key(Op<V> op, int version) { this.op = op; this.version = version; for (Op<?> dop : op.getSources()) deps.add(dop.newKey()); hash = op.hashCode() ^ version ^ deps.hashCode(); } /** {@inheritDoc} */ @Override public boolean equals(Object o) { if (o == this) return true; if (o.getClass() != this.getClass()) return false; final Key<?> k = (Key<?>) o; return version == k.version && op.equals(k.op) && deps.equals(k.deps); } /** {@inheritDoc} */ @Override public int hashCode() { return hash; } /** {@inheritDoc} */ @Override public String toString() { return this.getClass().getName() + "[op=" + op + ",version=" + version + "]"; } } protected final ConcurrentMap<Key<?>,Future<?>> cache = new ConcurrentSoftHashMap<Key<?>,Future<?>>(); /** * A request for execution of an {@link Op} which will be completed * synchronously and set manually. */ private static final class Result<V> implements Future<V> { private static final long serialVersionUID = 1L; private V value = null; private boolean failed = false; private final CountDownLatch done = new CountDownLatch(1); public void set(V value) { this.value = value; done.countDown(); } public void fail() { failed = true; } public boolean cancel(boolean mayInterruptIfRunning) { return false; } public boolean isCancelled() { return false; } public boolean isDone() { return done.getCount() == 0; } public V get() throws InterruptedException, ExecutionException { done.await(); if (failed) throw new ExecutionException(new OpFailedException()); return value; } public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { if (done.await(timeout, unit)) { if (failed) throw new ExecutionException(new OpFailedException()); return value; } throw new TimeoutException(); } } /** * The {@link Future} which is cached on failure of an {@link Op}. */ private static final Future<Void> failure = new Future<Void>() { public boolean cancel(boolean mayInterruptIfRunning) { return false; } public Void get() throws ExecutionException { throw new ExecutionException(new OpFailedException()); } public Void get(long timeout, TimeUnit unit) throws ExecutionException { throw new ExecutionException(new OpFailedException()); } public boolean isCancelled() { return false; } public boolean isDone() { return true; } }; /** * A request for execution of an {@link Op}, to be queued. */ private class Request<V> extends SwingWorker<V,Void> { private final Key<V> key; private final OpObserver<V> obs; public Request(Key<V> key, OpObserver<V> obs) { if (key == null) throw new IllegalArgumentException(); if (obs == null) throw new IllegalArgumentException(); this.key = key; this.obs = obs; } @Override protected V doInBackground() throws Exception { return key.op.eval(); } @Override protected void done() { try { final V val = get(); if (obs != null) obs.succeeded(key.op, val); } catch (CancellationException e) { cache.remove(key, this); if (obs != null) obs.cancelled(key.op, e); } catch (InterruptedException e) { cache.remove(key, this); if (obs != null) obs.interrupted(key.op, e); } catch (ExecutionException e) { cache.replace(key, this, failure); if (obs != null) obs.failed(key.op, e); } } } /** * Gets a value from the cache. * * @param key the <code>Key</code> for which to retrieve a value * @return the value associated with <code>key</code> */ public <V> V get(Key<V> key) { try { return get(key, null); } catch (CancellationException e) { // FIXME: bug until we permit cancellation ErrorDialog.bug(e); } catch (InterruptedException e) { ErrorDialog.bug(e); } catch (ExecutionException e) { ErrorDialog.bug(e); } return null; } /** * Gets a value from the cache, possibly asynchronously. * * @param key the <code>Key</code> for which to retrieve a value * @param obs the <code>OpObserver</code> to notify when the value is * available * @return the value associated with <code>key</code> * * @throws CancellationException if the request is cancelled * @throws InterruptedException if the request is interrupted * @throws ExecutionException if the request fails */ public <V> V get(Key<V> key, OpObserver<V> obs) throws CancellationException, InterruptedException, ExecutionException { Future<V> fut = getFuture(key, obs); // We block on the op when there is no observer, and // return right away if the op is already done. if (obs == null || fut.isDone()) { try { return fut.get(); } catch (CancellationException e) { cache.remove(key, fut); throw (CancellationException) new CancellationException().initCause(e); } catch (InterruptedException e) { cache.remove(key, fut); throw (InterruptedException) new InterruptedException().initCause(e); } catch (ExecutionException e) { cache.replace(key, fut, failure); throw new ExecutionException(e); } } // We have an observer and the op is still running. return null; } /** * Gets a {@link Future} from the cache. If <code>obs == null</code>, then * the {@link Op} associated with <code>key</code> will be executed * synchronously, and asynchronously otherwise. * * @param key the <code>Key</code> for which to retrieve a * <code>Future</code> * @param obs the <code>OpObserver</code> to notify when the value is * available * @return the <code>Future</code> associated with <code>key</code> * * @throws ExecutionException if the request is synchronous and fails */ @SuppressWarnings("unchecked") public <V> Future<V> getFuture(Key<V> key, OpObserver<V> obs) throws ExecutionException { // The code in this method was inspired by the article at // http://www.javaspecialists.eu/archive/Issue125.html. Future<V> fut = (Future<V>) cache.get(key); if (fut == null) { if (obs == null) { // check whether any other op has beat us into the cache final Result<V> res = new Result<V>(); fut = (Future<V>) cache.putIfAbsent(key, res); // if not, then apply the op if (fut == null) { V val = null; try { val = key.op.eval(); } catch (Throwable t) { res.fail(); cache.put(key, failure); throw new ExecutionException(t); } finally { res.set(val); } fut = res; } } else { final Request<V> req = new Request<V>(key, obs); fut = (Future<V>) cache.putIfAbsent(key, req); if (fut == null) { threadPool.submit(req); fut = req; } } } else { // Are we a synchronous request in the queue being re-requested? if (obs == null && fut instanceof Runnable) { if (requestQueue.remove(fut)) { // Then run on this thread to prevent deadlock. ((Runnable) fut).run(); } } } return fut; } ///// // FIXME: finalize this... private final BlockingQueue<Runnable> requestQueue = new LinkedBlockingQueue<Runnable>(); private static class Ex extends ThreadPoolExecutor { public Ex(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } public <V> Future<V> submit(SwingWorker<V,?> req) { execute(req); return req; } } private final Ex threadPool = new Ex(2, 2, 60, TimeUnit.SECONDS, requestQueue); ///// /** * Gets a value from the cache, if it is already calculated. * * @param key the <code>Key</code> for which to retrieve a value * @return the value associated with <code>key</code>, or <code>null</code> */ @SuppressWarnings("unchecked") public <V> V getIfDone(Key<V> key) { final Future<V> fut = (Future<V>) cache.get(key); if (fut != null && fut.isDone()) { try { return fut.get(); } catch (CancellationException e) { } catch (InterruptedException e) { } catch (ExecutionException e) { } } return null; } public void clear() { // FIXME: should cancel all pending requests? cache.clear(); } }