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