org.apache.hadoop.hdfs.notifier.server.ServerHistory.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.hdfs.notifier.server.ServerHistory.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.hadoop.hdfs.notifier.server;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.DFSUtil;
import org.apache.hadoop.hdfs.notifier.NamespaceEvent;
import org.apache.hadoop.hdfs.notifier.NamespaceNotification;
import org.apache.hadoop.hdfs.notifier.NotifierUtils;
import org.apache.hadoop.hdfs.notifier.TransactionIdTooOldException;

public class ServerHistory implements IServerHistory {
    public static final Log LOG = LogFactory.getLog(ServerHistory.class);

    public int loopSleepTime = 100;

    // Configuration keys
    public static final String HISTORY_LENGTH = "notifier.history.length";
    public static final String HISTORY_LIMIT = "notifier.history.limit";

    // true if in the ramp-up phase
    private volatile boolean rampUp = true;

    // store the timestamp/transaction_id order of the history entries.
    final ArrayList<HistoryTreeEntry> orderedHistoryList;
    // the root of the history tree.
    final HistoryNode historyTree;

    ReentrantReadWriteLock historyLock = new ReentrantReadWriteLock();

    private IServerCore core;

    // The length in time over which the history is kept
    private volatile long historyLength;

    // The number of queues in the history tree
    volatile long historyQueuesCount = 0;

    // The physical limit of the number of items in the history
    private volatile long historyLimit;
    private volatile boolean historyLimitDisabled = false;

    // compare by transaction id
    private final HistoryTreeEntryComparatorById comparatorByID = new HistoryTreeEntryComparatorById();

    // compare by the timestamp when we store the event.
    private final HistoryTreeEntryComparatorByTS comparatorByTS = new HistoryTreeEntryComparatorByTS();

    public ServerHistory(IServerCore core, boolean initialRampUp) throws ConfigurationException {
        this.core = core;
        historyLength = core.getConfiguration().getLong(HISTORY_LENGTH, -1);
        historyLimit = core.getConfiguration().getLong(HISTORY_LIMIT, -1);
        historyTree = new HistoryNode("");
        orderedHistoryList = new ArrayList<HistoryTreeEntry>();
        rampUp = initialRampUp;

        if (historyLength == -1) {
            LOG.error("Missing default configuration: historyLength");
            throw new ConfigurationException("Missing historyLength");
        }
        if (historyLimit == -1) {
            LOG.warn("Starting history without any physical limit ...");
            historyLimitDisabled = true;
        }

    }

    /**
     * Set the length in time 
     */
    @Override
    public void setHistoryLength(long newHistoryLength) {
        historyLength = newHistoryLength;
    }

    /**
     * set the number of transactions kept in history
     */
    @Override
    public void setHistoryLimit(long newHistoryLimit) {
        if (newHistoryLimit > 0) {
            historyLimitDisabled = false;
        }
        historyLimit = newHistoryLimit;
    }

    @Override
    public boolean isRampUp() {
        return rampUp;
    }

    private void checkRampUp(long startTime) {
        boolean initialIsRampUp = rampUp;

        if (startTime + historyLength < System.currentTimeMillis()) {
            rampUp = false;
        }

        if (initialIsRampUp && !rampUp) {
            LOG.info("Server went out of ramp up phase ...");
        }
    }

    /**
     * Checks if there are notifications in our tree which are older than
     * historyLength. It removes does which are older.
     */
    private void cleanUpHistory() {
        long oldestAllowedTimestamp = System.currentTimeMillis() - historyLength;
        int trashedNotifications = 0;

        if (LOG.isDebugEnabled()) {
            LOG.debug("History cleanup: Checking old notifications to remove from history list ...");
        }

        HistoryTreeEntry key = new HistoryTreeEntry(oldestAllowedTimestamp, 0, (byte) 0);
        int notificationsCount = 0;
        historyLock.writeLock().lock();
        try {
            notificationsCount = orderedHistoryList.size();
            LOG.warn("History cleanup: size of the history before cleanup: " + notificationsCount);

            if (!historyLimitDisabled && notificationsCount > historyLimit) {
                LOG.warn("History cleanup: Reached physical limit. Number of stored notifications: "
                        + notificationsCount + ". Clearing ...");
            }

            int index = Collections.binarySearch(orderedHistoryList, key, comparatorByTS);
            int toDeleteByTS = index >= 0 ? index : -(index + 1);
            int toDeleteByLimit = historyLimitDisabled ? 0 : notificationsCount - (int) historyLimit;
            toDeleteByLimit = toDeleteByLimit > 0 ? toDeleteByLimit : 0;

            int toDelete = Math.max(toDeleteByTS, toDeleteByLimit);

            // Delete items which are too old
            if (toDelete > 0) {
                LOG.warn("History cleanup: number of the history to cleanup: " + toDelete);
                for (int i = 0; i < toDelete; i++) {
                    orderedHistoryList.get(i).removeFromTree();
                }

                orderedHistoryList.subList(0, toDelete).clear();

                if (toDeleteByLimit > toDeleteByTS) {
                    // If we delete a notification because we don't have space left
                    trashedNotifications++;
                }
                notificationsCount = orderedHistoryList.size();
                LOG.warn("History cleanup: size of the history after cleanup: " + notificationsCount);

                // clean up history tree, remove the node that has no children and
                // no notifications associated with them.
                cleanUpHistoryTree(historyTree);
            }

        } finally {
            historyLock.writeLock().unlock();
        }

        core.getMetrics().trashedHistoryNotifications.inc(trashedNotifications);
        core.getMetrics().historySize.set(notificationsCount);
        core.getMetrics().historyQueues.set(historyQueuesCount);
    }

    /**
     * Clean up the Tree by DFS traversal.
     * 
     * Remove the node that has no children and no notifications associated
     * with them.
     * @param node
     */
    private void cleanUpHistoryTree(HistoryNode node) {
        if (node == null || node.children == null) {
            return;
        }

        Iterator<HistoryNode> iterator = node.children.iterator();
        while (iterator.hasNext()) {
            HistoryNode child = iterator.next();

            // clean up child
            cleanUpHistoryTree(child);

            // clean up current node;
            if (shouldRemoveNode(child)) {
                iterator.remove();
            }
        }
    }

    /**
     * Should remove the node from the history tree if both the notifications 
     * and children list are empty.
     * @param node
     * @return
     */
    private boolean shouldRemoveNode(HistoryNode node) {
        if (node == null) {
            return true;
        }

        int sizeOfChildren = 0;
        if (node.children != null) {
            sizeOfChildren = node.children.size();
        }

        if (sizeOfChildren > 0) {
            return false;
        }

        int sizeOfNotifications = 0;
        if (node.notifications != null) {
            for (List<HistoryTreeEntry> notiList : node.notifications.values()) {
                if (notiList != null) {
                    sizeOfNotifications += notiList.size();
                    if (sizeOfNotifications > 0) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Should be called when the server starts to start recording the history
     * (the namespace operations from the edit log).
     */
    @Override
    public void run() {
        long startTime = System.currentTimeMillis();

        try {
            LOG.info("Starting the history thread ...");
            while (!core.shutdownPending()) {
                checkRampUp(startTime);
                cleanUpHistory();
                Thread.sleep(loopSleepTime);
            }
            LOG.info("History thread shutdown.");
        } catch (Exception e) {
            LOG.error("ServerHistory died", e);
        } finally {
            core.shutdown();
        }
    }

    /**
     * Called when we should store a notification in the our history.
     * The timestamp used to store it is generated when this method is
     * called.
     * 
     * It doesn't provide ordering if it is called with miss-ordered
     * notifications (e.g. it's called once with a notification with
     * the transaction id "i" and after some time with a notification
     * with the transaction id "j", where j < i).
     * 
     * @param notification The notification to be stored.
     */
    @Override
    public void storeNotification(NamespaceNotification notification) {
        int notificationsCount = 0;
        historyLock.writeLock().lock();
        try {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Storing into history: " + NotifierUtils.asString(notification));
            }

            String[] paths = DFSUtil.split(notification.path, Path.SEPARATOR_CHAR);

            long timestamp = System.currentTimeMillis();
            HistoryTreeEntry entry = new HistoryTreeEntry(timestamp, notification.txId, notification.type);

            // Store the notification
            HistoryNode node = historyTree;
            for (String path : paths) {
                if (path.trim().length() == 0) {
                    continue;
                }

                node = node.addOrGetChild(path);
            }

            if (node.notifications == null) {
                node.notifications = new HashMap<Byte, List<HistoryTreeEntry>>();
            }
            if (!node.notifications.containsKey(notification.type)) {
                node.notifications.put(notification.type, new LinkedList<HistoryTreeEntry>());
            }

            entry.node = node;
            node.notifications.get(notification.type).add(entry);

            orderedHistoryList.add(entry);
            notificationsCount = orderedHistoryList.size();
        } finally {
            historyLock.writeLock().unlock();
        }

        core.getMetrics().historySize.set(notificationsCount);
        core.getMetrics().historyQueues.set(historyQueuesCount);

        if (LOG.isDebugEnabled()) {
            LOG.debug("Notification stored.");
        }
    }

    /**
     * Checks what notifications are saved in history for the given event and
     * adds those notifications in the given queue. Only the notifications
     * which happened strictly after the edit log operations with the given
     * transaction id are put in the queue.
     * The notifications are put in the queue in the order of their
     * transaction id.
     * 
     * @param event the event for which the notifications should be stored
     *        in the queue.
     * @param txId the given transaction id
     * @param notifications the queue in which the notifications will be placed.
     * 
     * @throws TransactionIdTooOldException raised when we can't guarantee that
     *         we got all notifications that happened after the given
     *         transaction id.
     */
    @Override
    public void addNotificationsToQueue(NamespaceEvent event, long txId, Queue<NamespaceNotification> notifications)
            throws TransactionIdTooOldException {

        if (LOG.isDebugEnabled()) {
            LOG.debug("Got addNotificationsToQueue for: " + NotifierUtils.asString(event) + " and txId: " + txId);
        }

        historyLock.readLock().lock();
        try {
            if (orderedHistoryList == null || orderedHistoryList.size() == 0) {
                throw new TransactionIdTooOldException("No data in history.");
            }

            if (orderedHistoryList.get(0).txnId > txId
                    || orderedHistoryList.get(orderedHistoryList.size() - 1).txnId < txId) {
                throw new TransactionIdTooOldException("No data in history for txId " + txId);
            }

            int index = Collections.binarySearch(orderedHistoryList, new HistoryTreeEntry(0, txId, event.type),
                    comparatorByID);

            if (index < 0) {
                // If we got here, there are 2 possibilities:
                // * The client gave us a bad transaction id.
                // * We missed one (or more) transaction(s)
                LOG.error("Potential corrupt history. Got request for: " + NotifierUtils.asString(event)
                        + " and txId: " + txId);
                throw new TransactionIdTooOldException("Potentially corrupt server history");
            }

            String dirFormatPath = event.path;
            if (!dirFormatPath.endsWith(Path.SEPARATOR)) {
                dirFormatPath += Path.SEPARATOR;
            }
            for (int i = index + 1; i < orderedHistoryList.size(); i++) {
                HistoryTreeEntry entry = orderedHistoryList.get(i);
                if (event.type != entry.type) {
                    continue;
                }

                String entryPath = entry.getFullPath();
                if (entryPath.startsWith(dirFormatPath)) {
                    notifications.add(new NamespaceNotification(entryPath, entry.type, entry.txnId));
                }
            }
        } finally {
            historyLock.readLock().unlock();
        }
    }

    private class HistoryTreeEntryComparatorById implements Comparator<HistoryTreeEntry> {

        @Override
        public int compare(HistoryTreeEntry o1, HistoryTreeEntry o2) {
            return (int) (o1.txnId - o2.txnId);
        }
    }

    private class HistoryTreeEntryComparatorByTS implements Comparator<HistoryTreeEntry> {

        @Override
        public int compare(HistoryTreeEntry o1, HistoryTreeEntry o2) {
            return (int) (o1.timestamp - o2.timestamp);
        }
    }

    protected class HistoryNode implements Comparable<String> {
        final String name;
        Map<Byte, List<HistoryTreeEntry>> notifications = null;
        final ArrayList<HistoryNode> children;
        HistoryNode parent = null;

        public HistoryNode(String name) {
            this.name = name;
            this.children = new ArrayList<HistoryNode>();
        }

        public HistoryNode addOrGetChild(String childName) {
            int index = Collections.binarySearch(children, childName);
            if (index >= 0) {
                return children.get(index);
            }

            index = -(index + 1);
            HistoryNode child = new HistoryNode(childName);
            child.parent = this;
            children.add(index, child);
            return child;
        }

        @Override
        public int hashCode() {
            return name.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null || !(obj instanceof HistoryNode)) {
                return false;
            }
            return this.name.equals(((HistoryNode) obj).name);
        }

        @Override
        public int compareTo(String name2) {
            return this.name.compareTo(name2);
        }
    }

    protected class HistoryTreeEntry {
        long timestamp;
        long txnId;
        byte type;
        HistoryNode node;

        public HistoryTreeEntry(long timestamp, long txnId, byte type) {
            this.timestamp = timestamp;
            this.txnId = txnId;
            this.type = type;
        }

        /**
         * Get the full event path.
         * @return
         */
        public String getFullPath() {
            if (node == null) {
                return null;
            }

            StringBuilder sb = new StringBuilder();
            HistoryNode t = node;
            sb.append(t.name);
            while (t.parent != null) {
                t = t.parent;
                sb.insert(0, t.name + Path.SEPARATOR);
            }
            return sb.toString();
        }

        public boolean removeFromTree() {
            return node.notifications.get(type).remove(this);
        }
    }

}