new utility for updated cached values in a multi-threaded environment.
authorerik <erik@77661180-640e-0410-b3a8-9f9b13e6d0e0>
Mon, 10 May 2010 21:56:50 +0000 (21:56 +0000)
committererik <erik@77661180-640e-0410-b3a8-9f9b13e6d0e0>
Mon, 10 May 2010 21:56:50 +0000 (21:56 +0000)
support/general/src/main/java/org/wamblee/cache/ComputedValue.java [new file with mode: 0644]
support/general/src/test/java/org/wamblee/cache/ComputedValueTest.java [new file with mode: 0644]
support/general/src/test/java/org/wamblee/concurrency/ReadWriteLockTest.java

diff --git a/support/general/src/main/java/org/wamblee/cache/ComputedValue.java b/support/general/src/main/java/org/wamblee/cache/ComputedValue.java
new file mode 100644 (file)
index 0000000..24bbe5b
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2005-2010 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.wamblee.cache;
+
+/**
+ * Utility class to deal with recomputation of a certain value. The goal is to
+ * have only one thread at a time compute the value while other threads that
+ * simulateneously detect recomputation continue with the old value.
+ * 
+ * 
+ * @author Erik Brakkee
+ * 
+ * @param <T>
+ */
+public class ComputedValue<T> {
+
+    /**
+     * Computation
+     * 
+     * @param <T>
+     *            Type of object to compute.
+     */
+    public static interface Computation<T> {
+        /**
+         * Checks whether the object is out of date. This will be invoked while
+         * holding the lock passed at construction of the compute guard.
+         * 
+         * Any runtime exceptions thrown are passed back through the
+         * {@link ComputedValue#get()}.
+         * 
+         * @return True iff recomputation is necessary.
+         */
+        boolean isOutOfDate();
+
+        /**
+         * Computes the object. This will be invoked while <em>not</em> holding
+         * the lock passed at construction of the compute guard. It is
+         * guaranteed that per ComputeGuard, no concurrent calls to compute()
+         * are done.
+         * 
+         * Any runtime exceptions thrown are passed back through the
+         * {@link ComputedValue#get()}.
+         * 
+         * @return Computed object.
+         */
+        T compute();
+    }
+
+    private Object lock;
+    private Computation<T> computedValue;
+
+    private Boolean busy;
+    private T value; 
+
+    /**
+     * Constructs the compute guard
+     * 
+     * @param aLock
+     *            Lock to use during computation and to guard the value.
+     * @param aComputation
+     *            Computation to use.
+     */
+    public ComputedValue(Object aLock, Computation aComputation) {
+        lock = aLock;
+        computedValue = aComputation;
+        busy = false;
+        value = null; 
+    }
+
+    /**
+     * Triggers computation of the value (if no other thread is currently
+     * computing the value).
+     */
+    public void compute() {
+        synchronized (this) {
+            if (busy) {
+                return; // another thread is already taking care of it.
+            }
+            busy = true;
+        }
+        try {
+            T newvalue = computedValue.compute();
+            set(newvalue);
+        } finally {
+            synchronized (this) {
+                busy = false;
+            }
+        }
+    }
+
+    /**
+     * Gets the current value of the object, recomputing it if the object is out
+     * of date. This method ensures that only one thread at a time will do
+     * recomputations.
+     * 
+     * @return Current value.
+     */
+    public T get() {
+        boolean mustCompute = false;
+        synchronized (lock) {
+            if (computedValue.isOutOfDate()) {
+                mustCompute = true;
+            }
+        }
+        if (mustCompute) {
+            compute();
+        }
+        synchronized (lock) {
+            return value;
+        }
+    }
+    
+    /**
+     * Gets the currently cached value. No recomputation is performed. 
+     * @return Cached value. 
+     */
+    public T getCached() { 
+        synchronized(lock) { 
+            return value; 
+        }
+        
+    }
+    
+    /**
+     * Sets the value explicitly. 
+     * @param aValue value to set. 
+     */
+    public void set(T aValue) { 
+        synchronized(lock) { 
+            value = aValue;
+        }
+    }
+}
diff --git a/support/general/src/test/java/org/wamblee/cache/ComputedValueTest.java b/support/general/src/test/java/org/wamblee/cache/ComputedValueTest.java
new file mode 100644 (file)
index 0000000..d13cd41
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2005-2010 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.wamblee.cache;
+
+import static org.mockito.Mockito.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.wamblee.cache.ComputedValue.Computation;
+
+public class ComputedValueTest extends TestCase {
+
+    private Computation<Integer> computation;
+    private ComputedValue<Integer> guard;
+    
+
+    @Override
+    protected void setUp() throws Exception {
+        computation = mock(Computation.class);
+    }
+
+    public void testComputeOutOfDate() {
+        initGuard();
+    }
+
+    private void initGuard() {
+        guard = new ComputedValue<Integer>(this, computation);
+
+        assertNull(guard.getCached());
+        
+        when(computation.isOutOfDate()).thenReturn(true);
+        when(computation.compute()).thenReturn(10);
+       
+        int value = guard.get();
+        assertEquals(10, value);
+        verify(computation).compute();
+        reset(computation);
+    }
+    
+    public void testGetCached() { 
+        initGuard();
+        assertEquals(10, (int)guard.getCached());
+        verifyNoMoreInteractions(computation);
+    }
+
+    public void testNoComputationWhenNotOutOfDate() {
+        initGuard();
+        when(computation.isOutOfDate()).thenReturn(false);
+        assertEquals(10, (int) guard.get());
+        verify(computation, never()).compute();
+    }
+
+    public void testOnlyOneConcurrentComputation() throws Exception {
+        initGuard();
+        final int computeTime = 500;
+        when(computation.isOutOfDate()).thenReturn(true);
+        when(computation.compute()).thenAnswer(new Answer<Integer>() {
+            @Override
+            public Integer answer(InvocationOnMock aInvocation)
+                throws Throwable {
+                Thread.sleep(computeTime);
+                return 100;
+            }
+        });
+
+        final List<Integer> results = new ArrayList<Integer>();
+
+        Runnable task = new Runnable() {
+            @Override
+            public void run() {
+                int res = guard.get();
+                results.add(res);
+            }
+        };
+
+        // concurrent computation in two threads.
+        Thread t1 = new Thread(task);
+        Thread t2 = new Thread(task);
+        t1.start();
+        Thread.sleep(computeTime / 2);
+        // second task will return old value 10 because first one is still busy.
+        t2.start();
+        t1.join();
+        t2.join();
+        assertEquals(2, results.size());
+        assertEquals(10, (int) results.get(0));
+        assertEquals(100, (int) results.get(1));
+        verify(computation, times(2)).isOutOfDate();
+    }
+
+    public void testExceptionWhileComputing() {
+        initGuard();
+        when(computation.isOutOfDate()).thenReturn(true);
+        when(computation.compute()).thenThrow(new RuntimeException("xx"));
+        try {
+            guard.get();
+            fail();
+        } catch (RuntimeException e) {
+            assertEquals("xx", e.getMessage());
+        }
+    }
+
+    public void testExceptionWhileCheckingOutOfDate() {
+        initGuard();
+        when(computation.isOutOfDate()).thenThrow(new RuntimeException("xx"));
+        try {
+            guard.get();
+            fail();
+        } catch (RuntimeException e) {
+            assertEquals("xx", e.getMessage());
+        }
+    }
+}
index 273abd0ed3039f44234f736cc00a306dd31d0b6c..4b4bd42628046dc34d2a24a378402dc9b828d1ea 100644 (file)
@@ -24,11 +24,11 @@ import junit.framework.TestCase;
  * @see ReadWriteLock
  */
 public class ReadWriteLockTest extends TestCase {
-    private static final int HALF_SECOND = 500;
+    private static final int HALF_SECOND = 100;
 
-    private static final int ONE_SECOND = 1000;
+    private static final int ONE_SECOND = 200;
 
-    private static final int TWO_SECONDS = 2000;
+    private static final int TWO_SECONDS = 400;
     private ReadWriteLock lock;
     private int nReaders;
     private int nWriters;