com.addthis.hydra.data.tree.nonconcurrent.NonConcurrentTree.java Source code

Java tutorial

Introduction

Here is the source code for com.addthis.hydra.data.tree.nonconcurrent.NonConcurrentTree.java

Source

/*
 * 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);
        }
    }
}