co.paralleluniverse.galaxy.core.Cache.java Source code

Java tutorial

Introduction

Here is the source code for co.paralleluniverse.galaxy.core.Cache.java

Source

/*
 * Galaxy
 * Copyright (c) 2012-2014, Parallel Universe Software Co. All rights reserved.
 * 
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *  
 *   or (per the licensee's choosing)
 *  
 * under the terms of the GNU Lesser General Public License version 3.0
 * as published by the Free Software Foundation.
 */
package co.paralleluniverse.galaxy.core;

import co.paralleluniverse.common.MonitoringType;
import co.paralleluniverse.common.collection.LongObjectProcedure;
import co.paralleluniverse.common.io.Checksum;
import co.paralleluniverse.common.io.HashFunctionChecksum;
import co.paralleluniverse.common.io.Persistable;
import co.paralleluniverse.common.io.Persistables;
import co.paralleluniverse.common.io.VersionedPersistable;
import static co.paralleluniverse.common.logging.LoggingUtils.hex;
import co.paralleluniverse.common.util.DegenerateInvocationHandler;
import co.paralleluniverse.common.util.Enums;
import co.paralleluniverse.galaxy.CacheListener;
import co.paralleluniverse.galaxy.Cluster;
import co.paralleluniverse.galaxy.ItemState;
import co.paralleluniverse.galaxy.LineFunction;
import co.paralleluniverse.galaxy.RefNotFoundException;
import co.paralleluniverse.galaxy.TimeoutException;
import co.paralleluniverse.galaxy.cluster.NodeChangeListener;
import co.paralleluniverse.galaxy.core.Message.INVRES;
import co.paralleluniverse.galaxy.core.Message.LineMessage;
import co.paralleluniverse.galaxy.core.Transaction.RollbackInfo;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.googlecode.concurrentlinkedhashmap.EvictionListener;
import com.googlecode.concurrentlinkedhashmap.Weigher;
import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.shorts.ShortArraySet;
import it.unimi.dsi.fastutil.shorts.ShortIterator;
import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
import it.unimi.dsi.fastutil.shorts.ShortSet;
import java.beans.ConstructorProperties;
import java.lang.reflect.Proxy;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.cliffc.high_scale_lib.NonBlockingHashMapLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jmx.export.annotation.ManagedAttribute;

/**
 * This is the big one. This is where most of Galaxy's logic is found. In particular it handles all of the MOESI protocol.<br/>
 * Other important classes:
 *
 * <ul> <li>{@link MainMemory}</li> <li>{@link co.paralleluniverse.galaxy.netty.UDPComm UDPComm}</li> <li>{@link AbstractCluster}</li>
 * </ul>
 *
 * @author pron
 */
public class Cache extends ClusterService
        implements MessageReceiver, NodeChangeListener, co.paralleluniverse.galaxy.Cache {
    /*
     * To preserve memory ordering semantics, all messages from node N must be received and processed in the order in which they
     * were sent.
     */
    static final long MAX_RESERVED_REF_ID = 0xffffffffL;
    private static final boolean STALE_READS = true;
    private static final int SHARER_SET_DEFAULT_SIZE = 10;
    private static final Logger LOG = LoggerFactory.getLogger(Cache.class);
    private long timeout = 200000;
    private int maxItemSize = 1024;
    private boolean compareBeforeWrite = true;
    //
    private final Comm comm;
    private final Backup backup;
    private final CacheStorage storage;
    private final CacheMonitor monitor;
    private MessageReceiver receiver;
    private final boolean hasServer;
    //
    private final NonBlockingHashMapLong<CacheLine> owned;
    private final ConcurrentMap<Long, CacheLine> shared;
    private final NonBlockingHashMapLong<ArrayList<Op>> pendingOps;
    private final NonBlockingHashMapLong<LinkedHashSet<LineMessage>> pendingMessages;
    private ConcurrentLinkedDeque<CacheLine> freeLineList;
    private ConcurrentLinkedDeque<ShortSet> freeSharerSetList;
    private final ThreadLocal<Queue<Message>> shortCircuitMessage = new ThreadLocal<Queue<Message>>();
    private boolean reuseLines = true;
    private boolean reuseSharerSets = false;
    private boolean broadcastsRoutedToServer;
    private boolean rollbackSupported = true;
    private boolean synchronous = false;
    private final Set<NodeEvent> nodeEvents = new CopyOnWriteArraySet<NodeEvent>();
    private long maxStaleReadMillis = 500;
    //
    private final IdAllocator idAllocator;
    private final NonBlockingHashMapLong<OwnerClock> ownerClocks;
    private final OwnerClock globalOwnerClock;
    private final AtomicLong clock = new AtomicLong();
    private final ThreadLocal<Boolean> recursive = new ThreadLocal<Boolean>();
    private final ThreadLocal<Boolean> inNodeEventHandler = new ThreadLocal<Boolean>();
    private final List<CacheListener> listeners = new CopyOnWriteArrayList<CacheListener>();
    //
    static final Object PENDING = new Object() {
        @Override
        public String toString() {
            return "PENDING";
        }
    };
    static final Object DIDNT_HANDLE = new Object();
    //
    private static final int LINE_NO_CHANGE = 0;
    private static final int LINE_STATE_CHANGED = 1;
    private static final int LINE_OWNER_CHANGED = 1 << 1;
    private static final int LINE_MODIFIED_CHANGED = 1 << 2;
    private static final int LINE_EVERYTHING_CHANGED = -1;
    //
    private static final long HIT_OR_MISS_OPS = Enums.setOf(Op.Type.GET, Op.Type.GETS, Op.Type.GETX, Op.Type.SET,
            Op.Type.DEL);
    private static final long FAST_TRACK_OPS = Enums.setOf(Op.Type.GET, Op.Type.GETS, Op.Type.GETX, Op.Type.SET,
            Op.Type.DEL, Op.Type.INVOKE, Op.Type.LSTN);
    private static final long LOCKING_OPS = Enums.setOf(Op.Type.GETS, Op.Type.GETX, Op.Type.SET, Op.Type.DEL);
    private static final long PUSH_OPS = Enums.setOf(Op.Type.PUSH, Op.Type.PUSHX);

    private static final long MESSAGES_BLOCKED_BY_LOCK = Enums.setOf(Message.Type.GET, Message.Type.GETX,
            Message.Type.INV, Message.Type.PUT, Message.Type.PUTX, Message.Type.INVOKE);

    private static final long MESSAGES_WITH_FAST_REPLY = Enums.setOf(Message.Type.GET, Message.Type.GETX,
            Message.Type.INVOKE);

    //<editor-fold defaultstate="collapsed" desc="Constructors and Config">
    /////////////////////////// Constructors and Config ///////////////////
    @ConstructorProperties({ "name", "cluster", "comm", "storage", "backup", "monitoringType", "maxCapacity" })
    public Cache(String name, Cluster cluster, Comm comm, CacheStorage storage, Backup backup,
            MonitoringType monitoringType, long maxCapacity) {
        this(name, cluster, comm, storage, backup, createMonitor(monitoringType, name), maxCapacity);
    }

    @SuppressWarnings("LeakingThisInConstructor")
    Cache(String name, Cluster cluster, Comm comm, CacheStorage storage, Backup backup, CacheMonitor monitor,
            long maxCapacity) {
        super(name, cluster);
        this.comm = comm;
        this.storage = storage;
        this.hasServer = cluster.hasServer();
        this.monitor = monitor;

        this.backup = backup; // new Backup(comm, null);
        this.idAllocator = new IdAllocator(this, hasServer ? new ServerRefAllocator(comm) : (RefAllocator) cluster);

        if (STALE_READS) {
            this.ownerClocks = new NonBlockingHashMapLong<OwnerClock>();
            this.globalOwnerClock = getOwnerClock((short) -1);
        } else {
            this.ownerClocks = null;
            this.globalOwnerClock = null;
        }

        this.monitor.setMonitoredObject(this);
        getCluster().addNodeChangeListener(this);
        this.comm.setReceiver(this);
        this.backup.setCache(this);

        this.owned = new NonBlockingHashMapLong<CacheLine>();
        this.shared = buildSharedCache(maxCapacity);
        this.pendingOps = new NonBlockingHashMapLong<ArrayList<Op>>();
        this.pendingMessages = new NonBlockingHashMapLong<LinkedHashSet<LineMessage>>();
    }

    private ConcurrentMap<Long, CacheLine> buildSharedCache(long maxCapacity) {
        return new ConcurrentLinkedHashMap.Builder<Long, CacheLine>().initialCapacity(1000)
                .maximumWeightedCapacity(maxCapacity).weigher(new Weigher<CacheLine>() {
                    @Override
                    public int weightOf(CacheLine line) {
                        return 1 + line.size();
                    }
                }).listener(new EvictionListener<Long, CacheLine>() {
                    @Override
                    public void onEviction(Long id, CacheLine line) {
                        evictLine(line, true);
                    }
                }).build();
    }

    static CacheMonitor createMonitor(MonitoringType monitoringType, String name) {
        if (monitoringType == null)
            return (CacheMonitor) Proxy.newProxyInstance(Cache.class.getClassLoader(),
                    new Class<?>[] { CacheMonitor.class }, DegenerateInvocationHandler.INSTANCE);
        else
            switch (monitoringType) {
            case JMX:
                return new JMXCacheMonitor(name);
            case METRICS:
                return new MetricsCacheMonitor();
            }
        throw new IllegalArgumentException("Unknown MonitoringType " + monitoringType);
    }

    public void setCompareBeforeWrite(boolean value) {
        assertDuringInitialization();
        this.compareBeforeWrite = value;
    }

    @ManagedAttribute
    public boolean isCompareBeforeWrite() {
        return compareBeforeWrite;
    }

    public void setMaxItemSize(int maxItemSize) {
        assertDuringInitialization();
        this.maxItemSize = maxItemSize;
    }

    @ManagedAttribute
    public int getMaxItemSize() {
        return maxItemSize;
    }

    public void setTimeout(long timeout) {
        assertDuringInitialization();
        this.timeout = timeout;
    }

    @ManagedAttribute
    public long getTimeout() {
        return timeout;
    }

    public void setReuseLines(boolean value) {
        assertDuringInitialization();
        this.reuseLines = value;
    }

    @ManagedAttribute
    public boolean isReuseLines() {
        return reuseLines;
    }

    public void setReuseSharerSets(boolean value) {
        assertDuringInitialization();
        this.reuseSharerSets = value;
    }

    @ManagedAttribute
    public boolean isReuseSharerSets() {
        return reuseSharerSets;
    }

    public void setRollbackSupported(boolean value) {
        assertDuringInitialization();
        this.rollbackSupported = value;
    }

    public void setSynchronous(boolean value) {
        assertDuringInitialization();
        this.synchronous = value;
    }

    @ManagedAttribute
    public boolean isRollbackSupported() {
        return rollbackSupported;
    }

    private Checksum getChecksum() {
        assert compareBeforeWrite;
        return new HashFunctionChecksum(com.google.common.hash.Hashing.murmur3_128()); // new DoubleHasher(); // new MessageDigestChecksum("MD5"); // new MessageDigestChecksum("SHA-1"); // new MessageDigestChecksum("SHA-256"); 
    }

    public long getMaxStaleReadMillis() {
        assertDuringInitialization();
        return maxStaleReadMillis;
    }

    @ManagedAttribute
    public void setMaxStaleReadMillis(long maxStaleReadMillis) {
        this.maxStaleReadMillis = maxStaleReadMillis;
    }

    @Override
    public void init() throws Exception {
        super.init();

        if (synchronous)
            throw new RuntimeException("Synchronous mode has not been implemented yet.");

        this.freeLineList = reuseLines ? new ConcurrentLinkedDeque<CacheLine>() : null;
        this.freeSharerSetList = reuseSharerSets ? new ConcurrentLinkedDeque<ShortSet>() : null;
        this.broadcastsRoutedToServer = hasServer && ((AbstractComm) comm).isSendToServerInsteadOfMulticast(); // this is a special case that requires special handling b/c of potential consistency problems (see MainMemory)
    }

    void allocatorReady() {
        LOG.info("Id allocator is ready");
        if (getCluster().isOnline() && getCluster().isMaster())
            setReady(true);
    }

    @Override
    protected void start(boolean master) {
        if (idAllocator.isReady())
            setReady(true);
    }

    @Override
    public void awaitAvailable() throws InterruptedException {
        super.awaitAvailable();
    }
    //</editor-fold>

    public void setReceiver(MessageReceiver receiver) {
        assertDuringInitialization();
        this.receiver = receiver;
    }

    public boolean hasServer() {
        return hasServer;
    }

    public void addCacheListener(CacheListener listener) {
        listeners.add(listener);
    }

    public void removeCacheListener(CacheListener listener) {
        listeners.remove(listener);
    }

    Iterator<CacheLine> ownedIterator() {
        return owned.values().iterator();
    }

    RefAllocator getRefAllocator() {
        return idAllocator.getRefAllocator();
    }

    //<editor-fold defaultstate="collapsed" desc="Types">
    /////////////////////////// Types ///////////////////////////////////////////
    enum State {
        // Invalid, Shared, Owned, Exclusive
        I, S, O, E; // Order matters! (used by setNextState)

        public boolean isLessThan(State other) {
            return compareTo(other) < 0;
        }
    }

    static class CacheLine {
        private static final byte LOCKED = 1;
        public static final byte MODIFIED = 1 << 1;
        public static final byte SLAVE = 1 << 2; // true when slave(s) think line is owned by us
        public static final byte DELETED = 1 << 3;
        public static final byte INCOMPLETE = 1 << 4;
        private long id; // 8
        private byte flags; // 1
        //private short sem;              // 2
        long timeAccessed;
        private volatile State state; // 4
        private State nextState; // 4
        private volatile long version; // 8 
        private long ownerClock; // 8 must contain a counter that is monotonically increasing for each owner, e.g, the message id
        private ByteBuffer data; // 4
        private short parts; // 2
        private short owner = -1; // 2
        private ShortSet sharers; // 4
        private volatile CacheListener listener; // 4
        // =
        // 49 (+ 8 = 57)

        public long getId() {
            return id;
        }

        private void clearFlags() {
            flags = 0;
        }

        void lock() {
            flags |= LOCKED;//sem++;
        }

        boolean unlock() {
            if (!is(LOCKED | DELETED))
                throw new IllegalStateException("Item " + hex(id) + " has not been pinned!");
            flags &= ~LOCKED;
            return true;
            //            sem--;
            //            if (sem < 0)
            //                throw new IllegalStateException("Item has been released more time than it's been pinned!");
            //            return sem == 0;
        }

        public boolean isLocked() {
            return (flags & LOCKED) != 0; // sem > 0;
        }

        public boolean isIncomplete() {
            return parts > 0;
        }

        public boolean is(byte flag) {
            return (flags & flag) != 0;
        }

        public boolean is(int flag) {
            return (flags & (byte) flag) != 0;
        }

        private void set(byte flag, boolean value) {
            flags = (byte) (value ? (flags | flag) : (flags & ~flag));
        }

        public State getNextState() {
            return nextState;
        }

        public short getOwner() {
            return owner;
        }

        public State getState() {
            return state;
        }

        public long getVersion() {
            return version;
        }

        public long getOwnerClock() {
            return ownerClock;
        }

        public void setOwnerClock(long clock) {
            this.ownerClock = clock;
        }

        public ByteBuffer getData() {
            return data;
        }

        public CacheListener getListener() {
            return listener;
        }

        private CacheListener setListener(CacheListener listener, boolean ifAbsent) {
            if (!ifAbsent | this.listener == null)
                this.listener = listener;
            return this.listener;
        }

        public int size() {
            return data != null ? data.capacity() : 0; // user's care about capacity, not actual usage
        }

        public void rewind() {
            if (data != null)
                data.rewind();
        }

        @Override
        public String toString() {
            final StringBuffer sb = new StringBuffer();
            sb.append("LINE: ").append(hex(id));
            sb.append(" ").append(state).append(" ");
            if (nextState != null)
                sb.append("(->").append(nextState).append(")");
            sb.append(" OWN: ").append(owner);
            sb.append(" SHARE: ").append(sharers);
            sb.append(" VER: ").append(version);
            sb.append(" DATA: ").append(data != null ? "(" + size() + " bytes)" : "null");
            if (isLocked())
                sb.append(" LOCKED");
            if (is(MODIFIED))
                sb.append(" MODIFIED");
            if (is(SLAVE))
                sb.append(" SLAVE");
            if (is(DELETED))
                sb.append(" DELETED");
            return sb.toString();
        }
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="Execution flow">
    /////////////////////////// Execution flow ///////////////////////////////////////////
    public Object doOp(Op.Type type, long id, Object data, Object extra, Transaction txn) throws TimeoutException {
        if (!getCluster().isMaster() && type != Op.Type.LSTN)
            throw new IllegalStateException("Node is a slave. Cannot run grid operations");

        if (LOG.isDebugEnabled())
            LOG.debug("Run(fast): Op.{}(line:{}{}{})", type, hex(id),
                    ((data != null && LOG.isTraceEnabled()) ? ", data:" + data : ""),
                    (extra != null ? ", extra:" + extra : ""));

        Object result = runFastTrack(id, type, data, extra, txn);
        if (result instanceof Op)
            return doOp((Op) result);
        else if (result == PENDING) {
            if (Thread.currentThread() instanceof CommThread)
                throw new RuntimeException("This operation blocks a comm thread.");
            return doOp(new Op(type, id, data, extra, txn)); // "slow" track
        } else
            return result;
    }

    public ListenableFuture<Object> doOpAsync(Op.Type type, long id, Object data, Object extra, Transaction txn) {
        if (!getCluster().isMaster())
            throw new IllegalStateException("Node is a slave. Cannot run grid operations");

        if (LOG.isDebugEnabled())
            LOG.debug("Run(fast): Op.{}(line:{}{}{})", type, hex(id),
                    ((data != null && LOG.isTraceEnabled()) ? ", data:" + data : ""),
                    (extra != null ? ", extra:" + extra : ""));
        Object result = runFastTrack(id, type, data, extra, txn);
        if (result instanceof Op)
            return doOpAsync((Op) result);
        else if (result == PENDING)
            return doOpAsync(new Op(type, id, data, extra, txn)); // "slow" track
        else
            return Futures.immediateFuture(result);
    }

    /**
     * This one blocks!
     *
     * @param op
     * @return
     */
    private Object doOp(Op op) throws TimeoutException {
        try {
            if (op.txn != null)
                op.txn.add(op);
            Object result = runOp(op);
            if (result == PENDING)
                return op.getResult(timeout, TimeUnit.MILLISECONDS);
            else if (result == null && op.isCancelled())
                throw new CancellationException();
            else
                return result;
        } catch (java.util.concurrent.TimeoutException e) {
            throw new TimeoutException(e);
        } catch (InterruptedException e) {
            return null;
        } catch (ExecutionException e) {
            Throwable ex = e.getCause();
            if (ex instanceof TimeoutException)
                throw (TimeoutException) ex;
            Throwables.propagateIfPossible(ex);
            throw Throwables.propagate(ex);
        }
    }

    private ListenableFuture<Object> doOpAsync(Op op) {
        if (op.txn != null)
            op.txn.add(op);
        Object result = runOp(op);
        if (result == PENDING)
            return op.getFuture();
        else
            return Futures.immediateFuture(result);
    }

    /**
     * We try to run the op w/o creating an Op object.
     */
    private Object runFastTrack(long id, Op.Type type, Object data, Object extra, Transaction txn) {
        if (!type.isOf(FAST_TRACK_OPS))
            return PENDING; // no fast track
        CacheLine line = getLine(id);
        if (line == null) {
            final Object res = handleOpNoLine(type, id, extra);
            if (res != DIDNT_HANDLE)
                return res;
            else
                line = createNewCacheLine(id);
        }

        final Object res;
        synchronized (line) {
            res = handleOp(line, type, data, extra, txn, false, LINE_EVERYTHING_CHANGED, null);
        }

        if (res != PENDING)
            monitor.addOp(type, 0);
        return res;
    }

    // visible for testing
    Object runOp(Op op) {
        LOG.debug("Run: {}", op);
        recursive.set(Boolean.TRUE);
        try {
            if (op.type == Op.Type.PUT || op.type == Op.Type.ALLOC)
                return execOp(op, null);

            final long id = op.line;
            CacheLine line = getLine(id);
            if (line == null) {
                Object res = handleOpNoLine(op.type, op.line, op.getExtra());
                if (res != DIDNT_HANDLE)
                    return res;
                else
                    line = createNewCacheLine(op);
            }

            final Object res;
            synchronized (line) {
                res = execOp(op, line);
            }

            receiveShortCircuit();

            if (res instanceof Op)
                return runOp((Op) res);
            return res;
        } finally {
            recursive.remove();
        }
    }

    private Object execOp(Op op, CacheLine line) {
        Object res;
        try {
            res = handleOp(line, op, false, LINE_EVERYTHING_CHANGED);
        } catch (Throwable t) {
            if (op.hasFuture()) {
                op.setException(t);
                return null;
            } else {
                return Throwables.propagate(t);
            }
        }
        if (res == PENDING) {
            op.setStartTime(System.nanoTime());
            LOG.debug("Adding op to pending {} on line {}", op, line);
            addPendingOp(line, op);
        }
        return res;
    }

    /**
     * @param pending true if this op is already pending
     */
    private Object handleOp(CacheLine line, Op op, boolean pending, int lineChange) {
        LOG.debug("handleOp: {} line: {}", op, line);
        if (op.isCancelled()) {
            LOG.debug("handleOp: {} line: {}: CANCELLED", op, line);
            return null;
        }
        try {
            Object res;
            switch (op.type) {
            case PUT:
                res = handleOpPut(op, line);
                break;
            case ALLOC:
                res = handleOpAlloc(op, line);
                break;
            default:
                res = handleOp(line, op.type, op.data, op.getExtra(), op.txn, pending, lineChange, op);
                break;
            }
            if (LOG.isDebugEnabled())
                LOG.debug("handleOp: {} -> {} line: {}", op,
                        (res instanceof byte[] ? "(" + ((byte[]) res).length + " bytes)" : res), line);
            if (res == PENDING)
                return res;
            else if (res instanceof Op)
                return res;
            else {// res != PENDING
                completeOp(line, op, res, pending);
                return res;
            }
        } catch (Exception e) {
            opException(line, op, e, pending);
            return null;
        }
    }

    private Object handleOp(CacheLine line, Op.Type type, Object data, Object extra, Transaction txn,
            boolean pending, int lineChange, Op op) {
        assert line != null || type == Op.Type.PUT || type == Op.Type.ALLOC;
        handleNodeEvents(line);

        Object res = null;

        if (line != null && shouldHoldOp(line, type))
            res = PENDING;
        else {
            switch (type) {
            case GET:
            case GETS:
                res = handleOpGet(line, type, data, nodeHint(extra), txn, lineChange);
                break;
            case GETX:
                res = handleOpGetX(line, data, nodeHint(extra), txn, lineChange, pending);
                break;
            case GET_FROM_OWNER:
                res = handleOpGetFromOwner(line, extra);
                break;
            case SET:
                res = handleOpSet(line, data, nodeHint(extra), txn, lineChange);
                break;
            case DEL:
                res = handleOpDel(line, nodeHint(extra), txn, lineChange);
                break;
            case SEND:
                res = handleOpSend(line, extra, lineChange);
                break;
            case PUSH:
                res = handleOpPush(line, extra, lineChange);
                break;
            case PUSHX:
                res = handleOpPushX(line, extra, lineChange);
                break;
            case LSTN:
                res = handleOpListen(line, data, extra);
                break;
            case INVOKE:
                res = handleOpInvoke(line, data, op, extra, txn, lineChange);
                break;
            }

            accessLine(line);
        }

        if (!pending && type.isOf(HIT_OR_MISS_OPS)) {
            if (res != PENDING) {
                if (line.getState() == State.I)
                    monitor.addStaleHit();
                else
                    monitor.addHit();
            }
            // addMiss and addInvalidates are handled by setNextState();
        }

        return res;
    }

    private void completeOp(CacheLine line, Op op, Object res, boolean pending) {
        long duration = 0;
        if (pending) {
            assert op.getStartTime() != 0;
            duration = (System.nanoTime() - op.getStartTime()) / 1000; // Microseconds
        }
        if (op.hasFuture()) {
            if (LOG.isDebugEnabled())
                LOG.debug("completeOp: {} -> {} line: {}", op,
                        (res instanceof byte[] ? "(" + ((byte[]) res).length + " bytes)" : res), line);
            op.setResult(res);
        }
        monitor.addOp(op.type, duration);
    }

    private void opException(CacheLine line, Op op, Throwable t, boolean pending) {
        if (pending) {
            if (!op.hasFuture())
                op.createFuture();

            op.setException(t);
        } else
            throw Throwables.propagate(t);
    }

    /**
     * Called for a LineMessage when the line is not found
     *
     * @return true if the op was handled - nothing else necessary. false if not, and creating the line is required
     */
    protected Object handleOpNoLine(Op.Type type, long id, Object extra) {
        if (LOG.isDebugEnabled())
            LOG.debug("Line {} not found.", hex(id));
        switch (type) {
        case GET_FROM_OWNER:
            return extra;
        case PUSH:
        case PUSHX:
            LOG.info("Attempt to push line {}, but line is not in cache. ", hex(id));
            return null;
        default:
            return DIDNT_HANDLE;
        }
    }

    private boolean shouldHoldOp(CacheLine line, Op.Type op) {
        return (hasPendingBlockingMessages(line) // give messages a chance if we're not part of a transaction (line isn't locked) and messages are simply waiting for backups or another independent set (that isn't part of a transaction)
                && op.isOf(LOCKING_OPS) && !line.isLocked()
                && !(line.getState() != State.E && line.getNextState() == State.E))
                || (line.is(CacheLine.MODIFIED) && op.isOf(PUSH_OPS));
    }

    private void handlePendingOps(CacheLine line, int change) {
        if (line == null)
            return;
        for (Iterator<Op> it = getPendingOps(line).iterator(); it.hasNext();) {
            final Op op = it.next();
            if (LOG.isDebugEnabled())
                LOG.debug("Handling pending op {}, change = {}", op, change);
            if (handleOp(line, op, true, change) != PENDING)
                it.remove();
        }
    }

    @Override
    @SuppressWarnings({ "BoxedValueEquality" })
    public void receive(Message message) {
        if (recursive.get() != Boolean.TRUE) {
            recursive.set(Boolean.TRUE);
            try {
                LOG.debug("Received: {}", message);
                receive1(message);
                receiveShortCircuit();
            } finally {
                recursive.remove();
            }
        } else { // short-circuit
            LOG.debug("Received short-circuit: {}", message);
            Queue<Message> ms = shortCircuitMessage.get();
            if (ms == null) {
                ms = new ArrayDeque<Message>();
                shortCircuitMessage.set(ms);
            }
            ms.add(message);
        }
    }

    private void receive1(Message message) {
        switch (message.getType()) {
        case MSG:
            if (handleMessageMessengerMsg((Message.MSG) message))
                return;
            break;
        case MSGACK:
            if (((LineMessage) message).getLine() == -1) {
                if (receiver != null)
                    receiver.receive(message);
                return;
            }
            break;
        case BACKUP_PACKETACK:
            backup.receive(message);
            return;
        case ALLOCED_REF:
            if (idAllocator.getRefAllocator() instanceof MessageReceiver) {
                ((MessageReceiver) idAllocator.getRefAllocator()).receive(message);
                return;
            }
            break;
        default:
        }
        runMessage((LineMessage) message);
        monitor.addMessageReceived(message.getType());
    }

    private void runMessage(LineMessage message) {
        final long id = message.getLine();
        CacheLine line = getLine(id);

        if (LOG.isDebugEnabled())
            LOG.debug("runMessage: {} {} {}", line, hex(id), message);

        if (line == null) {
            if (handleMessageNoLine(message))
                return;
            else
                line = createNewCacheLine(message);
        }

        synchronized (line) {
            handleMessage(message, line);
        }
    }

    /**
     * Special handling for msg.
     */
    private boolean handleMessageMessengerMsg(Message.MSG message) {
        if (!message.isMessenger())
            return false;
        if (receiver == null)
            return true;

        setOwnerClockPut(message);

        if (message.getLine() == -1) {
            receiver.receive(message);
            if (message.isReplyRequired())
                send(Message.MSGACK(message));
            return true;
        }

        CacheLine line = getLine(message.getLine());
        if (line == null) {
            boolean res = handleMessageNoLine(message);
            assert res;
            return true;
        }

        synchronized (line) {
            if (handleNotOwner(message, line))
                return true;

            receiver.receive(message);
        }

        if (message.isReplyRequired())
            send(Message.MSGACK(message));
        return true;
    }

    private void handleMessage(LineMessage message, CacheLine line) {
        assert line != null;
        handleNodeEvents(line);
        int change = handleMessage1(message, line);
        handlePendingOps(line, change);
        handlePendingMessagesAfterMessage(line, change);
    }

    private int handleMessage1(LineMessage message, CacheLine line) {
        if (shouldHoldMessage(line, message)) {
            if (message.isBroadcast() && quickReplyToBroadcast(line, message))
                LOG.debug("quickReplyToBroadcast {}", message);
            else {
                LOG.debug("Adding message to pending {} on line {}", message, line);
                addPendingMessage(line, message);
            }
            if (line.is(CacheLine.MODIFIED))
                backup.flush();
            return LINE_NO_CHANGE;
        }

        accessLine(line);
        try {
            switch (message.getType()) {
            case PUT:
                return handleMessagePut((Message.PUT) message, line);
            case PUTX:
                return handleMessagePutX((Message.PUTX) message, line);
            case GET:
                return handleMessageGet((Message.GET) message, line);
            case GETX:
                return handleMessageGetX((Message.GET) message, line);
            case INV:
                return handleMessageInvalidate((Message.INV) message, line);
            case INVACK:
                return handleMessageInvalidateAck(message, line);
            case NOT_FOUND:
                return handleMessageNotFound(message, line);
            case CHNGD_OWNR:
                return handleMessageChngdOwnr((Message.CHNGD_OWNR) message, line);
            case MSG:
                return handleMessageMsg((Message.MSG) message, line);
            case MSGACK:
                return handleMessageMsgAck(message, line);
            case BACKUP: // in slave mode only
                return handleMessageBackup((Message.PUT) message, line);
            case BACKUPACK:
                return handleMessageBackupAck((Message.BACKUPACK) message, line);
            case TIMEOUT:
                return handleMessageTimeout(message, line);
            case INVOKE:
                return handleMessageInvoke((Message.INVOKE) message, line);
            case INVRES:
                return handleMessageInvRes((Message.INVRES) message, line);
            default:
                LOG.warn("Unhandled message {}", message);
                return LINE_NO_CHANGE;
            }
        } catch (IrrelevantStateException e) {
            if (message.getType() != Message.Type.CHNGD_OWNR) // TODO: check!!
                LOG.warn("Got message {} when at irrelevant state {}", message, line.state);
            return LINE_NO_CHANGE;
        }
    }

    /**
     * Called for a LineMessage when the line is not found
     *
     * @param message
     * @return true if the message was handled - nothing else necessary. false if not, and creating the line is required
     */
    protected boolean handleMessageNoLine(LineMessage message) {
        if (LOG.isDebugEnabled())
            LOG.debug("Line {} not found.", hex(message.getLine()));
        switch (message.getType()) {
        case INV:
            send(Message.INVACK((Message.INV) message));
            return true;
        //case CHNGD_OWNR:
        case INVACK:
            return true;
        case GET:
        case GETX:
        case MSG:
            handleNotOwner(message, null);
            return true;
        default:
            return false;
        }
    }

    private boolean handleNotOwner(LineMessage msg, CacheLine line) {
        if (line != null && line.is(CacheLine.DELETED)) {
            send(Message.NOT_FOUND(msg));
            return true;
        }
        if (line == null || line.state == State.I || line.state == State.S) {
            final long id;
            final short owner;
            final boolean certain;
            if (line == null) {
                id = msg.getLine();
                owner = (short) -1;
                certain = false;
            } else {
                id = line.getId();
                owner = line.getOwner();
                // actually, S doesn't mean we're certain about the owner b/c transfer of ownership (PUTX) is done before sending INVs. 
                // However, we're more certain than when we're I. 
                // It doesn't matter, though, since at the moment the certain field is ignored.
                certain = (line.state == State.S);
            }

            if (certain || !msg.isBroadcast())
                send(Message.CHNGD_OWNR(msg, id, owner, certain));
            else if (msg.isBroadcast())
                send(Message.ACK(msg));

            return true;
        } else
            return false;
    }

    private int handlePendingMessages(CacheLine line, CacheMonitor.MessageDelayReason reason) {
        int change = LINE_NO_CHANGE;

        final long now = System.nanoTime();
        int messageCount = 0;
        long totalDelay = 0;

        final Collection<LineMessage> pending = getPendingMessages(line);
        final int n = pending.size();
        for (int i = 0; i < n; i++) {
            final Iterator<LineMessage> it = pending.iterator();
            if (!it.hasNext())
                break;

            final LineMessage msg = it.next();
            it.remove();

            LOG.debug("Handling pending message {}", msg);
            change |= handleMessage1(msg, line);

            messageCount++;
            totalDelay += now - msg.getTimestamp();
        }

        if (messageCount > 0)
            monitor.addMessageHandlingDelay(messageCount, totalDelay, reason);

        if (change != LINE_NO_CHANGE) {
            handlePendingOps(line, change);
            handlePendingMessagesAfterMessage(line, change);
        }
        return change;
    }

    private boolean shouldHoldMessage(CacheLine line, Message message) {
        final boolean res = message.getType().isOf(MESSAGES_BLOCKED_BY_LOCK)
                && (line.isLocked() || line.is(CacheLine.MODIFIED) || line.isIncomplete()
                        || (line.getState() != State.E && line.getNextState() == State.E));
        //        if (res && message.getType() == Message.Type.PUTX && line.getVersion() == ((Message.PUT)message).getVersion())
        //            return false;
        if (res && message.getType() == Message.Type.INV && !line.isLocked() && !line.is(CacheLine.MODIFIED)) // INV isn't locked by -> E
            return false;
        return res;
    }

    private boolean quickReplyToBroadcast(CacheLine line, LineMessage msg) {
        // we quickly reply to a broadcast if we're the line owner so that the sender's UDPComm
        // won't be blocked in broadcast mode.
        assert msg.isBroadcast();
        if (!msg.getType().isOf(MESSAGES_WITH_FAST_REPLY))
            return false;

        if (line.state == State.O || line.state == State.E) {
            send(Message.CHNGD_OWNR(msg, line.getId(), myNodeId(), true));
            return true;
        }
        return false;
    }

    private void handlePendingMessagesAfterMessage(CacheLine line, int change) {
        if (!line.isLocked() && !line.is(CacheLine.MODIFIED)) {
            CacheMonitor.MessageDelayReason reason = null;
            if ((change & LINE_MODIFIED_CHANGED) != 0)
                reason = CacheMonitor.MessageDelayReason.BACKUP;
            else if ((change & LINE_STATE_CHANGED) != 0)
                reason = CacheMonitor.MessageDelayReason.OTHER;

            if (reason != null)
                handlePendingMessages(line, reason);
        }
    }

    public void send(Message.MSG message) {
        send((Message) message);
        monitor.addOp(Op.Type.SEND, 0);
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="Public Ops">
    /////////////////////////// Public Ops ///////////////////
    boolean tryLock(long id, ItemState state, Transaction txn) {
        final CacheLine line = getLine(id);
        if (line == null)
            return false;
        synchronized (line) {
            if ((state == ItemState.OWNED & line.getState() == State.E)
                    | (state == ItemState.SHARED & !line.getState().isLessThan(State.S))) {
                lockLine(line, txn);
                return true;
            }
        }
        return false;
    }

    public boolean isLocked(long id) {
        final CacheLine line = getLine(id);
        if (line == null)
            return false;
        else
            synchronized (line) {
                return line.isLocked();
            }
    }

    State getState(long id) {
        final CacheLine line = getLine(id);
        if (line == null)
            return null;
        else
            return line.getState();
    }

    public long getVersion(long id) {
        final CacheLine line = getLine(id);
        if (line == null)
            return -1;
        return line.getVersion();
    }

    @Override
    public CacheListener setListenerIfAbsent(long id, CacheListener listener) {
        return setListener(id, listener, true);
    }

    @Override
    public void setListener(long id, CacheListener listener) {
        setListener(id, listener, false);
    }

    @Override
    public CacheListener getListener(long id) {
        final CacheLine line = getLine(id);
        if (line == null)
            return null;
        return line.getListener();
    }

    private CacheListener setListener(long id, CacheListener listener, boolean ifAbsent) {
        try {
            return (CacheListener) doOp(Op.Type.LSTN, id, ifAbsent, listener, null);
        } catch (TimeoutException e) {
            throw new AssertionError();
        }
    }
    //</editor-fold>

    //<editor-fold defaultstate="expanded" desc="Logic">
    /////////////////////////// Logic ///////////////////////////////////////////
    //<editor-fold defaultstate="expanded" desc="Transactions">
    /////////////////////////// Transactions ///////////////////////////////////////////
    public Transaction beginTransaction() {
        Transaction txn = new Transaction(rollbackSupported);
        LOG.debug("Starting transaction: {}", txn);
        return txn;
    }

    public void rollback(Transaction txn) {
        if (!rollbackSupported)
            throw new IllegalStateException("Cache nconfigured to not support rollbacks");

        txn.forEachRollback(new LongObjectProcedure<RollbackInfo>() {
            @Override
            public boolean execute(long id, RollbackInfo r) {
                final CacheLine line = getLine(id);
                synchronized (line) {
                    if (LOG.isDebugEnabled())
                        LOG.debug("Rolling back line {} to version {}. Modified = {}", hex(line.getId()), r.version,
                                r.modified);
                    line.version = r.version;
                    line.set(CacheLine.MODIFIED, r.modified);
                    writeData(line, r.data);
                    return true;
                }
            }
        });
    }

    public void endTransaction(Transaction txn, boolean abort) throws InterruptedException {
        LOG.debug("Ending transaction: {} {}", txn, abort ? "ABORT" : "COMMIT");
        Throwable ex = null;
        for (Op op : txn.getOps()) {
            try {
                if (op.hasFuture())
                    op.getResult();
            } catch (Throwable e) {
                LOG.debug("Error in op: " + op, e);
                if (ex == null)
                    ex = e;
            }
        }

        boolean flush = false;

        final ArrayList<CacheLine> unmodified = new ArrayList<CacheLine>();
        final boolean locked = backup.startBackup();
        try {
            for (LongIterator it = txn.getLines().iterator(); it.hasNext();) {
                final long id = it.next();
                final CacheLine line = getLine(id);
                LOG.debug("Ending transaction: {}, line {}", txn, line);
                synchronized (line) {
                    if (unlockLine(line, txn)) {
                        if (!line.is(CacheLine.MODIFIED))
                            unmodified.add(line);
                        else {
                            line.set(CacheLine.SLAVE, true);
                            backup.backup(line.getId(), line.getVersion());
                            if (hasPendingMessages(line))
                                flush = true;
                        }
                    }
                }
            }
        } finally {
            backup.endBackup(locked);
        }
        if (flush)
            backup.flush();

        for (CacheLine line : unmodified) {
            synchronized (line) {
                handlePendingMessages(line, CacheMonitor.MessageDelayReason.LOCK);
            }
        }

        if (!abort) {
            if (ex != null) {
                if (ex instanceof ExecutionException) {
                    ex = ex.getCause();
                }
                Throwables.propagateIfPossible(ex);
                throw Throwables.propagate(ex);
            }
        }
    }

    public void release(long id) {
        final CacheLine line = getLine(id);
        boolean bckp = false;
        final boolean locked = backup.startBackup();
        try {
            synchronized (line) {
                if (unlockLine(line, null)) {
                    if (!line.is(CacheLine.MODIFIED))
                        handlePendingMessages(line, CacheMonitor.MessageDelayReason.LOCK);
                    else {
                        bckp = true;
                        backupLine(line);
                    }
                }
            }
        } finally {
            backup.endBackup(locked);
        }
        if (bckp && hasPendingMessages(line))
            backup.flush();
    }

    boolean cancelOp(Op op) {
        final CacheLine line = getLine(op.line);
        if (line == null)
            return false;
        synchronized (line) {
            final boolean completed;
            if (!(completed = op.isCompleted())) {
                op.setCancelled();
                removePendingOp(line, op);
            }
            if (LOG.isDebugEnabled())
                LOG.debug("Cancelling op {} on line {}: {}", op, line, completed ? "FAILED" : "SUCCESS");
            return !completed;
        }
    }

    private void backupLine(CacheLine line) {
        line.set(CacheLine.SLAVE, true);
        backup.backup(line.getId(), line.getVersion());
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="Op handling">
    /////////////////////////// Op handling ///////////////////////////////////////////
    private Object handleOpGet(CacheLine line, Op.Type type, Object data, short nodeHint, Transaction txn,
            int change) {
        if ((change & (LINE_STATE_CHANGED | LINE_OWNER_CHANGED | (synchronous ? LINE_MODIFIED_CHANGED : 0))) == 0)
            return PENDING;

        if (line.is(CacheLine.DELETED))
            handleDeleted(line);

        if (!transitionToS(line, nodeHint)) {
            if (type != Op.Type.GETS && line.version > 0 && !isPossibleInconsistencies(line)) {
                if (data != null) {
                    readData(line, (Persistable) data);
                    return null;
                } else
                    return readData(line);
            } else
                return PENDING;
        }

        if (type == Op.Type.GETS)
            lockLine(line, txn);

        if (data != null) {
            readData(line, (Persistable) data);
            return null;
        } else
            return readData(line);
    }

    private Object handleOpGetX(CacheLine line, Object data, short nodeHint, Transaction txn, int change,
            boolean pending) {
        if ((change & (LINE_STATE_CHANGED | LINE_OWNER_CHANGED)) == 0)
            return PENDING;

        if (line.is(CacheLine.DELETED))
            handleDeleted(line);

        if (!pending)
            verifyNoUpgrade(line);

        if (!transitionToE(line, nodeHint))
            return PENDING;

        lockLine(line, txn); // we get here when were O (see transitionToE or E). 

        if (data != null) {
            readData(line, (Persistable) data);
            return null;
        } else
            return readData(line);
    }

    private boolean verifyNoUpgrade(CacheLine line) {
        if (line.isLocked() && line.getState() == State.S) {
            RuntimeException e = new IllegalStateException("Can't upgrade to X. Line " + line + " is pinned S");
            LOG.error("Attempt to upgrade line " + line + " from S to X. Line is pinned S.", e);
            throw e;
        }
        if (hasPendingOp(line, Op.Type.GETS)) {
            RuntimeException e = new IllegalStateException(
                    "Can't upgrade to X. Line " + line + " has a pending gets operation.");
            LOG.error("Attempt to upgrade line " + line + " from S to X. Line is pinned S.", e);
            throw e;
        }
        return true;
    }

    private Object handleOpGetFromOwner(CacheLine line, Object extra) {
        final Op get = (Op) extra;
        final short owner = line.getOwner();
        if (owner >= 0)
            get.setExtra(owner);
        return get;
    }

    private boolean transitionToS(CacheLine line, short nodeHint) {
        if (line.state.isLessThan(State.S)) {
            if (setNextState(line, State.S))
                send(Message.GET(getTarget(line, nodeHint), line.id));
            return false;
        } else
            return true;
    }

    private boolean transitionToO(CacheLine line, short nodeHint) {
        if (line.state.isLessThan(State.O)) {
            if (setNextState(line, State.O))
                send(Message.GETX(getTarget(line, nodeHint), line.id));
            return false;
        } else
            return true;
    }

    private boolean transitionToE(CacheLine line, short nodeHint) {
        if (!transitionToO(line, nodeHint))
            return false;
        assert !line.state.isLessThan(State.O);

        final boolean res;

        if (line.state.isLessThan(State.E)) {
            if (setNextState(line, State.E)) {
                assert !line.sharers.isEmpty();
                for (ShortIterator it = line.sharers.iterator(); it.hasNext();) {
                    final short sharer = it.next();
                    if (sharer != Comm.SERVER) // we've already INVed server in handleMessagePutX
                        send(Message.INV(sharer, line.getId(), line.getOwner())); // owner may not be us but the previous owner - see handleMessagePutX
                }
            }
            res = false;
            /*
             * The following optimization has been temporarily disabled, due to the following scenario:
             *
             * Node A owns X and Y. B shares X and pins it. A modifies X, and doesn't wait for B's INVACK, which won't come until
             * it unpins X. Then A also modifies Y (which can be published because it's not shared). B then requests Y and accepts it,
             * but it is now inconsistent with X.
             * 
             * An alternative solution will be to enable this optimization for a single line per transaction, so that A won't be able
             * to modify Y until X is released and INVACKed by B.
             */
            //            if (broadcastsRoutedToServer)
            //                res = !line.sharers.contains(Comm.SERVER); // in this particular case, we wait for server to INVACK (this case may have consistency problems, otherwise)
            //            else if (!hasServer)
            //                res = !line.sharers.contains(line.getOwner()); // getOwner still has the old owner. when it invacks, it means it has inved its slaves so we're safe.
            //            else
            //                res = true; // we don't wait for acks, but GET messages are kept pending until the transition
        } else
            res = true;
        // INVACKs cannot cause deadlocks (really? proof?), so we don't need to wait and see if they timeout.

        if (res)
            line.set(CacheLine.MODIFIED, true); // let slaves know we own the line
        return res;
    }

    private Object handleOpSet(CacheLine line, Object data, short nodeHint, Transaction txn, int change) {
        if ((change & (LINE_STATE_CHANGED | LINE_OWNER_CHANGED)) == 0)
            return PENDING;

        if (line.is(CacheLine.DELETED))
            handleDeleted(line);

        if (!transitionToE(line, nodeHint))
            return PENDING;

        setData(line, data, txn);

        if (txn == null && !line.isLocked())
            backupLine(line);

        return null;
    }

    private void handleDeleted(CacheLine line) {
        if (isReserved(line.getId())) {
            line.set(CacheLine.DELETED, false);
            setState(line, State.E);
        } else
            throw new RefNotFoundException(line.getId());
    }

    private Object handleOpPut(Op op, CacheLine line) {
        assert line == null;

        long id = idAllocator.allocateIds(op, 1);
        if (id == -1)
            return PENDING;

        line = allocateCacheLine();
        line.id = id;
        setState(line, State.E);
        setOwner(line, myNodeId());
        setData(line, op.data, op.txn);

        lockLine(line, op.txn);

        putLine(id, line, 0, line.size()); // we put this last so that no one can touch this line while it's being built.
        return id;
    }

    private Object handleOpAlloc(Op op, CacheLine line) {
        assert line == null;

        final int count = (Integer) op.getExtra();
        long id = idAllocator.allocateIds(op, count);
        if (id == -1)
            return PENDING;

        for (int i = 0; i < count; i++) {
            line = allocateCacheLine();
            line.id = id + i;
            setState(line, State.E);
            setOwner(line, myNodeId());
            setData(line, null, op.txn);
            lockLine(line, op.txn);

            putLine(id + i, line, 0, line.size()); // we put this last so that even in the locking cache no one can touch this line while it's being built.
        }
        return id;
    }

    private Object handleOpDel(CacheLine line, short nodeHint, Transaction txn, int change) {
        if ((change & (LINE_STATE_CHANGED | LINE_OWNER_CHANGED)) == 0)
            return PENDING;

        if (!transitionToE(line, nodeHint))
            return PENDING;

        final long id = line.getId();

        line.set(CacheLine.DELETED, true);

        if (hasServer()) {
            if (line.state == State.E)
                setState(line, State.O);
            line.sharers.add(Comm.SERVER);
            send(Message.DEL(Comm.SERVER, id));
        } else
            setState(line, State.I);

        deallocateStorage(id, line.data);

        fireLineEvicted(line);
        return null;
    }

    private Object handleOpSend(CacheLine line, Object extra, int change) {
        if (line.is(CacheLine.DELETED))
            handleDeleted(line);

        if ((change & LINE_OWNER_CHANGED) == 0)
            return PENDING; // there's no reason to resend

        final Message.MSG msg = (Message.MSG) extra;
        if (msg.getNode() != -1 && msg.getNode() == line.getOwner())
            return PENDING; // there's no reason to resend

        if (!line.getState().isLessThan(State.O)) {
            msg.setNode(myNodeId());
            msg.setReplyRequired(false);
            msg.setIncoming(); // must be done last! (this is a special trick. in the normal case setAckRequired must never be done on incoming messages so we assert it - to bypass in this case, this must come last)
            receive(msg);
            return null;
        }

        // we make a copy of the message because it may have been sent and sits in some comm queues,
        // so changing the target node might cause trouble.
        final Message.MSG msg1 = Message.MSG(line.getOwner(), msg.getLine(), msg.isMessenger(), msg.getData());
        send((Message) msg1);
        // We have to remember the message ID in order to process MSGACKs later
        assert msg1.getMessageId() > 0;
        msg.setMessageId(msg1.getMessageId());

        return PENDING; // unlike other ops, this one always returns pending, and is completed by handleMessageMsgAck
    }

    private Object handleOpPush(CacheLine line, Object extra, int change) {
        if ((change & LINE_MODIFIED_CHANGED) == 0) {
            assert line.is(CacheLine.MODIFIED);
            return PENDING;
        }

        if (line.getState().isLessThan(State.O)) {
            LOG.info("Attempt to push line {} while state is only {}", hex(line.getId()), line.getState());
            return null; // throw new IllegalStateException("Line " + line.getId() + " is not owned by this cache.");
        }

        setState(line, State.O);
        final short[] toNodes = (short[]) extra;
        for (short s : toNodes)
            line.sharers.add(s);

        for (short node : toNodes) {
            send(Message.PUT(node, line.id, line.version, readOnly(line.data)));
            line.rewind();
        }
        return null;
    }

    private Object handleOpPushX(CacheLine line, Object extra, int change) {
        if ((change & LINE_MODIFIED_CHANGED) == 0) {
            assert line.is(CacheLine.MODIFIED);
            return PENDING;
        }

        if (line.getState().isLessThan(State.E)) {
            LOG.info("Attempt to push line {} while state is only {}", line.getId(), line.getState());
            return null; // throw new IllegalStateException("Line " + line.getId() + " is not owned exclusively by this cache.");
        }

        short toNode = (Short) extra;
        setOwner(line, toNode);
        final short[] sharers = line.sharers.toShortArray();
        // TODO: maybe S, or, rather, transitional O. We could add this node to sharers and  if new owner dies, we become owner here and in the server
        setState(line, State.I);

        final List<Message.MSG> pendingMSGs = getAndClearPendingMSGs(line);
        send(Message.PUTX(toNode, line.id, sharers, pendingMSGs.size(), line.version, readOnly(line.data)));
        line.rewind();
        for (Message.MSG m : getAndClearPendingMSGs(line)) {
            m = toOutgoing(m, toNode);
            m.setPending(true);
            m.setReplyRequired(false);
            send(m);
        }

        fireLineInvalidated(line);

        return null;
    }

    private CacheListener handleOpListen(CacheLine line, Object data, Object listener) {
        final boolean ifAbsent = (boolean) (data != null ? data : false);
        final CacheListener lst = line.setListener((CacheListener) listener, ifAbsent);

        // send pending MSG messages to listener
        if (lst != null && lst == listener) {
            for (Message.MSG msg : getAndClearPendingMSGs(line))
                handleMessageMsg(msg, line);
        }
        return lst;
    }

    private List<Message.MSG> getAndClearPendingMSGs(CacheLine line) {
        final List<Message.MSG> ms = new ArrayList<>();
        final Collection<LineMessage> msgs = getPendingMessages(line);
        for (Iterator<LineMessage> it = msgs.iterator(); it.hasNext();) {
            final LineMessage msg = it.next();
            if (msg.getType() == Message.Type.MSG) {
                ms.add((Message.MSG) msg);
                it.remove();
            }
        }
        return ms;
    }

    private Object handleOpInvoke(CacheLine line, Object function, Op op, Object extra, Transaction txn,
            int lineChange) {
        if ((lineChange & (LINE_STATE_CHANGED | LINE_OWNER_CHANGED)) == 0)
            return PENDING;

        if (line.is(CacheLine.DELETED))
            handleDeleted(line);

        final LineFunction f = (LineFunction) function;
        if (line.state.isLessThan(State.O)) {
            if (op != null) { // when in slow track
                short nodeHint = extra instanceof Short ? nodeHint(extra) : (short) -1;
                final Message.INVOKE msg = Message.INVOKE(getTarget(line, nodeHint), line.id, f);
                send(msg);
                // We have to remember the message ID in order to process MSGACKs later
                assert msg.getMessageId() > 0;
                op.setExtra(msg);

                //                if (isVoidLineFunction(f)) // TODO!!
                //                    return null;
            }
            return PENDING;
        } else {
            if (!transitionToE(line, (short) -1))
                return PENDING;
            Object res = execInvoke(line, f);
            if (txn == null && !line.isLocked())
                backupLine(line);
            return res;
        }
    }

    private static short nodeHint(Object obj) {
        return obj != null ? (Short) obj : -1;
    }

    private static short getTarget(CacheLine line, short nodeHint) {
        short target = line.getOwner();
        if (target < 0)
            target = nodeHint;
        return target;
    }

    private void setData(CacheLine line, Object data, Transaction txn) {
        assert !line.state.isLessThan(State.O);

        if (txn != null && rollbackSupported && !txn.isRecorded(line.getId()))
            txn.recordRollback(line.getId(), line.getVersion(), line.is(CacheLine.MODIFIED),
                    line.getData() != null ? Persistables.toByteArray(line.getData()) : null);
        if (writeData(line, data) || line.version == 0) { // first write always updates version, even if it's a null.
            line.version++;
            line.set(CacheLine.MODIFIED, true);
            if (LOG.isDebugEnabled())
                LOG.debug("Line {} now has a new version {}. Setting to modified.", hex(line.getId()),
                        line.getVersion());
        }
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="Message handling">
    /////////////////////////// Message handling ///////////////////////////////////////////
    private int handleMessageGet(Message.GET msg, CacheLine line) throws IrrelevantStateException {
        if (handleNotOwner(msg, line))
            return LINE_NO_CHANGE;
        relevantStates(line, State.E, State.O);

        int change = LINE_NO_CHANGE;
        change |= setState(line, State.O) ? LINE_STATE_CHANGED : 0;
        line.sharers.add(msg.getNode());

        send(Message.PUT(msg, line.id, line.version, readOnly(line.data)));
        line.rewind();
        return change;
    }

    private int handleMessageGetX(Message.GET msg, CacheLine line) throws IrrelevantStateException {
        if (handleNotOwner(msg, line))
            return 0;
        relevantStates(line, State.E, State.O);

        if (line.is(CacheLine.SLAVE)) {
            if (backup.inv(line.getId(), msg.getNode()))
                line.set(CacheLine.SLAVE, false);
        }

        if (!hasServer && line.is(CacheLine.SLAVE))
            line.sharers.add(myNodeId());

        final short[] sharers = line.sharers.toShortArray(); // setState will nullify sharers

        int change = 0;
        // TODO: maybe S, or, rather, transitional O. We could add this node to sharers and  if new owner dies, we become owner here and in the server
        change |= setState(line, (hasServer | !line.is(CacheLine.SLAVE)) ? State.I : State.S) ? LINE_STATE_CHANGED
                : 0;
        change |= setOwner(line, msg.getNode()) ? LINE_OWNER_CHANGED : 0;

        final List<Message.MSG> pendingMSGs = getAndClearPendingMSGs(line);
        send(Message.PUTX(msg, line.id, sharers, pendingMSGs.size(), line.version, readOnly(line.data)));
        line.rewind();
        for (Message.MSG m : pendingMSGs) {
            m = toOutgoing(m, msg.getNode());
            m.setPending(true);
            m.setReplyRequired(false);
            send(m);
        }

        if (line.getState() == State.I && !line.is(CacheLine.DELETED))
            fireLineInvalidated(line);

        return change;
    }

    private static <M extends Message> M toOutgoing(M m, short node) {
        m.setOutgoing();
        m.setMessageId(-1);
        m.setNode(node);
        return m;
    }

    private int handleMessagePut(Message.PUT msg, CacheLine line) throws IrrelevantStateException {
        relevantStates(line, State.I, State.S);

        if (line.version > msg.getVersion())
            return LINE_NO_CHANGE;

        setOwnerClock(line, msg); // must be called before set owner

        int change = LINE_NO_CHANGE;
        change |= setState(line, State.S) ? LINE_STATE_CHANGED : 0;
        change |= setOwner(line, msg.getNode()) ? LINE_OWNER_CHANGED : 0;
        line.version = msg.getVersion();
        writeData(line, msg.getData());

        fireLineReceived(line);
        return change;
    }

    private int handleMessagePutX(Message.PUTX msg, CacheLine line) throws IrrelevantStateException {
        relevantStates(line, State.I, State.S);
        if (line.version > msg.getVersion()) {
            LOG.warn("Got PUTX with version {} which is older than current version {}", msg.getVersion(),
                    line.version);
            return LINE_NO_CHANGE;
        }

        final ShortSet sharers = new ShortArraySet((msg.getSharers() != null ? msg.getSharers().length : 0) + 1);
        if (msg.getSharers() != null) {
            for (short s : msg.getSharers())
                sharers.add(s);
        }
        if (hasServer && msg.getNode() != Comm.SERVER)
            sharers.add(Comm.SERVER); // this is so we make sure the server was notified for the ownership transfer. this is done by INV
        sharers.remove(myNodeId()); // don't INV myself

        int change = LINE_NO_CHANGE;
        change |= line.getState().isLessThan(State.O) ? LINE_OWNER_CHANGED : 0;
        change |= setState(line, sharers.isEmpty() ? State.E : State.O) ? LINE_STATE_CHANGED : 0;
        if (sharers.isEmpty())
            change |= setOwner(line, myNodeId()) ? LINE_OWNER_CHANGED : 0;
        else
            setOwner(line, msg.getNode()); // We set owner to the PREVIOUS owner - used// change |= setOwner(line, cluster.getMyNodeId()) ? LINE_OWNER_CHANGED : 0;
        line.sharers.addAll(sharers);
        line.version = msg.getVersion();
        writeData(line, (Object) msg.getData());
        line.parts = (short) msg.getMessages();

        setOwnerClock(line, msg);

        fireLineReceived(line);

        if (hasServer && msg.getNode() != Comm.SERVER)
            send(Message.INV(Comm.SERVER, line.id, msg.getNode()));
        return change;
    }

    private int handleMessageInvalidate(Message.INV msg, CacheLine line) throws IrrelevantStateException {
        if (getCluster().isMaster())
            relevantStates(line, State.S, State.I, State.O);
        else
            relevantStates(line, State.I, State.E);

        assert line.getState().isLessThan(State.O) || msg.getNode() == Comm.SERVER || !getCluster().isMaster(); // We may get an INV from server when O if line has been transferred to another node as a result of some cluster failure

        final short owner = ((msg.getNode() == Comm.SERVER || msg.getNode() == getCluster().getMyNodeId())
                ? msg.getPreviousOwner()
                : msg.getNode());
        int change = LINE_NO_CHANGE;
        setNextState(line, null);
        change |= setState(line, State.I) ? LINE_STATE_CHANGED : 0;
        change |= setOwner(line, owner) ? LINE_OWNER_CHANGED : 0;
        // If we have a nextState? (i.e. pending ops)? - we do nothing. when the owner unlocks the line it will respond.
        setOwnerClock(line, msg);

        if (getCluster().isMaster()) {
            if (line.is(CacheLine.SLAVE)) {
                if (backup.inv(line.getId(), owner))
                    line.set(CacheLine.SLAVE, false);
            }

            if (line.is(CacheLine.SLAVE))
                addPendingMessage(line, msg);
            else if (msg.getNode() != Comm.SERVER)
                send(Message.INVACK(msg));
        }

        if (line.getState() == State.I && !line.is(CacheLine.DELETED))
            fireLineInvalidated(line);

        return change;
    }

    private int handleMessageInvalidateAck(LineMessage msg, CacheLine line) throws IrrelevantStateException {
        // invack from slaves
        if (msg.getNode() == myNodeId()) {
            assert line.is(CacheLine.SLAVE);
            if (line.isLocked()) {
                addPendingMessage(line, msg);
                return LINE_NO_CHANGE;
            }

            relevantStates(line, State.I, State.S);

            line.set(CacheLine.SLAVE, false);
            int change = LINE_MODIFIED_CHANGED;
            if (line.getState() == State.S) { // we assume the owner would want us to INV
                setNextState(line, null);
                change |= setState(line, State.I) ? LINE_STATE_CHANGED : 0;
                setOwnerClock(line, msg);
                send(Message.INVACK(line.getOwner(), line.getId()));

                if (line.getState() == State.I && !line.is(CacheLine.DELETED))
                    fireLineInvalidated(line);
            }
            return change;
        }

        // invack from peer
        relevantStates(line, State.O);
        int change = LINE_NO_CHANGE;
        line.sharers.remove(msg.getNode());
        if (line.sharers.isEmpty()) {
            change |= setState(line, line.is(CacheLine.DELETED) ? State.I : State.E) ? LINE_STATE_CHANGED : 0;
            change |= setOwner(line, myNodeId()) ? LINE_OWNER_CHANGED : 0;
            change |= LINE_STATE_CHANGED;
        } else if ((broadcastsRoutedToServer && msg.getNode() == Comm.SERVER)
                || (!hasServer && msg.getNode() == line.getOwner())) {
            change |= LINE_STATE_CHANGED;
        }
        if (!msg.isResponse())
            send(Message.ACK(msg));

        return change;
    }

    private int handleMessageNotFound(LineMessage msg, CacheLine line) throws IrrelevantStateException {
        relevantStates(line, State.I);

        if (msg.getNode() == Comm.SERVER || !hasServer) {
            line.set(CacheLine.DELETED, true);
            return LINE_STATE_CHANGED;
        } else {
            setOwner(line, Comm.SERVER);
            setNextState(line, null);
            return LINE_OWNER_CHANGED;
        }
    }

    private int handleMessageChngdOwnr(Message.CHNGD_OWNR msg, CacheLine line) throws IrrelevantStateException {
        relevantStates(line, State.I, State.S); // S doesn't mean we're certain about the owner b/c transfer of ownership (PUTX) is done before sending INVs. 

        if (msg.getNewOwner() != -1 && getCluster().getMaster(msg.getNewOwner()) == null) {
            // either the node that sent the message has not received a node removal event for the new owner
            // or that we have not received a node addition event.
            if (LOG.isDebugEnabled())
                LOG.debug("Not changing owner of {} to {} because node is not in the cluster.", hex(line.getId()),
                        msg.getNewOwner());
            setNextState(line, null);
            return LINE_OWNER_CHANGED; // ... but we're re-trying the op. hopefully, the cluster info will be in sync.
        }

        if (setOwner(line, msg.getNewOwner())) {
            int change = LINE_OWNER_CHANGED;

            if (msg.getNode() == Comm.SERVER && msg.getNewOwner() == myNodeId()) {
                setState(line, State.E); // it's me! probably we sent PUTX to a node that died. 
                change |= LINE_STATE_CHANGED;
            }

            // Force resending of messages by ops
            // This can also be solved by tracking the target of the message in the Op (this would require passing the old owner to the transitionToX() methods), but I opted for the simpler solution for now.
            setNextState(line, null);
            return change;
        }
        return LINE_NO_CHANGE;
    }

    private int handleMessageMsg(Message.MSG msg, CacheLine line) {
        setOwnerClock(line, msg);

        int change = LINE_NO_CHANGE;
        if (msg.isPending()) {
            line.parts--;
            if (line.parts == 0)
                change |= LINE_STATE_CHANGED;

            msg.setPending(false);
        }
        if (line.getListener() != null) {
            try {
                line.getListener().messageReceived(msg.getData());
            } catch (Exception e) {
                LOG.error("Listener threw an exception on messageReceived.", e);
            }
        } else
            addPendingMessage(line, msg);
        if (msg.isReplyRequired())
            send(Message.MSGACK(msg));
        return change;
    }

    private int handleMessageMsgAck(LineMessage ack, CacheLine line) throws IrrelevantStateException {
        Op sendOp = null;

        int change = LINE_NO_CHANGE;
        change |= setOwner(line, ack.getNode()) ? LINE_OWNER_CHANGED : 0;

        final Collection<Op> pending = getPendingOps(line);
        for (Iterator<Op> it = pending.iterator(); it.hasNext();) {
            final Op op = it.next();
            if (op.type == Op.Type.SEND) {
                final Message.MSG msg = (Message.MSG) op.getExtra();
                if (msg.getMessageId() == ack.getMessageId()) {
                    sendOp = op;
                    break;
                }
            }
        }
        if (sendOp != null) {
            completeOp(line, sendOp, null, true);
            removePendingOp(line, sendOp);
        }
        return change;
    }

    private int handleMessageTimeout(LineMessage msg, CacheLine line) throws IrrelevantStateException {
        for (Iterator<Op> it = getPendingOps(line).iterator(); it.hasNext();) {
            final Op op = it.next();
            if (!op.hasFuture())
                op.createFuture();
            LOG.warn("TIMEOUT: {}", op);
            op.setException(new TimeoutException("Timeout while processing op " + op + ": " + msg));
            it.remove();
        }
        line.nextState = null;
        // TODO: push? send to owner?
        return LINE_STATE_CHANGED;
    }

    private int handleMessageBackup(Message.PUT msg, CacheLine line) throws IrrelevantStateException {
        if (getCluster().isMaster()) {
            LOG.warn("Received backup message while master (ignoring): {}", msg);
            return LINE_NO_CHANGE;
        }

        assert !getCluster().isMaster();
        assert msg.getNode() == myNodeId();

        if (line.version > msg.getVersion())
            return LINE_NO_CHANGE;

        // state is set to E. When the master dies, processLineOnNodeEvent in other peers will set S -> I,
        // so we don't need to track sharers.
        int change = LINE_NO_CHANGE;
        change |= setState(line, State.E) ? LINE_STATE_CHANGED : 0;
        change |= setOwner(line, msg.getNode()) ? LINE_OWNER_CHANGED : 0;
        line.version = msg.getVersion();
        writeData(line, msg.getData());

        fireLineReceived(line);
        return change;
    }

    private int handleMessageBackupAck(Message.BACKUPACK msg, CacheLine line) throws IrrelevantStateException {
        if (line.is(CacheLine.DELETED))
            relevantStates(line, State.I);
        else
            relevantStates(line, State.O, State.E);
        assert line.is(CacheLine.MODIFIED);

        assert line.getVersion() >= msg.getVersion();

        int change = LINE_NO_CHANGE;
        if (line.is(CacheLine.MODIFIED) && line.getVersion() == msg.getVersion()) {
            if (LOG.isDebugEnabled())
                LOG.debug("Backup of line {} version {} done. Setting to unmodified.", hex(line.getId()),
                        line.getVersion());
            line.set(CacheLine.MODIFIED, false);
            change |= LINE_MODIFIED_CHANGED;
        }
        return change;
    }

    private int handleMessageInvoke(Message.INVOKE msg, CacheLine line) throws IrrelevantStateException {
        if (handleNotOwner(msg, line))
            return LINE_NO_CHANGE;
        relevantStates(line, State.E, State.O);

        if (!transitionToE(line, (short) -1)) {
            addPendingMessage(line, msg);
            return LINE_NO_CHANGE;
        }

        final Object invokeRes = execInvoke(line, msg.getFunction());
        backupLine(line);

        fireLineReceived(line);

        send(Message.INVRES(msg, line.id, invokeRes));
        return LINE_EVERYTHING_CHANGED;
    }

    private int handleMessageInvRes(INVRES res, CacheLine line) {
        Op invokeOp = null;
        final Collection<Op> pending = getPendingOps(line);
        for (Iterator<Op> it = pending.iterator(); it.hasNext();) {
            final Op op = it.next();
            if (op.type == Op.Type.INVOKE) {
                Message.INVOKE msg = (Message.INVOKE) op.getExtra();
                if (msg.getMessageId() == res.getMessageId()) {
                    invokeOp = op;
                    break;
                }
            }
        }
        if (invokeOp != null) {
            completeOp(line, invokeOp, res.getResult(), true);
            removePendingOp(line, invokeOp);
        }
        return LINE_NO_CHANGE;
    }
    //</editor-fold>
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="Stale Reads">
    /////////////////////////// Stale Reads ///////////////////////////////////////////
    /*
     * The stale reads mechanism enables reading invalidated data (I lines) as long as this doesn't result in inconsistent views.
     *
     * The way this is done is by keeping track of INV and PUT messages from each host. Once an invalidated line has been PUT, we
     * cannot allow any stale (I) lines from the same owner to be read (until they've all been PUT or evicted).
     */
    private boolean isPossibleInconsistencies(CacheLine line) {
        assert line.getState() == State.I;
        final short owner = line.getOwner();
        if (System.currentTimeMillis() - line.timeAccessed > maxStaleReadMillis)
            return true;
        if (owner == -1)
            return false;
        final OwnerClock oc = ownerClocks.get(owner);
        if (oc == null)
            return false;
        long lastPut = Math.max(oc.lastPut.get(), globalOwnerClock.lastPut.get());
        return line.getOwnerClock() <= lastPut;
    }

    private void setOwnerClock(CacheLine line, Message msg) {
        if (!STALE_READS)
            return;

        final long _clock = clock.incrementAndGet(); // msg.getMessageId();
        line.setOwnerClock(_clock);

        final short owner; // owner is the node that invalidates/invalidated the line
        switch (msg.getType()) {
        case INV:
            owner = msg.getNode();
            getOwnerClock(owner).invCounter.incrementAndGet();
            break;

        case PUT:
        case PUTX:
        case MSG:
            owner = line.getOwner();
            setOwnerClockPut(owner, getOwnerClock(owner), _clock);
            break;
        default:
            break;
        }
    }

    private void setOwnerClockPut(Message msg) {
        final short owner = msg.getNode();
        final OwnerClock oc = getOwnerClock(owner);
        final long _clock = clock.incrementAndGet(); // msg.getMessageId();
        setOwnerClockPut(owner, oc, _clock);
    }

    private void setOwnerClockPut(short owner, OwnerClock oc, long clock) {
        for (;;) {
            final long current = oc.lastPut.get();
            if (clock <= current)
                break;
            if (oc.lastPut.compareAndSet(current, clock)) {
                if (owner >= 0) {
                    monitor.addStalePurge(oc.invCounter.get());
                    oc.invCounter.set(0);
                } else {
                    int count = 0;
                    for (OwnerClock oc1 : ownerClocks.values()) { // counting is approximate due to concurrent updates, but it's only used for monitoring.
                        count += oc1.invCounter.get();
                        oc1.invCounter.set(0);
                    }
                    monitor.addStalePurge(count);

                }

                break;
            }
        }
    }

    private OwnerClock getOwnerClock(short owner) {
        OwnerClock oc = ownerClocks.get(owner);
        if (oc == null) {
            oc = new OwnerClock();
            final OwnerClock tmp = ownerClocks.putIfAbsent(owner, oc);
            if (tmp != null)
                oc = tmp;
        }
        return oc;
    }

    private static class OwnerClock {
        public final AtomicLong lastPut = new AtomicLong();
        public final AtomicInteger invCounter = new AtomicInteger();
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="Node Event Handling">
    /////////////////////////// Node Event Handling ///////////////////////////////////////////
    @Override
    public void nodeRemoved(final short node) {
        LOG.info("Node {} removed.", node);
        final short newOwner = hasServer ? Comm.SERVER : (short) -1;
        final NodeEvent event = new NodeEvent(node, newOwner);
        inNodeEventHandler.set(Boolean.TRUE);
        nodeEvents.add(event);
        try {
            processLines(new LinePredicate() {
                @Override
                public boolean processLine(CacheLine line) {
                    // remove pending messages from node
                    for (Iterator<LineMessage> it = getPendingMessages(line).iterator(); it.hasNext();) {
                        LineMessage message = it.next();
                        if (message.getNode() == node)
                            it.remove();
                    }
                    processLineOnNodeEvent(line, node, newOwner);
                    return true;
                }
            });
        } finally {
            nodeEvents.remove(event);
            inNodeEventHandler.remove();
        }
    }

    @Override
    public void nodeSwitched(final short node) {
        final NodeEvent event = new NodeEvent(node, node);
        inNodeEventHandler.set(Boolean.TRUE);
        nodeEvents.add(event);
        try {
            processLines(new LinePredicate() {
                @Override
                public boolean processLine(CacheLine line) {
                    // we don't inform slave of sharers, so it assumes its lines are E, therefore we must INV shared
                    // also we don't backup S lines, so we remove node from sharers
                    processLineOnNodeEvent(line, node, node);
                    return true;
                }
            });
        } finally {
            nodeEvents.remove(event);
            inNodeEventHandler.remove();
        }
    }

    @Override
    public void nodeAdded(short node) {
    }

    @SuppressWarnings({ "BoxedValueEquality" })
    private void handleNodeEvents(CacheLine line) {
        if (inNodeEventHandler.get() == Boolean.TRUE)
            return;
        for (NodeEvent event : nodeEvents)
            processLineOnNodeEvent(line, event.node, event.newOwner);
    }

    private void processLineOnNodeEvent(CacheLine line, short node, short newOwner) {
        if (line.getState().isLessThan(State.O) && line.getOwner() == node) { // remove shared lines owned by node
            if (LOG.isDebugEnabled())
                LOG.debug("Node {} switched/removed - owned line {}. Setting to I and owner to {}", node, line,
                        newOwner);

            // We must S -> I, because the dead node's slaves have E (see handleMessageBackup)
            int change = 0;
            change |= setState(line, State.I) ? LINE_STATE_CHANGED : 0;
            setNextState(line, null);
            if (node != newOwner) {
                change |= setOwner(line, newOwner) ? LINE_OWNER_CHANGED : 0;
                fireLineKilled(line);
            }
            line.setOwnerClock(0);// setOwnerClockInv(line, newOwner); - TODO ???
            boolean stop = false;
            do {
                try {
                    handlePendingOps(line, change);
                    stop = true;
                } catch (ConcurrentModificationException e) {
                    LOG.debug("processLineOnNodeEvent: OOPS. CME. Retrying");
                }
            } while (!stop);
        } else if (line.getState() == State.O && line.sharers.remove(node)) {
            if (LOG.isDebugEnabled())
                LOG.debug("Node {} switched/removed - removing from sharers of line {}", node, line);
            if (line.sharers.isEmpty()) {
                setState(line, State.E);
                handlePendingOps(line, LINE_STATE_CHANGED);
            }
        }
    }

    private static final class NodeEvent {
        public final short node;
        public final short newOwner;

        public NodeEvent(short node, short newOwner) {
            this.node = node;
            this.newOwner = newOwner;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null)
                return false;
            if (obj == this)
                return true;
            if (!(obj instanceof NodeEvent))
                return false;
            return this.node == ((NodeEvent) obj).node;
        }

        @Override
        public int hashCode() {
            return node;
        }
    }
    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="Implementation details">
    /////////////////////////// Implementation details ///////////////////
    private boolean setNextState(CacheLine line, State nextState) {
        if (line.nextState == nextState)
            return false;
        if (line.nextState == null || nextState == null || line.nextState.isLessThan(nextState)) {
            line.nextState = nextState;
            if (nextState == State.S | nextState == State.O)
                monitor.addMiss();
            if (nextState == State.E)
                monitor.addInvalidate(line.sharers.size());
            return true;
        } else
            return false;
    }

    private boolean setState(CacheLine line, State state) {
        if (line.nextState != null && (line.nextState == state || line.nextState.isLessThan(state)))
            line.nextState = null;
        if (line.state != state) {
            if (LOG.isDebugEnabled())
                LOG.debug("Set state {} {} -> {}", hex(line.getId()), line.state, state);

            if (!state.isLessThan(State.O) && line.getState().isLessThan(State.O)) {
                owned.put(line.getId(), line);
                shared.remove(line.getId());
            } else if (state.isLessThan(State.O) && !line.getState().isLessThan(State.O)) {
                shared.put(line.getId(), line);
                owned.remove(line.getId());
            }

            //            if (state.isLessThan(State.O) && !line.getState().isLessThan(State.O))
            //                line.timeAccessed = System.currentTimeMillis();
            line.state = state;
            if (line.sharers == null || !state.isLessThan(State.O))
                line.sharers = allocateSharerSet(SHARER_SET_DEFAULT_SIZE);
            else if (line.sharers != null || state.isLessThan(State.O)) {
                deallocateSharerSet(line.id, line.sharers);
                line.sharers = null;
            }
            return true;
        } else
            return false;
    }

    // should be called AFTER setState
    private boolean setOwner(CacheLine line, short owner) {
        short oldOwner = line.owner;
        if (owner != oldOwner) {
            if (LOG.isDebugEnabled())
                LOG.debug("Set owner {} {} -> {}", hex(line.getId()), line.owner, owner);
            line.owner = owner;
            return true;
        } else
            return false;
    }

    private void accessLine(CacheLine line) {
        if (line != null) {
            if (line.getState().isLessThan(State.O))
                line.timeAccessed = System.currentTimeMillis();
        }
    }

    private boolean writeData(CacheLine line, Object data) {
        if (data == null)
            return writeNull(line);
        else if (data instanceof Persistable)
            return writeData(line, (Persistable) data);
        else if (data instanceof ByteBuffer)
            return writeData(line, (ByteBuffer) data);
        else
            return writeData(line, (byte[]) data);
    }

    private boolean writeData(CacheLine line, byte[] data) {
        if (data.length > maxItemSize)
            throw new IllegalArgumentException(
                    "Data size is " + data.length + " bytes and exceeds the limit of " + maxItemSize + " bytes.");

        if (compareBeforeWrite) {
            if (line.data != null && data.length == line.data.remaining()) {
                final int p = line.data.position();
                boolean modified = false;
                for (int i = 0; i < data.length; i++) {
                    if (line.data.get(p + i) != data[i]) {
                        modified = true;
                        break;
                    }
                }
                if (!modified)
                    return false;
            }
        }

        allocateLineData(line, data.length);
        line.data.put(data);
        line.data.flip();
        return true;
    }

    private boolean writeData(CacheLine line, ByteBuffer data) {
        if (data.remaining() > maxItemSize)
            throw new IllegalArgumentException("Data size is " + data.remaining()
                    + " bytes and exceeds the limit of " + maxItemSize + " bytes.");

        if (compareBeforeWrite) {
            if (line.data != null && data.remaining() == line.data.remaining()) {
                final int p1 = line.data.position();
                final int p2 = data.position();
                boolean modified = false;
                for (int i = 0; i < data.remaining(); i++) {
                    if (line.data.get(p1 + i) != data.get(p2 + i)) {
                        modified = true;
                        break;
                    }
                }
                if (!modified)
                    return false;
            }
        }

        allocateLineData(line, data.remaining());
        line.data.put(data);
        line.data.flip();
        return true;
    }

    private boolean writeData(CacheLine line, Persistable object) {
        if (object.size() > maxItemSize)
            throw new IllegalArgumentException("Object size is " + object.size()
                    + " bytes and exceeds the limit of " + maxItemSize + " bytes: " + object);

        if (compareBeforeWrite) {
            final Checksum chksm = getChecksum();
            if (line.data != null && object.size() == line.data.remaining()) {
                final int n = line.data.remaining();

                chksm.update(line.data);
                line.data.rewind();
                final byte[] hash = chksm.getChecksum();

                object.write(line.data);
                line.data.flip();

                chksm.reset();
                chksm.update(line.data);
                line.data.rewind();

                if (!Arrays.equals(hash, chksm.getChecksum()))
                    return true;
                return false;
            }
        }

        allocateLineData(line, object.size());
        object.write(line.data);
        line.data.flip();
        return true;
    }

    private boolean writeNull(CacheLine line) {
        if (line.data == null)
            return false;
        final int oldSize = line.size();
        deallocateStorage(line.id, line.data);
        line.data = null;
        if (line.getState().isLessThan(State.O)) // => state must be set before this is called
            putLine(line.id, line, oldSize, 0); // size changed
        return true;
    }

    private Object execInvoke(final CacheLine line, LineFunction f) {
        final LineAccess la = new LineAccess(line);
        final Object res = f.invoke(la);
        if (la.flip)
            line.data.flip();
        else
            line.data.rewind();
        return res;
    }

    private class LineAccess implements LineFunction.LineAccess {
        final CacheLine line;
        boolean flip;

        public LineAccess(CacheLine line) {
            this.line = line;
        }

        @Override
        public ByteBuffer getForRead() {
            return (ByteBuffer) line.getData().asReadOnlyBuffer().rewind();
        }

        @Override
        public ByteBuffer getForWrite(int size) {
            if (size >= 0 && line.data.capacity() < size)
                extendLineData(size);
            line.version++;
            line.set(CacheLine.MODIFIED, true);
            flip = size >= 0;
            return (ByteBuffer) line.getData().rewind();
        }

        private void extendLineData(int size) {
            if (LOG.isDebugEnabled())
                LOG.debug("Extend storage to {} bytes for line {}", size, hex(line.getId()));
            ByteBuffer allocated = allocateStorage(size);
            allocated.put((ByteBuffer) line.data.rewind());
            allocated.flip();
            deallocateStorage(line.id, line.data);
            line.data = allocated;
        }
    }

    private void allocateLineData(CacheLine line, int size) {
        final int oldSize = line.size();
        if (line.data != null) {
            if (line.data.capacity() >= size && line.data.capacity() < size * 4) {
                if (LOG.isDebugEnabled())
                    LOG.debug("Reusing (clearing) storage for line {}. Storage: {} bytes. Data: {} bytes",
                            hex(line.getId()), line.data.capacity(), size);
                line.data.clear();
            } else {
                deallocateStorage(line.id, line.data);
                line.data = null;
            }
        }
        boolean resized = false;
        if (line.data == null) {
            if (LOG.isDebugEnabled())
                LOG.debug("Allocating storage ({} bytes) for line {}", size, hex(line.getId()));
            line.data = allocateStorage(size);
            resized = true;
        }
        line.data.limit(size);
        if (resized && line.getState().isLessThan(State.O)) // => state must be set before this is called
            putLine(line.id, line, oldSize, line.size()); // size changed
    }

    /**
     * DO NOT modify returned array.
     */
    private byte[] readData(CacheLine line) {
        accessLine(line);

        if (line.data == null)
            return null;
        byte[] data = new byte[line.data.remaining()];
        line.data.get(data);
        line.data.rewind();
        return data;
    }

    private void readData(CacheLine line, Persistable object) {
        if (object == null | object == NULL_PERSISTABLE)
            return;
        accessLine(line);

        final ByteBuffer buffer = line.data != null ? line.data : EMPTY_BUFFER;
        if (object instanceof VersionedPersistable)
            ((VersionedPersistable) object).read(line.getVersion(), buffer);
        else
            object.read(buffer);
        line.rewind();
    }

    private CacheLine createNewCacheLine(long id) {
        CacheLine line = allocateCacheLine();
        line.id = id;
        return putLine(id, line, 0, 0);
    }

    private CacheLine createNewCacheLine(Op op) {
        return createNewCacheLine(op.line);
    }

    private CacheLine createNewCacheLine(Message message) {
        return createNewCacheLine(((LineMessage) message).getLine());
    }

    // visible for testing
    void evictLine(CacheLine line, boolean invack) {
        final long id = line.getId();
        final int oldSize = line.size();
        discardLine(line, invack);
        removeLine(id, line, oldSize);
    }

    private void discardLine(CacheLine line, boolean invack) {
        LOG.debug("Evicted {}", line);
        fireLineEvicted(line);
        final long id = line.getId();
        deallocateStorage(id, line.data);
        if (invack && line.getState() == State.S)
            send(Message.INVACK(line.getOwner(), line.getId()));
        clearLine(line);
        deallocateCacheLine(id, line);
    }

    private void clearLine(CacheLine line) {
        if (line.sharers != null)
            deallocateSharerSet(line.id, line.sharers);
        line.id = 0;
        line.clearFlags();
        //line.timeAccessed = 0;
        line.state = State.I;
        line.nextState = null;
        line.owner = -1;
        line.sharers = null;
        line.version = 0;
        line.data = null;
    }

    void lockLine(CacheLine line, Transaction txn) {
        LOG.debug("Locking line {}", line);
        if (LOG.isTraceEnabled())
            LOG.trace("Locked:", new Throwable());
        line.lock();
        if (txn != null)
            txn.add(line.getId());
    }

    boolean unlockLine(CacheLine line, Transaction txn) {
        LOG.debug("Unlocking line {}", line);
        if (LOG.isTraceEnabled())
            LOG.trace("Unlocked:", new Throwable());
        assert txn == null || txn.contains(line.getId());
        return line.unlock();
    }

    /**
     * Blocks
     */
    void send(Message message) {
        LOG.debug("Sending: {}", message);
        try {
            comm.send(message);
        } catch (NodeNotFoundException e) {
            final Message response = genResponse(message);
            LOG.debug("Auto response: {} (to: {})", response, message);
            if (response != null)
                receive(shortCircuitMessage(message.getNode(), response));
        }
        LOG.debug("Sent: {}", message);
        monitor.addMessageSent(message.getType());
    }

    private void receiveShortCircuit() {
        Queue<Message> ms = this.shortCircuitMessage.get();
        if (ms != null) {
            while (!ms.isEmpty()) {
                Message m = ms.remove();
                receive1(m);
            }
        }
        this.shortCircuitMessage.remove();
    }

    private Message genResponse(Message message) {
        switch (message.getType()) {
        case INV:
            return Message.INVACK((Message.INV) message);
        case GET:
        case GETX:
            return Message.CHNGD_OWNR(((LineMessage) message), ((LineMessage) message).getLine(), (short) -1,
                    false);
        default:
            return null;
        // don't send message
        }
    }

    private Message shortCircuitMessage(short node, Message message) {
        message.setIncoming();
        message.setNode(node);
        return message;
    }

    private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);

    // visible for testing
    CacheLine getLine(long id) {
        CacheLine line = owned.get(id);
        if (line == null)
            line = shared.get(id);
        return line;
    }

    private CacheLine putLine(long id, Cache.CacheLine line, int oldSize, int newSize) {
        final CacheLine old;
        if (line.getState().isLessThan(State.O)) {
            old = shared.put(id, line); // to make sure eviction data is updated we must put rather than putIfAbsent
            if (old != null && old != line)
                evictLine(old, false);
            return line;
        } else {
            old = owned.putIfAbsent(id, line);
            if (old != null && old != line) {
                evictLine(line, false);
                return old;
            } else
                return line;
        }
    }

    // visible for testing
    void removeLine(long id, CacheLine line, int oldSize) {
        if (owned.remove(id) == null)
            shared.remove(id);
    }

    private void addPendingOp(CacheLine line, Op op) {
        if (op.hasFuture())
            return;
        op.createFuture();

        ArrayList<Op> ops = pendingOps.get(op.line);
        if (ops == null) {
            ops = new ArrayList<Op>();
            pendingOps.put(op.line, ops);
        }
        ops.add(op);
    }

    /**
     * Returns, but DOES NOT CLEAR the pending ops queue.
     */
    private Collection<Op> getPendingOps(CacheLine line) {
        ArrayList<Op> ops = pendingOps.get(line.getId());
        return (ops != null ? ops : Collections.EMPTY_LIST);
    }

    private void removePendingOp(CacheLine line, Op op) {
        ArrayList<Op> ops = pendingOps.get(op.line);
        if (ops == null)
            return;
        ops.remove(op);
        if (ops.isEmpty())
            pendingOps.remove(op.line);
    }

    private boolean hasPendingOp(CacheLine line, Op.Type opType) {
        for (Op op : getPendingOps(line)) {
            if (op.type == opType)
                return true;
        }
        return false;
    }

    private void addPendingMessage(CacheLine line, LineMessage message) {
        LinkedHashSet<LineMessage> msgs = pendingMessages.get(line.getId());
        if (msgs == null) {
            msgs = new LinkedHashSet<LineMessage>();
            pendingMessages.put(line.getId(), msgs);
        }
        msgs.add(message);
        if (LOG.isDebugEnabled())
            LOG.debug("addPendingMessage {} to line {}", message, line);
    }

    private boolean hasPendingMessages(CacheLine line) {
        final Collection<LineMessage> msgs = pendingMessages.get(line.getId());
        return msgs != null && !msgs.isEmpty();
    }

    private boolean hasPendingBlockingMessages(CacheLine line) {
        final Collection<LineMessage> msgs = pendingMessages.get(line.getId());
        if (msgs == null || msgs.isEmpty())
            return false;
        for (LineMessage m : msgs) {
            if (!(m instanceof Message.MSG))
                return true;
        }
        return false;
    }

    /**
     * Returns and CLEARS the pending messages queue.
     */
    private Set<LineMessage> getAndClearPendingMessages(CacheLine line) {
        Set<LineMessage> msgs = pendingMessages.remove(line.getId());
        if (msgs == null)
            msgs = Collections.emptySet();
        return msgs;
    }

    private Set<LineMessage> getPendingMessages(CacheLine line) {
        Set<LineMessage> msgs = pendingMessages.get(line.getId());
        if (msgs == null)
            msgs = Collections.emptySet();
        return msgs;
    }

    interface LinePredicate {
        boolean processLine(CacheLine line);
    }

    private void processLines(LinePredicate lp) {
        for (ConcurrentMap<Long, CacheLine> map : new ConcurrentMap[] { owned, shared }) {
            for (Iterator<CacheLine> it = map.values().iterator(); it.hasNext();) {
                CacheLine line = it.next();
                final boolean retain;
                synchronized (line) {
                    retain = lp.processLine(line);
                    if (!retain)
                        discardLine(line, false);
                }
                if (!retain)
                    it.remove();
            }
        }
    }

    private short myNodeId() {
        return getCluster().getMyNodeId();
    }

    private CacheLine allocateCacheLine() {
        CacheLine line;
        if (freeLineList == null)
            line = new CacheLine();
        else {
            line = freeLineList.pollFirst();
            if (line == null)
                line = new CacheLine();
        }
        clearLine(line);
        return line;
    }

    private void deallocateCacheLine(long id, Cache.CacheLine line) {
        if (freeLineList == null)
            return;

        freeLineList.addFirst(line);
    }

    private ShortSet allocateSharerSet(int size) {
        if (freeSharerSetList == null)
            return new ShortOpenHashSet(size);

        ShortSet sharers = freeSharerSetList.pollFirst();
        if (sharers != null)
            return sharers;
        return new ShortOpenHashSet(size);
    }

    private void deallocateSharerSet(long id, ShortSet sharers) {
        if (freeSharerSetList == null)
            return;

        freeSharerSetList.addFirst(sharers);
    }

    ByteBuffer allocateStorage(int length) {
        return storage.allocateStorage(length);
    }

    void deallocateStorage(long id, ByteBuffer buffer) {
        if (buffer != null) {
            if (LOG.isDebugEnabled())
                LOG.debug("Deallocating storage for line {}", hex(id));
            storage.deallocateStorage(id, buffer);
        }
    }

    private void fireLineInvalidated(CacheLine line) {
        LOG.debug("fireLineInvalidated {}", line);
        if (line.getListener() != null) {
            try {
                line.getListener().invalidated(this, line.getId());
            } catch (Exception e) {
                LOG.error("Listener threw an exception.", e);
            }
        }
        for (CacheListener listener : listeners) {
            try {
                listener.invalidated(this, line.getId());
            } catch (Exception e) {
                LOG.error("Listener threw an exception.", e);
            }
        }
    }

    private void fireLineReceived(CacheLine line) {
        LOG.debug("fireLineReceived {}", line);
        final long id = line.getId();
        final long version = line.getVersion();
        final ByteBuffer data = line.data;

        if (line.getListener() != null) {
            line.rewind();
            try {
                line.getListener().received(this, id, version, data);
            } catch (Exception e) {
                LOG.error("Listener threw an exception.", e);
            }
            line.rewind();
        }
        for (CacheListener listener : listeners) {
            try {
                listener.received(this, id, version, data);
            } catch (Exception e) {
                LOG.error("Listener threw an exception.", e);
            }
            line.rewind();
        }
    }

    private void fireLineEvicted(CacheLine line) {
        LOG.debug("fireLineEviceted {}", line);
        if (line.getListener() != null) {
            try {
                line.getListener().evicted(this, line.getId());
            } catch (Exception e) {
                LOG.error("Listener threw an exception.", e);
            }
        }
        for (CacheListener listener : listeners) {
            try {
                listener.evicted(this, line.getId());
            } catch (Exception e) {
                LOG.error("Listener threw an exception.", e);
            }
        }
    }

    private void fireLineKilled(CacheLine line) {
        LOG.debug("fireLineEviceted {}", line);
        if (line.getListener() != null) {
            try {
                line.getListener().killed(this, line.getId());
            } catch (Exception e) {
                LOG.error("Listener threw an exception.", e);
            }
        }
        for (CacheListener listener : listeners) {
            try {
                listener.killed(this, line.getId());
            } catch (Exception e) {
                LOG.error("Listener threw an exception.", e);
            }
        }
    }

    private static ByteBuffer readOnly(ByteBuffer buffer) {
        return buffer != null ? buffer.asReadOnlyBuffer() : null;
    }

    public static boolean isReserved(long id) {
        return id <= MAX_RESERVED_REF_ID;
    }

    private static class IrrelevantStateException extends Exception {
    }

    private static final IrrelevantStateException IRRELEVANT_STATE = new IrrelevantStateException();

    private void relevantStates(CacheLine line, State... states) throws IrrelevantStateException {
        final State state = line.state;
        for (State s : states) {
            if (state == s)
                return;
        }
        throw IRRELEVANT_STATE;
    }

    static final Persistable NULL_PERSISTABLE = new Persistable() {
        @Override
        public int size() {
            throw new NullPointerException();
        }

        @Override
        public void write(ByteBuffer buffer) {
            throw new NullPointerException();
        }

        @Override
        public void read(ByteBuffer buffer) {
            throw new NullPointerException();
        }
    };

    static boolean isVoidLineFunction(LineFunction<?> function) {
        return isVoidLineFunction.get(function.getClass());
    }

    private static final ClassValue<Boolean> isVoidLineFunction = new ClassValue<Boolean>() {
        @Override
        protected Boolean computeValue(Class<?> type) {
            boolean res = isVoidLineFunction(type);
            return res;
        }
    };

    private static boolean isVoidLineFunction(Class<?> clazz) {
        if (clazz == null)
            return false;
        for (java.lang.reflect.Type iface : clazz.getGenericInterfaces()) {
            if (iface instanceof java.lang.reflect.ParameterizedType) {
                java.lang.reflect.ParameterizedType pt = (java.lang.reflect.ParameterizedType) iface;
                if (pt.getRawType() == LineFunction.class)
                    return (pt.getActualTypeArguments()[0] == Void.class);
                boolean r = isVoidLineFunction((Class) pt.getRawType());
                if (r)
                    return true;
            } else if (iface == LineFunction.class)
                return false;

            boolean r = isVoidLineFunction((Class) iface);
            if (r)
                return true;
        }
        return isVoidLineFunction(clazz.getSuperclass());
    }
    //</editor-fold>
}