/*
* 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.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 gnu.trove.iterator.TLongIterator;
import gnu.trove.iterator.TShortIterator;
import gnu.trove.procedure.TLongObjectProcedure;
import gnu.trove.set.hash.TShortHashSet;
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.HashSet;
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<HashSet<LineMessage>> pendingMessages;
private ConcurrentLinkedDeque<CacheLine> freeLineList;
private ConcurrentLinkedDeque<TShortHashSet> 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 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);
@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, (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<HashSet<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 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<TShortHashSet>() : 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();
}
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();
}
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;
}
//<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;
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 owner = -1; // 2
private TShortHashSet sharers; // 4
private volatile CacheListener listener; // 4
// =
// 47 (+ 8 = 56)
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 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>
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();
}
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();
}
}
@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();
}
//<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 ? ", 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 ? ", 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 = (CacheLine) 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 = (CacheLine) 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, 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())
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 (hasPendingMessages(line) // give mesages a chance if we're not part of a transaction (line isn't locked) and messages are simply waiting for backups or another independent sets (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
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:
handleMessageMsg((Message.MSG) message);
return;
case MSGACK:
if (((LineMessage) message).getLine() == -1) {
if (receiver != null)
receiver.receive(message);
return;
}
break;
case BACKUP_PACKETACK:
backup.receive(message);
return;
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 = (CacheLine) createNewCacheLine(message);
}
synchronized (line) {
handleMessage(message, line);
}
}
/**
* Special handling for msg.
*/
private void handleMessageMsg(Message.MSG message) {
if (receiver == null)
return;
setOwnerClockPut(message);
if (message.getLine() == -1) {
receiver.receive(message);
if (message.isReplyRequired())
send(Message.MSGACK(message));
return;
}
CacheLine line = getLine(message.getLine());
if (line == null) {
boolean res = handleMessageNoLine(message);
assert res;
return;
}
synchronized (line) {
if (handleNotOwner(message, line))
return;
}
receiver.receive(message);
if (message.isReplyRequired())
send(Message.MSGACK(message));
}
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 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;
for (LineMessage msg : getAndClearPendingMessages(line)) {
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 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 boolean shouldHoldMessage(CacheLine line, Message message) {
final boolean res = message.getType().isOf(MESSAGES_BLOCKED_BY_LOCK)
&& (line.isLocked() || line.is(CacheLine.MODIFIED) || (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 static final long MESSAGES_WITH_FAST_REPLY = Enums.setOf(Message.Type.GET, Message.Type.GETX, Message.Type.INVOKE);
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="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 TLongObjectProcedure<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>();
backup.startBackup();
try {
for (TLongIterator 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();
}
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);
synchronized (line) {
if (unlockLine(line, null)) {
if (!line.is(CacheLine.MODIFIED))
handlePendingMessages(line, CacheMonitor.MessageDelayReason.LOCK);
else
backupLine(line);
}
}
}
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.startBackup();
try {
backup.backup(line.getId(), line.getVersion());
} finally {
backup.endBackup();
}
if (hasPendingMessages(line))
backup.flush();
}
//</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 (TShortIterator 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.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);
short[] toNodes = (short[]) extra;
line.sharers.addAll(toNodes);
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.toArray();
// 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);
send(Message.PUTX(toNode, line.id, sharers, line.version, readOnly(line.data)));
line.rewind();
return null;
}
private CacheListener handleOpListen(CacheLine line, Object data, Object listener) {
final boolean ifAbsent = (boolean) (data != null ? data : false);
return line.setListener((CacheListener) listener, ifAbsent);
}
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.toArray(); // 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;
send(Message.PUTX(msg, line.id, sharers, line.version, readOnly(line.data)));
line.rewind();
return change;
}
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 TShortHashSet sharers = new TShortHashSet((msg.getSharers() != null ? msg.getSharers().length : 0) + 1);
if (msg.getSharers() != null)
sharers.addAll(msg.getSharers());
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());
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));
}
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()));
}
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 handleMessageMsgAck(LineMessage ack, CacheLine line) throws IrrelevantStateException {
Op sendOp = 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.SEND) {
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 LINE_NO_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 ///////////////////////////////////////////
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;
}
/*
* 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 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) {
}
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;
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;
}
if (state == State.I && !line.is(CacheLine.DELETED))
fireLineInvalidated(line);
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, (CacheLine) 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) {
HashSet<LineMessage> msgs = pendingMessages.get(line.getId());
if (msgs == null) {
msgs = new LinkedHashSet<LineMessage>();
pendingMessages.put(line.getId(), msgs);
}
msgs.add(message);
}
private boolean hasPendingMessages(CacheLine line) {
final Collection<LineMessage> msgs = pendingMessages.get(line.getId());
return msgs != null && !msgs.isEmpty();
}
/**
* 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 TShortHashSet allocateSharerSet(int size) {
if (freeSharerSetList == null)
return new TShortHashSet(size);
TShortHashSet sharers = freeSharerSetList.pollFirst();
if (sharers != null)
return sharers;
return new TShortHashSet(size);
}
private void deallocateSharerSet(long id, TShortHashSet 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 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;
}
//</editor-fold>
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());
}
}