Java tutorial
/** * 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.net.unix; import java.io.Closeable; import java.io.EOFException; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.io.IOUtils; import java.io.IOException; import java.nio.channels.ClosedChannelException; import java.util.Iterator; import java.util.LinkedList; import java.util.TreeMap; import java.util.Map; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.lang.SystemUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.util.NativeCodeLoader; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.Uninterruptibles; /** * The DomainSocketWatcher watches a set of domain sockets to see when they * become readable, or closed. When one of those events happens, it makes a * callback. * * See {@link DomainSocket} for more information about UNIX domain sockets. */ @InterfaceAudience.LimitedPrivate("HDFS") public final class DomainSocketWatcher implements Closeable { static { if (SystemUtils.IS_OS_WINDOWS) { loadingFailureReason = "UNIX Domain sockets are not available on Windows."; } else if (!NativeCodeLoader.isNativeCodeLoaded()) { loadingFailureReason = "libhadoop cannot be loaded."; } else { String problem; try { anchorNative(); problem = null; } catch (Throwable t) { problem = "DomainSocketWatcher#anchorNative got error: " + t.getMessage(); } loadingFailureReason = problem; } } static Log LOG = LogFactory.getLog(DomainSocketWatcher.class); /** * The reason why DomainSocketWatcher is not available, or null if it is * available. */ private final static String loadingFailureReason; /** * Initializes the native library code. */ private static native void anchorNative(); public static String getLoadingFailureReason() { return loadingFailureReason; } public interface Handler { /** * Handles an event on a socket. An event may be the socket becoming * readable, or the remote end being closed. * * @param sock The socket that the event occurred on. * @return Whether we should close the socket. */ boolean handle(DomainSocket sock); } /** * Handler for {DomainSocketWatcher#notificationSockets[1]} */ private class NotificationHandler implements Handler { public boolean handle(DomainSocket sock) { assert (lock.isHeldByCurrentThread()); try { kicked = false; if (LOG.isTraceEnabled()) { LOG.trace(this + ": NotificationHandler: doing a read on " + sock.fd); } if (sock.getInputStream().read() == -1) { if (LOG.isTraceEnabled()) { LOG.trace(this + ": NotificationHandler: got EOF on " + sock.fd); } throw new EOFException(); } if (LOG.isTraceEnabled()) { LOG.trace(this + ": NotificationHandler: read succeeded on " + sock.fd); } return false; } catch (IOException e) { if (LOG.isTraceEnabled()) { LOG.trace(this + ": NotificationHandler: setting closed to " + "true for " + sock.fd); } closed = true; return true; } } } private static class Entry { final DomainSocket socket; final Handler handler; Entry(DomainSocket socket, Handler handler) { this.socket = socket; this.handler = handler; } DomainSocket getDomainSocket() { return socket; } Handler getHandler() { return handler; } } /** * The FdSet is a set of file descriptors that gets passed to poll(2). * It contains a native memory segment, so that we don't have to copy * in the poll0 function. */ private static class FdSet { private long data; private native static long alloc0(); FdSet() { data = alloc0(); } /** * Add a file descriptor to the set. * * @param fd The file descriptor to add. */ native void add(int fd); /** * Remove a file descriptor from the set. * * @param fd The file descriptor to remove. */ native void remove(int fd); /** * Get an array containing all the FDs marked as readable. * Also clear the state of all FDs. * * @return An array containing all of the currently readable file * descriptors. */ native int[] getAndClearReadableFds(); /** * Close the object and de-allocate the memory used. */ native void close(); } /** * Lock which protects toAdd, toRemove, and closed. */ private final ReentrantLock lock = new ReentrantLock(); /** * Condition variable which indicates that toAdd and toRemove have been * processed. */ private final Condition processedCond = lock.newCondition(); /** * Entries to add. */ private final LinkedList<Entry> toAdd = new LinkedList<Entry>(); /** * Entries to remove. */ private final TreeMap<Integer, DomainSocket> toRemove = new TreeMap<Integer, DomainSocket>(); /** * Maximum length of time to go between checking whether the interrupted * bit has been set for this thread. */ private final int interruptCheckPeriodMs; /** * A pair of sockets used to wake up the thread after it has called poll(2). */ private final DomainSocket notificationSockets[]; /** * Whether or not this DomainSocketWatcher is closed. */ private boolean closed = false; /** * True if we have written a byte to the notification socket. We should not * write anything else to the socket until the notification handler has had a * chance to run. Otherwise, our thread might block, causing deadlock. * See HADOOP-11333 for details. */ private boolean kicked = false; public DomainSocketWatcher(int interruptCheckPeriodMs, String src) throws IOException { if (loadingFailureReason != null) { throw new UnsupportedOperationException(loadingFailureReason); } Preconditions.checkArgument(interruptCheckPeriodMs > 0); this.interruptCheckPeriodMs = interruptCheckPeriodMs; notificationSockets = DomainSocket.socketpair(); watcherThread.setDaemon(true); watcherThread.setName(src + " DomainSocketWatcher"); watcherThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable t) { LOG.error(thread + " terminating on unexpected exception", t); } }); watcherThread.start(); } /** * Close the DomainSocketWatcher and wait for its thread to terminate. * * If there is more than one close, all but the first will be ignored. */ @Override public void close() throws IOException { lock.lock(); try { if (closed) return; if (LOG.isDebugEnabled()) { LOG.debug(this + ": closing"); } closed = true; } finally { lock.unlock(); } // Close notificationSockets[0], so that notificationSockets[1] gets an EOF // event. This will wake up the thread immediately if it is blocked inside // the select() system call. notificationSockets[0].close(); // Wait for the select thread to terminate. Uninterruptibles.joinUninterruptibly(watcherThread); } @VisibleForTesting public boolean isClosed() { lock.lock(); try { return closed; } finally { lock.unlock(); } } /** * Add a socket. * * @param sock The socket to add. It is an error to re-add a socket that * we are already watching. * @param handler The handler to associate with this socket. This may be * called any time after this function is called. */ public void add(DomainSocket sock, Handler handler) { lock.lock(); try { if (closed) { handler.handle(sock); IOUtils.cleanup(LOG, sock); return; } Entry entry = new Entry(sock, handler); try { sock.refCount.reference(); } catch (ClosedChannelException e1) { // If the socket is already closed before we add it, invoke the // handler immediately. Then we're done. handler.handle(sock); return; } toAdd.add(entry); kick(); while (true) { processedCond.awaitUninterruptibly(); if (!toAdd.contains(entry)) { break; } } } finally { lock.unlock(); } } /** * Remove a socket. Its handler will be called. * * @param sock The socket to remove. */ public void remove(DomainSocket sock) { lock.lock(); try { if (closed) return; toRemove.put(sock.fd, sock); kick(); while (true) { processedCond.awaitUninterruptibly(); if (!toRemove.containsKey(sock.fd)) { break; } } } finally { lock.unlock(); } } /** * Wake up the DomainSocketWatcher thread. */ private void kick() { assert (lock.isHeldByCurrentThread()); if (kicked) { return; } try { notificationSockets[0].getOutputStream().write(0); kicked = true; } catch (IOException e) { if (!closed) { LOG.error(this + ": error writing to notificationSockets[0]", e); } } } /** * Send callback and return whether or not the domain socket was closed as a * result of processing. * * @param caller reason for call * @param entries mapping of file descriptor to entry * @param fdSet set of file descriptors * @param fd file descriptor * @return true if the domain socket was closed as a result of processing */ private boolean sendCallback(String caller, TreeMap<Integer, Entry> entries, FdSet fdSet, int fd) { if (LOG.isTraceEnabled()) { LOG.trace(this + ": " + caller + " starting sendCallback for fd " + fd); } Entry entry = entries.get(fd); Preconditions.checkNotNull(entry, this + ": fdSet contained " + fd + ", which we were " + "not tracking."); DomainSocket sock = entry.getDomainSocket(); if (entry.getHandler().handle(sock)) { if (LOG.isTraceEnabled()) { LOG.trace(this + ": " + caller + ": closing fd " + fd + " at the request of the handler."); } if (toRemove.remove(fd) != null) { if (LOG.isTraceEnabled()) { LOG.trace(this + ": " + caller + " : sendCallback processed fd " + fd + " in toRemove."); } } try { sock.refCount.unreferenceCheckClosed(); } catch (IOException e) { Preconditions.checkArgument(false, this + ": file descriptor " + sock.fd + " was closed while " + "still in the poll(2) loop."); } IOUtils.cleanup(LOG, sock); fdSet.remove(fd); return true; } else { if (LOG.isTraceEnabled()) { LOG.trace(this + ": " + caller + ": sendCallback not " + "closing fd " + fd); } return false; } } /** * Send callback, and if the domain socket was closed as a result of * processing, then also remove the entry for the file descriptor. * * @param caller reason for call * @param entries mapping of file descriptor to entry * @param fdSet set of file descriptors * @param fd file descriptor */ private void sendCallbackAndRemove(String caller, TreeMap<Integer, Entry> entries, FdSet fdSet, int fd) { if (sendCallback(caller, entries, fdSet, fd)) { entries.remove(fd); } } @VisibleForTesting final Thread watcherThread = new Thread(new Runnable() { @Override public void run() { if (LOG.isDebugEnabled()) { LOG.debug(this + ": starting with interruptCheckPeriodMs = " + interruptCheckPeriodMs); } final TreeMap<Integer, Entry> entries = new TreeMap<Integer, Entry>(); FdSet fdSet = new FdSet(); addNotificationSocket(entries, fdSet); try { while (true) { lock.lock(); try { for (int fd : fdSet.getAndClearReadableFds()) { sendCallbackAndRemove("getAndClearReadableFds", entries, fdSet, fd); } if (!(toAdd.isEmpty() && toRemove.isEmpty())) { // Handle pending additions (before pending removes). for (Iterator<Entry> iter = toAdd.iterator(); iter.hasNext();) { Entry entry = iter.next(); iter.remove(); DomainSocket sock = entry.getDomainSocket(); Entry prevEntry = entries.put(sock.fd, entry); Preconditions.checkState(prevEntry == null, this + ": tried to watch a file descriptor that we " + "were already watching: " + sock); if (LOG.isTraceEnabled()) { LOG.trace(this + ": adding fd " + sock.fd); } fdSet.add(sock.fd); } // Handle pending removals while (true) { Map.Entry<Integer, DomainSocket> entry = toRemove.firstEntry(); if (entry == null) break; sendCallbackAndRemove("handlePendingRemovals", entries, fdSet, entry.getValue().fd); } processedCond.signalAll(); } // Check if the thread should terminate. Doing this check now is // easier than at the beginning of the loop, since we know toAdd and // toRemove are now empty and processedCond has been notified if it // needed to be. if (closed) { if (LOG.isDebugEnabled()) { LOG.debug(toString() + " thread terminating."); } return; } // Check if someone sent our thread an InterruptedException while we // were waiting in poll(). if (Thread.interrupted()) { throw new InterruptedException(); } } finally { lock.unlock(); } doPoll0(interruptCheckPeriodMs, fdSet); } } catch (InterruptedException e) { LOG.info(toString() + " terminating on InterruptedException"); } catch (Throwable e) { LOG.error(toString() + " terminating on exception", e); } finally { lock.lock(); try { kick(); // allow the handler for notificationSockets[0] to read a byte for (Entry entry : entries.values()) { // We do not remove from entries as we iterate, because that can // cause a ConcurrentModificationException. sendCallback("close", entries, fdSet, entry.getDomainSocket().fd); } entries.clear(); fdSet.close(); closed = true; if (!(toAdd.isEmpty() && toRemove.isEmpty())) { // Items in toAdd might not be added to entries, handle it here for (Iterator<Entry> iter = toAdd.iterator(); iter.hasNext();) { Entry entry = iter.next(); entry.getDomainSocket().refCount.unreference(); entry.getHandler().handle(entry.getDomainSocket()); IOUtils.cleanup(LOG, entry.getDomainSocket()); iter.remove(); } // Items in toRemove might not be really removed, handle it here while (true) { Map.Entry<Integer, DomainSocket> entry = toRemove.firstEntry(); if (entry == null) break; sendCallback("close", entries, fdSet, entry.getValue().fd); } } processedCond.signalAll(); } finally { lock.unlock(); } } } }); private void addNotificationSocket(final TreeMap<Integer, Entry> entries, FdSet fdSet) { entries.put(notificationSockets[1].fd, new Entry(notificationSockets[1], new NotificationHandler())); try { notificationSockets[1].refCount.reference(); } catch (IOException e) { throw new RuntimeException(e); } fdSet.add(notificationSockets[1].fd); if (LOG.isTraceEnabled()) { LOG.trace(this + ": adding notificationSocket " + notificationSockets[1].fd + ", connected to " + notificationSockets[0].fd); } } public String toString() { return "DomainSocketWatcher(" + System.identityHashCode(this) + ")"; } private static native int doPoll0(int maxWaitMs, FdSet readFds) throws IOException; }