/*
* 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 net.jini.jeri.connection;
import com.sun.jini.action.GetLongAction;
import com.sun.jini.jeri.internal.mux.MuxClient;
import com.sun.jini.logging.Levels;
import com.sun.jini.thread.Executor;
import com.sun.jini.thread.GetThreadPoolAction;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.jeri.Endpoint;
import net.jini.jeri.OutboundRequest;
import net.jini.jeri.OutboundRequestIterator;
/**
* Provides client-side connection management using the <a
* href="{@docRoot}/net/jini/jeri/connection/doc-files/mux.html">Jini
* extensible remote invocation (Jini ERI) multiplexing protocol</a>
* to frame and multiplex requests and responses over connections.
*
* <p>A <code>ConnectionManager</code> is created by a
* connection-based {@link Endpoint} implemention to manage
* connections to a particular {@link ConnectionEndpoint}. The {@link
* #newRequest newRequest} method is used to send a request to the
* connection endpoint.
*
* <p>Each request attempt is mapped to a new <i>session</i> of the
* Jini ERI multiplexing protocol on an established connection chosen
* by the <code>ConnectionEndpoint</code>. Request data is written as
* the data sent for the session, and response data is read as the
* data recdeived for the session.
*
* @author Sun Microsystems, Inc.
* @since 2.0
*
* @com.sun.jini.impl
*
* This implementation uses the {@link Logger} named
* <code>net.jini.jeri.connection.ConnectionManager</code> to log
* information at the following levels:
*
* <p><table summary="Describes what is logged by ConnectionManager to
* its logger at various logging levels" border=1 cellpadding=5>
*
* <tr> <th> Level <th> Description
*
* <tr> <td> {@link Level#FINEST FINEST} <td> connection opened or
* reused
*
* </table>
*
* <p>This implementation uses the {@link Logger} named
* <code>net.jini.jeri.connection.mux</code> to log information at the
* following levels:
*
* <p><table summary="Describes what is logged by ConnectionManager to
* the mux logger at various logging levels" border=1 cellpadding=5>
*
* <tr> <th> Level <th> Description
*
* <tr> <td> {@link Level#WARNING WARNING} <td> unexpected exception
* during asynchronous I/O processing, or thread creation failure
*
* <tr> <td> {@link Levels#HANDLED HANDLED} <td> I/O exception during
* asynchronous I/O processing
*
* <tr> <td> {@link Level#FINEST FINEST} <td> detailed implementation
* activity
*
* </table>
*
* <p>This implementation recognizes the following system properties:
*
* <p><ul>
*
* <li><code>com.sun.jini.jeri.connectionTimeout</code> - Time in
* milliseconds to leave idle client-side connections around before
* closing them. The default value is 15000 milliseconds (15 seconds).
*
* </ul>
**/
public final class ConnectionManager {
/**
* How long to leave idle muxes around before closing them.
*/
private static final long TIMEOUT =
((Long) AccessController.doPrivileged(new GetLongAction(
"com.sun.jini.jeri.connectionTimeout",
15000))).longValue();
/**
* ConnectionManager logger.
*/
private static final Logger logger =
Logger.getLogger("net.jini.jeri.connection.ConnectionManager");
/**
* Executor that executes tasks in pooled system threads.
*/
private static final Executor systemThreadPool =
(Executor) AccessController.doPrivileged(
new GetThreadPoolAction(false));
/**
* The endpoint.
*/
private final ConnectionEndpoint ep;
/**
* The OutboundMuxes.
*/
private final List muxes = new ArrayList(1);
/**
* The active muxes (during connect).
*/
private final List active = new ArrayList(1);
/**
* Unmodifiable view of active.
*/
private final Collection roactive =
Collections.unmodifiableCollection(active);
/**
* The idle muxes (during connect).
*/
private final List idle = new ArrayList(1);
/**
* Unmodifiable view of idle.
*/
private final Collection roidle =
Collections.unmodifiableCollection(idle);
/**
* Number of pending connect calls.
*/
private int pendingConnects = 0; // REMIND: no longer necessary?
private Reaper reaper = null; // non-null if reaper running
/**
* Creates a new <code>ConnectionManager</code> that manages
* client-side connections to the specified connection endpoint.
*
* @param ep the connection endpoint
**/
public ConnectionManager(ConnectionEndpoint ep) {
this.ep = ep;
}
/**
* Registers a pending connect call.
*/
synchronized void connectPending() {
pendingConnects++;
}
/**
* Calls connect on the connection endpoint with the active and idle
* muxes and the specified handle. If no connection is returned, calls
* connect with the handle. In either case, if a new connection is
* returned, creates and adds a mux for it. In all cases, bumps the
* newRequest count for the mux and returns it. Removes any dead muxes
* along the way.
*/
OutboundMux connect(OutboundRequestHandle handle) throws IOException {
try {
synchronized (this) {
active.clear();
idle.clear();
for (int i = muxes.size(); --i >= 0; ) {
OutboundMux mux = (OutboundMux) muxes.get(i);
try {
int n = mux.requestsInProgress();
if (n == 0) {
idle.add(mux.getConnection());
} else if (n < OutboundMux.MAX_REQUESTS) {
active.add(mux.getConnection());
}
} catch (IOException e) {
muxes.remove(i);
}
}
Connection c = ep.connect(handle, roactive, roidle);
if (c != null) {
for (int i = muxes.size(); --i >= 0; ) {
OutboundMux mux = (OutboundMux) muxes.get(i);
if (c == mux.getConnection()) {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "using {0}", c);
}
mux.newRequestPending();
return mux;
}
}
OutboundMux mux = OutboundMux.create(c);
mux.newRequestPending();
if (reaper == null) {
reaper = new Reaper();
systemThreadPool.execute(
reaper, "ConnectionManager[" + ep + "].Reaper");
}
muxes.add(mux);
return mux;
}
}
Connection c = ep.connect(handle);
OutboundMux mux = OutboundMux.create(c);
mux.newRequestPending();
synchronized (this) {
if (reaper == null) {
reaper = new Reaper();
systemThreadPool.execute(
reaper, "ConnectionManager[" + ep + "].Reaper");
}
muxes.add(mux);
}
return mux;
} finally {
synchronized (this) {
assert pendingConnects > 0;
pendingConnects--;
}
}
}
/**
* For each mux, calls checkIdle on the mux, and if checkIdle returns
* true, removes the mux and adds it to the idle list. Returns true
* if no connects are pending and no muxes remain.
*/
synchronized boolean checkIdle(long now, List idle) {
for (int i = muxes.size(); --i >= 0; ) {
OutboundMux mux = (OutboundMux) muxes.get(i);
if (mux.checkIdle(now)) {
muxes.remove(i);
idle.add(mux);
}
}
return pendingConnects == 0 && muxes.isEmpty();
}
/**
* Removes and shuts down a mux.
*/
void remove(OutboundMux mux) {
synchronized (this) {
muxes.remove(mux);
}
mux.shutdown("writeRequestData failed");
}
/**
* Subclass wrapper around MuxClient for outbound connections.
*/
private static final class OutboundMux extends MuxClient {
/**
* The outbound connection.
*/
private final Connection c;
/**
* True if the mux needs to be started.
*/
private boolean pendingStart = true;
/**
* Number of pending newRequest calls.
*/
private int pendingNewRequests = 0;
/**
* The time this mux was found to be idle by the Reaper thread. Set
* to zero each time a request is initiated.
*/
private long idleTime = 0;
/**
* Constructs an instance from the connection's streams.
*/
private OutboundMux(Connection c) throws IOException {
super(c.getOutputStream(), c.getInputStream());
this.c = c;
}
/**
* Constructs an instance from the connection's channel.
*/
private OutboundMux(Connection c, boolean ignore) throws IOException {
super(c.getChannel());
this.c = c;
}
/**
* Constructs an instance from the connection.
*/
static OutboundMux create(Connection c) throws IOException {
logger.log(Level.FINEST, "opened {0}", c);
OutboundMux mux = null;
try {
mux = (c.getChannel() == null) ?
new OutboundMux(c) : new OutboundMux(c, true);
} finally {
if (mux == null) {
try {
c.close();
} catch (IOException e) {
}
}
}
return mux;
}
/**
* Returns the outbound connection.
*/
Connection getConnection() {
return c;
}
/**
* Registers a pending newRequest call.
*/
synchronized void newRequestPending() {
pendingNewRequests++;
}
/**
* Initiates a new request on the mux and returns it, and sets the
* idle time to zero. Starts the mux if necessary, and decrements
* the pending newRequest count.
*/
public synchronized OutboundRequest newRequest() throws IOException {
assert pendingNewRequests > 0;
pendingNewRequests--;
if (pendingStart) {
pendingStart = false;
start();
}
idleTime = 0;
return super.newRequest();
}
/**
* Returns the number of active and pending requests.
*/
public synchronized int requestsInProgress() throws IOException {
return super.requestsInProgress() + pendingNewRequests;
}
/**
* Returns true if the mux is dead, or the mux is idle and the
* recorded idle time is more than TIMEOUT milliseconds before now,
* and returns false otherwise. If the mux is idle and the recorded
* idle time is zero, sets the recorded idle time to now.
*/
synchronized boolean checkIdle(long now) {
try {
if (requestsInProgress() == 0) {
if (idleTime == 0) {
idleTime = now;
} else {
return now - idleTime > TIMEOUT;
}
}
return false;
} catch (IOException e) {
return true;
}
}
/**
* Close the connection, so that the provider is notified.
*/
protected void handleDown() {
try {
c.close();
} catch (IOException e) {
}
}
boolean shouldRetry() {
return false; // XXX
}
}
/**
* Outbound request wrapper around the outbound request created by the mux.
*/
private static final class Outbound implements OutboundRequest {
/**
* The outbound request created by the mux.
*/
private final OutboundRequest req;
/**
* The connection on which the outbound request originates.
*/
private final Connection c;
/**
* The outbound request handle.
*/
private final OutboundRequestHandle handle;
/**
* Wrapper around the mux's response input stream.
*/
private final InputStream in;
/**
* Delivery status override from readResponseData.
*/
private boolean status = true;
Outbound(OutboundRequest req,
Connection c,
OutboundRequestHandle handle)
{
this.req = req;
this.c = c;
this.handle = handle;
in = new Input(handle);
}
/* pass-through to the underlying request */
public OutputStream getRequestOutputStream() {
return req.getRequestOutputStream();
}
/* return the wrapper */
public InputStream getResponseInputStream() {
return in;
}
/* delegate to the connection */
public void populateContext(Collection context) {
c.populateContext(handle, context);
}
/* delegate to the connection */
public InvocationConstraints getUnfulfilledConstraints() {
return c.getUnfulfilledConstraints(handle);
}
/**
* False if readResponseData returned an exception, otherwise
* pass-through to the underlying request.
*/
public boolean getDeliveryStatus() {
return status && req.getDeliveryStatus();
}
/* pass-through to the underlying request */
public void abort() {
req.abort();
}
/**
* Wrapper for the response input stream of an outbound request,
* used to call readResponseData on the underlying connection
* before subsequent data is read by higher levels. Note that this
* class does not support mark/reset.
*/
private final class Input extends InputStream {
/**
* The underlying input stream from the outbound request.
*/
private final InputStream in;
/**
* The handle, or null if readResponseData has been called.
*/
private OutboundRequestHandle handle;
Input(OutboundRequestHandle handle) {
in = req.getResponseInputStream();
this.handle = handle;
}
/**
* Calls readResponseData on the connection, exactly once.
* Sets the handle to null to indicate that it has been called.
*/
synchronized private void readFirst() throws IOException {
if (handle != null) {
try {
IOException e = c.readResponseData(handle, in);
if (e != null) {
status = false;
throw e;
}
} finally {
handle = null;
}
}
}
/** Call readFirst, then pass through. */
public int read() throws IOException {
readFirst();
return in.read();
}
/** Call readFirst, then pass through. */
public int read(byte[] b, int off, int len) throws IOException {
readFirst();
return in.read(b, off, len);
}
/** Call readFirst, then pass through. */
public long skip(long n) throws IOException {
readFirst();
return in.skip(n);
}
/** Call readFirst, then pass through. */
public int available() throws IOException {
readFirst();
return in.available();
}
/** pass-through */
public void close() throws IOException {
in.close();
}
}
}
/**
* Records idle times in muxes and shuts down muxes that have been
* idle for at least TIMEOUT milliseconds.
*
* REMIND: It should be possible to have a shared reaper once
* again, with some care regarding GC issues.
*/
private final class Reaper implements Runnable {
Reaper() {
}
/**
* Sleep for TIMEOUT milliseconds. Then call checkIdle,
* shutdown all of idle muxes that have been collected, and if
* checkIdle returned true, terminate, else repeat (go back to
* sleep).
*/
public void run() {
List idle = new ArrayList(1);
boolean done;
do {
try {
Thread.sleep(TIMEOUT);
} catch (InterruptedException e) {
return;
}
long now = System.currentTimeMillis();
synchronized (ConnectionManager.this) {
checkIdle(now, idle);
done = muxes.isEmpty();
if (done) {
reaper = null;
}
}
for (int i = idle.size(); --i >= 0; ) {
((OutboundMux) idle.get(i)).shutdown("idle");
}
idle.clear();
} while (!done);
}
}
/**
* Outbound request iterator returned by newRequest.
*/
private final class ReqIterator implements OutboundRequestIterator {
/**
* The request handle.
*/
private final OutboundRequestHandle handle;
/**
* True if next has not yet been called.
*/
private boolean first = true;
/**
* The outbound mux from the last call to next, if any.
*/
private OutboundMux mux;
ReqIterator(OutboundRequestHandle handle) {
this.handle = handle;
}
/**
* Returns true if next has not yet been called or if the last mux
* returned had an asynchronous close.
*/
public synchronized boolean hasNext() {
return first || (mux != null && mux.shouldRetry());
}
/**
* If hasNext returns true, finds the entry (if any) for the
* connection endpoint. If no entry is found, creates one and spawns
* a Reaper if this is the only entry. Either way, bumps the connect
* count for the entry. Calls connect on the entry to get a mux, then
* calls newRequest on the mux, calls writeRequestData on the
* connection, and returns a new outbound request wrapper.
*/
public synchronized OutboundRequest next() throws IOException {
if (!hasNext()) {
throw new NoSuchElementException();
}
first = false;
mux = null;
connectPending();
mux = connect(handle);
OutboundRequest req = mux.newRequest();
OutboundRequest sreq = null;
try {
Connection c = mux.getConnection();
c.writeRequestData(handle, req.getRequestOutputStream());
sreq = new Outbound(req, c, handle);
} finally {
if (sreq == null) {
remove(mux);
}
}
return sreq;
}
}
/**
* Returns an <code>OutboundRequestIterator</code> to use to send
* a new request for the specified handle to this connection
* manager's <code>ConnectionEndpoint</code>.
*
* <p>If the <code>hasNext</code> method of the returned iterator
* returns <code>true</code>, the <code>next</code> method behaves
* as follows:
*
* <blockquote>
*
* The connection endpoint's {@link
* ConnectionEndpoint#connect(OutboundRequestHandle,Collection,Collection)
* connect} method is invoked with any active connections that
* have not reached their maximum number of in-progress requests,
* any idle connections, and <code>handle</code>. If that returns
* <code>null</code>, the endpoint's {@link
* ConnectionEndpoint#connect(OutboundRequestHandle) connect}
* method is invoked with <code>handle</code>. In either case, if
* a new connection is returned, the Jini ERI multiplexing
* protocol is started on the connection (as the client).
* Finally, the {@link Connection#writeRequestData
* writeRequestData} method of the connection is invoked with
* <code>handle</code> and the request output stream of the {@link
* OutboundRequest} that is created for the request. If any
* exception is thrown while obtaining a connection from the
* endpoint or writing the request data, that exception is thrown
* to the caller. The <code>OutboundRequest</code> returned by
* <code>next</code> will invoke the {@link
* Connection#readResponseData readResponseData} method of the
* connection with the specified handle and the response input
* stream before any other data is read from the response input
* stream. The {@link OutboundRequest#populateContext
* populateContext} and {@link
* OutboundRequest#getUnfulfilledConstraints
* getUnfulfilledConstraints} methods of the
* <code>OutboundRequest</code> are implemented by delegating to
* the corresponding method of the connection passing
* <code>handle</code> and the other arguments (if any).
*
* </blockquote>
*
* <p>The returned iterator might allow continued iteration if the
* connection used for the most recent request attempt was shut
* down gracefully by the server.
*
* @param handle a handle to identify the request in later
* invocations on the connection endpoint and its connections
*
* @return an <code>OutboundRequestIterator</code> to use to send
* a new request for the specified handle to this connection
* manager's <code>ConnectionEndpoint</code>
*
* @throws NullPointerException if <code>handle</code> is
* <code>null</code>
**/
public OutboundRequestIterator newRequest(OutboundRequestHandle handle) {
return new ReqIterator(handle);
}
}