/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.addthis.hydra.data.tree.concurrent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.addthis.basis.util.ClosableIterator;
import com.addthis.basis.util.MemoryCounter.Mem;
import com.addthis.hydra.data.tree.AbstractTreeNode;
import com.addthis.hydra.data.tree.DataTreeNode;
import com.addthis.hydra.data.tree.DataTreeNodeActor;
import com.addthis.hydra.data.tree.DataTreeNodeInitializer;
import com.addthis.hydra.data.tree.DataTreeNodeUpdater;
import com.addthis.hydra.data.tree.TreeDataParameters;
import com.addthis.hydra.data.tree.TreeDataParent;
import com.addthis.hydra.data.tree.TreeNodeData;
import com.addthis.hydra.data.tree.TreeNodeDataDeferredOperation;
import com.addthis.hydra.store.db.DBKey;
import com.addthis.hydra.store.db.IPageDB.Range;
/**
* Each instance has an AtomicInteger 'lease' that records the current
* activity of a node. Values of lease signify the following behavior:
*
* N (for N > 0) : There are N threads that may be modifying the node. The node is active.
* 0 : The node is idle. It may be evicted.
* -1 : The node is currently being evicted. This is a transient state.
* -2 : The node has been deleted.
* -3 : The node has been evicted.
*
* Only an idle node may be evicted. Any node that has 0 or more leases
* can be deleted (yes it is counterintuitive but for legacy purposes
* deleting nodes is a higher priority operation that modifying nodes).
*
*/
public class ConcurrentTreeNode extends AbstractTreeNode {
public static ConcurrentTreeNode getTreeRoot(ConcurrentTree tree) {
ConcurrentTreeNode node = new ConcurrentTreeNode() {
@Override
void requireEditable() {
}
};
node.tree = tree;
node.leases.incrementAndGet();
node.nodedb = 1L;
return node;
}
/**
* required for Codable. must be followed by an init() call.
*/
public ConcurrentTreeNode() {
}
protected void initIfDecoded(ConcurrentTree tree, DBKey key, String name) {
if (decoded.get()) {
synchronized (initLock) {
if (initOnce.compareAndSet(false, true)) {
this.tree = tree;
this.dbkey = key;
this.name = name;
decoded.set(false);
}
}
}
}
protected void init(ConcurrentTree tree, DBKey key, String name) {
this.tree = tree;
this.dbkey = key;
this.name = name;
}
@Mem(estimate = false, size = 64)
private ConcurrentTree tree;
@Mem(estimate = false, size = 64)
private AtomicInteger leases = new AtomicInteger(0);
@Mem(estimate = false, size = 64)
private AtomicBoolean changed = new AtomicBoolean(false);
@Mem(estimate = false, size = 64)
private ReadWriteLock lock = new ReentrantReadWriteLock();
private AtomicBoolean decoded = new AtomicBoolean(false);
private AtomicBoolean initOnce = new AtomicBoolean(false);
private final Object initLock = new Object();
protected String name;
protected DBKey dbkey;
public String toString() {
return "TN[k=" + dbkey + ",db=" + nodedb + ",n#=" + nodes + ",h#=" + hits +
",nm=" + name + ",le=" + leases + ",ch=" + changed + ",bi=" + bits + "]";
}
@Override public String getName() {
return name;
}
public DBKey getDbkey() {
return dbkey;
}
@Override @SuppressWarnings("unchecked")
public Map<String, TreeNodeData> getDataMap() {
return data;
}
public int getLeaseCount() {
return leases.get();
}
/**
* The synchronized methods protecting the {@code nodes} field
* is a code smell. This should probably be protected by the
* encoding reader/writer {@code lock} field. There is an invariant
* for the page storage system that the encoding (write) locks of two nodes
* cannot be held simultaneously and switching to the encoding lock
* for these methods may violate the invariant.
*/
protected synchronized int incrementNodeCount() {
return nodes++;
}
protected synchronized int updateNodeCount(int delta) {
nodes += delta;
changed.set(true);
return nodes;
}
void requireEditable() {
int count = leases.get();
if (!(count == -2 || count > 0)) {
throw new RuntimeException("fail editable requirement: lease state is " + count);
}
}
public final boolean isBitSet(int bitcheck) {
return (bits & bitcheck) == bitcheck;
}
public boolean isAlias() {
return isBitSet(ALIAS);
}
public boolean isDeleted() {
int count = leases.get();
return count == -2;
}
public void markChanged() {
requireEditable();
changed.set(true);
}
protected boolean markDeleted() {
return leases.getAndSet(-2) != -2;
}
protected void evictionComplete() {
leases.compareAndSet(-1, -3);
}
protected synchronized void markAlias() {
bitSet(ALIAS);
}
protected boolean isChanged() {
return changed.get();
}
private final void bitSet(int set) {
bits |= set;
}
private final void bitUnset(int set) {
bits &= (~set);
}
/**
* A node is reactivated when it is retrieved from the backing storage
* and its state transitions from the inactive state to the active state
* with 0 leases.
*
*/
void reactivate() {
while(true) {
int count = leases.get();
if (count == -3 && leases.compareAndSet(-3, 0)) {
return;
} else if (count == -1 && leases.compareAndSet(-1, 0)) {
return;
} else if (count != -3 && count != -1) {
return;
}
}
}
/**
* Atomically try to acquire a lease. If the node is either
* in the inactive state or the deleted state then a lease
* cannot be acquired.
*
* @return {@code true} if a lease is acquired
*/
boolean tryLease() {
while (true) {
int count = leases.get();
if (count < 0) {
return false;
}
if (leases.compareAndSet(count, count + 1)) {
return true;
}
}
}
/**
* The node can be evicted if it has been deleted
* or it transitions from the active leases with 0 leases
* to the inactive state. If the node is already in the
* inactive state then it cannot be evicted.
*
* @return true if the node can be evicted
*/
boolean trySetEviction() {
while (true) {
int count = leases.get();
if (count == -2) {
return true;
} else if (count != 0) {
return false;
}
if (leases.compareAndSet(0, -1)) {
return true;
}
}
}
/**
* Atomically decrement the number of active leases.
*/
@Override
public void release() {
while (true) {
int count = leases.get();
if (count <= 0) {
return;
}
if (leases.compareAndSet(count, count - 1)) {
return;
}
}
}
/**
* double-checked locking idiom to avoid unnecessary synchronization.
*/
protected void requireNodeDB() {
if (!hasNodes()) {
synchronized (this) {
if (!hasNodes()) {
nodedb = tree.getNextNodeDB();
}
}
}
}
protected long nodeDB() {
return nodedb;
}
/**
* returns an iterator of read-only nodes
*/
public ClosableIterator<DataTreeNode> getNodeIterator() {
return !hasNodes() || isDeleted() ? new Iter(null, false) : new Iter(tree.fetchNodeRange(nodedb), true);
}
/**
* returns an iterator of read-only nodes
*/
public ClosableIterator<DataTreeNode> getNodeIterator(String prefix) {
if (!isDeleted() && prefix != null && prefix.length() > 0) {
/**
* the reason this behaves as a prefix as opposed to a "from" is the way
* the "to" endpoint is calculated. the last byte of the prefix is incremented
* by a value of one and used as "to". this is the effect of excluding all
* other potential matches with a lexicographic value greater than prefix.
*/
StringBuilder sb = new StringBuilder(prefix.substring(0, prefix.length() - 1));
sb.append((char) (prefix.charAt(prefix.length() - 1) + 1));
return getNodeIterator(prefix, sb.toString());
} else {
return new Iter(null, false);
}
}
/**
* returns an iterator of read-only nodes
*/
public ClosableIterator<DataTreeNode> getNodeIterator(String from, String to) {
if (!hasNodes() || isDeleted()) {
return new Iter(null, false);
}
return new Iter(tree.fetchNodeRange(nodedb, from, to), true);
}
@Override public ConcurrentTreeNode getNode(String name) {
return tree.getNode(this, name, false);
}
@Override public ConcurrentTreeNode getLeasedNode(String name) {
return tree.getNode(this, name, true);
}
public ConcurrentTreeNode getOrCreateEditableNode(String name) {
return getOrCreateEditableNode(name, null);
}
public ConcurrentTreeNode getOrCreateEditableNode(String name, DataTreeNodeInitializer creator) {
return tree.getOrCreateNode(this, name, creator);
}
@Override public boolean deleteNode(String name) {
return tree.deleteNode(this, name);
}
/**
* link this node (aliasing) to another node in the tree. they will share
* children, but not meta-data. should only be called from within a
* TreeNodeInitializer passed to getOrCreateEditableNode.
*/
@Override
public boolean aliasTo(DataTreeNode node) {
if (node.getClass() != ConcurrentTreeNode.class) {
return false;
}
requireEditable();
if (hasNodes()) {
return false;
}
((ConcurrentTreeNode) node).requireNodeDB();
nodedb = ((ConcurrentTreeNode) node).nodedb;
markAlias();
return true;
}
protected HashMap<String, TreeNodeData> createMap() {
if (data == null) {
data = new HashMap<>();
}
return data;
}
/**
* TODO: warning. if you annotate a path with data then have another path
* that intersects that node in the tree with some other data, the first one
* wins and the new data will not be added. further, every time the
* annotated node is crossed, the attached data will be updated if that path
* declares annotated data.
*/
@Override @SuppressWarnings("unchecked")
public void updateChildData(DataTreeNodeUpdater state, TreeDataParent path) {
requireEditable();
boolean updated = false;
HashMap<String, TreeDataParameters> dataconf = path.dataConfig();
lock.writeLock().lock();
try {
if (path.assignHits()) {
hits = state.getAssignmentValue();
updated = true;
} else if (path.countHits()) {
hits += state.getCountValue();
updated = true;
}
if (dataconf != null) {
if (data == null) {
data = new HashMap<>(dataconf.size());
}
for (Entry<String, TreeDataParameters> el : dataconf.entrySet()) {
TreeNodeData tnd = data.get(el.getKey());
if (tnd == null) {
tnd = el.getValue().newInstance(this);
data.put(el.getKey(), tnd);
updated = true;
}
if (tnd.updateChildData(state, this, el.getValue())) {
updated = true;
}
}
}
} finally {
lock.writeLock().unlock();
}
if (updated) {
changed.set(true);
}
}
/**
* @return true if data was changed
*/
@Override public void updateParentData(DataTreeNodeUpdater state, DataTreeNode child, boolean isnew) {
requireEditable();
List<TreeNodeDataDeferredOperation> deferredOps = null;
lock.writeLock().lock();
try {
if (child != null && data != null) {
deferredOps = new ArrayList<>(1);
for (TreeNodeData<?> tnd : data.values()) {
if (isnew && tnd.updateParentNewChild(state, this, child, deferredOps)) {
changed.set(true);
}
if (tnd.updateParentData(state, this, child, deferredOps)) {
changed.set(true);
}
}
}
} finally {
lock.writeLock().unlock();
}
if (deferredOps != null) {
for (TreeNodeDataDeferredOperation currentOp : deferredOps) {
currentOp.run();
}
}
}
// TODO concurrent broken -- data classes should be responsible for their
// own get/update sync
@Override public DataTreeNodeActor getData(String key) {
lock.readLock().lock();
try {
return data != null ? data.get(key) : null;
} finally {
lock.readLock().unlock();
}
}
// TODO concurrent broken -- data classes should be responsible for their
// own get/update sync
public Collection<String> getDataFields() {
lock.readLock().lock();
try {
if (data == null || data.size() == 0) {
return null;
}
return data.keySet();
} finally {
lock.readLock().unlock();
}
}
@Override
public int getNodeCount() {
return nodes;
}
@Override
public void postDecode() {
super.postDecode();
decoded.set(true);
}
/**
* TODO warning: not thread safe. sync around next(), hasNext() when
* concurrency is required.
*/
private final class Iter implements ClosableIterator<DataTreeNode> {
private Range<DBKey, ConcurrentTreeNode> range;
private ConcurrentTreeNode next;
private boolean filterDeleted;
private Iter(Range<DBKey, ConcurrentTreeNode> range, boolean filterDeleted) {
this.range = range;
this.filterDeleted = filterDeleted;
fetchNext();
}
public String toString() {
return "Iter(" + range + "," + next + ")";
}
void fetchNext() {
if (range != null) {
next = null;
while (range.hasNext()) {
Entry<DBKey, ConcurrentTreeNode> tne = range.next();
next = tree.getNode(ConcurrentTreeNode.this, tne.getKey().rawKey().toString(), false);
if (next != null) {
if (filterDeleted && next.isDeleted()) {
next = null;
continue;
}
break;
}
}
}
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public ConcurrentTreeNode next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
ConcurrentTreeNode ret = next;
fetchNext();
return ret;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public void close() {
if (range != null) {
range.close();
range = null;
}
}
}
@Override
public void encodeLock() {
lock.readLock().lock();
}
@Override
public void encodeUnlock() {
lock.readLock().unlock();
}
@Override
public void writeLock() {
lock.writeLock().lock();
}
@Override
public void writeUnlock() {
lock.writeLock().unlock();
}
@Override
public Iterator<DataTreeNode> iterator() {
return getNodeIterator();
}
@Override
public ClosableIterator<DataTreeNode> getIterator() {
return getNodeIterator();
}
@Override
public ConcurrentTree getTreeRoot() {
return tree;
}
@Override
public ClosableIterator<DataTreeNode> getIterator(String begin) {
return getNodeIterator(begin);
}
@Override
public ClosableIterator<DataTreeNode> getIterator(String from, String to) {
return getNodeIterator(from, to);
}
@Override
public DataTreeNode getOrCreateNode(String name, DataTreeNodeInitializer init) {
return getOrCreateEditableNode(name, init);
}
/**
* The synchronized methods protecting the {@code counter} field
* is a code smell. This should probably be protected by the
* encoding reader/writer {@code lock} field. There is an invariant
* for the page storage system that the encoding (write) locks of two nodes
* cannot be held simultaneously and switching to the encoding lock
* for these methods may violate the invariant.
*/
@Override
public synchronized long getCounter() {
return hits;
}
@Override
public synchronized void incrementCounter() {
hits++;
}
@Override
public synchronized long incrementCounter(long val) {
hits += val;
return hits;
}
@Override
public synchronized void setCounter(long val) {
hits = val;
}
}