/*
* 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 com.sun.jini.jeri.internal.runtime;
import com.sun.jini.logging.Levels;
import com.sun.jini.thread.Executor;
import com.sun.jini.thread.GetThreadPoolAction;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.IllegalBlockingModeException;
import java.nio.channels.Pipe;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.security.AccessController;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A SelectionManager provides an event dispatching layer on top of the
* java.nio.Selector and java.nio.SelectableChannel abstractions; it manages
* one-shot registrations of interest in I/O readiness events and
* dispatching notifications of such events to registered callback objects.
*
* SelectionManager is designed to support multiple select/dispatch threads
* to allow for improved I/O concurrency on symmetric multiprocessor systems.
*
* If interest in a particular I/O event is (re-)registered while there is a
* blocking select operation in progress, that select operation must be woken
* up so that it can take the new interest into account; this wakeup is
* achieved by writing a byte to an internal pipe that is registered with the
* underlying selector.
*
* A current limitation of this API is that it does not allow an executing
* callback to yield control in such a way that it will be scheduled to
* execute again without another I/O readiness event occurring. Therefore,
* data that gets read during a callback must also get processed, unless such
* processing depends on more data that has not yet been read (like the
* remainder of a partial message).
*
* <p>This implementation uses the {@link Logger} named
* <code>com.sun.jini.jeri.internal.runtime.SelectionManager</code> to
* log information at the following levels:
*
* <p><table summary="Describes what is logged by SelectionManager at
* various logging levels" border=1 cellpadding=5>
*
* <tr> <th> Level <th> Description
*
* <tr> <td> {@link Levels#HANDLED HANDLED} <td> I/O exception caught
* from select operation
*
* </table>
*
* @author Sun Microsystems, Inc.
**/
public final class SelectionManager {
/** number of concurrent I/O processing threads */
private static final int concurrency = 1; // REMIND: get from property?
private static final Logger logger = Logger.getLogger(
"com.sun.jini.jeri.internal.runtime.SelectionManager");
/** pool of threads for executing tasks in system thread group */
private static final Executor systemThreadPool = (Executor)
AccessController.doPrivileged(new GetThreadPoolAction(false));
/** shared Selector used by this SelectionManager */
private final Selector selector;
/** internal pipe used to wake up a blocked select operation */
private final Pipe.SinkChannel wakeupPipeSink;
private final Pipe.SourceChannel wakeupPipeSource;
private final SelectionKey wakeupPipeKey;
private final ByteBuffer wakeupBuffer = ByteBuffer.allocate(2);
/** set of registered channels, to detect duplicate registrations */
private final Map registeredChannels =
Collections.synchronizedMap(new WeakHashMap());
/**
* lock guarding selectingThread, wakeupPending, renewQueue, readyQueue,
* renewMaskRef, and mutable state of all Key instances.
*/
private final Object lock = new Object();
/** thread with exclusive right to perform a select operation, if any */
private Thread selectingThread = null;
/** true if a wakeup has been requested but not yet processed */
private boolean wakeupPending = false;
/*
* The following two queues of Key objects are implemented as LIFO
* linked lists with internally threaded links for fast addition and
* removal of elements (no memory allocation overhead) and so that
* multiple entries for the same channel get implicitly combined.
* LIFO ordering is OK because the entire queue always gets drained
* and processed at once, and in between such processing, addition
* order is arbitrary anyway.
*/
/** queue of keys that need to have interest masks updated */
private Key renewQueue = null;
/** queue of keys that have I/O operations ready to be handled */
private Key readyQueue = null;
/** holder used for pass-by-reference invocations */
private final int[] renewMaskRef = new int[1];
/**
* Creates a new SelectionManager.
*
* REMIND: Is this necessary, or should we just provide access to
* a singleton instance?
*/
public SelectionManager() throws IOException {
// REMIND: create threads and other resources lazily?
selector = Selector.open();
Pipe pipe = Pipe.open();
wakeupPipeSink = pipe.sink();
wakeupPipeSource = pipe.source();
wakeupPipeSource.configureBlocking(false);
wakeupPipeKey = wakeupPipeSource.register(selector,
SelectionKey.OP_READ);
for (int i = 0; i < concurrency; i++) {
systemThreadPool.execute(new SelectLoop(),
"I/O SelectionManager-" + i);
}
// REMIND: How do these threads and other resources get cleaned up?
// REMIND: Should there be an explicit close method?
}
/**
* Registers the given SelectableChannel with this SelectionManager.
* After registration, the returned Key's renewInterestMask method may
* be used to register one-shot interest in particular I/O events.
*/
public Key register(SelectableChannel channel, SelectionHandler handler) {
if (registeredChannels.containsKey(channel)) {
throw new IllegalStateException("channel already registered");
}
Key key = new Key(channel, handler);
registeredChannels.put(channel, null);
return key;
}
/**
* SelectionHandler is the callback interface for an object that will
* process an I/O readiness event that has been detected by a
* SelectionManager.
*/
public interface SelectionHandler {
void handleSelection(int readyMask, Key key);
}
/**
* A Key represents a given SelectableChannel's registration with this
* SelectionManager. Externally, this object is used to re-register
* interest in I/O readiness events that have been previously detected
* and dispatched.
*/
public final class Key {
/** the channel that this Key represents a registration for */
final SelectableChannel channel;
/** the supplied callback object for dispatching I/O events */
final SelectionHandler handler;
// mutable instance state guarded by enclosing SelectionManager's lock:
/**
* the SelectionKey representing this Key's registration with the
* internal Selector, or null if it hasn't yet been registered
*/
SelectionKey selectionKey = null;
/** the current interest mask established with the SelectionKey */
int interestMask = 0;
boolean onRenewQueue = false; // invariant: == (renewMask != 0)
Key renewQueueNext = null; // null if !onRenewQueue
int renewMask = 0;
boolean onReadyQueue = false; // invariant: == (readyMask != 0)
Key readyQueueNext = null; // null if !onReadyQueue
int readyMask = 0;
/*
* other invariants:
*
* (renewMask & interestMask) == 0
* (interestMask & readyMask) == 0
* (renewMask & readyMask) == 0
*/
Key(SelectableChannel channel, SelectionHandler handler) {
this.channel = channel;
this.handler = handler;
}
/**
* Renews interest in receiving notifications when the I/O operations
* identified by the specified mask are ready for the associated
* SelectableChannel. The specified mask identifies I/O operations
* with the same bit values as would a java.nio.SelectionKey for the
* same SelectableChannel.
*
* Some time after one of the operations specified in the mask is
* detected to be ready, the previously-registered SelectionHandler
* callback object will be invoked to handle the readiness event.
*
* An event for each operation specified will only be dispatched to the
* callback handler once for the invocation of this method; to
* re-register interest in subsequent readiness of the same operation
* for the given channel, this method must be invoked again.
*/
public void renewInterestMask(int mask)
throws ClosedChannelException
{
if (!channel.isOpen()) {
throw new ClosedChannelException();
}
if ((mask & ~channel.validOps()) != 0) {
throw new IllegalArgumentException(
"invalid mask " + mask +
" (valid mask " + channel.validOps() + ")");
}
if (channel.isBlocking()) {
throw new IllegalBlockingModeException();
}
synchronized (lock) {
int delta = mask & ~(renewMask | interestMask | readyMask);
if (delta != 0) {
addOrUpdateRenewQueue(this, delta);
if (selectingThread != null && !wakeupPending) {
wakeupSelector();
wakeupPending = true;
}
}
}
}
}
/**
* SelectLoop provides the main loop for each I/O processing thread.
*/
private class SelectLoop implements Runnable {
private long lastExceptionTime = 0L; // local to select thread
private int recentExceptionCount; // local to select thread
public void run() {
int[] readyMaskRef = new int[1];
while (true) {
try {
Key readyKey = waitForReadyKey(readyMaskRef);
readyKey.handler.handleSelection(readyMaskRef[0],
readyKey);
} catch (Throwable t) {
try {
logger.log(Level.WARNING, "select loop throws", t);
} catch (Throwable tt) {
}
throttleLoopOnException();
}
}
}
/**
* Throttles the select loop after an exception has been
* caught: if a burst of 10 exceptions in 5 seconds occurs,
* then wait for 10 seconds to curb busy CPU usage.
**/
private void throttleLoopOnException() {
long now = System.currentTimeMillis();
if (lastExceptionTime == 0L || (now - lastExceptionTime) > 5000) {
// last exception was long ago (or this is the first)
lastExceptionTime = now;
recentExceptionCount = 0;
} else {
// exception burst window was started recently
if (++recentExceptionCount >= 10) {
try {
Thread.sleep(10000);
} catch (InterruptedException ignore) {
}
}
}
}
}
/**
* Waits until one of the registered channels is ready for one or more
* I/O operations. The Key for the ready channel is returned, and the
* first element of the supplied array is set to the mask of the
* channel's ready operations.
*
* If there is a ready channel available, then its key is returned.
* If another thread is already performing a select operation, then the
* current thread waits for that thread to complete and then begins
* again. Otherwise, the current thread assumes the responsibility of
* performing the next select operation.
*/
private Key waitForReadyKey(int[] readyMaskOut)
throws InterruptedException
{
assert !Thread.holdsLock(lock);
assert readyMaskOut != null && readyMaskOut.length == 1;
boolean needToClearSelectingThread = false;
Set selectedKeys = selector.selectedKeys();
try {
synchronized (lock) {
while (isReadyQueueEmpty() && selectingThread != null) {
lock.wait();
}
if (!isReadyQueueEmpty()) {
Key readyKey = removeFromReadyQueue(readyMaskOut);
lock.notify();
return readyKey;
}
assert selectingThread == null;
selectingThread = Thread.currentThread();
needToClearSelectingThread = true;
processRenewQueue();
} // wakeup allowed
while (true) {
try {
int n = selector.select();
if (Thread.interrupted()) {
throw new InterruptedException();
}
} catch (Error e) {
if (e.getMessage().startsWith("POLLNVAL")) {
Thread.yield();
continue; // work around 4458268
} else {
throw e;
}
} catch (CancelledKeyException e) {
continue; // work around 4458268
} catch (NullPointerException e) {
continue; // work around 4729342
} catch (IOException e) {
logger.log(Levels.HANDLED,
"thrown by select, continuing", e);
continue; // work around 4504001
}
synchronized (lock) {
if (wakeupPending &&
selectedKeys.contains(wakeupPipeKey))
{
drainWakeupPipe(); // clear wakeup state
wakeupPending = false;
selectedKeys.remove(wakeupPipeKey);
}
if (selectedKeys.isEmpty()) {
processRenewQueue();
continue;
}
selectingThread = null;
needToClearSelectingThread = false;
lock.notify();
Iterator iter = selectedKeys.iterator();
assert iter.hasNext(); // there must be at least one
while (iter.hasNext()) {
SelectionKey selectionKey = (SelectionKey) iter.next();
Key key = (Key) selectionKey.attachment();
int readyMask = 0;
try {
readyMask = selectionKey.readyOps();
assert readyMask != 0;
assert (key.interestMask & readyMask) == readyMask;
/*
* Remove interest in I/O events detected to be
* ready; interest must be renewed after each
* notification.
*/
int newInterestMask =
key.interestMask & ~readyMask;
assert key.interestMask ==
selectionKey.interestOps();
key.selectionKey.interestOps(newInterestMask);
key.interestMask = newInterestMask;
} catch (CancelledKeyException e) {
/*
* If channel is closed, then all interested events
* become considered ready immediately.
*/
readyMask |= key.interestMask;
key.interestMask = 0;
}
addOrUpdateReadyQueue(key, readyMask);
iter.remove();
}
return removeFromReadyQueue(readyMaskOut);
} // wakeup NOT allowed
}
} finally {
if (needToClearSelectingThread) {
synchronized (lock) {
if (wakeupPending &&
selectedKeys.contains(wakeupPipeKey))
{
drainWakeupPipe(); // clear wakeup state
wakeupPending = false;
selectedKeys.remove(wakeupPipeKey);
}
selectingThread = null;
needToClearSelectingThread = false;
lock.notify();
} // wakeup NOT allowed
}
}
}
private void wakeupSelector() {
assert Thread.holdsLock(lock);
assert wakeupPending == false;
wakeupBuffer.clear().limit(1);
try {
wakeupPipeSink.write(wakeupBuffer);
} catch (IOException e) {
// REMIND: what if thread was interrupted?
Error error = new AssertionError("unexpected I/O exception");
error.initCause(e);
throw error;
}
}
private void drainWakeupPipe() {
assert Thread.holdsLock(lock);
assert selectingThread != null;
do {
wakeupBuffer.clear();
try {
wakeupPipeSource.read(wakeupBuffer);
} catch (IOException e) {
// REMIND: what if thread was interrupted?
Error error = new AssertionError("unexpected I/O exception");
error.initCause(e);
throw error;
}
} while (!wakeupBuffer.hasRemaining());
}
/**
* In preparation for performing a select operation, process all new
* and renewed interest registrations so that current SelectionKey
* interest masks are up to date.
*
* This method must not be invoked while there is a select operation in
* progress (because otherwise it could block indefinitely); therefore,
* it must be invoked only by a thread that has the exclusive right to
* perform a select operation.
*/
private void processRenewQueue() {
assert Thread.holdsLock(lock);
assert selectingThread != null;
while (!isRenewQueueEmpty()) {
Key key = removeFromRenewQueue(renewMaskRef);
int renewMask = renewMaskRef[0];
assert renewMask != 0;
if (key.selectionKey == null) {
assert key.interestMask == 0 && key.readyMask == 0;
try {
key.selectionKey = key.channel.register(selector,
renewMask);
key.selectionKey.attach(key);
key.interestMask = renewMask;
} catch (ClosedChannelException e) {
addOrUpdateReadyQueue(key, renewMask);
} catch (IllegalBlockingModeException e) {
addOrUpdateReadyQueue(key, renewMask);
}
} else {
assert (key.interestMask & renewMask) == 0;
int newInterestMask = key.interestMask | renewMask;
try {
assert key.interestMask == key.selectionKey.interestOps();
key.selectionKey.interestOps(newInterestMask);
key.interestMask = newInterestMask;
} catch (CancelledKeyException e) {
addOrUpdateReadyQueue(key, newInterestMask);
key.interestMask = 0;
}
assert (key.interestMask & key.readyMask) == 0;
}
}
}
/*
* Queue manipulation utilities:
*/
private boolean isRenewQueueEmpty() {
assert Thread.holdsLock(lock);
return renewQueue == null;
}
private Key removeFromRenewQueue(int[] renewMaskOut) {
assert renewMaskOut != null && renewMaskOut.length == 1;
assert Thread.holdsLock(lock);
Key key = renewQueue;
assert key != null;
assert key.onRenewQueue;
assert key.renewMask != 0;
renewMaskOut[0] = key.renewMask;
key.renewMask = 0;
renewQueue = key.renewQueueNext;
key.renewQueueNext = null;
key.onRenewQueue = false;
return key;
}
private void addOrUpdateRenewQueue(Key key, int newRenewMask) {
assert newRenewMask != 0;
assert Thread.holdsLock(lock);
if (!key.onRenewQueue) {
assert key.renewMask == 0;
assert key.renewQueueNext == null;
key.renewMask = newRenewMask;
key.renewQueueNext = renewQueue;
renewQueue = key;
key.onRenewQueue = true;
} else {
assert key.renewMask != 0;
assert (key.renewMask & newRenewMask) == 0;
key.renewMask |= newRenewMask;
}
}
private boolean isReadyQueueEmpty() {
assert Thread.holdsLock(lock);
return readyQueue == null;
}
private Key removeFromReadyQueue(int[] readyMaskOut) {
assert readyMaskOut != null && readyMaskOut.length == 1;
assert Thread.holdsLock(lock);
Key key = readyQueue;
assert key != null;
assert key.onReadyQueue;
assert key.readyMask != 0;
readyMaskOut[0] = key.readyMask;
key.readyMask = 0;
readyQueue = key.readyQueueNext;
key.readyQueueNext = null;
key.onReadyQueue = false;
return key;
}
private void addOrUpdateReadyQueue(Key key, int newReadyMask) {
assert newReadyMask != 0;
assert Thread.holdsLock(lock);
if (!key.onReadyQueue) {
assert key.readyMask == 0;
assert key.readyQueueNext == null;
key.readyMask = newReadyMask;
key.readyQueueNext = readyQueue;
readyQueue = key;
key.onReadyQueue = true;
} else {
assert key.readyMask != 0;
assert (key.readyMask & newReadyMask) == 0;
key.readyMask |= newReadyMask;
}
}
}