/*
* Copyright 2007-2010 Sun Microsystems, Inc.
*
* This file is part of Project Darkstar Server.
*
* Project Darkstar Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation and
* distributed hereunder to you.
*
* Project Darkstar Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* --
*/
package com.sun.sgs.impl.nio;
import java.io.IOException;
import java.nio.channels.SelectableChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sun.sgs.nio.channels.AsynchronousChannelGroup;
import com.sun.sgs.nio.channels.ShutdownChannelGroupException;
/**
* A select-based AsynchronousChannelGroup.
* <p>
* This class is a container for a set of {@link Reactor}s which do the
* actual work of registering and dispatching asynchronous operations on
* channels. It provides:
* <ul>
* <li>Lifecycle support: creating and configuring the {@code Reactor} set,
* supporting {@linkplain AsynchronousChannelGroup#shutdown() graceful} and
* {@linkplain AsynchronousChannelGroup#shutdownNow() immediate shutdown},
* and {@linkplain AsynchronousChannelGroup#awaitTermination awaiting
* termination}.
* <li>Channel registration and load balancing across multiple
* {@code Reactor}s: channels are assigned to one of the reactors in
* the set. Since reactors are single-threaded, the reactor set allows
* multiple CPUs to be utilized by having multiple separate reactors.
* </ul>
* The default number of {@code Reactor}s is set as the number of
* {@linkplain Runtime#availableProcessors() available processors}, but it
* can be changed by setting the requested number in the system property
* {@value #REACTORS_PROPERTY}.
*/
class ReactiveChannelGroup
extends AsyncGroupImpl
{
/** The logger for this class. */
static final Logger log =
Logger.getLogger(ReactiveChannelGroup.class.getName());
/**
* Lock held on updates to lifecycleState and reactors list,
* and the condition variable for awaiting group termination.
*/
final Object stateLock = new Object();
/**
* The lifecycle state of this group. Increases monotonically.
* It may only be accessed with stateLock held.
*/
protected int lifecycleState = RUNNING;
/** State: open and running */
protected static final int RUNNING = 0;
/** State: graceful shutdown in progress */
protected static final int SHUTDOWN = 1;
/** State: forced shutdown in progress */
protected static final int SHUTDOWN_NOW = 2;
/** State: terminated */
protected static final int DONE = 3;
/**
* The active {@linkplain Reactor reactors} in this group.
* It may only be accessed with stateLock held.
*/
final List<Reactor> reactors;
/**
* The property to specify the number of reactors to be used by
* channel groups: {@value}
*/
public static final String REACTORS_PROPERTY =
"com.sun.sgs.nio.async.reactive.reactors";
/**
* The default number of reactors to be used by channel groups:
* {@code Runtime.getRuntime().availableProcessors()}
*/
public static final int DEFAULT_REACTORS =
Runtime.getRuntime().availableProcessors();
/** The reactor load-balance strategy. */
final ReactorAssignmentStrategy reactorAssignmentStrategy;
/**
* Creates a new group with the default number of reactors.
*
* @param provider the provider that created this group
* @param executor the executor for this group
*
* @throws IOException if an I/O error occurs
*/
ReactiveChannelGroup(ReactiveAsyncChannelProvider provider,
ExecutorService executor)
throws IOException
{
this(provider, executor, 0);
}
/**
* Creates a new group with the requested number of reactors. If {code
* 0} reactors are requested, a default is chosen as the number of
* {@link Runtime#availableProcessors() available processors}.
*
* @param provider the provider that created this group
* @param executor the executor for this group
* @param requestedReactors the number of reactors to create in this
* group, or {@code 0} to use the default
*
* @throws IllegalArgumentException if a negative number of reactors is
* requested
* @throws IOException if an I/O error occurs
*/
ReactiveChannelGroup(ReactiveAsyncChannelProvider provider,
ExecutorService executor,
int requestedReactors)
throws IOException
{
super(provider, executor);
int n = requestedReactors;
// TODO determine how security model interacts with properties needed
// for group creation
if (n == 0) {
try {
n = Integer.valueOf(System.getProperty(REACTORS_PROPERTY));
} catch (NumberFormatException e) {
n = DEFAULT_REACTORS;
}
}
if (n <= 0) {
throw new IllegalArgumentException("non-positive reactor count");
}
reactorAssignmentStrategy = new HashingReactorAssignmentStrategy();
reactors = new ArrayList<Reactor>(n);
// TODO it might be interesting to provide each Reactor with its
// own private executor (perhaps using threads from this group's
// executor).
for (int i = 0; i < n; ++i) {
reactors.add(new Reactor(this, executor()));
}
for (Reactor reactor : reactors) {
// Use the reactor's executor, in case we've set up
// per-reactor executors.
reactor.executor.execute(new Worker(reactor));
}
}
/**
* {@inheritDoc}
*/
AsyncKey register(SelectableChannel ch) throws IOException {
ch.configureBlocking(false);
AsyncKey asyncKey = null;
Reactor reactor = null;
synchronized (stateLock) {
if (lifecycleState != RUNNING) {
throw new ShutdownChannelGroupException();
}
reactor = reactorAssignmentStrategy.getReactorFor(ch);
}
try {
asyncKey = reactor.register(ch);
return asyncKey;
} finally {
if (asyncKey == null) {
try {
ch.close();
} catch (IOException ignore) { }
}
}
}
/**
* Interface for {@code Reactor} load balancing strategies.
*/
interface ReactorAssignmentStrategy {
/**
* Returns the {@code Reactor} for a newly-registering channel.
*
* @param channel a channel to assign to a {@code Reactor}
* @return the {@code Reactor} to use for the channel
*/
Reactor getReactorFor(SelectableChannel channel);
}
/**
* A {@code Reactor} load balancing strategy that chooses a reactor
* based on the {@linkplain Object#hashCode() hash} of the channel.
*/
final class HashingReactorAssignmentStrategy
implements ReactorAssignmentStrategy
{
/**
* {@inheritDoc}
* <p>
* This implementation chooses the reactor based on the hash of the
* channel.
*/
public Reactor getReactorFor(SelectableChannel channel) {
return reactors.get(
Math.abs(channel.hashCode() % reactors.size()));
}
}
/**
* Worker to run a reactor and check termination when a reactor completes.
*/
class Worker implements Runnable {
/** This worker's reactor. */
private final Reactor reactor;
/**
* Creates a worker instance for the reactor.
*
* @param reactor the reactor to run
*/
Worker(Reactor reactor) {
this.reactor = reactor;
}
/**
* {@inheritDoc}
*/
public void run() {
Throwable exception = null;
// TODO experiment with looping versus re-executing the
// task in the reactor's executor. Requires some care to
// handle termination correctly.
try {
for (;; ) {
boolean keepGoing = reactor.performWork();
if (!keepGoing) {
break;
}
}
} catch (IOException t) {
exception = t;
} catch (RuntimeException t) {
exception = t;
} catch (Error t) {
exception = t;
}
synchronized (stateLock) {
reactors.remove(reactor);
tryTerminate();
}
try {
// Make sure the reactor has shutdown
reactor.shutdownNow();
} catch (IOException e) {
log.log(Level.WARNING, "exception closing reactor", e);
}
if (exception != null) {
log.log(Level.SEVERE, "reactor exception", exception);
if (exception instanceof Error) {
throw (Error) exception;
} else if (exception instanceof RuntimeException) {
throw (RuntimeException) exception;
} else if (exception instanceof IOException) {
throw new RuntimeException(
exception.getMessage(), exception);
} else {
throw Util.unexpected(exception);
}
}
}
}
/* Termination support. */
/**
* {@inheritDoc}
*/
@Override
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException
{
long millis = unit.toMillis(timeout);
final long deadline = System.currentTimeMillis() + millis;
synchronized (stateLock) {
for (;; ) {
if (lifecycleState == DONE) {
return true;
}
if (millis <= 0) {
return false;
}
stateLock.wait(millis);
millis = deadline - System.currentTimeMillis();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isShutdown() {
synchronized (stateLock) {
return lifecycleState != RUNNING;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isTerminated() {
synchronized (stateLock) {
return lifecycleState == DONE;
}
}
/**
* {@inheritDoc}
*/
@Override
public ReactiveChannelGroup shutdown() {
synchronized (stateLock) {
if (lifecycleState < SHUTDOWN) {
lifecycleState = SHUTDOWN;
for (Reactor reactor : reactors) {
reactor.shutdown();
}
tryTerminate();
}
return this;
}
}
/**
* {@inheritDoc}
*/
@Override
public ReactiveChannelGroup shutdownNow() throws IOException {
Throwable exception = null;
synchronized (stateLock) {
if (lifecycleState < SHUTDOWN_NOW) {
lifecycleState = SHUTDOWN_NOW;
for (Reactor reactor : reactors) {
try {
reactor.shutdownNow();
} catch (Exception e) {
exception = e;
}
}
tryTerminate();
}
if (exception != null) {
if (exception instanceof RuntimeException) {
throw (RuntimeException) exception;
} else if (exception instanceof IOException) {
throw (IOException) exception;
} else {
throw Util.unexpected(exception);
}
}
return this;
}
}
/**
* If the group is trying to shutdown, check that all the reactors
* have shutdown. If they have, mark this group as done and wake
* anyone blocked on awaitTermination.
*
* NOTE: Must be called with {@code stateLock} held.
*/
private void tryTerminate() {
assert Thread.holdsLock(stateLock);
if (lifecycleState == RUNNING || lifecycleState == DONE) {
return;
}
if (reactors.isEmpty()) {
lifecycleState = DONE;
stateLock.notifyAll();
}
}
}