/*
* 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.nonconcurrent;
import com.addthis.basis.util.ClosableIterator;
import com.addthis.basis.util.LessBytes;
import com.addthis.basis.util.LessFiles;
import com.addthis.basis.util.Meter;
import com.addthis.basis.util.Parameter;
import com.addthis.hydra.common.Configuration;
import com.addthis.hydra.data.tree.CacheKey;
import com.addthis.hydra.data.tree.DataTree;
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.TreeCommonParameters;
import com.addthis.hydra.data.tree.TreeDataParent;
import com.addthis.hydra.data.tree.TreeNodeData;
import com.addthis.hydra.data.tree.concurrent.ConcurrentTreeNode;
import com.addthis.hydra.store.common.PageFactory;
import com.addthis.hydra.store.db.CloseOperation;
import com.addthis.hydra.store.db.DBKey;
import com.addthis.hydra.store.db.IPageDB;
import com.addthis.hydra.store.db.PageDB;
import com.addthis.hydra.store.nonconcurrent.NonConcurrentPage;
import com.addthis.hydra.store.util.MeterFileLogger;
import com.addthis.hydra.store.util.MeterFileLogger.MeterDataSource;
import com.addthis.hydra.store.util.Raw;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.concurrent.GuardedBy;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BooleanSupplier;
import org.apache.commons.lang3.mutable.MutableLong;
/**
* This is a non-concurrent version of the {@link DataTree}. The idea here is
* that in many cases a single thread that does not require concurrency constructs
* may out preform concurrent implementations and it should be easier to reason about
* the behavior of the class.
* <p>
* This class is explicitly not thread safe. There is no protection against concurrent
* modification of tree nodes or conflicting writes to the backing cache.
* <p>
* There are certainly more optimizations that can be made to differentiate this class
* from its Concurrent cousin {@link com.addthis.hydra.data.tree.concurrent.ConcurrentTree}
* but Rome wasn't built in a day.
*/
public final class NonConcurrentTree implements DataTree, MeterDataSource {
static final Logger log = LoggerFactory.getLogger(NonConcurrentTree.class);
@Override
public Map<String, Long> getIntervalData() {
return new HashMap<>();
}
public static enum METERTREE {
NODE_PUT, NODE_CREATE, NODE_DELETE, SOURCE_MISS
}
private final File root;
final IPageDB<DBKey, NonConcurrentTreeNode> source;
private final NonConcurrentTreeNode treeRootNode;
private final NonConcurrentTreeNode treeTrashNode;
private final AtomicLong nextDBID;
final AtomicBoolean closed = new AtomicBoolean(false);
private final Meter<METERTREE> meter;
private final MeterFileLogger logger;
// number of nodes in between trash removal logging messages
@Configuration.Parameter
static final int deletionLogInterval = Parameter.intValue("hydra.tree.clean.logging", 100000);
public NonConcurrentTree(File root, int maxCacheSize,
int maxPageSize, PageFactory factory) throws Exception {
LessFiles.initDirectory(root);
this.root = root;
long start = System.currentTimeMillis();
// setup metering
meter = new Meter<>(METERTREE.values());
for (METERTREE m : METERTREE.values()) {
meter.addCountMetric(m, m.toString());
}
// create meter logging thread
if (TreeCommonParameters.meterLogging > 0) {
logger = new MeterFileLogger(this, root, "tree-metrics",
TreeCommonParameters.meterLogging, TreeCommonParameters.meterLogLines);
} else {
logger = null;
}
source = new PageDB.Builder<>(root, NonConcurrentTreeNode.class, maxPageSize, maxCacheSize)
.pageFactory(factory).build();
source.setCacheMem(TreeCommonParameters.maxCacheMem);
source.setPageMem(TreeCommonParameters.maxPageMem);
source.setMemSampleInterval(TreeCommonParameters.memSample);
// get stored next db id
File idFile = new File(root, "nextID");
if (idFile.exists() && idFile.isFile() && idFile.length() > 0) {
nextDBID = new AtomicLong(Long.parseLong(LessBytes.toString(LessFiles.read(idFile))));
} else {
nextDBID = new AtomicLong(1);
}
// get tree root
NonConcurrentTreeNode dummyRoot = NonConcurrentTreeNode.getTreeRoot(this);
treeRootNode = dummyRoot.getOrCreateEditableNode("root");
treeTrashNode = dummyRoot.getOrCreateEditableNode("trash");
long openTime = System.currentTimeMillis() - start;
log.info("dir={} root={} nextdb={} openms={}",
root, treeRootNode, nextDBID, openTime);
}
public NonConcurrentTree(File root) throws Exception {
this(root, TreeCommonParameters.maxCacheSize, TreeCommonParameters.maxPageSize,
NonConcurrentPage.NonConcurrentPageFactory.singleton);
}
public void meter(METERTREE meterval) {
meter.inc(meterval);
}
/**
* This method is only for testing purposes.
* It has a built in safeguard but nonetheless
* it should not be invoked for other purposes.
*/
@VisibleForTesting
boolean setNextNodeDB(long id) {
while (true) {
long current = nextDBID.get();
if (current > id) {
return false;
} else if (nextDBID.compareAndSet(current, id)) {
return true;
}
}
}
long getNextNodeDB() {
return nextDBID.incrementAndGet();
}
public NonConcurrentTreeNode getNode(final NonConcurrentTreeNode parent, final String child, final boolean lease) {
long nodedb = parent.nodeDB();
if (nodedb <= 0) {
log.trace("[node.get] {} --> {} NOMAP --> null", parent, child);
return null;
}
CacheKey key = new CacheKey(nodedb, child);
DBKey dbkey = key.dbkey();
NonConcurrentTreeNode node = source.get(dbkey);
if (node == null) {
meter.inc(METERTREE.SOURCE_MISS);
return null; // (3)
}
node.initNode(this, dbkey, key.name);
return node;
}
public NonConcurrentTreeNode getOrCreateNode(final NonConcurrentTreeNode parent, final String child,
final DataTreeNodeInitializer creator) {
parent.requireNodeDB();
CacheKey key = new CacheKey(parent.nodeDB(), child);
DBKey dbkey = key.dbkey();
NonConcurrentTreeNode node = source.get(dbkey);
if (node != null) {
node.initNode(this, dbkey, key.name);
return node;
} else { // create a new node
NonConcurrentTreeNode newNode = new NonConcurrentTreeNode();
newNode.init(this, dbkey, key.name);
if (creator != null) {
creator.onNewNode(newNode);
}
source.put(dbkey, newNode);
parent.updateNodeCount(1);
return newNode;
}
}
@Override
public void foregroundNodeDeletion(BooleanSupplier terminationCondition) {
IPageDB.Range<DBKey, NonConcurrentTreeNode> range = fetchNodeRange(treeTrashNode.nodeDB());
Map.Entry<DBKey, NonConcurrentTreeNode> entry;
MutableLong totalCount = new MutableLong();
MutableLong nodeCount = new MutableLong();
try {
while (range.hasNext() && !terminationCondition.getAsBoolean()) {
entry = range.next();
if (entry != null) {
NonConcurrentTreeNode node = entry.getValue();
NonConcurrentTreeNode prev = source.remove(entry.getKey());
if (prev != null) {
deleteSubTree(node, totalCount, nodeCount, terminationCondition);
nodeCount.increment();
treeTrashNode.incrementCounter();
}
}
}
} finally {
range.close();
}
}
@Override
public int getCacheSize() {
return 0;
}
@Override
public double getCacheHitRate() {
return 0;
}
boolean deleteNode(final NonConcurrentTreeNode parent, final String child) {
log.trace("[node.delete] {} --> {}", parent, child);
long nodedb = parent.nodeDB();
if (nodedb <= 0) {
log.debug("parent has no children on delete : {} --> {}", parent, child);
return false;
}
CacheKey key = new CacheKey(nodedb, child);
NonConcurrentTreeNode node = source.remove(key.dbkey());
if (node != null) {
parent.updateNodeCount(-1);
if (node.hasNodes() && !node.isAlias()) {
markForChildDeletion(node);
}
return true;
} else {
return false;
}
}
private void markForChildDeletion(final NonConcurrentTreeNode node) {
assert node.hasNodes();
assert !node.isAlias();
long nodeDB = treeTrashNode.nodeDB();
int next = treeTrashNode.updateNodeCount(1);
DBKey key = new DBKey(nodeDB, Raw.get(LessBytes.toBytes(next)));
source.put(key, node);
log.trace("[trash.mark] {} --> {}", next, treeTrashNode);
}
@SuppressWarnings("unchecked")
IPageDB.Range<DBKey, NonConcurrentTreeNode> fetchNodeRange(long db) {
return source.range(new DBKey(db), new DBKey(db + 1));
}
@SuppressWarnings({"unchecked", "unused"})
private IPageDB.Range<DBKey, NonConcurrentTreeNode> fetchNodeRange(long db, String from) {
return source.range(new DBKey(db, Raw.get(from)), new DBKey(db + 1));
}
@SuppressWarnings("unchecked")
IPageDB.Range<DBKey, NonConcurrentTreeNode> fetchNodeRange(long db, String from, String to) {
return source.range(new DBKey(db, Raw.get(from)),
to == null ? new DBKey(db + 1, (Raw) null) : new DBKey(db, Raw.get(to)));
}
@Override
public NonConcurrentTreeNode getRootNode() {
return treeRootNode;
}
@Override
public long getDBCount() {
return nextDBID.get();
}
/**
* Close the tree.
*
* @param cleanLog if true then wait for the BerkeleyDB clean thread to finish.
* @param operation optionally test or repair the berkeleyDB.
*/
@Override
public void close(boolean cleanLog, CloseOperation operation) throws IOException {
if (!closed.compareAndSet(false, true)) {
log.trace("already closed");
return;
}
log.debug("closing {}", this);
sync();
if (source != null) {
int status = source.close(cleanLog, operation);
if (status != 0) {
throw new RuntimeException("page db close returned a non-zero exit code : " + status);
}
}
if (logger != null) {
logger.terminate();
}
}
@Override
public void sync() throws IOException {
source.put(treeRootNode.getDbkey(), treeRootNode);
source.put(treeTrashNode.getDbkey(), treeTrashNode);
}
@Override
public void close() throws IOException {
close(false, CloseOperation.NONE);
}
@Override
public String toString() {
return "Tree@" + root;
}
@Override
public DataTreeNode getLeasedNode(String name) {
return getRootNode().getLeasedNode(name);
}
@Override
public DataTreeNode getOrCreateNode(String name, DataTreeNodeInitializer init) {
return getRootNode().getOrCreateNode(name, init);
}
@Override
public boolean deleteNode(String node) {
return getRootNode().deleteNode(node);
}
@Override
public ClosableIterator<DataTreeNode> getIterator() {
NonConcurrentTreeNode rootNode = getRootNode();
if (rootNode != null) {
return getRootNode().getIterator();
}
return null;
}
@Override
public ClosableIterator<DataTreeNode> getIterator(String begin) {
return getRootNode().getIterator(begin);
}
@Override
public ClosableIterator<DataTreeNode> getIterator(String from, String to) {
return getRootNode().getIterator(from, to);
}
@Override
public Iterator<DataTreeNode> iterator() {
return getRootNode().iterator();
}
@Override
public String getName() {
return getRootNode().getName();
}
@Override
public int getNodeCount() {
return getRootNode().getNodeCount();
}
@Override
public long getCounter() {
return getRootNode().getCounter();
}
@Override
public void incrementCounter() {
getRootNode().incrementCounter();
}
@Override
public long incrementCounter(long val) {
return getRootNode().incrementCounter(val);
}
@Override
public void writeLock() {
getRootNode().writeLock();
}
@Override
public void writeUnlock() {
getRootNode().writeUnlock();
}
@Override
public void setCounter(long val) {
getRootNode().setCounter(val);
}
@Override
public void updateChildData(DataTreeNodeUpdater state, TreeDataParent path) {
getRootNode().updateChildData(state, path);
}
@Override
public void updateParentData(DataTreeNodeUpdater state, DataTreeNode child, boolean isnew) {
getRootNode().updateParentData(state, child, isnew);
}
@Override
public boolean aliasTo(DataTreeNode target) {
throw new RuntimeException("root node cannot be an alias");
}
@Override
public void release() {
getRootNode().release();
}
@Override
public DataTreeNodeActor getData(String key) {
return getRootNode().getData(key);
}
@Override
public Map<String, TreeNodeData> getDataMap() {
return getRootNode().getDataMap();
}
/**
* Iteratively delete all the children of the input node.
* Use a non-negative value for the counter parameter to
* tally the nodes that have been deleted. Use a negative
* value to disable logging of the number of deleted nodes.
*
* @param rootNode root of the subtree to delete
*/
void deleteSubTree(NonConcurrentTreeNode rootNode,
MutableLong totalCount,
MutableLong nodeCount,
BooleanSupplier terminationCondition) {
long nodeDB = rootNode.nodeDB();
IPageDB.Range<DBKey, NonConcurrentTreeNode> range = fetchNodeRange(nodeDB);
DBKey endRange;
boolean reschedule;
try {
while (range.hasNext() && !terminationCondition.getAsBoolean()) {
totalCount.increment();
if ((totalCount.longValue() % deletionLogInterval) == 0) {
log.info("Deleted {} total nodes in {} trash nodes from the trash.",
totalCount.longValue(), nodeCount.longValue());
}
Map.Entry<DBKey, NonConcurrentTreeNode> entry = range.next();
NonConcurrentTreeNode next = entry.getValue();
if (next.hasNodes() && !next.isAlias()) {
deleteSubTree(next, totalCount, nodeCount, terminationCondition);
}
}
if (range.hasNext()) {
endRange = range.next().getKey();
reschedule = true;
} else {
endRange = new DBKey(nodeDB + 1);
reschedule = false;
}
} finally {
range.close();
}
source.remove(new DBKey(nodeDB), endRange);
if (reschedule) {
markForChildDeletion(rootNode);
}
}
}