/* Copyright (c) 2007 Health Market Science, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA You can contact Health Market Science at info@healthmarketscience.com or at the following address: Health Market Science 2700 Horizon Drive Suite 200 King of Prussia, PA 19406 */ package com.healthmarketscience.rmiio; import java.io.Closeable; import java.io.IOException; import java.io.InterruptedIOException; import java.io.NotSerializableException; import java.io.ObjectStreamException; import java.io.Serializable; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.server.Unreferenced; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import com.healthmarketscience.rmiio.exporter.RemoteStreamExporter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Common base class for remote stream implementations which handles the * basic status of the stream (whether or not it is exported, closed). * * @author James Ahlborn */ public abstract class RemoteStreamServer<StreamServerType, StreamType> implements Remote, Unreferenced, Closeable, Serializable { protected static final Logger LOG = LoggerFactory.getLogger(RemoteStreamServer.class); private static final long serialVersionUID = 20080212L; /** the initial sequence id for server methods which have not yet been invoked */ protected static final int INITIAL_INVALID_SEQUENCE_ID = -1; /** the initial sequence id for client code which has not yet invoked any remote methods */ protected static final int INITIAL_VALID_SEQUENCE_ID = 0; /** this set will temporarily maintain a hard reference to a newly created RemoteStreamServer (via a HardRefMonitor) so that the stream remote stub isn't prematurely garbage collected. see HardRefMonitor for more details. */ private static final Set<Object> _hardRefSet = Collections.synchronizedSet(new HashSet<Object>()); private enum State { OPEN, ABORTED, CLOSED; } /** whether or not this stream has been closed yet */ private transient final AtomicReference<State> _state = new AtomicReference<State>(State.OPEN); /** the monitor which is following our progress */ protected transient RemoteStreamMonitor<StreamServerType> _monitor; /** the exporter used to export this stream */ private transient RemoteStreamExporter _exporter; /** the implicitly exported stub for this object, created by a call to writeReplace, if any */ private transient volatile StreamType _writeReplacement; public RemoteStreamServer(RemoteStreamMonitor<StreamServerType> monitor) { _monitor = monitor; } /** * Convenience method which exports this object for use using the exporter * retrieved from {@link RemoteStreamExporter#getInstance}. */ public StreamType export() throws RemoteException { return RemoteStreamExporter.getInstance().export(this); } /** * Indicates to this object that it was exported remotely. Should be called * by the RemoteStreamExporter after this object has been exported. * @param exporter the exporter that exported this object */ public synchronized void exported(RemoteStreamExporter exporter) throws RemoteException { if(_exporter != null) { throw new IllegalStateException("Re-exporting still exported stream " + this); } // keep a reference to the exporter which exported us _exporter = exporter; if(!(HardRefMonitor.class.isInstance(_monitor))) { // we temporarily wrap the monitor in order to keep our remote stub from // getting prematurely garbage collected. see HardRefMonitor for more // details. (we do this after a successful export only, or else we may // have a memory leak because this object would get stuck in the // _hardRefSet and never removed). _monitor = new HardRefMonitor(_monitor); } } /** * Makes this object no longer accessible remotely. */ private synchronized void unexport() { _writeReplacement = null; try { if(HardRefMonitor.class.isInstance(_monitor)) { // premature unexport, make sure to ditch local hard reference HardRefMonitor.class.cast(_monitor).cleanup(); } if(_exporter != null) { _exporter.unexport(this); } else { LOG.info("Unexporting object " + this + " which was not exported"); } } finally { _exporter = null; } } public void unreferenced() { // close up everything. note, that if we get here, the remote end did // *not* call the close(boolean) method successfully, so this is a "dirty" // *close. try { finish(false, false); } catch(IOException ignored) { if(LOG.isDebugEnabled()) { LOG.debug("Ignoring exception while closing unreferenced stream", ignored); } } } /** * @return <code>true</code> iff this stream server has been closed (one way * or another), <code>false</code> otherwise. */ public final boolean isClosed() { return(_state.get() == State.CLOSED); } /** * Forces this stream server to close (if not already closed), will * <b>break</b> any outstanding client interactions. Should be called one * way or another on the server object (may be left to the * <code>unreferenced</code> method if the server object must live beyond * the creation method call). */ public final void close() { unreferenced(); } /** * Cleans up after this stream. Unexports the Remote object, closes the * underlying stream, and makes the final call(s) to the stream monitor. * * @param remoteClose indicates whether this was a remote close() call * or a local cleanup close after a failed transfer. * @param transferSuccess <code>true</code> iff all data was successfully * transferred, <code>false</code> otherwise */ protected final void finish(boolean remoteClose, boolean transferSuccess) throws IOException { State oldState = _state.getAndSet(State.CLOSED); if(oldState == State.CLOSED) { // nothing more to do (already closed) return; } boolean closeCompleted = false; try { // do actual close work closeImpl(transferSuccess); closeCompleted = true; } catch(IOException e) { // update the monitor _monitor.failure(getAsSub(), e); throw e; } catch(RuntimeException e) { // update the monitor _monitor.failure(getAsSub(), e); throw e; } finally { try { // update the monitor _monitor.closed(getAsSub(), (remoteClose && closeCompleted && (oldState == State.OPEN))); } finally { // finally, unexport ourselves unexport(); } } } /** * Aborts the current transfer without closing this RemoteStreamServer. * This method is thread safe. This will usually shutdown a currently * working transfer faster than just closing the RemoteStreamServer directly * (because this will cause the client to get an IOException instead of a * RemoteException, which may cause retries, etc.). This RemoteStreamServer * should still be closed as normal. */ public final void abort() throws IOException { // can only abort it currently in the OPEN state. note we don't care if // this set fails, because that implies the stream was closed first, which // is no big deal _state.compareAndSet(State.OPEN, State.ABORTED); } /** * Throws an IOException if the stream has been aborted. Should be called * at the beginning of any method which accesses the underlying stream, * except for the <code>close</code> method. */ protected final void checkAborted() throws IOException { if(_state.get() == State.ABORTED) { throw new InterruptedIOException("stream server was aborted"); } } /** * Manages serialization for all remote stream instances by returning the * result of a call to {@link #export} on this instance as a Serializable * replacement for an instance of this class. While generally the developer * should be managing the call to export, implementing this method in a * useful way makes the simple things simple (passing a reference to a * server implementation in a remote method call will "do the right thing", * replacing the actual reference to this instance with a reference to an * automagically generated remote reference to this server instance). * * @return an exported remote stub for this instance * @throws NotSerializableException if the export attempt fails * @serialData the serialized data is the object returned by the * {@link #export} method */ protected final Object writeReplace() throws ObjectStreamException { // note, we only want to do implicit export once. it's possible that a // remote invocation failed and needs to be re-attempted, in which case we // don't want to re-export this instance, cause that will fail. StreamType replacement = _writeReplacement; if(replacement == null) { try { replacement = export(); _writeReplacement = replacement; } catch(RemoteException e) { throw (NotSerializableException) (new NotSerializableException( getClass().getName() + ": Could not export stream server")) .initCause(e); } } return replacement; } /** * Sets the monitor of this class to the actual monitor (instead of the * temporary HardRefMonitor). Only called by HardRefMonitor. */ private void setRealMonitor( RemoteStreamMonitor<StreamServerType> realMonitor) { _monitor = realMonitor; } /** * Closes (possibly flushes) the underlying streams and cleans up any * resources. Called by the finish() method. * * @param transferSuccess <code>true</code> iff all data was successfully * transferred, <code>false</code> otherwise */ protected abstract void closeImpl(boolean transferSuccess) throws IOException; /** * Returns a handle to the object used to lock the underlying stream * operations for this remote stream. */ protected abstract Object getLock(); /** * @return the class of the remote stream interface for this server */ public abstract Class<StreamType> getRemoteClass(); /** * Returns a handle to this object as a subclass instance. */ protected abstract StreamServerType getAsSub(); /** * Utility class which temporarily maintains a hard reference to the * RemoteStreamServer so that its remote stub does not get prematurely * garbage collected. Basically the remote stub garbage collection will * collect any stubs with no remote references and no *local* (hard) * references. In a common usage of a RemoteStreamServer, the class is * created on the fly, handed to the remote client, and then the local * server discards its local (hard) reference to the stream. This presents * a small window of opportunity where the remote stub can be garbage * collected before the remote client ever gets a handle to it (causing the * remote client to immediately get a NoSuchObjectException). This class * solves that problem by maintaining a hard reference in a private set * (_hardRefSet) until the remote client has successfully called this class, * at which point this class removes itself from existence and everything * proceeds happily. Note that even if the remote client never makes a * call, this class will get cleaned up when the unreferenced() method is * called, so there is no risk of memory leak. */ private class HardRefMonitor implements RemoteStreamMonitor<StreamServerType> { /** handle to the actual monitor for the outer RemoteStreamServer. */ private final RemoteStreamMonitor<StreamServerType> _realMonitor; public HardRefMonitor(RemoteStreamMonitor<StreamServerType> realMonitor) { _realMonitor = realMonitor; _hardRefSet.add(this); } private void cleanup() { // now that a remote method has been called, we no longer need the local // hard reference _hardRefSet.remove(this); // break the indirection to this monitor by resetting the monitor in the // RemoteStreamServer to the real one setRealMonitor(_realMonitor); } public void failure(StreamServerType stream, Exception e) { cleanup(); _realMonitor.failure(stream, e); } public void bytesMoved(StreamServerType stream, int numBytes, boolean isReattempt) { cleanup(); _realMonitor.bytesMoved(stream, numBytes, isReattempt); } public void bytesSkipped(StreamServerType stream, long numBytes, boolean isReattempt) { cleanup(); _realMonitor.bytesSkipped(stream, numBytes, isReattempt); } public void localBytesMoved(StreamServerType stream, int numBytes) { cleanup(); _realMonitor.localBytesMoved(stream, numBytes); } public void localBytesSkipped(StreamServerType stream, long numBytes) { cleanup(); _realMonitor.localBytesSkipped(stream, numBytes); } public void closed(StreamServerType stream, boolean clean) { cleanup(); _realMonitor.closed(stream, clean); } } }