Package co.paralleluniverse.galaxy.core

Source Code of co.paralleluniverse.galaxy.core.CacheTest

/*
* Galaxy
* Copyright (C) 2012 Parallel Universe Software Co.
*
* This file is part of Galaxy.
*
* Galaxy is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Galaxy 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Galaxy. If not, see <http://www.gnu.org/licenses/>.
*/
package co.paralleluniverse.galaxy.core;

import co.paralleluniverse.galaxy.CacheListener;
import co.paralleluniverse.common.io.Persistable;
import co.paralleluniverse.galaxy.AbstractCacheListener;
import co.paralleluniverse.galaxy.LineFunction;
import co.paralleluniverse.galaxy.RefNotFoundException;
import co.paralleluniverse.galaxy.TimeoutException;
import co.paralleluniverse.galaxy.cluster.NodeInfo;
import com.google.common.base.Charsets;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.ExecutionException;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import org.hamcrest.Matcher;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.matchers.JUnitMatchers.*;

import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;
import static org.mockito.Matchers.*;
import static org.mockito.Matchers.any;
import static co.paralleluniverse.galaxy.test.LogMock.startLogging;
import static co.paralleluniverse.galaxy.test.LogMock.stopLogging;
import static co.paralleluniverse.galaxy.test.LogMock.when;
import static co.paralleluniverse.galaxy.test.LogMock.doAnswer;
import static co.paralleluniverse.galaxy.test.LogMock.doNothing;
import static co.paralleluniverse.galaxy.test.LogMock.doReturn;
import static co.paralleluniverse.galaxy.test.LogMock.doThrow;
import static co.paralleluniverse.galaxy.test.LogMock.mock;
import static co.paralleluniverse.galaxy.test.LogMock.spy;
import static co.paralleluniverse.galaxy.test.MockitoUtil.*;

import static co.paralleluniverse.galaxy.core.MessageMatchers.*;
import static co.paralleluniverse.galaxy.core.Cache.CacheLine;
import static co.paralleluniverse.galaxy.core.Cache.State;
import static co.paralleluniverse.galaxy.core.Cache.State.*;
import static co.paralleluniverse.galaxy.core.Cache.PENDING;
import co.paralleluniverse.galaxy.core.RefAllocator.RefAllocationsListener;
import co.paralleluniverse.galaxy.core.Message.BACKUP;
import co.paralleluniverse.galaxy.core.Message.LineMessage;
import co.paralleluniverse.galaxy.core.Message.MSG;
import co.paralleluniverse.galaxy.core.Message.Type;
import static co.paralleluniverse.galaxy.core.Op.Type.*;
import com.google.common.util.concurrent.ListenableFuture;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;

import org.junit.Ignore;
import org.junit.Rule;
import org.junit.rules.TestName;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.internal.util.MockUtil;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

/**
*
* @author pron
*/
@RunWith(Parameterized.class)
public class CacheTest {
    private static final int DEFAULT_ALLOC_COUNT = 10000;
    Cache cache;
    FullCluster cluster;
    AbstractComm comm;
    Backup backup;
    CacheStorage storage;
    CacheMonitor monitor;
    boolean hasServer;
    long messageId = 0;

//    public CacheTest() {
//        this.hasServer = true;
//    }
    public CacheTest(boolean hasServer) {
        this.hasServer = hasServer;
    }

    @Parameterized.Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][]{
            {true},
            {false},});
    }

    @Before
    public void setUp() throws Exception {
        java.util.logging.Logger.getLogger("").setLevel(java.util.logging.Level.WARNING);

        cluster = mock(FullCluster.class);
        when(cluster.isMaster()).thenReturn(true);
        when(cluster.hasServer()).thenReturn(hasServer);
        when(cluster.getMyNodeId()).thenReturn(sh(5));
        comm = mock(AbstractComm.class);
        backup = mock(Backup.class);
        when(backup.inv(anyLong(), anyShort())).thenReturn(true);

        storage = spy(new HeapLocalStorage("test", null));
        monitor = mock(CacheMonitor.class);
        cache = makeCache(10000);
        cache.setReceiver(mock(MessageReceiver.class));

        messageId = 0;
    }

    Cache makeCache(int capacity) throws Exception {
        return makeCache(capacity, false);
    }

    Cache makeCache(boolean syncrhonous) throws Exception {
        return makeCache(1000, syncrhonous);
    }

    Cache makeCache(int capacity, boolean syncrhonous) throws Exception {
        Cache _cache = new Cache("test", cluster, comm, storage, backup, monitor, capacity);
        _cache.setReuseLines(false);
        _cache.setReuseSharerSets(false);
        _cache.setSynchronous(syncrhonous);
        _cache.init();
        // verify uninteresting interactions so that test can use verifyNoMoreInteractions().
        verify(monitor).setMonitoredObject(_cache);
        verify(comm).setReceiver(_cache);
        if (hasServer)
            verify(comm, atLeastOnce()).isSendToServerInsteadOfMulticast();
        return _cache;
    }

    private void reset() throws Exception {
        setUp();
    }

    @After
    public void tearDown() {
    }

    ///////////////////////////////////////////////////////////////////////
    /**
     * A get returns data after PUT has been received.
     */
    @Test
    public void whenPUTThenGetLine() throws Exception {
        PUT(1, sh(10), 1, "hello");

        byte[] res = (byte[]) doOp(GET, 1L);

        assertState(1, S, null);
        assertVersion(1, 1);
        assertThat(deserialize(res), is("hello"));

        verify(monitor).addMessageReceived(Type.PUT);
        verify(monitor).addOp(eq(GET), anyLong());
        verify(monitor).addHit();
        verify(monitor).addStalePurge(0);
        verifyNoMoreInteractions(monitor);
    }

    /**
     * A getx returns data after PUTX has been received.
     */
    @Test
    public void whenPUTXThenGetXLine() throws Exception {
        PUTX(1, sh(10), 1, "hello");

        byte[] res = (byte[]) doOp(GETX, 1L);

        assertState(1, E, null);
        assertVersion(1, 1);
        assertThat(deserialize(res), is("hello"));

        verify(monitor).addMessageReceived(Type.PUTX);
        if (hasServer())
            verify(monitor).addMessageReceived(Type.INVACK);
        verify(monitor).addOp(eq(GETX), anyLong());
        if (hasServer)
            verify(monitor).addMessageSent(Type.INV);
        verify(monitor).addHit();
    }

    /**
     * A GET/X message is broadcast when get/x a missing line.
     */
    @Test
    public void whenGetMissingLineThenBroadcastGET() throws Exception {
        for (Op.Type getType : new Op.Type[]{GET, GETX}) {
            Object res = cache.runOp(new Op(getType, 1L, null));

            assertThat(res, is(PENDING));
            assertState(1, I, getType == GETX ? O : S);
            verify(comm).send(argThat(equalTo(matchingMessage(getType, sh(-1), 1L))));

            verify(monitor).addMessageSent(matchingMessage(getType, sh(-1), 1L).getType());
            verify(monitor).addMiss();
            verifyNoMoreInteractions(monitor);

            reset();
        }
    }

    /**
     * A GETX message is broadcast when getting, and getting X a missing line at the same time.
     */
    @Test
    public void whenGetXAndGetMissingLineThenBroadcastGETX() throws Exception {
        Object res = cache.runOp(new Op(GETX, 1L, null));
        assertThat(res, is(PENDING));

        res = cache.runOp(new Op(GET, 1L, null));

        assertThat(res, is(PENDING));
        assertState(1, I, O);
        assertVersion(1, 0);
        verify(comm).send(argThat(equalTo(Message.GETX(sh(-1), 1L))));
        verify(comm, never()).send(argThat(equalTo(Message.GET(sh(-1), 1L))));
    }

    /**
     * a getx of a shared line causes a GETX message is sent to owner.
     */
    @Test
    public void whenPUTAndGetXThenGETX() throws Exception {
        PUT(1, sh(10), 1, "hello");

        Object res = cache.runOp(new Op(GETX, 1L, null));

        assertThat(res, is(PENDING));
        assertState(1, S, O);
        assertVersion(1, 1);
        verify(comm).send(argThat(equalTo(Message.GETX(sh(10), 1L))));
    }

    /**
     * get/x + hint results in GET/X sent only to hinted line.
     */
    @Test
    public void whenGetWithHintThenGETFromHint() throws Exception {
        for (Op.Type getType : new Op.Type[]{GET, GETX}) {
            Object res = cache.runOp(new Op(getType, 1L, sh(15), null));

            assertThat(res, is(PENDING));
            assertState(1, I, getType == GETX ? O : S);
            verify(comm).send(argThat(equalTo(matchingMessage(getType, sh(15), 1L))));
            verify(comm, never()).send(argThat(equalTo(matchingMessage(getType, sh(-1), 1L))));

            reset();
        }
    }

    /**
     * get/x + hint of a shared line results in GET/X sent only to owner (and not hinted line).
     */
    @Test
    public void whenGetWithHintAndKnownOwnerThenGETFromOwner() throws Exception {
        for (Op.Type getType : new Op.Type[]{GET, GETX}) {
            PUT(1, sh(10), 1, "hello");
            makeInvalid(sh(10), 1L);

            cache.runOp(new Op(getType, 1L, sh(15), null));

            verify(comm).send(argThat(equalTo(matchingMessage(getType, sh(10), 1L))));
            verify(comm, never()).send(argThat(equalTo(matchingMessage(getType, sh(15), 1L))));
            verify(comm, never()).send(argThat(equalTo(matchingMessage(getType, sh(-1), 1L))));

            reset();
        }
    }

    /**
     * get/x from owner results in GET/X sent only owner of the requested line line.
     */
    @Test
    public void whenGetFromOwnerThenGETFromOwner() throws Exception {
        for (Op.Type getType : new Op.Type[]{GET, GETX}) {
            PUT(2, sh(10), 1, "hello");

            cache.runOp(new Op(GET_FROM_OWNER, 2, new Op(getType, 1, null), null));

            verify(comm).send(argThat(equalTo(matchingMessage(getType, sh(10), 1L))));
            verify(comm, never()).send(argThat(equalTo(matchingMessage(getType, sh(-1), 1L))));

            reset();
        }
    }

    /**
     * get/x from owner broadcasts GET/X if owner line does not exist.
     */
    @Test
    public void whenGetFromOwnerAndNoLineThenBroadcastGET() throws Exception {
        for (Op.Type getType : new Op.Type[]{GET, GETX}) {
            Op get = (Op) cache.runOp(new Op(GET_FROM_OWNER, 2, new Op(getType, 1, null), null));
            cache.runOp(get);

            verify(comm).send(argThat(equalTo(matchingMessage(getType, sh(-1), 1L))));
            verify(comm, never()).send(argThat(equalTo(matchingMessage(getType, sh(10), 1L))));

            reset();
        }
    }

    /**
     * get/x from owner sends GET/X to the actual owner of the requested line if it's known, not to the owner of the "owned" line.
     */
    @Test
    public void whenGetFromOwnerAndKnownLineThenGETFromActualOwner() throws Exception {
        for (Op.Type getType : new Op.Type[]{GET, GETX}) {
            PUT(2, sh(10), 1, "hello");
            PUT(1, sh(15), 1, "hello");
            makeInvalid(sh(15), 1);

            cache.runOp(new Op(GET_FROM_OWNER, 2, new Op(getType, 1, null), null));

            verify(comm).send(argThat(equalTo(matchingMessage(getType, sh(15), 1L))));
            verify(comm, never()).send(argThat(equalTo(matchingMessage(getType, sh(10), 1L))));

            reset();
        }
    }

    /**
     * When GET is received then PUT is sent
     */
    @Test
    public void whenGETThenPUT() throws Exception {
        PUTX(1234L, sh(10), 2, "hello", 20, 30, 40);
        if (hasServer())
            cache.receive(Message.INVACK(Message.INV(sh(0), 1234L, sh(10))));

        assertState(1234L, O, null);

        final LineMessage get1 = Message.GET(sh(50), 1234L);
        final LineMessage get2 = Message.GET(sh(60), 1234L);
        cache.receive(get1);
        cache.receive(get2);

        assertState(1234L, O, null);
        verify(comm).send(argThat(equalTo(Message.PUT(get1, 1234L, 2, toBuffer("hello")))));
        verify(comm).send(argThat(equalTo(Message.PUT(get2, 1234L, 2, toBuffer("hello")))));
    }

    public static LineFunction<Long> storefunc(final long set) {
        return new LineFunction<Long>() {
            @Override
            public Long invoke(LineAccess lineAccess) {
                try {
                    ByteBuffer get = lineAccess.getForRead();
                    if (!Charset.forName("ISO-8859-1").newDecoder().decode(get).toString().equals("hello"))
                        return 0L;
                    ByteBuffer bb = lineAccess.getForWrite(8);
                    bb.clear();
                    bb.putLong(set);
                    bb.flip();
                    return set;
                } catch (CharacterCodingException ex) {
                    throw new AssertionError(ex);
                }
            }
        };
    }

//    @Test
    public void whenInvokeLocalAndOThenInvokeAndSendINVAndBecomeE() throws Exception {
        PUTX(1234L, sh(10), 2, "hello", 20);
        if (hasServer())
            cache.receive(Message.INVACK(Message.INV(sh(0), 1234L, sh(10))));
        assertState(1234L, O, null);
        long l = (Long) cache.doOp(Op.Type.INVOKE, 1234L, storefunc(45L), null, null);
        assertModified(1234L, true);
        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 1234L, sh(10)))));
        assertState(1234L, O, E);
        assertThat(l, equalTo(45L));
    }

    @Test
    public void testInvokeNotOwnerInI() throws Exception {
        setCommMsgCounter();
        if (hasServer())
            cache.receive(Message.INVACK(Message.INV(sh(0), 1234L, sh(10))));
        final LineFunction<Long> storefunc = storefunc(45L);
        ListenableFuture<Object> future = cache.doOpAsync(Op.Type.INVOKE, 1234L, storefunc, null, null);
        final Message.INVOKE msg = Message.INVOKE(sh(-1), 1234L, storefunc);
        verify(comm).send(argThat(equalTo(msg.setMessageId(1))));
        assertState(1234L, I, null);
        cache.receive(Message.INVRES(msg, 1234L, 45L));
        assertThat((long) future.get(), equalTo(45L));
    }

    @Test
    public void testInvokeNotOwnerInS() throws Exception {
        setCommMsgCounter();
        PUT(1234L, sh(10), 2, "hello");
        final LineFunction<Long> storefunc = storefunc(45L);
        assertState(1234L, S, null);
        ListenableFuture<Object> future = cache.doOpAsync(Op.Type.INVOKE, 1234L, storefunc, null, null);
        final Message.INVOKE msg = Message.INVOKE(sh(10), 1234L, storefunc);
        verify(comm).send(argThat(equalTo(msg.setMessageId(2))));
        assertState(1234L, S, null);
        cache.receive(Message.INVRES(msg, 1234L, 45L));
        assertThat((long) future.get(), equalTo(45L));
    }

    @Test
    public void testInvokeWhenServerIsOwner() throws Exception {
        setCommMsgCounter();
        PUT(1234L, sh(10), 2, "hello");
        final LineFunction<Long> storefunc = storefunc(45L);
        assertState(1234L, S, null);
        ListenableFuture<Object> future = cache.doOpAsync(Op.Type.INVOKE, 1234L, storefunc, null, null);
        final Message msg = Message.INVOKE(sh(10), 1234L, storefunc).setMessageId(2);
        verify(comm).send(argThat(equalTo(msg)));
        assertState(1234L, S, null);
        // Server reply with putx
        PUTX(1234L, sh(10), 2, "hello");
        assertState(1234L, E, null);
        assertThat((long) future.get(), equalTo(45L));
    }

    @Test
    public void testInvokeLocalWhenOwnerIsE() throws Exception {
        setCommMsgCounter();
        PUTX(1234L, sh(10), 2, "hello");
        if (hasServer())
            cache.receive(Message.INVACK(Message.INV(sh(0), 1234L, sh(10))));
        assertState(1234L, E, null);
        long l = (Long) cache.doOp(Op.Type.INVOKE, 1234L, storefunc(45L), null, null);
        assertState(1234L, E, null);
        assertThat(l, equalTo(45L));
    }

    @Test
    public void testHandleInvokeWhenE() throws Exception {
        PUTX(1234L, sh(10), 2, "hello");
        if (hasServer())
            cache.receive(Message.INVACK(Message.INV(sh(0), 1234L, sh(10))));
        assertState(1234L, E, null);
        final LineFunction<Long> storefunc = storefunc(45L);
        final Message.INVOKE msg = Message.INVOKE(sh(10), 1234L, storefunc);
        cache.receive(msg);
        verify(comm).send(argThat(equalTo(Message.INVRES(msg, 1234L, 45L))));
        assertModified(1234L, true);
    }

    @Ignore // not true since c62a579 when the optimization in transitionToE has been removed
    @Test
    public void testHandleInvokeWhenO() throws Exception {
        PUTX(1234L, sh(10), 2, "hello", 20);
        if (hasServer())
            cache.receive(Message.INVACK(Message.INV(sh(0), 1234L, sh(10))));
        assertState(1234L, O, null);
        final LineFunction<Long> storefunc = storefunc(45L);
        final Message.INVOKE msg = Message.INVOKE(sh(10), 1234L, storefunc);
        cache.receive(msg);
        assertModified(1234L, true);
        assertState(1234L, O, E);
        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 1234L, sh(10)))));
        verify(comm).send(argThat(equalTo(Message.INVRES(msg, 1234L, 45L))));
    }

    /**
     * When GETX is received then PUTX is sent
     */
    @Test
    public void whenGETXThenPUTX() throws Exception {
        PUTX(1234L, sh(10), 2, "hello", 20, 30, 40);
        if (hasServer())
            cache.receive(Message.INVACK(Message.INV(sh(0), 1234L, sh(10))));

        assertState(1234L, O, null);

        final LineMessage get1 = Message.GET(sh(50), 1234L);
        final LineMessage get2 = Message.GET(sh(60), 1234L);
        cache.receive(get1);
        cache.receive(get2);

        assertState(1234L, O, null);
        verify(comm).send(argThat(equalTo(Message.PUT(get1, 1234L, 2, toBuffer("hello")))));
        verify(comm).send(argThat(equalTo(Message.PUT(get2, 1234L, 2, toBuffer("hello")))));

        final LineMessage getx = Message.GETX(sh(50), 1234L);
        cache.receive(getx);

        assertOwner(1234L, sh(50));

        assertState(1234L, I, null);
        verify(comm).send(argThat(equalTo(Message.PUTX(getx, 1234L, sh(20, 30, 40, 50, 60), 0, 2, toBuffer("hello")))));
    }

    /**
     * When GET/X is received and not broadcast and line is not found, send CHNGD_OWNER
     */
    @Test
    public void whenGETAndNoLineThenCHNGD_OWNER() throws Exception {
        for (Message.Type getType : new Message.Type[]{Message.Type.GET, Message.Type.GETX}) {
            final LineMessage get = new Message.GET(getType, sh(50), 1234L);
            cache.receive(get);

            verify(comm).send(argThat(equalTo(Message.CHNGD_OWNR(get, 1234L, sh(-1), false))));

            reset();
        }
    }

    /**
     * When GET/X broadcast is received and line is not found, send ACK
     */
    @Test
    public void whenGETBcastAndNoLineThenACK() throws Exception {
        for (Message.Type getType : new Message.Type[]{Message.Type.GET, Message.Type.GETX}) {
            final LineMessage get = new Message.GET(getType, sh(-1), 1234L);
            cache.receive(get);

            verify(comm).send(argThat(equalTo(Message.ACK(get))));

            reset();
        }
    }

    /**
     * When CHNGD_OWNR is received as a response to a GET, then re-send GET to the new owner.
     */
    @Test
    public void whenGetAndCHNGD_OWNRThenGETAgain() throws Exception {
        cache.runOp(new Op(GET, 1234L, null));

        when(cluster.getMaster(sh(20))).thenReturn(makeNodeInfo(sh(20)));

        LineMessage msg = (LineMessage) captureMessage();
        cache.receive(Message.CHNGD_OWNR(msg, 1234L, sh(20), true));

        verify(comm).send(argThat(equalTo(Message.GET(sh(20), 1234L))));
    }

    /**
     * When CHNGD_OWNR is received as a response to a GET, but new owner is not in cluster then re-send GET to the old owner.
     */
    @Test
    public void whenGetAndCHNGD_OWNRAndNoNodeThenGETAgainIgnoreChange() throws Exception {
        PUT(1234L, sh(10), 1, "x");
        INV(1234L, sh(10));
        Mockito.reset(comm); // to enable capture

        cache.runOp(new Op(GET, 1234L, null));

        when(cluster.getMaster(sh(20))).thenReturn(null);

        LineMessage msg = (LineMessage) captureMessage();
        cache.receive(Message.CHNGD_OWNR(msg, 1234L, sh(20), true));

        verify(comm).send(argThat(equalTo(Message.GET(sh(10), 1234L))));
    }

    /**
     * When CHNGD_OWNR is received as a response to a GETX, then re-send GETX to the new owner.
     */
    @Test
    public void whenGetxAndCHNGD_OWNRThenGETXAgain() throws Exception {
        PUT(1234L, sh(10), 1L, "foo");

        when(cluster.getMaster(sh(20))).thenReturn(makeNodeInfo(sh(20)));

        cache.runOp(new Op(GETX, 1234L, null));

        LineMessage msg = (LineMessage) captureMessage();
        cache.receive(Message.CHNGD_OWNR(msg, 1234L, sh(20), true));

        verify(comm).send(argThat(equalTo(Message.GETX(sh(20), 1234L))));
    }

    @Test
    public void whenGetxAndCHNGD_OWNRToYou() throws Exception {
        ListenableFuture<Object> future = cache.doOpAsync(GETX, 1234L, null, null, null);
        LineMessage msg = (LineMessage) captureMessage();
        cache.receive(Message.CHNGD_OWNR(msg, 1234L, sh(10), true));
        assertThat(future.isDone(), is(false));
        cache.receive(Message.PUTX(msg, 1234L, null, 0, 1L, toBuffer("foo")));
        if (hasServer())
            INVACK(1234L, sh(0));
        assertThat(future.isDone(), is(true));
        assertThat(deserialize(future.get()), equalTo("foo"));
    }

    @Test
    public void whenSendToOwnerOfAndCHNGD_OWNRToYou() throws Exception {
        /**
         * When CHNGD_OWNR is received, resend message to new owner
         */
        setCommMsgCounter();
        PUT(1234L, sh(10), 1L, "xxx");
        INV(1234L, sh(10));
        verify(comm).send(argThat(equalTo(Message.INVACK(Message.INV(sh(10), 1234L, sh(-1))).setMessageId(2))));

        MSG msg = Message.MSG(sh(-1), 1234L, true, serialize("foo"));
        Op send = new Op(SEND, 1234, msg, null);
        Object res = cache.runOp(send);

        assertThat(res, is(PENDING));
        verify(comm).send(argThat(equalTo(Message.MSG(sh(10), 1234L, true, serialize("foo")).setMessageId(3))));
        assertThat(send.getFuture().isDone(), is(false));

        //when(cluster.getMaster(sh(20))).thenReturn(makeNodeInfo(sh(20)));
        cache.receive(Message.CHNGD_OWNR(msg, 1234L, sh(10), true));
        cache.receive(Message.CHNGD_OWNR(msg, 1234L, sh(10), true)); // twice, but only resend once
        cache.receive(Message.TIMEOUT(msg));
        cache.receive(Message.PUTX(msg, 1234L, null, 0, 1L, toBuffer("xxx")));

        assertThat(send.getFuture().isDone(), is(true));

        try {
            send.getResult();
            fail("TimeoutException not thrown");
        } catch (InterruptedException | ExecutionException e) {
            assertThat(e.getCause(), is(instanceOf(TimeoutException.class)));
        }
    }

    /**
     * When INV is received as a response from server to an INV, then re-send GETX to the new owner.
     */
    @Test
    public void whenGetxAndINVFromServerThenGETXAgain() throws Exception {
        assumeTrue(hasServer);
        when(comm.isSendToServerInsteadOfMulticast()).thenReturn(true);
        cache = makeCache(10000);

        cache.runOp(new Op(GETX, 1234L, null));

        verify(comm).send(argThat(equalTo(Message.GETX(sh(-1), 1234L))));

        PUTX(1234L, sh(10), 1L, "foo", 20, 30);

        verify(comm).send(argThat(equalTo(Message.INV(sh(0), 1234L, sh(10)))));

        cache.receive(Message.INV(Message.INV(sh(0), 1234L, sh(10)), 1234, sh(20)));

        verify(comm).send(argThat(equalTo(Message.GETX(sh(20), 1234L))));
        verify(comm, never()).send(argThat(equalTo(Message.INVACK(Message.INV(sh(0), 1234L, sh(10))))));
    }

    /**
     * When NOT_FOUND is received (from peers) as a response to a GET, get from server.
     */
    @Test
    public void whenGetAndNOT_FOUNDAndHasServerThenGETFromServer() throws Exception {
        assumeTrue(hasServer);
        cache.runOp(new Op(GET, id(1234L), null));

        verify(comm).send(argThat(equalTo(Message.GET(sh(-1), id(1234)))));

        cache.receive(Message.NOT_FOUND(Message.GET(sh(-1), id(1234))));

        verify(comm).send(argThat(equalTo(Message.GET(sh(0), id(1234)))));
    }

    /**
     * When NOT_FOUND is received and no server, then throw exception.
     */
    @Test
    public void whenGetAndNOT_FOUNDAndNotHasServerThenThrowException() throws Exception {
        assumeTrue(!hasServer);
        final Op get = new Op(GET, id(1234), null);
        cache.runOp(get);

        cache.receive(Message.NOT_FOUND(Message.GET(sh(-1), id(1234))));

        try {
            get.getResult();
            fail("exception not thrown");
        } catch (ExecutionException e) {
            assertThat(e.getCause(), is(instanceOf(RefNotFoundException.class)));
        }
    }

    /**
     * When NOT_FOUND is received from server then throw exception.
     */
    @Test
    public void whenGetAndNOT_FOUNDFromServerThenThrowException() throws Exception {
        assumeTrue(hasServer);
        final Op get = new Op(GET, id(1234), null);
        cache.runOp(get);

        cache.receive(Message.NOT_FOUND(Message.GET(sh(0), id(1234))));

        try {
            get.getResult();
            fail("exception not thrown");
        } catch (ExecutionException e) {
            assertThat(e.getCause(), is(instanceOf(RefNotFoundException.class)));
        }
    }

    /**
     * When MSG is received and not broadcast and line is not found, send CHNGD_OWNER
     */
    @Test
    public void whenMSGAndNoLineThenCHNGD_OWNER() throws Exception {
        final LineMessage msg = Message.MSG(sh(10), 1234L, true, serialize("foo"));
        cache.receive(msg);

        verify(comm).send(argThat(equalTo(Message.CHNGD_OWNR(msg, 1234L, sh(-1), false))));
    }

    /**
     * When MSG broadcast is received and line is not found, send ACK
     */
    @Test
    public void whenMSGBcastAndNoLineThenACK() throws Exception {
        final LineMessage msg = Message.MSG(sh(-1), 1234L, true, serialize("foo"));
        cache.receive(msg);

        verify(comm).send(argThat(equalTo(Message.ACK(msg))));
    }

    /**
     * When GET/X is received and line is not owner, send CHNGD_OWNER
     */
    @Test
    public void whenGETAndNotOwnerThenCHNGD_OWNER() throws Exception {
        for (Message.Type getType : new Message.Type[]{Message.Type.GET, Message.Type.GETX}) {
            PUT(1234L, sh(10), 1L, "hello");

            final LineMessage get = new Message.GET(getType, sh(50), 1234L);
            cache.receive(get);

            verify(comm).send(argThat(equalTo(Message.CHNGD_OWNR(get, 1234L, sh(10), true))));

            reset();
        }
    }

    /**
     * When GET/X broadcast is received and line is not owner but owner is known, send CHNGD_OWNER
     */
    @Test
    public void whenGETBcastAndKnownOwnerThenCHNGD_OWNER() throws Exception {
        for (Message.Type getType : new Message.Type[]{Message.Type.GET, Message.Type.GETX}) {
            PUT(1234L, sh(10), 1L, "hello");

            final LineMessage get = new Message.GET(getType, sh(-1), 1234L);
            cache.receive(get);

            verify(comm).send(argThat(equalTo(Message.CHNGD_OWNR(get, 1234L, sh(10), true))));

            reset();
        }
    }

    /**
     * When GET/X broadcast is received and line owner is unknown, send ACK
     */
    @Test
    public void whenGETBcastAndUnknownOwnerThenACK() throws Exception {
        for (Message.Type getType : new Message.Type[]{Message.Type.GET, Message.Type.GETX}) {
            cache.runOp(new Op(GET, 1234L, null)); // create an I line with unknown owner

            final LineMessage get = new Message.GET(getType, sh(-1), 1234L);
            cache.receive(get);

            verify(comm).send(argThat(equalTo(Message.ACK(get))));

            reset();
        }
    }

    /**
     * When INV is received and line does not exist INVACK
     */
    @Test
    public void whenINVAndNoLineThenINVACK() throws Exception {
        Message.INV inv = Message.INV(sh(10), 1234L, sh(10));
        cache.receive(inv);

        verify(comm).send(argThat(equalTo(Message.INVACK(inv))));
    }

    /**
     * When GETX is receive, inv slaves before responding with PUTX
     */
    @Ignore
    @Test
    public void whenGetXAndHasSlavesThenWaitForSlavesBeforePUTX() throws Exception {
        when(backup.inv(anyLong(), anyShort())).thenReturn(false);

        PUTX(1234L, sh(10), 1L, "hello");

        set(1234L, "bye");
        cache.receive(Message.BACKUPACK(sh(0), 1234L, 2L));

        assertState(1234L, E, null);
        assertThat(cache.getLine(1234L).is(CacheLine.SLAVE), is(true));

        Message.GET getx = Message.GETX(sh(10), 1234L);
        cache.receive(getx);

        verify(backup).inv(1234L, sh(10));
        verify(comm, never()).send(argThat(equalTo(Message.PUTX(getx, 1234L, new short[0], 0, 2L, toBuffer("bye")))));

        cache.receive(Message.INVACK(sh(5), 1234L).setIncoming());

        assertThat(cache.getLine(1234L).is(CacheLine.SLAVE), is(false));
        verify(comm).send(argThat(equalTo(Message.PUTX(getx, 1234L, new short[0], 0, 2L, toBuffer("bye")))));
    }

    /**
     * When INV is receive, inv slaves before responding with INVACK
     */
//    @Test
//    public void whenInvAndHasSlavesThenWaitForSlavesBeforeINVACK() throws Exception {
//        when(backup.inv(anyLong(), anyShort())).thenReturn(false);
//
//        PUTX(1234L, sh(10), 1L, "hello");
//
//        set(1234L, "bye");
//        cache.receive(Message.BACKUPACK(sh(0), 1234L, 2L));
//        cache.receive(Message.GET(sh(20), 1234L));
//
//        assertState(1234L, S, null);
//        assertThat(cache.getLine(1234L).is(CacheLine.SLAVE), is(true));
//
//        Message.INV inv = Message.INV(sh(10), 1234L, sh(10));
//        cache.receive(inv);
//
//        verify(backup).inv(1234L, sh(10));
//        verify(comm, never()).send(argThat(equalTo(Message.INVACK(inv))));
//
//        cache.receive(Message.INVACK(sh(5), 1234L).setIncoming());
//
//        assertThat(cache.getLine(1234L).is(CacheLine.SLAVE), is(false));
//        verify(comm).send(argThat(equalTo(Message.INVACK(inv))));
//    }
    /**
     * A put allocates ID and line
     */
    @Test
    public void whenPutThenAllocateId() throws Exception {
        final RefAllocationsListener listener = hasServer ? null : getRefAllocationListener(cache.getRefAllocator());
        Object res;
        Op put;

        put = new Op(PUT, -1L, serialize("1111"), null);
        res = cache.runOp(put);

        assertThat(res, is(PENDING));
        if (hasServer)
            verify(comm).send(argThat(equalTo(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT))));
        else
            verify(cluster).allocateRefs(anyInt());

        if (hasServer)
            cache.receive(Message.ALLOCED_REF(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT), 100, 3));
        else
            listener.refsAllocated(100, 3);

        res = put.getResult();

        assertThat((Long) res, is(100L));

        res = get(100L);
        assertThat((String) res, is("1111"));
        assertModified(100L, true);
        assertThat(cache.getLine(100L).isLocked(), is(true));

        res = doOp(PUT, -1L, serialize("2222"));
        assertThat((Long) res, is(101L));
        assertModified(101L, true);
        assertThat(cache.getLine(101L).isLocked(), is(true));
        res = get(101L);
        assertThat((String) res, is("2222"));

        if (hasServer)
            verify(comm, times(2)).send(argThat(equalTo(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT))));
        else
            verify(cluster, times(2)).allocateRefs(anyInt()); // allocates after id > (min + max) / 2

        res = doOp(PUT, -1L, serialize("3333"));
        assertThat((Long) res, is(102L));
        assertModified(102L, true);
        assertThat(cache.getLine(102L).isLocked(), is(true));
        res = get(102);
        assertThat((String) res, is("3333"));

        put = new Op(PUT, -1L, serialize("4444"), null);
        res = cache.runOp(put);

        assertThat(res, is(PENDING));

        if (hasServer)
            cache.receive(Message.ALLOCED_REF(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT), 200, 3));
        else
            listener.refsAllocated(200, 3);

        res = put.getResult();

        assertThat((Long) res, is(200L));

        res = doOp(GET, 200L);
        assertThat(deserialize((byte[]) res), is("4444"));
        assertModified(200L, true);
        assertThat(cache.getLine(200L).isLocked(), is(true));

        res = doOp(PUT, -1L, serialize("5555"));
        assertThat((Long) res, is(201L));
        assertModified(201L, true);
        assertThat(cache.getLine(201L).isLocked(), is(true));
        res = get(201L);
        assertThat((String) res, is("5555"));

        if (hasServer)
            verify(comm, times(3)).send(argThat(equalTo(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT))));
        else
            verify(cluster, times(3)).allocateRefs(anyInt()); // allocates after id > (min + max) / 2

        res = doOp(PUT, -1L, serialize("6666"));
        assertThat((Long) res, is(202L));
        assertModified(202L, true);
        assertThat(cache.getLine(202L).isLocked(), is(true));
        res = get(202L);
        assertThat((String) res, is("6666"));
    }

    /**
     * When an owned line is getx, INV all sharers, including server if hasServer(), but if we have a server, we can get and even
     * set. We don't wait for ACKs to complete.
     */
    @Test
    public void whenPUTXAndGetXThenINV() throws Exception {
        PUTX(1, sh(10), 1, "hello", 20, 30);

        Object res = cache.runOp(new Op(GETX, 1L, null));

        assertState(1, O, E);
        assertVersion(1, 1);
        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 1L, sh(10)))));
        verify(comm).send(argThat(equalTo(Message.INV(sh(30), 1L, sh(10)))));
        if (hasServer)
            verify(comm).send(argThat(equalTo(Message.INV(Comm.SERVER, 1L, sh(10)))));

        // assertThat(deserialize((byte[]) res), is("hello")); // not true since c62a579 when the optimization in transitionToE has been removed
    }

    /**
     * When an owned line is getx, INV all sharers, including server if hasServer(), but we don't wait for ACKs to complete if
     * hasServer.
     */
    @Test
    @Ignore // not true since c62a579 when the optimization in transitionToE has been removed
    public void whenGetxAndPUTXThenDontWaitForINVACKIffServer() throws Exception {
        Op getx = new Op(GETX, 1L, null);
        Object res = cache.runOp(getx);

        assertThat(res, is(PENDING));
        verify(comm).send(argThat(equalTo(Message.GETX(sh(-1), 1L))));

        if (hasServer)
            PUTX(1L, sh(10), 1L, "hello", 20, 30);
        else
            PUTX(1L, sh(10), 1L, "hello", 10, 20, 30);

        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 1L, sh(10)))));
        verify(comm).send(argThat(equalTo(Message.INV(sh(30), 1L, sh(10)))));
        if (hasServer)
            verify(comm).send(argThat(equalTo(Message.INV(Comm.SERVER, 1L, sh(10)))));
        else
            verify(comm).send(argThat(equalTo(Message.INV(sh(10), 1L, sh(10)))));

        if (hasServer) {
            assertThat(getx.getFuture().isDone(), is(true));
            assertThat(deserialize((byte[]) getx.getResult()), is("hello"));
        } else {
            assertThat(getx.getFuture().isDone(), is(false));
        }
    }

    /**
     * When an owned line is getx and isSendToServerInsteadOfMulticast then wait for an INVACK from the server.
     */
    @Test
    public void whenGetxAndPUTXAndSendToServerThenDoWaitForINVACKFromServer() throws Exception {
        assumeTrue(hasServer);
        when(comm.isSendToServerInsteadOfMulticast()).thenReturn(true);
        cache = makeCache(10000);

        Op getx = new Op(GETX, 1L, null);
        Object res = cache.runOp(getx);

        assertThat(res, is(PENDING));
        verify(comm).send(argThat(equalTo(Message.GETX(sh(-1), 1L))));

        PUTX(1L, sh(10), 1L, "hello", 20, 30);

        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 1L, sh(10)))));
        verify(comm).send(argThat(equalTo(Message.INV(sh(30), 1L, sh(10)))));
        verify(comm).send(argThat(equalTo(Message.INV(Comm.SERVER, 1L, sh(10)))));

        assertThat(getx.getFuture().isDone(), is(false));

        // - since c62a579
        cache.receive(Message.INVACK(Message.INV(sh(20), 1L, sh(10))));
        cache.receive(Message.INVACK(Message.INV(sh(30), 1L, sh(10))));
        assertThat(getx.getFuture().isDone(), is(false));
        // -

        cache.receive(Message.INVACK(Message.INV(Comm.SERVER, 1L, sh(10))));

        assertThat(getx.getFuture().isDone(), is(true));
        assertThat(deserialize((byte[]) getx.getResult()), is("hello"));
    }

    /**
     * When an owned line is getx and no server then wait for an INVACK from the previous owner (means that it inved its slaves).
     */
    @Test
    @Ignore // not true since c62a579
    public void whenGetxAndPUTXAndNoServerThenDoWaitForINVACKFromPreviousOwner() throws Exception {
        assumeTrue(!hasServer);

        Op getx = new Op(GETX, 1L, null);
        Object res = cache.runOp(getx);

        assertThat(res, is(PENDING));
        verify(comm).send(argThat(equalTo(Message.GETX(sh(-1), 1L))));

        PUTX(1L, sh(10), 1L, "hello", 10, 20, 30);

        verify(comm).send(argThat(equalTo(Message.INV(sh(10), 1L, sh(10)))));
        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 1L, sh(10)))));
        verify(comm).send(argThat(equalTo(Message.INV(sh(30), 1L, sh(10)))));

        assertThat(getx.getFuture().isDone(), is(false));

        cache.receive(Message.INVACK(sh(10), 1L));

        assertThat(getx.getFuture().isDone(), is(true));
        assertThat(deserialize((byte[]) getx.getResult()), is("hello"));
    }

    /**
     * When a line is S and we getx, the current node will be one of the sharers. It must not try to INV itself.
     */
    @Test
    public void whenSAndGetxDontINVSelf() throws Exception {
        PUT(1L, sh(10), 1, "hello");
        PUTX(1L, sh(10), 1, "hello", 5, 20);

        cache.runOp(new Op(GETX, 1L, null));
        assertState(1, O, E);
        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 1L, sh(10)))));
    }

    /**
     * Don't respond to GET message if all INVACKs have not yet been received.
     */
    @Test
    public void whenPUTXAndGetXThenDontRespondToRequests() throws Exception {
        PUTX(1, sh(10), 1, "hello", 20, 30);

        Object res = cache.runOp(new Op(GETX, 1L, null));

//        if (hasServer()) {
//            assertThat(res, is(not(PENDING)));
//            cache.unlockLine(cache.getLine(1L), null);
//        }
        cache.receive(Message.GET(sh(100), 1));

        assertState(1, O, E);
        assertVersion(1, 1);
        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 1L, sh(10)))));
        verify(comm).send(argThat(equalTo(Message.INV(sh(30), 1L, sh(10)))));
        if (hasServer)
            verify(comm).send(argThat(equalTo(Message.INV(Comm.SERVER, 1L, sh(10)))));
        verify(comm, never()).send(argThat(equalTo(Message.PUT(sh(100), 1L, 1L, null))));
    }

    /**
     * Once all INVACKs have been received, respond to GET and set state from E to O.
     */
    @Test
    @Ignore // - not true since c62a579
    public void whenPUTXAndGetXAndAcksAndRequestsThenRespond() throws Exception {
        PUTX(1, sh(10), 1, "hello", 20, 30);

        Op getx = new Op(GETX, 1L, null);
        Object res = cache.runOp(getx);

        assertThat(res, is(not(PENDING))); // - not true since c62a579
        cache.unlockLine(cache.getLine(1L), null);

        cache.receive(Message.GET(sh(100), 1));
        cache.receive(Message.INVACK(Message.INV(sh(20), 1L, sh(10))));
        cache.receive(Message.INVACK(Message.INV(sh(30), 1L, sh(10))));
        if (hasServer())
            cache.receive(Message.INVACK(Message.INV(sh(0), 1L, sh(10))));
        cache.receive(Message.BACKUPACK(sh(0), 1L, 1L));

        assertState(1, O, null); // after c62a579 this would be O -> E b/c op GETX is still pending
        assertVersion(1, 1);
        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 1L, sh(10)))));
        verify(comm).send(argThat(equalTo(Message.INV(sh(30), 1L, sh(10)))));
        verify(comm).send(argThat(equalTo(Message.PUT(Message.GET(sh(100), 1), 1L, 1L, toBuffer("hello")))));

        assertThat(deserialize(res), is("hello"));
    }

    /**
     * When PUT is received, the get op is completed
     */
    @Test
    public void whenPUTThenCompleteGet() throws Exception {
        final Op get = new Op(GET, 1L, null);
        Object res = cache.runOp(get);

        assertThat(res, is(PENDING));
        assertState(1, I, S);

        PUT(1, sh(10), 1, "hello");

        assertState(1, S, null);
        assertThat(deserialize(get.getResult()), is("hello"));
    }

    /**
     * When PUTX is received, the getx op is completed
     */
//    @Test
    public void whenPUTXThenCompleteGetx() throws Exception {
        final Op getx = new Op(GETX, 1L, null);
        Object res = cache.runOp(getx);

        assertThat(res, is(PENDING));
        assertState(1, I, O);

        PUTX(1, sh(10), 1, "hello", 20, 30);

        assertState(1, O, E);

        if (hasServer)
            assertThat(deserialize(getx.getResult()), is("hello"));

        cache.receive(Message.INVACK(Message.INV(sh(20), 1L, sh(10))));
        cache.receive(Message.INVACK(Message.INV(sh(30), 1L, sh(10))));
        if (hasServer())
            cache.receive(Message.INVACK(Message.INV(sh(0), 1L, sh(10))));

        assertThat(deserialize(getx.getResult()), is("hello"));

        assertState(1, E, null);
        assertOwner(1, sh(5));
    }

    /**
     * Tests execution of pending messages on a locked line.
     */
    @Test
    public void testPendingMessages1() throws Exception {
        //public void whenLockedThenDontProecssMessagesUntilRelease() {
        for (Op.Type getType : new Op.Type[]{GETX, GETS}) {
            PUTX(1234L, sh(1), 1, "hello");
            cache.runOp(new Op(getType, 1234L, null));

            cache.receive(Message.GET(sh(10), 1234L));
            cache.receive(Message.GET(sh(20), 1234L));
            cache.receive(Message.GETX(sh(30), 1234L));

            if (hasServer)
                verify(comm).send(argThat(equalTo(Message.INV(sh(0), 1234L, sh(1)))));
            verifyNoMoreInteractions(comm);

            cache.release(1234);
            cache.receive(Message.BACKUPACK(sh(0), 1234L, 1L));

            verify(comm).send(argThat(equalTo(Message.PUT(Message.GET(sh(10), 1234L), 1234L, 1L, toBuffer("hello")))));
            verify(comm).send(argThat(equalTo(Message.PUT(Message.GET(sh(20), 1234L), 1234L, 1L, toBuffer("hello")))));
            verify(comm).send(argThat(equalTo(Message.PUTX(Message.GETX(sh(30), 1234L), 1234L, sh(10, 20), 0, 1L, toBuffer("hello")))));

            reset();
        }
    }

    /**
     * Tests execution of pending messages on a locked line.
     */
    @Test
    public void testPendingMessages2() throws Exception {
        PUTX(1234L, sh(1), 1, "hello");
        cache.runOp(new Op(GETX, 1234L, null));

        cache.receive(Message.GET(sh(10), 1234L));
        cache.receive(Message.GETX(sh(20), 1234L));
        cache.receive(Message.GET(sh(30), 1234L));

        if (hasServer)
            verify(comm).send(argThat(equalTo(Message.INV(sh(0), 1234L, sh(1)))));
        verifyNoMoreInteractions(comm);

        cache.release(1234);
        cache.receive(Message.BACKUPACK(sh(0), 1234L, 1L));

        verify(comm).send(argThat(equalTo(Message.PUT(Message.GET(sh(10), 1234L), 1234L, 1L, toBuffer("hello")))));
        verify(comm).send(argThat(equalTo(Message.PUTX(Message.GETX(sh(20), 1234L), 1234L, new short[]{sh(10)}, 0, 1L, toBuffer("hello")))));
        verify(comm).send(argThat(equalTo(Message.CHNGD_OWNR(Message.GET(sh(30), 1234L), 1234L, sh(20), false)))); // b/c hasServer: I, no-server: S
    }

    /**
     * When there are messages waiting (b/c of MODIFIED) and the line isn't locked, new ops will wait as well and let the messages
     * go first to prevent starvation.
     */
    @Test
    public void testPendingOps() throws Exception {
        PUTX(1234L, sh(1), 1, "hello");
        Mockito.reset(comm); // forget sends

        assertLocked(1234L, false);
        assertState(1234, E, null);

        set(1234L, "bye");
        set(1234L, "1234"); // this length must be less than the sets' buffer length to make sure a new buffer is allocated to them
        // otherwise, if this is longer, the sets will write over the buffer. This is OK in practice, because send will have been finished
        // by then, but here, mockito is capturing pointers to the buffers and verifying them after the set.

        assertModified(1234, true);
        assertVersion(1234, 3);

        assertThat(get(1234L), is("1234"));

        GET(1234L, sh(10)); // now get is waiting

        verify(comm, never()).send(any(Message.class));

        Object res;
        res = cache.runOp(new Op(SET, 1234L, serialize("why?????"), null)); // op is waiting because GET is waiting

        assertThat(res, is(PENDING));

        res = cache.runOp(new Op(SET, 1234L, serialize("because!!!!!"), null)); // op is waiting because GET is waiting

        assertThat(res, is(PENDING));

        cache.receive(Message.BACKUPACK(sh(0), 1234L, 3L));

        verify(comm).send(argThat(equalTo(Message.PUT(Message.GET(sh(10), 1234L), 1234L, 3L, toBuffer("1234"))))); // handling the GET
        verify(comm).send(argThat(equalTo(Message.INV(sh(10), 1234L, sh(5))))); // for the sets

        INVACK(1234L, sh(10)); // added b/c of c62a579

        assertThat(get(1234L), is("because!!!!!"));
        assertModified(1234, true);
        assertVersion(1234, 5);

//        assertState(1234L, O, E); -- since c62a579
//        INVACK(1234L, sh(10));
        assertState(1234L, E, null);
    }

    /**
     * When there are messages waiting and the line isn't locked (b/c O -> E), new ops will execute
     */
//    @Test
    public void testPendingOps2() throws Exception {
        PUTX(1234L, sh(1), 1, "hello", 20);
        if (hasServer()) {
            verify(comm).send(argThat(equalTo(Message.INV(sh(0), 1234L, sh(1)))));
            cache.receive(Message.INVACK(Message.INV(sh(0), 1234L, sh(1))));
        }

        assertLocked(1234L, false);
        assertState(1234L, O, null);

        System.out.println("111: " + cache.getLine(1234L));

        set(1234L, "bye");
        set(1234L, "1234"); // this length must be less than the sets' buffer length to make sure a new buffer is allocated to them
        // otherwise, if this is longer, the sets will write over the buffer. This is OK in practice, because send will have been finished
        // by then, but here, mockito is capturing pointers to the buffers and verifying them after the set.

        assertState(1234L, O, E);
        assertModified(1234, true);
        assertVersion(1234, 3);
        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 1234L, sh(1))))); // we verify just for the sake of verifyNoMoreInteractions(comm) later

        assertThat(get(1234L), is("1234"));

        GET(1234L, sh(10)); // now get is waiting

        verifyNoMoreInteractions(comm);

        Object res;
        res = cache.runOp(new Op(SET, 1234L, serialize("why?????"), null)); // op is waiting because GET is waiting

        assertThat(res, is(not(PENDING)));

        res = cache.runOp(new Op(SET, 1234L, serialize("because!!!!!"), null)); // op is waiting because GET is waiting

        assertThat(res, is(not(PENDING)));

        cache.receive(Message.BACKUPACK(sh(0), 1234L, 5L));

        verifyNoMoreInteractions(comm);

        cache.receive(Message.INVACK(Message.INV(sh(20), 1234L, sh(1))));

        verify(comm).send(argThat(equalTo(Message.PUT(Message.GET(sh(10), 1234L), 1234L, 5L, toBuffer("because!!!!!"))))); // handling the GET

        assertThat(get(1234L), is("because!!!!!"));
        assertModified(1234, false);
        assertVersion(1234, 5);
        assertState(1234L, O, null);
    }

    @Test
    public void whenPushThenPUTtoTarget() throws Exception {
        PUTX(1234L, sh(1), 1, "hello");

        cache.runOp(new Op(PUSH, 1234L, sh(20, 30), null));

        verify(comm).send(argThat(equalTo(Message.PUT(sh(20), 1234L, 1L, toBuffer("hello")))));
        verify(comm).send(argThat(equalTo(Message.PUT(sh(30), 1234L, 1L, toBuffer("hello")))));
    }

    @Test
    public void whenPushxThenPUTXtoTarget() throws Exception {
        PUTX(1234L, sh(1), 1, "hello");

        cache.runOp(new Op(PUSHX, 1234L, sh(20), null));
        cache.runOp(new Op(PUSHX, 1234L, sh(30), null));

        verify(comm).send(argThat(equalTo(Message.PUTX(sh(20), 1234L, sh(), 0, 1L, toBuffer("hello")))));
        verify(comm, never()).send(argThat(equalTo(Message.PUTX(sh(30), 1234L, sh(), 0, 1L, toBuffer("hello")))));
    }

    @Test
    public void whenPushAndWrongStateThenIgnore() throws Exception {
        PUT(1234L, sh(1), 1, "hello");

        cache.runOp(new Op(PUSH, 1234L, sh(20, 30), null));
        cache.runOp(new Op(PUSH, 1111L, sh(10, 20), null));
        cache.runOp(new Op(PUSHX, 1234L, sh(20), null));
        cache.runOp(new Op(PUSHX, 1111L, sh(10), null));

        verify(comm, never()).send(any(Message.class));
    }

    /**
     * When line is M, allow local operations (get and set).
     */
    @Test
    public void whenSetThenAllowLocalOps() throws Exception {
        PUTX(1234L, sh(1), 1, "hello");

        assert !cache.getLine(1234).isLocked();

        assertState(1234, E, null);

        set(1234L, "bye");

        assertModified(1234, true);
        assertVersion(1234, 2);

        assertThat(get(1234L), is("bye"));

        // b/c line was not locked, set's autorelease issues a backup
        verify(backup).backup(1234, 2);

        set(1234L, "woohoo");

        assertModified(1234, true);
        assertVersion(1234, 3);

        assertThat(get(1234L), is("woohoo"));

        verify(backup).backup(1234, 3);
    }

    /**
     * When line is M and and cache is in synchronous mode, don't allow local get until BACKUPACK
     */
    @Ignore
    @Test
    public void whenSetSetAndSynchronousDontGetUntilBACKUPACK() throws Exception {
        cache = makeCache(true);

        PUTX(1234L, sh(1), 1, "hello");

        assert !cache.getLine(1234).isLocked();

        assertState(1234, E, null);

        set(1234L, "bye");

        assertModified(1234, true);
        assertVersion(1234, 2);

        Op get = new Op(GET, 1234L, null);
        Object res = cache.runOp(get);

        assertThat(res, is(PENDING));
        verify(backup).backup(1234, 2); // b/c line was not locked, set's autorelease issues a backup

        set(1234L, "woohoo");

        verify(backup).backup(1234, 3); // b/c line was not locked, set's autorelease issues a backup

        assertModified(1234, true);
        assertVersion(1234, 3);

        cache.receive(Message.BACKUPACK(sh(0), 1234L, 3L));

        assertThat(get(1234L), is("woohoo"));

        assertThat(get.getFuture().isDone(), is(true));
        assertThat(deserialize((byte[]) get.getResult()), is("woohoo"));
    }

    /**
     * When updating a line, no messages can be processed until backup has completed.
     */
    @Test
    public void whenSetThenDontProecssMessagesUntilBACKUPACK() throws Exception {
        PUTX(1234L, sh(1), 1, "hello");

        assert !cache.getLine(1234).isLocked();

        assertState(1234, E, null);

        set(1234L, "bye");

        assertModified(1234, true);
        assertVersion(1234, 2);

        cache.receive(Message.GET(sh(100), 1234));

        verify(backup).backup(1234, 2);
        verify(comm, never()).send(argThat(equalTo(Message.PUT(Message.GET(sh(100), 1234L), 1234L, 2L, toBuffer("bye")))));

        cache.receive(Message.BACKUPACK(sh(0), 1234L, 2L));

        verify(comm).send(argThat(equalTo(Message.PUT(Message.GET(sh(100), 1234L), 1234L, 2L, toBuffer("bye")))));
    }

    /**
     * When line is M, hold push/x
     */
    @Test
    public void whenSetThenDontPushUntilBACKUPACK() throws Exception {
        PUTX(1234L, sh(1), 1, "hello");
        assertState(1234, E, null);

        set(1234L, "bye");

        assert !cache.getLine(1234).isLocked();

        assertModified(1234, true);
        assertVersion(1234, 2);

        cache.runOp(new Op(PUSH, 1234L, sh(10, 20), null));
        GET(1234L, sh(100));
        //cache.runOp(new Op(PUSHX, 1234L, sh(30)));

        if (hasServer)
            verify(comm).send(argThat(equalTo(Message.INV(sh(0), 1234L, sh(1)))));
        verifyNoMoreInteractions(comm);

        cache.receive(Message.BACKUPACK(sh(0), 1234L, 2L));

        verify(comm).send(argThat(equalTo(Message.PUT(Message.GET(sh(100), 1234L), 1234L, 2L, toBuffer("bye")))));
        verify(comm).send(argThat(equalTo(Message.PUT(sh(10), 1234L, 2L, toBuffer("bye")))));
        verify(comm).send(argThat(equalTo(Message.PUT(sh(20), 1234L, 2L, toBuffer("bye")))));
        // By the time we get to pushx, we're not exclusive, so nothing will be sent
        verify(comm, never()).send(argThat(equalTo(Message.PUTX(sh(30), 1234L, hasServer() ? new short[]{sh(0)} : sh(), 0, 2L, toBuffer("bye")))));
    }

    /**
     * When putting a new line, no messages can be processed until backup has completed.
     */
    @Test
    public void whenPutThenDontProecssMessagesUntilBACKUPACK() throws Exception {
        long id = put("hello");

        cache.release(id);

        assert !cache.getLine(id).isLocked();

        assertState(id, E, null);

        assertModified(id, true);
        assertVersion(id, 1);

        cache.receive(Message.GET(sh(100), id));

        verify(backup).backup(id, 1);
        verify(comm, never()).send(argThat(equalTo(Message.PUT(Message.GET(sh(100), id), id, 1L, toBuffer("hello")))));

        cache.receive(Message.BACKUPACK(sh(0), id, 1L));

        verify(comm).send(argThat(equalTo(Message.PUT(Message.GET(sh(100), id), id, 1L, toBuffer("hello")))));
    }

    /**
     * When multiple sets are issued, only backup when releasing the line.
     */
    @Test
    public void onlyBackupUponEndTransaction() throws Exception {
        Transaction txn = cache.beginTransaction();

        cache.runOp(new Op(GETX, 1234L, (Persistable) null, null, txn));

        PUTX(1234L, sh(1), 1, "hello");

        assert cache.getLine(1234).isLocked();

        assertState(1234, E, null);

        set(1234L, "bye", txn);

        assertModified(1234, true);
        assertVersion(1234, 2);

        assertThat(get(1234L), is("bye"));

        // b/c line was locked, set does not autorelease and does not issue a backup
        verify(backup, never()).backup(anyLong(), anyLong());

        set(1234L, "woohoo", txn);

        assertModified(1234, true);
        assertVersion(1234, 3);

        assertThat(get(1234L), is("woohoo"));

        cache.endTransaction(txn, false);

        verify(backup).backup(1234, 3);
    }

    /**
     * When we receive GET or GETX, we flush the backups
     */
    @Test
    public void whenGETThenFlushBackups() {
        PUTX(1234L, sh(1), 1, "11");
        cache.runOp(new Op(SET, 1234L, serialize("22"), null));

        assertState(1234, E, null);
        assertModified(1234, true);

        verify(backup).backup(anyLong(), anyLong());
        verify(backup, never()).flush();

        cache.receive(Message.GET(sh(100), 1234));

        verify(backup).flush();
    }

    /**
     * Make sure we allow stale reads as long as there were no PUTs from the same owner.
     */
    @Test
    public void testStaleReads1() {
        PUT(101L, sh(10), 1L, "1");
        PUT(102L, sh(10), 1L, "2");
        PUT(103L, sh(10), 1L, "3");
        PUT(104L, sh(10), 1L, "4");
        PUT(201L, sh(20), 1L, "1");
        PUT(202L, sh(20), 1L, "2");

        assertThat(cache.runOp(new Op(GET, 101, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 102, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 103, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 104, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 201, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 202, null)), is(not(PENDING)));

        INV(101L, sh(10));
        INV(103L, sh(10));
        INV(104L, sh(10));
        INV(201L, sh(10));
        INV(202L, sh(20));

        assertThat(cache.runOp(new Op(GET, 101, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 102, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 103, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 104, null)), is(not(PENDING)));

        PUT(103L, sh(10), 2L, "3");

        assertThat(cache.runOp(new Op(GET, 103, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 101, null)), is(PENDING));
        assertThat(cache.runOp(new Op(GET, 102, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 104, null)), is(PENDING));

        assertThat(cache.runOp(new Op(GET, 201, null)), is(PENDING));
        assertThat(cache.runOp(new Op(GET, 202, null)), is(not(PENDING)));
    }

    /**
     * A PUT of a new line purges all I lines.
     */
    @Test
    public void testStaleReads2() {
        PUT(101L, sh(10), 1L, "1");
        PUT(102L, sh(10), 1L, "2");
        PUT(103L, sh(10), 1L, "3");
        PUT(104L, sh(10), 1L, "4");
        PUT(201L, sh(20), 1L, "1");
        PUT(202L, sh(20), 1L, "2");

        assertThat(cache.runOp(new Op(GET, 101, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 102, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 103, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 104, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 201, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 202, null)), is(not(PENDING)));

        INV(101L, sh(10));
        INV(103L, sh(10));
        INV(104L, sh(10));
        INV(201L, sh(20));

        assertThat(cache.runOp(new Op(GET, 101, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 102, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 103, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 104, null)), is(not(PENDING)));

        PUT(105L, sh(30), 1L, "5");

        assertThat(cache.runOp(new Op(GET, 103, null)), is(PENDING));
        assertThat(cache.runOp(new Op(GET, 101, null)), is(PENDING));
        assertThat(cache.runOp(new Op(GET, 102, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 104, null)), is(PENDING));

        assertThat(cache.runOp(new Op(GET, 201, null)), is(PENDING));
        assertThat(cache.runOp(new Op(GET, 202, null)), is(not(PENDING)));
    }

    /**
     * Eviction does not prevent stale reads.
     */
    @Test
    public void testStaleReads4() {
        PUT(101L, sh(10), 1L, "1");
        PUT(102L, sh(10), 1L, "2");
        PUT(103L, sh(10), 1L, "3");
        PUT(104L, sh(10), 1L, "4");

        assertThat(cache.runOp(new Op(GET, 101, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 102, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 103, null)), is(not(PENDING)));
        assertThat(cache.runOp(new Op(GET, 104, null)), is(not(PENDING)));

        evict(101, false);
        evict(102, false);

        PUT(103L, sh(10), 2L, "3");
        INV(104L, sh(10));

        assertThat(cache.runOp(new Op(GET, 104, null)), is(not(PENDING)));

        INV(103L, sh(10));
        PUT(103L, sh(10), 3L, "3");

        assertThat(cache.runOp(new Op(GET, 104, null)), is(PENDING));
    }

    @Test
    public void testCacheListeners() {
        CacheListener listener = mock(CacheListener.class);

        cache.addCacheListener(listener);

        PUT(100L, sh(10), 1L, "hello");

        verify(listener).received(cache, 100L, 1L, toBuffer("hello"));

        PUT(100L, sh(10), 2L, "bye");

        verify(listener).received(cache, 100L, 2L, toBuffer("bye"));

        INV(100L, sh(10));

        verify(listener).invalidated(cache, 100L);

        evict(100L, false);

        verify(listener).evicted(cache, 100L);
    }

    @Test
    public void testCacheLineListeners() throws Exception {
        CacheListener listener = mock(CacheListener.class);

        doOp(LSTN, 100L, listener);

        PUT(100L, sh(10), 1L, "hello");

        verify(listener).received(cache, 100L, 1L, toBuffer("hello"));

        PUT(100L, sh(10), 2L, "bye");

        verify(listener).received(cache, 100L, 2L, toBuffer("bye"));

        INV(100L, sh(10));

        verify(listener).invalidated(cache, 100L);

        evict(100L, false);

        verify(listener).evicted(cache, 100L);

        PUT(100L, sh(20), 3L, "xxx");

        verifyNoMoreInteractions(listener); // listener gone after eviction
    }

    /**
     * Local operations such as put and get do not notify listeners (but evict does)!
     */
    @Test
    public void whenLocalInteractionThenDontNotifyListeners() throws Exception {
        CacheListener listener1 = mock(CacheListener.class);
        CacheListener listener2 = mock(CacheListener.class);

        cache.addCacheListener(listener1);

        long id = put("hello");

        doOp(LSTN, id, listener2);

        set(id, "bye");
        assert get(id).equals("bye");
        set(id, "xxx");
        assert get(id).equals("xxx");

        del(id);
        //evict(id, false);

        verify(listener1).evicted(cache, id);
        verify(listener2).evicted(cache, id);

        verifyNoMoreInteractions(listener1);
        verifyNoMoreInteractions(listener2);
    }

    /**
     * A cache listener is able to set a line listener.
     */
    @Test
    public void testSetLineListenerInCacheListener() throws Exception {
        final CacheListener lineListener = mock(CacheListener.class);
        cache.addCacheListener(new AbstractCacheListener() {
            @Override
            public void received(co.paralleluniverse.galaxy.Cache cache, long id, long version, ByteBuffer data) {
                try {
                    doOp(LSTN, id, lineListener);
                } catch (TimeoutException e) {
                    throw new AssertionError(e);
                }
            }
        });

        PUT(100L, sh(10), 1L, "hello");

        PUT(100L, sh(10), 2L, "bye");

        verify(lineListener).received(cache, 100L, 2L, toBuffer("bye"));

        INV(100L, sh(10));

        verify(lineListener).invalidated(cache, 100L);

        evict(100L, false);

        verify(lineListener).evicted(cache, 100L);

        PUT(100L, sh(20), 3L, "xxx");

        verifyNoMoreInteractions(lineListener); // listener gone after eviction
    }

    @Test
    public void testDel() throws Exception {
        PUTX(1234L, sh(1), 1, "hello");

        del(1234L);

        assertThat(cache.getLine(1234L).is(CacheLine.DELETED), is(true));

        if (hasServer()) {
            assertLocked(1234L, false);
            verify(comm).send(argThat(equalTo(Message.DEL(Comm.SERVER, 1234L))));

            cache.receive(Message.INVACK(Comm.SERVER, 1234L));
        }

        assertState(1234L, I, null);
    }

    //@Test
    public void whenDeletedAndGETThenNOT_FOUND() {
        pending();
    }

    @Test
    public void testSend() throws Exception {
        MSG msg = Message.MSG(sh(15), -1L, true, serialize("hello"));
        cache.send(msg);

        verify(comm).send(argThat(equalTo(msg)));
    }

    /**
     * When I'm the owner, message is immediately received (through shortCircuitReceive)
     */
    @Test
    public void testSendToOwner1() {
        MessageReceiver receiver = mock(MessageReceiver.class);

        cache.setReceiver(receiver);

        PUTX(1234L, sh(1), 1, "hello");

        cache.runOp(new Op(SEND, 1234, Message.MSG(sh(-1), 1234L, true, serialize("foo")), null));

        verify(receiver).receive(argThat(equalTo(Message.MSG(sh(5), 1234L, true, serialize("foo")))));
    }

    /**
     * When line not found then broadcast message
     */
    @Test
    public void testSendToOwner2() throws Exception {
        setCommMsgCounter();
        MSG msg = Message.MSG(sh(-1), 1234L, true, serialize("foo"));
        Op send = new Op(SEND, 1234, msg, null);
        Object res = cache.runOp(send);

        assertThat(res, is(PENDING));
        verify(comm).send(argThat(equalTo(Message.MSG(sh(-1), 1234L, true, serialize("foo")).setMessageId(1))));
        assertThat(send.getFuture().isDone(), is(false));

        cache.receive(Message.MSGACK(msg));

        assertThat(send.getResult(), is(nullValue()));
    }

    /**
     * When INV is received, resend message to new owner
     */
    @Test
    public void testSendToOwner3() throws Exception {
        setCommMsgCounter();
        PUT(1234L, sh(10), 1L, "xxx");

        MSG msg = Message.MSG(sh(-1), 1234L, true, serialize("foo"));
        Op send = new Op(SEND, 1234, msg, null);
        Object res = cache.runOp(send);

        assertThat(res, is(PENDING));
        verify(comm).send(argThat(equalTo(Message.MSG(sh(10), 1234L, true, serialize("foo")).setMessageId(2))));
        assertThat(send.getFuture().isDone(), is(false));

        INV(1234L, sh(20));
        INV(1234L, sh(20)); // twice, but only resend once
        verify(comm).send(argThat(equalTo(Message.INVACK(Message.INV(sh(20), 1234L, sh(-1))).setMessageId(3))));
        verify(comm).send(argThat(equalTo(Message.MSG(sh(20), 1234L, true, serialize("foo")).setMessageId(4))));
        verify(comm).send(argThat(equalTo(Message.INVACK(Message.INV(sh(20), 1234L, sh(-1))).setMessageId(5))));
        assertThat(send.getFuture().isDone(), is(false));

        INV(1234L, sh(30));
        INV(1234L, sh(30)); // twice, but only resend once

        verify(comm).send(argThat(equalTo(Message.INVACK(Message.INV(sh(30), 1234L, sh(-1))).setMessageId(6))));
        verify(comm).send(argThat(equalTo(Message.MSG(sh(30), 1234L, true, serialize("foo")).setMessageId(7))));
        assertThat(send.getFuture().isDone(), is(false));

        cache.receive(Message.MSGACK(msg));

        assertThat(send.getResult(), is(nullValue()));
    }

    /**
     * When CHNGD_OWNR is received, resend message to new owner
     */
    @Test
    public void testSendToOwner4() throws Exception {
        setCommMsgCounter();
        PUT(1234L, sh(10), 1L, "xxx");
        INV(1234L, sh(10));
        verify(comm).send(argThat(equalTo(Message.INVACK(Message.INV(sh(10), 1234L, sh(-1))).setMessageId(2))));

        MSG msg = Message.MSG(sh(-1), 1234L, true, serialize("foo"));
        Op send = new Op(SEND, 1234, msg, null);
        Object res = cache.runOp(send);

        assertThat(res, is(PENDING));
        verify(comm).send(argThat(equalTo(Message.MSG(sh(10), 1234L, true, serialize("foo")).setMessageId(3))));
        assertThat(send.getFuture().isDone(), is(false));

        when(cluster.getMaster(sh(20))).thenReturn(makeNodeInfo(sh(20)));

        cache.receive(Message.CHNGD_OWNR(msg, 1234L, sh(20), true));
        cache.receive(Message.CHNGD_OWNR(msg, 1234L, sh(20), true)); // twice, but only resend once

        verify(comm).send(argThat(equalTo(Message.MSG(sh(20), 1234L, true, serialize("foo")).setMessageId(4))));
        assertThat(send.getFuture().isDone(), is(false));

        when(cluster.getMaster(sh(30))).thenReturn(makeNodeInfo(sh(30)));

        cache.receive(Message.CHNGD_OWNR(msg, 1234L, sh(30), true));
        cache.receive(Message.CHNGD_OWNR(msg, 1234L, sh(30), true)); // twice, but only resend once

        verify(comm).send(argThat(equalTo(Message.MSG(sh(30), 1234L, true, serialize("foo")).setMessageId(5))));
        assertThat(send.getFuture().isDone(), is(false));

        cache.receive(Message.MSGACK(msg));

        assertThat(send.getResult(), is(nullValue()));
    }

    @Test
    public void testSend1() throws Exception {
        CacheListener listener = mock(CacheListener.class);

        doOp(LSTN, 1234L, listener);

        PUTX(1234L, sh(10), 1L, "xxx");

        MSG msg = Message.MSG(sh(-1), 1234L, false, serialize("foo"));
        Op send = new Op(SEND, 1234, msg, null);
        Object res = cache.runOp(send);

        assertThat(res, is(not(PENDING)));

        verify(listener).messageReceived(serialize("foo"));
    }

    @Test
    public void testSend2() throws Exception {
        CacheListener listener = mock(CacheListener.class);

        PUTX(1234L, sh(10), 1L, "xxx");

        MSG msg = Message.MSG(sh(-1), 1234L, false, serialize("foo"));
        Op send = new Op(SEND, 1234, msg, null);
        Object res = cache.runOp(send);

        assertThat(res, is(not(PENDING)));

        verify(listener, never()).messageReceived(any(byte[].class));

        doOp(LSTN, 1234L, listener);

        verify(listener).messageReceived(serialize("foo"));
    }

    @Test
    public void testSend3() throws Exception {
        setCommMsgCounter();
        CacheListener listener = mock(CacheListener.class);

        doOp(LSTN, 1234L, listener);

        PUT(1234L, sh(10), 1L, "xxx");

        MSG msg = Message.MSG(sh(-1), 1234L, false, serialize("foo"));
        Op send = new Op(SEND, 1234, msg, null);
        Object res = cache.runOp(send);

        assertThat(res, is(PENDING));

        verify(listener, never()).messageReceived(any(byte[].class));

        verify(comm).send(argThat(equalTo(Message.MSG(sh(10), 1234L, false, serialize("foo")).setMessageId(2))));
    }

    @Test
    public void whenPendingMSGsAndGETXThenSend() throws Exception {
        PUTX(1234L, sh(10), 1L, "xxx");

        cache.receive(Message.MSG(sh(20), 1234L, false, serialize("foo")));
        cache.receive(Message.MSG(sh(20), 1234L, false, serialize("bar")));

        GETX(1234, sh(30));

        InOrder inOrder = inOrder(comm);
        inOrder.verify(comm).send(argThat(equalTo(Message.PUTX(Message.GET(sh(30), 1234L), 1234L, new short[0], 2, 1, toBuffer("xxx")))));
        inOrder.verify(comm).send(argThat(equalTo(Message.MSG(sh(30), 1234L, false, true, serialize("foo")).setReplyRequired(false))));
        inOrder.verify(comm).send(argThat(equalTo(Message.MSG(sh(30), 1234L, false, true, serialize("bar")).setReplyRequired(false))));
    }

    @Test
    public void whenPUTXandPendingAndGETXThenSend() throws Exception {
        PUTX(1234L, sh(10), 2, 1L, "xxx");

        GETX(1234, sh(30));

        verify(comm, never()).send(argThat(equalTo(Message.PUTX(Message.GET(sh(30), 1234L), 1234L, new short[0], 2, 1, toBuffer("xxx")))));

        cache.receive(Message.MSG(sh(20), 1234L, false, true, serialize("foo")));

        verify(comm, never()).send(argThat(equalTo(Message.PUTX(Message.GET(sh(30), 1234L), 1234L, new short[0], 2, 1, toBuffer("xxx")))));

        cache.receive(Message.MSG(sh(20), 1234L, false, true, serialize("bar")));

        InOrder inOrder = inOrder(comm);
        inOrder.verify(comm).send(argThat(equalTo(Message.PUTX(Message.GET(sh(30), 1234L), 1234L, new short[0], 2, 1, toBuffer("xxx")))));
        inOrder.verify(comm).send(argThat(equalTo(Message.MSG(sh(30), 1234L, false, true, serialize("foo")).setReplyRequired(false))));
        inOrder.verify(comm).send(argThat(equalTo(Message.MSG(sh(30), 1234L, false, true, serialize("bar")).setReplyRequired(false))));
    }

    /**
     * When NodeNotFoundException is thrown during send INV, short-circuit an INVACK
     */
    @Test
    public void whenINVAndNodeNotFoundThenINVACKSelf() throws Exception {
        doThrow(new NodeNotFoundException(sh(20))).when(comm).send(argThat(equalTo(Message.INV(sh(20), 1234L, sh(10)))));
        doThrow(new NodeNotFoundException(sh(40))).when(comm).send(argThat(equalTo(Message.INV(sh(40), 1234L, sh(10)))));
        PUTX(1234L, sh(10), 1, "hello", 20, 30, 40);

        cache.runOp(new Op(GETX, 1234L, null));

        assertState(1234L, O, E);

        cache.receive(Message.INVACK(sh(30), 1234L));
        if (hasServer)
            cache.receive(Message.INVACK(sh(0), 1234L));

        assertState(1234L, E, null); // 20 and 40 were self-ACKed when the exception was thrown
    }

    /**
     * When NodeNotFoundException is thrown during send GET/X, short-circuit a CHNGD_OWNR
     */
    @Test
    public void whenGETAndNodeNotFoundThenCHNGD_OWNRSelf() throws Exception {
        for (Op.Type getType : new Op.Type[]{GET, GETX}) {
            PUT(1234L, sh(10), 1L, "hello");
            INV(1234L, sh(10));
            PUT(2222L, sh(10), 1L, "foo"); // prevent dirty read of 1234

            doThrow(new NodeNotFoundException(sh(10))).when(comm).send(argThat(equalTo(matchingMessage(getType, sh(10), 1234L))));

            cache.runOp(new Op(getType, 1234L, null));

            verify(comm).send(argThat(equalTo(matchingMessage(getType, sh(-1), 1234L)))); // a CHNGD_OWNR to -1 is received during get from 10

            reset();
        }
    }

    /**
     * When TIMEOUT is received, all pending ops should be interrupted with a TimeoutException.
     */
    @Test
    public void whenTimeoutThenInterruptPendingOps() throws Exception {
        setCommMsgCounter();
        final Op op1 = new Op(GET, 1L, null);
        final Op op2 = new Op(GETX, 1L, null);
        final Op op3 = new Op(SET, 1L, serialize("xxx"), null);
        final Op op4 = new Op(SEND, 1L, Message.MSG(sh(-1), 1234L, true, serialize("foo")), null);

        Object res1 = cache.runOp(op1);
        Object res2 = cache.runOp(op2);
        Object res3 = cache.runOp(op3);
        Object res4 = cache.runOp(op4);

        assertThat(res1, is(PENDING));
        assertThat(res2, is(PENDING));
        assertThat(res3, is(PENDING));
        assertThat(res4, is(PENDING));

        cache.receive(Message.TIMEOUT(new LineMessage(sh(1), Type.GET, 1L)));

        try {
            op1.getResult();
            fail("TimeoutException not thrown");
        } catch (Exception e) {
            assertThat(e.getCause(), is(instanceOf(TimeoutException.class)));
        }
        try {
            op2.getResult();
            fail("TimeoutException not thrown");
        } catch (Exception e) {
            assertThat(e.getCause(), is(instanceOf(TimeoutException.class)));
        }
        try {
            op3.getResult();
            fail("TimeoutException not thrown");
        } catch (Exception e) {
            assertThat(e.getCause(), is(instanceOf(TimeoutException.class)));
        }
        try {
            op4.getResult();
            fail("TimeoutException not thrown");
        } catch (Exception e) {
            assertThat(e.getCause(), is(instanceOf(TimeoutException.class)));
        }
    }

    /**
     * Verify LRU eviction of shared/invalidated lines
     */
    @Test
    public void testEviction1() throws Exception {
        cache = makeCache(120);

        CacheListener listener = mock(CacheListener.class);
        cache.addCacheListener(listener);

        PUTX(101L, sh(10), 1L, "0123456789");
        PUT(201L, sh(10), 1L, "0123456789");
        PUTX(102L, sh(10), 1L, "0123456789");
        PUT(202L, sh(10), 1L, "0123456789");
        PUTX(13L, sh(10), 1L, "0123456789");
        PUT(203L, sh(10), 1L, "0123456789");
        PUTX(104L, sh(10), 1L, "0123456789");
        PUTX(105L, sh(10), 1L, "0123456789");
        PUT(204L, sh(10), 1L, "0123456789");
        PUTX(16L, sh(10), 1L, "0123456789");

        GETX(13, sh(10)); // -> I

        get(202);
        get(204);
        get(203);
        get(201);

        GETX(16, sh(10));

        for (long i = 107; i <= 200; i++)
            PUTX(i, sh(10), 1L, "0123456789");
        for (long i = 300; i <= 400; i++)
            PUT(i, sh(10), 1L, "0123456789");

        InOrder inOrder = inOrder(listener);
        inOrder.verify(listener).evicted(cache, 13);
        inOrder.verify(listener).evicted(cache, 202);
        inOrder.verify(listener).evicted(cache, 204);
        inOrder.verify(listener).evicted(cache, 203);
        inOrder.verify(listener).evicted(cache, 201);
        inOrder.verify(listener).evicted(cache, 16);
        for (long i = 101; i <= 200; i++)
            verify(listener, never()).evicted(cache, i);
    }

    @Test
    public void whenNodeRemovedThenCleanUp() throws Exception {
        PUT(1L, sh(10), 1L, "11");
        PUT(2L, sh(10), 1L, "22");
        PUTX(3L, sh(20), 1L, "33", 10, 20, 30);
        PUT(4L, sh(20), 1L, "44");

        CacheListener listener = mock(CacheListener.class);
        cache.addCacheListener(listener);

        cache.nodeRemoved(sh(10));

        assertState(1L, I, null);
        assertState(2L, I, null);
        assertState(3L, O, null);
        assertState(4L, S, null);
//        verify(listener).evicted(1L);
//        verify(listener).evicted(2L);
        verify(listener, never()).evicted(cache, 3L);
        verify(listener, never()).evicted(cache, 4L);

//        assertThat(cache.getLine(1), is(nullValue()));
//        assertThat(cache.getLine(2), is(nullValue()));
//        assertThat(cache.getLine(3), is(not(nullValue())));
//        assertThat(cache.getLine(4), is(not(nullValue())));
        // check line 3's sharers by getx and verifying INV's
        cache.runOp(new Op(GETX, 3L, null));
        verify(comm).send(argThat(equalTo(Message.INV(sh(20), 3L, sh(20)))));
        verify(comm).send(argThat(equalTo(Message.INV(sh(30), 3L, sh(20)))));
        if (hasServer)
            verify(comm).send(argThat(equalTo(Message.INV(Comm.SERVER, 3L, sh(20)))));
        verify(comm, never()).send(argThat(equalTo(Message.INV(sh(10), 3L, sh(20)))));
    }

    /**
     * Make sure an exception is thrown when putting or setting data larger than the maximum data item size.
     */
    @Test
    public void whenPutOrSetOverMaxItemSizeThenThrowException() throws Exception {
        cache.setMaxItemSize(7);

        long id = put("0123456");

        try {
            put("01234567");
            fail("Exception not thrown");
        } catch (RuntimeException e) {
        }

        set(id, "012345");

        try {
            set(id, "01234567");
            fail("Exception not thrown");
        } catch (RuntimeException e) {
        }
    }

    /**
     * It is possible to set data items to null.
     */
    @Test
    public void testNullData() throws Exception {
        long ref1 = put(null);
        assertThat(get(ref1), is(nullValue()));

        set(ref1, "foo");
        assertThat(get(ref1), is("foo"));

        long ref2 = put("foo");
        assertThat(get(ref2), is("foo"));

        set(ref2, null);
        assertThat(get(ref2), is(nullValue()));
    }

    /**
     *
     */
    @Test
    public void testAlloc() throws Exception {
        final RefAllocationsListener listener = hasServer ? null : getRefAllocationListener(cache.getRefAllocator());
        Object res;
        Op alloc;

        Transaction txn = cache.beginTransaction();
        alloc = new Op(ALLOC, -1L, 10, txn);
        res = cache.runOp(alloc);

        assertThat(res, is(PENDING));
        if (hasServer)
            verify(comm).send(argThat(equalTo(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT))));
        else
            verify(cluster).allocateRefs(anyInt());

        if (hasServer)
            cache.receive(Message.ALLOCED_REF(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT), 100, 3));
        else
            listener.refsAllocated(100, 3);

        assertThat(alloc.getFuture().isDone(), is(false));

        if (hasServer)
            cache.receive(Message.ALLOCED_REF(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT), 200, 30));
        else
            listener.refsAllocated(200, 30);

        res = alloc.getResult();

        assertThat((Long) res, is(200L));

        for (int i = 0; i < 10; i++) {
            res = get(200L + i);
            assertThat(res, is(nullValue()));
            assertModified(200L + i, true);
            assertThat(cache.getLine(200L + i).isLocked(), is(true));
        }

        res = doOp(ALLOC, -1L, 7, txn);
        assertThat((Long) res, is(210L));

        alloc = new Op(ALLOC, -1L, 20, txn);
        res = cache.runOp(alloc);

        assertThat(res, is(PENDING));
        if (hasServer)
            verify(comm, atLeastOnce()).send(argThat(equalTo(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT))));
        else
            verify(cluster, atLeastOnce()).allocateRefs(anyInt());

        if (hasServer)
            cache.receive(Message.ALLOCED_REF(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT), 300, 10));
        else
            listener.refsAllocated(300, 10);

        assertThat(alloc.getFuture().isDone(), is(false));

        if (hasServer)
            cache.receive(Message.ALLOCED_REF(Message.ALLOC_REF(Comm.SERVER, DEFAULT_ALLOC_COUNT), 400, 50));
        else
            listener.refsAllocated(400, 50);

        res = alloc.getResult();

        assertThat((Long) res, is(400L));
    }

    @Ignore
    @Test
    public void testTransactions() {
        pending();
    }

    /////////////////////////////////////////////////////////////////////////////////
    private boolean hasServer() {
        return hasServer; //cluster.hasServer();
    }

    private LineMessage matchingMessage(Op.Type type, short node, long line) {
        switch (type) {
            case GET:
            case GETS:
                return Message.GET(node, line);
            case GETX:
                return Message.GETX(node, line);
            default:
                throw new IllegalArgumentException("Type must be GET/GETS/GETX but is " + type);
        }
    }

    void makeShared(long id) {
        cache.receive(Message.GET(sh(10), id));
    }

    void makeInvalid(short owner, long id) {
        cache.receive(Message.INV(owner, id, owner).setMessageId(++messageId));
    }

    void PUT(long id, short owner, long version, String obj) {
        cache.receive(Message.PUT(owner, id, version, toBuffer(obj)).setMessageId(++messageId));
    }

    void PUTX(long id, short owner, long version, String obj, int... sharers) {
        short[] ssharers = new short[sharers.length];
        for (int i = 0; i < sharers.length; i++)
            ssharers[i] = (short) sharers[i];
        cache.receive(Message.PUTX(Message.GETX(owner, id), id, ssharers, 0, version, toBuffer(obj)).setMessageId(++messageId));
        //cache.receive(Message.BACKUPACK(sh(-1), id, version));
    }

    void INVOKE(long id, short owner, long version, LineFunction function) {
        cache.receive(Message.INVOKE(owner, id, function));
    }

    void PUTX(long id, short owner, long version, String obj) {
        PUTX(id, owner, 0, version, obj);
    }

    void PUTX(long id, short owner, int parts, long version, String obj) {
        cache.receive(Message.PUTX(Message.GETX(owner, id), id, new short[0], parts, version, toBuffer(obj)).setMessageId(++messageId));
        if (hasServer())
            cache.receive(Message.INVACK(Message.INV(sh(0), id, owner)));
        //cache.receive(Message.BACKUPACK(sh(-1), id, version));
    }

    void GET(long id, short node) {
        cache.receive(Message.GET(node, id));
    }

    void GETX(long id, short node) {
        cache.receive(Message.GETX(node, id));
    }

    void INV(long id, short owner) {
        cache.receive(Message.INV(owner, id, owner).setMessageId(++messageId));
    }

    void INVACK(long id, short owner) {
        cache.receive(Message.INVACK(owner, id));
    }

    long put(String obj) throws Exception {
        getRefAllocationListener(cache.getRefAllocator()).refsAllocated(100, 1000);
        return (Long) doOp(PUT, -1L, serialize(obj));
    }

    String get(long id) throws Exception {
        return deserialize((byte[]) doOp(GET, id));
    }

    void set(long id, String obj) throws Exception {
        doOp(SET, id, serialize(obj));
    }

    void set(long id, String obj, Transaction txn) throws Exception {
        doOp(SET, id, serialize(obj), txn);
    }

    void del(long id) throws Exception {
        doOp(DEL, id);
    }

    private Message captureMessage() throws Exception {
        ArgumentCaptor<Message> captor = (ArgumentCaptor) ArgumentCaptor.forClass(Message.class);
        verify(comm).send(captor.capture());
        return captor.getValue();
    }

    private NodeInfo makeNodeInfo(final short node) {
        return new NodeInfo() {
            @Override
            public String getName() {
                return "NODE-" + node;
            }

            @Override
            public short getNodeId() {
                return node;
            }

            @Override
            public Object get(String property) {
                throw new UnsupportedOperationException();
            }

            @Override
            public Collection<String> getProperties() {
                throw new UnsupportedOperationException();
            }
        };
    }

    public static RefAllocationsListener getRefAllocationListener(RefAllocator allocator) {
        if (new MockUtil().isMock(allocator)) {
            try {
                return (RefAllocationsListener) capture(allocator, "addRefAllocationsListener", arg(RefAllocationsListener.class));
            } catch (Exception e) {
                return null;
            }
        } else
            return allocator.getRefAllocationsListeners().iterator().next();
    }

    void assertState(long id, State state, State nextState) {
        CacheLine line = cache.getLine(id);
        assertThat(line.getState(), is(state));
        assertThat(line.getNextState(), is(nextState));
    }

    void assertOwner(long id, short node) {
        CacheLine line = cache.getLine(id);
        assertThat(line.getOwner(), is(node));
    }

    void assertVersion(long id, long version) {
        CacheLine line = cache.getLine(id);
        assertThat(line.getVersion(), is(version));
    }

    void assertLocked(long id, boolean value) {
        CacheLine line = cache.getLine(id);
        assertThat(line.isLocked(), is(value));
    }

    void assertModified(long id, boolean value) {
        CacheLine line = cache.getLine(id);
        assertThat(line.is(CacheLine.MODIFIED), is(value));
    }

    void evict(long id, boolean invack) {
        CacheLine line = cache.getLine(id);
        cache.evictLine(line, invack);
    }

    static long id(long id) {
        return Cache.MAX_RESERVED_REF_ID + id;
    }

    static short sh(int x) {
        return (short) x;
    }

    static short[] sh(int... args) {
        final short[] array = new short[args.length];
        for (int i = 0; i < args.length; i++)
            array[i] = (short) args[i];
        return array;
    }

    static ByteBuffer toBuffer(String object) {
        return ByteBuffer.wrap(serialize(object));
    }

    static byte[] serialize(String object) {
        return object != null ? object.getBytes(Charsets.UTF_8) : null;
    }

    static String deserialize(Object obj) {
        return deserialize((byte[]) obj);
    }

    static String deserialize(byte[] array) {
        return array != null ? new String(array, Charsets.UTF_8) : null;
    }

    static void pending() {
        fail("Test pending");
    }

    public Object doOp(Op.Type type, long line, byte[] data, Object extra) throws TimeoutException {
        return cache.doOp(type, line, (Object) (data != null ? Arrays.copyOf(data, data.length) : null), extra, null);
    }

    public Object doOp(Op.Type type, long line, ByteBuffer data, Object extra) throws TimeoutException {
        return cache.doOp(type, line, (Object) data, extra, null);
    }

    public Object doOp(Op.Type type, long line, Persistable data, Object extra) throws TimeoutException {
        return cache.doOp(type, line, (Object) data, extra, null);
    }

    public Object doOp(Op.Type type, long line, Object extra) throws TimeoutException {
        return cache.doOp(type, line, (Object) null, extra, null);
    }

    public Object doOp(Op.Type type, long line) throws TimeoutException {
        return cache.doOp(type, line, (Object) null, null, null);
    }

    public Object doOp(Op.Type type, long line, byte[] data) throws TimeoutException {
        return doOp(type, line, data, null);
    }

    public Object doOp(Op.Type type, long line, ByteBuffer data) throws TimeoutException {
        return doOp(type, line, data, null);
    }

    public Object doOp(Op.Type type, long line, Persistable data) throws TimeoutException {
        return doOp(type, line, data, null);
    }

    public Object doOp(Op.Type type, long line, byte[] data, Object extra, Transaction txn) throws TimeoutException {
        return cache.doOp(type, line, (Object) (data != null ? Arrays.copyOf(data, data.length) : null), extra, txn);
    }

    public Object doOp(Op.Type type, long line, ByteBuffer data, Object extra, Transaction txn) throws TimeoutException {
        return cache.doOp(type, line, (Object) data, extra, txn);
    }

    public Object doOp(Op.Type type, long line, Persistable data, Object extra, Transaction txn) throws TimeoutException {
        return cache.doOp(type, line, (Object) data, extra, txn);
    }

    public Object doOp(Op.Type type, long line, Object extra, Transaction txn) throws TimeoutException {
        return cache.doOp(type, line, (Object) null, extra, txn);
    }

    public Object doOp(Op.Type type, long line, Transaction txn) throws TimeoutException {
        return cache.doOp(type, line, (Object) null, null, txn);
    }

    public Object doOp(Op.Type type, long line, byte[] data, Transaction txn) throws TimeoutException {
        return doOp(type, line, data, null, txn);
    }

    public Object doOp(Op.Type type, long line, ByteBuffer data, Transaction txn) throws TimeoutException {
        return doOp(type, line, data, null, txn);
    }

    public Object doOp(Op.Type type, long line, Persistable data, Transaction txn) throws TimeoutException {
        return doOp(type, line, data, null, txn);
    }

    private void setCommMsgCounter() throws NodeNotFoundException {
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                Message msg = (Message) invocation.getArguments()[0];
                System.out.println("mock send msg " + msg);
                if (msg.getMessageId() < 0)
                    msg.setMessageId(++messageId);
                return null;
            }
        }).when(comm).send(any(Message.class));
    }
}
TOP

Related Classes of co.paralleluniverse.galaxy.core.CacheTest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.