/*
* Copyright 2008-2010 Brian S O'Neill
*
* Licensed 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.cojen.dirmi;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import org.cojen.dirmi.core.StandardSession;
import org.cojen.dirmi.core.StandardSessionAcceptor;
import org.cojen.dirmi.io.BasicChannelBrokerAcceptor;
import org.cojen.dirmi.io.BasicChannelBrokerConnector;
import org.cojen.dirmi.io.BufferedSocketChannelAcceptor;
import org.cojen.dirmi.io.BufferedSocketChannelConnector;
import org.cojen.dirmi.io.ChannelAcceptor;
import org.cojen.dirmi.io.ChannelBroker;
import org.cojen.dirmi.io.ChannelBrokerAcceptor;
import org.cojen.dirmi.io.ChannelBrokerConnector;
import org.cojen.dirmi.io.ChannelConnector;
import org.cojen.dirmi.io.IOExecutor;
import org.cojen.dirmi.io.PipedChannelBroker;
import org.cojen.dirmi.io.RecyclableSocketChannelAcceptor;
import org.cojen.dirmi.io.RecyclableSocketChannelConnector;
import org.cojen.dirmi.io.RecyclableSocketChannelSelector;
import org.cojen.dirmi.io.SocketChannelSelector;
import org.cojen.dirmi.util.Cache;
import org.cojen.dirmi.util.ThreadPool;
import org.cojen.dirmi.util.Timer;
/**
* Sharable environment for connecting and accepting remote sessions. All
* sessions established from the same environment instance share an executor.
*
* @author Brian S O'Neill
*/
public class Environment implements Closeable {
private static final boolean RECYCLABLE_SOCKETS;
static {
boolean recyclableSockets = true;
try {
String prop = System.getProperty("org.cojen.dirmi.Environment.recyclableSockets");
if (prop != null && prop.equalsIgnoreCase("false")) {
recyclableSockets = false;
}
} catch (SecurityException e) {
}
RECYCLABLE_SOCKETS = recyclableSockets;
}
private final ScheduledExecutorService mExecutor;
private final IOExecutor mIOExecutor;
private final Cache<Closeable, Object> mCloseableSet;
private final AtomicBoolean mClosed;
private final boolean mRecyclableSockets = RECYCLABLE_SOCKETS;
private final SocketFactory mSocketFactory;
private final ServerSocketFactory mServerSocketFactory;
private final RecyclableSocketChannelSelector mSelector;
private final ClassLoader mClassLoader;
/**
* Construct environment which uses up to 1000 threads.
*/
public Environment() {
this(1000);
}
/**
* Construct environment with the given maximum number of threads.
*
* @param maxThreads maximum number of threads in pool
*/
public Environment(int maxThreads) {
this(maxThreads, null, null);
}
/**
* Construct environment with the given maximum number of threads, thread
* name prefix, and uncaught exception handler.
*
* @param maxThreads maximum number of threads in pool
* @param threadNamePrefix prefix given to thread name; pass null for default
* @param handler handler for uncaught exceptions; pass null for default
*/
public Environment(int maxThreads, String threadNamePrefix,
Thread.UncaughtExceptionHandler handler)
{
this(new ThreadPool(maxThreads,
false,
threadNamePrefix == null ? "dirmi" : threadNamePrefix,
handler));
}
/**
* Construct environment with a custom executor.
*
* @see ThreadPool
*/
public Environment(ScheduledExecutorService executor) {
this(executor, null, null, null, null, null, null, null);
}
private Environment(ScheduledExecutorService executor,
IOExecutor ioExecutor,
Cache<Closeable, Object> closeable,
AtomicBoolean closed,
SocketFactory sf,
ServerSocketFactory ssf,
RecyclableSocketChannelSelector selector,
ClassLoader classLoader)
{
if (executor == null) {
throw new IllegalArgumentException("Must provide an executor");
}
mExecutor = executor;
mIOExecutor = ioExecutor == null ? new IOExecutor(executor) : ioExecutor;
if (closeable == null) {
closeable = Cache.newWeakIdentityCache(17);
}
mCloseableSet = closeable;
mClosed = closed == null ? new AtomicBoolean(false) : closed;
mSocketFactory = sf;
mServerSocketFactory = ssf;
mSelector = selector;
mClassLoader = classLoader;
}
/**
* Returns an environment instance which connects using the given client
* socket factory. The returned environment is linked to this one, and
* closing either environment closes both.
*
* @throws IllegalStateException if environment uses a selector
*/
public Environment withClientSocketFactory(SocketFactory sf) {
if (mSelector != null) {
throw new IllegalStateException("Cannot combine socket factory and selector");
}
return new Environment(mExecutor, mIOExecutor, mCloseableSet, mClosed,
sf, mServerSocketFactory, null, mClassLoader);
}
/**
* Returns an environment instance which accepts using the given server
* socket factory. The returned environment is linked to this one, and
* closing either environment closes both.
*
* @throws IllegalStateException if environment uses a selector
*/
public Environment withServerSocketFactory(ServerSocketFactory ssf) {
if (mSelector != null) {
throw new IllegalStateException("Cannot combine socket factory and selector");
}
return new Environment(mExecutor, mIOExecutor, mCloseableSet, mClosed,
mSocketFactory, ssf, null, mClassLoader);
}
/**
* Returns an environment instance which uses selectable sockets, reducing
* the amount of idle threads. The returned environment is linked to this
* one, and closing either environment closes both.
*
* <p>Overall performance is lower with selectable sockets, but scalability
* is improved. Remote call overhead is about 2 to 4 times higher, which is
* largely due to the inefficient design of the nio library.
*
* @throws IllegalStateException if environment uses a socket factory
*/
public Environment withSocketSelector() throws IOException {
if (mSocketFactory != null || mServerSocketFactory != null) {
throw new IllegalStateException("Cannot combine socket factory and selector");
}
if (!mRecyclableSockets) {
throw new IllegalStateException("Cannot use unrecyclable sockets with selector");
}
// Don't allow if using buggy version.
check: if ("Linux".equals(System.getProperty("os.name")) &&
"Sun Microsystems Inc.".equals(System.getProperty("java.vendor")) &&
"1.6".equals(System.getProperty("java.specification.version")))
{
String version = System.getProperty("java.version");
int index = version.indexOf('_');
if (index > 0) {
int subVersion;
try {
subVersion = Integer.parseInt(version.substring(index + 1));
} catch (RuntimeException e) {
break check;
}
if (subVersion < 18) {
// select throws "File exists" IOException under load (lnx)
throw new IOException
("Java version doesn't have fix for bug 6693490: " + version);
}
}
}
final RecyclableSocketChannelSelector selector =
new RecyclableSocketChannelSelector(mIOExecutor);
addToClosableSet(selector);
mIOExecutor.execute(new Runnable() {
public void run() {
try {
selector.selectLoop();
} catch (IOException e) {
org.cojen.util.ThrowUnchecked.fire(e);
}
}
});
return new Environment(mExecutor, mIOExecutor, mCloseableSet, mClosed,
null, null, selector, mClassLoader);
}
/**
* Returns an environment instance which uses the provided classloader
* for all established sessions.
*
* The returned environment is linked to this one,
* and closing either environment closes both.
*/
public Environment withClassLoader(ClassLoader classLoader) {
return new Environment(mExecutor, mIOExecutor, mCloseableSet, mClosed,
mSocketFactory, mServerSocketFactory, mSelector, mClassLoader);
}
/**
* Returns a session connector for a remote endpoint. Call {@link
* SessionConnector#connect connect} to immediately establish a session.
*
* @param host required name of remote endpoint
* @param port remote port
*/
public SessionConnector newSessionConnector(String host, int port) {
return newSessionConnector(new InetSocketAddress(host, port));
}
/**
* Returns a session connector for a remote endpoint. Call {@link
* SessionConnector#connect connect} to immediately establish a session.
*
* @param remoteAddress required address of remote endpoint
*/
public SessionConnector newSessionConnector(SocketAddress remoteAddress) {
return newSessionConnector(remoteAddress, null);
}
/**
* Returns a session connector for a remote endpoint. Call {@link
* SessionConnector#connect connect} to immediately establish a session.
*
* @param remoteAddress required address of remote endpoint
* @param localAddress optional address of local bindpoint
*/
public SessionConnector newSessionConnector(SocketAddress remoteAddress,
SocketAddress localAddress)
{
return new SocketConnector(remoteAddress, localAddress);
}
/**
* Returns an acceptor of sessions. Call {@link SessionAcceptor#acceptAll
* acceptAll} to start automatically accepting sessions.
*
* @param port port for accepting socket connections; pass zero to choose
* any available port
*/
public SessionAcceptor newSessionAcceptor(int port) throws IOException {
return newSessionAcceptor(new InetSocketAddress(port));
}
/**
* Returns an acceptor of sessions. Call {@link SessionAcceptor#acceptAll
* acceptAll} to start automatically accepting sessions.
*
* @param localAddress address for accepting socket connections; use null to
* automatically select a local address and any available port
*/
public SessionAcceptor newSessionAcceptor(SocketAddress localAddress) throws IOException {
return StandardSessionAcceptor.create(this, newBrokerAcceptor(localAddress));
}
/**
* Returns an acceptor used for asynchronously accepting brokers. Sessions
* can be created from brokers by calling the {@link #newSession(Broker,
* Object)} method.
*
* @param localAddress address for accepting socket connections; use null to
* automatically select a local address and any available port
* @return an acceptor of brokers
*/
private ChannelBrokerAcceptor newBrokerAcceptor(SocketAddress localAddress)
throws IOException
{
checkClosed();
ChannelAcceptor channelAcceptor = newChannelAcceptor(localAddress);
ChannelBrokerAcceptor brokerAcceptor =
new BasicChannelBrokerAcceptor(mIOExecutor, channelAcceptor);
addToClosableSet(brokerAcceptor);
return brokerAcceptor;
}
/**
* Attempts to connect using given broker, blocking until session is
* established. Only one session can be created per broker instance.
*
* @param broker required broker for establishing connections; must always
* connect to same remote endpoint
*/
public Session newSession(ChannelBroker broker) throws IOException {
checkClosed();
try {
Session session = StandardSession.create(mIOExecutor, broker);
addToClosableSet(session);
if (mClassLoader != null) {
session.setClassLoader(mClassLoader);
}
return session;
} catch (IOException e) {
broker.close();
throw e;
}
}
/**
* Attempts to connect using given broker, blocking until session is
* established. Only one session can be created per broker instance.
*
* @param broker required broker for establishing connections; must always
* connect to same remote endpoint
* @throws RemoteTimeoutException
*/
public Session newSession(ChannelBroker broker, long timeout, TimeUnit unit)
throws IOException
{
return timeout < 0 ? newSession(broker) : newSession(broker, new Timer(timeout, unit));
}
/**
* Attempts to connect using given broker, blocking until session is
* established. Only one session can be created per broker instance.
*
* @param broker required broker for establishing connections; must always
* connect to same remote endpoint
* @throws RemoteTimeoutException
*/
Session newSession(ChannelBroker broker, Timer timer) throws IOException {
checkClosed();
try {
Session session = StandardSession.create(mIOExecutor, broker, timer);
addToClosableSet(session);
if (mClassLoader != null) {
session.setClassLoader(mClassLoader);
}
return session;
} catch (IOException e) {
broker.close();
throw e;
}
}
/**
* Returns two locally connected sessions.
*
* @return two Session objects connected to each other
* @throws RejectedException if thread pool is full or shutdown
*/
public Session[] newSessionPair() throws RejectedException {
final ChannelBroker[] brokers = PipedChannelBroker.newPair(mIOExecutor);
class Create implements Runnable {
private IOException mException;
private Session mSession;
public synchronized void run() {
try {
mSession = newSession(brokers[0]);
} catch (IOException e) {
mException = e;
}
notifyAll();
}
public synchronized Session waitForSession() throws IOException {
while (mException == null && mSession == null) {
try {
wait();
} catch (InterruptedException e) {
// Ignore.
}
}
if (mException != null) {
throw mException;
}
return mSession;
}
}
Create create = new Create();
mIOExecutor.execute(create);
final Session session_0, session_1;
try {
session_0 = newSession(brokers[1]);
session_1 = create.waitForSession();
} catch (IOException e) {
throw new AssertionError(e);
}
return new Session[] {session_0, session_1};
}
/**
* Returns the executor used by this environment.
*/
public ScheduledExecutorService executor() {
return mExecutor;
}
/**
* Closes all existing sessions and then shuts down the thread pool. New
* sessions cannot be established.
*/
public void close() throws IOException {
boolean wasClosed = mClosed.getAndSet(true);
IOException exception = null;
// Copy to avoid holding lock during close.
List<Closeable> closeable;
synchronized (mCloseableSet) {
closeable = new ArrayList<Closeable>(mCloseableSet.size());
mCloseableSet.copyKeysInto(closeable);
mCloseableSet.clear();
}
for (int i=1; i<=3; i++) {
for (Closeable c : closeable) {
// Close sessions before brokers to avoid blocking. Close
// selectors last, after all sockets are closed.
if ((i == 1) == (c instanceof Session) &&
(i == 3) == (c instanceof SocketChannelSelector))
{
try {
c.close();
} catch (IOException e) {
if (exception == null) {
exception = e;
}
}
}
}
}
if (!wasClosed) {
// Calling shutdownNow is dangerous, since the effect of
// interrupting active threads is undefined. Users may access the
// executor directly and call shutdownNow on it.
mExecutor.shutdown();
}
if (exception != null) {
throw exception;
}
}
void checkClosed() throws IOException {
if (mClosed.get()) {
throw new IOException("Environment is closed");
}
}
void addToClosableSet(Closeable c) throws IOException {
try {
synchronized (mCloseableSet) {
checkClosed();
mCloseableSet.put(c, "");
}
} catch (IOException e) {
try {
c.close();
} catch (IOException e2) {
// Ignore.
}
throw e;
}
}
ChannelAcceptor newChannelAcceptor(SocketAddress localAddress) throws IOException {
RecyclableSocketChannelSelector selector = mSelector;
if (selector != null) {
return selector.newChannelAcceptor(localAddress);
}
ServerSocketFactory ssf = mServerSocketFactory;
if (ssf == null) {
ssf = ServerSocketFactory.getDefault();
}
ServerSocket ss = ssf.createServerSocket();
if (mRecyclableSockets) {
return new RecyclableSocketChannelAcceptor(mIOExecutor, localAddress, ss);
} else {
return new BufferedSocketChannelAcceptor(mIOExecutor, localAddress, ss);
}
}
ChannelConnector newChannelConnector(SocketAddress remoteAddress, SocketAddress localAddress) {
RecyclableSocketChannelSelector selector = mSelector;
if (selector != null) {
return selector.newChannelConnector(remoteAddress, localAddress);
}
SocketFactory sf = mSocketFactory;
if (sf == null) {
sf = SocketFactory.getDefault();
}
if (mRecyclableSockets) {
return new RecyclableSocketChannelConnector
(mIOExecutor, remoteAddress, localAddress, sf);
} else {
return new BufferedSocketChannelConnector
(mIOExecutor, remoteAddress, localAddress, sf);
}
}
private class SocketConnector implements SessionConnector {
private final ChannelConnector mChannelConnector;
private final ChannelBrokerConnector mBrokerConnector;
SocketConnector(SocketAddress remoteAddress, SocketAddress localAddress) {
if (remoteAddress == null) {
throw new IllegalArgumentException("Must provide a remote address");
}
mChannelConnector = newChannelConnector(remoteAddress, localAddress);
mBrokerConnector = new BasicChannelBrokerConnector(mIOExecutor, mChannelConnector);
}
public Session connect() throws IOException {
checkClosed();
ChannelBroker broker = mBrokerConnector.connect();
addToClosableSet(broker);
try {
return newSession(broker);
} catch (IOException e) {
broker.close();
throw e;
}
}
public Session connect(long timeout, TimeUnit unit) throws IOException {
if (timeout < 0) {
return connect();
}
checkClosed();
Timer timer = new Timer(timeout, unit);
ChannelBroker broker = mBrokerConnector.connect(timer);
addToClosableSet(broker);
try {
return newSession(broker, timer);
} catch (IOException e) {
broker.close();
throw e;
}
}
public Object getRemoteAddress() {
return mChannelConnector.getRemoteAddress();
}
public Object getLocalAddress() {
return mChannelConnector.getLocalAddress();
}
@Override
public String toString() {
String str = "SessionConnector {remoteAddress=" + getRemoteAddress();
Object localAddress = getLocalAddress();
if (localAddress != null) {
str += ", localAddress=" + localAddress;
}
return str + '}';
}
}
}