2009/4/1 James Abley <
james.abley@...>:
> 2009/4/1 Nat Pryce <
nat.pryce@...>:
>> JMock is thread-safe if only one thread enters the Mockery at a time.
>
> That's an interesting definition of thread-safe - only safe when one
> thread uses it. :-D
>
>> (E.g. it doesn't use thread-local variables or any magic like that).
>> So, if your code is meant to guarantee that only one thread calls a
>> mock object at a time, but you're seeing the Mockery fail because of
>> race-conditions, then your code is not thread safe.
>>
>> If you want multiple threads to enter the Mockery at once, you should
>> use the SynchronisingImposteriser that is currently in Subversion head
>> but will be in the upcoming 2.6.0 release.
>>
>> --Nat
>
> I guess that's what I'm seeing. I'll write some tests against
> subversion trunk HEAD and see if I can get it working with the current
> code, or maybe come back to you with a possible new feature request.
>
> Thanks for the fast responses; this isn't blocking me at all; it's a
> minor annoyance that I'd like to scratch.
>
> Cheers,
>
> James
Works perfectly, plus gives me nice failures if I try to use the
non-thread-safe version. Lovely job.
package org.jmock.test.unit.lib.concurrent;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicInteger;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.jmock.lib.concurrent.SynchronisingImposteriser;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(JMock.class)
public class ConcurrentRequestsTest {
/**
* JMock context object.
*/
private final Mockery context = new JUnit4Mockery() {
{
/* Need to use the SynchronisingImposteriser otherwise it
will fail when use in a multi-threaded situation. */
setImposteriser(new SynchronisingImposteriser());
}
};
@Test
public void onlyASingleClientPopulatesTheCache() throws Exception {
final Cache cache = new Cache();
int clientRequests = 100;
final Integer key = Integer.valueOf(43);
ExecutorService exec = Executors.newFixedThreadPool(clientRequests);
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(clientRequests);
List<Future<Integer>> results = new ArrayList<Future<Integer>>();
for (int i = 0; i < clientRequests; ++i) {
final Request request = context.mock(Request.class, "request-" + i);
context.checking(new Expectations() {
{
allowing(request).getKey();
will(returnValue(key));
allowing(request).getCallable();
will(returnValue(new Callable<Integer>() {
public Integer call() throws Exception {
/*
* Simulate an operation taking a little
while to complete.
*/
Thread.sleep(10);
return request.getKey();
}
}));
}
});
FutureTask<Integer> requestThreadCall = new
FutureTask<Integer>(new Callable<Integer>() {
public Integer call() throws Exception {
startGate.await();
try {
return cache.retrieveFromCache(request);
} finally {
endGate.countDown();
}
}
});
results.add(requestThreadCall);
exec.execute(requestThreadCall);
}
startGate.countDown();
endGate.await();
for (Future<Integer> result : results) {
assertEquals(key, result.get());
}
assertEquals("Only a single cache miss", 1, cache.missCount.intValue());
assertEquals("everything else was a cache hit or waited for
task completion", clientRequests - 1,
cache.hitCount.intValue() + cache.waitingCount.intValue());
}
static interface Request {
Integer getKey();
Callable<Integer> getCallable();
}
static class Cache {
private final ConcurrentMap<Integer, Future<Integer>> cache;
private final AtomicInteger hitCount;
private final AtomicInteger missCount;
private final AtomicInteger waitingCount;
Cache() {
this.cache = new ConcurrentHashMap<Integer, Future<Integer>>();
this.hitCount = new AtomicInteger();
this.missCount = new AtomicInteger();
this.waitingCount = new AtomicInteger();
}
Integer retrieveFromCache(final Request request) throws
InterruptedException, ExecutionException {
Future<Integer> value = cache.get(request.getKey());
if (value == null) {
FutureTask<Integer> thunk = new
FutureTask<Integer>(request.getCallable());
value = cache.putIfAbsent(request.getKey(), thunk);
if (value == null) {
cacheMiss();
thunk.run();
value = thunk;
} else {
cacheHitOrWaitingForCompletion();
}
} else {
cacheHit();
}
return value.get();
}
private void cacheHit() {
this.hitCount.incrementAndGet();
}
private void cacheHitOrWaitingForCompletion() {
this.waitingCount.incrementAndGet();
}
private void cacheMiss() {
this.missCount.incrementAndGet();
}
}
}
Every time I think I've found a missing feature, I should just check
what's in Subversion since you've always already implemented it!
Thanks,
James
---------------------------------------------------------------------
To unsubscribe from this list, please visit:
http://xircles.codehaus.org/manage_email