« Return to Thread: JMock not thread safe - how does one do multi-threaded testing?

Re: JMock not thread safe - how does one do multi-threaded testing?

by James Abley :: Rate this Message:

Reply to Author | View in Thread

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


 « Return to Thread: JMock not thread safe - how does one do multi-threaded testing?