/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.clients.fcp;
import java.io.IOException;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import freenet.clients.fcp.FCPPluginConnection.SendDirection;
import freenet.pluginmanager.FredPluginFCPMessageHandler.ClientSideFCPMessageHandler;
import freenet.pluginmanager.FredPluginFCPMessageHandler.ServerSideFCPMessageHandler;
import freenet.pluginmanager.PluginRespirator;
import freenet.support.Logger;
import freenet.support.io.NativeThread;
/**
* Keeps a list of all {@link FCPPluginConnectionImpl}s which are connected to server plugins
* running in the node. Allows the server plugins to query a client connection by its {@link UUID}.
* <br><br>
*
* <p>To understand the purpose of this, please consider the following:<br/>
* The normal flow of plugin FCP is that clients send messages to a server plugin, and the server
* plugin immediately returns a reply from its message handling function
* {@link ServerSideFCPMessageHandler#handlePluginFCPMessage(FCPPluginConnection,
* FCPPluginMessage)}.<br/>
* This might not be sufficient for certain usecases: The reply to a message might take quite some
* time to compute, possibly hours. Then a reference to the original client connection needs to be
* stored in the plugin's database, not memory. A {@link FCPPluginConnection} cannot be
* serialized into a database, but an {@link UUID} can.<br>
* Thus, this class exists to serve the purpose of allowing plugin servers to query client
* connections by their ID (see {@link FCPPluginConnection#getID()}).</p>
*
* <p>Client connections are considered as closed once the client discards all strong references
* to the {@link FCPPluginConnection}. Or in other words: A {@link FCPPluginConnection} is closed
* once it becomes weakly reachable.<br>
* Thus, this class is implemented by keeping {@link WeakReference}s to plugin client connections,
* so they only stay in the memory of the tracker as long as they are still connected.</p>
*
* <p>After constructing an object of this class, you must call {@link #start()} to start its
* garbage collection thread.<br/>
* For shutdown, no action is required: The thread will be a daemon thread and thus the JVM will
* deal with shutdown.</p>
*
* @author xor (xor@freenetproject.org)
*/
final class FCPPluginConnectionTracker extends NativeThread {
/**
* Backend table of {@link WeakReference}s to known client connections. Monitored by a
* {@link ReferenceQueue} to automatically remove entries for connections which have been GCed.
*
* Not a {@link ConcurrentHashMap} because the creation of connections is exposed to the FCP
* network interface and thus DoS would be possible: Java HashMaps never shrink.
*/
private final TreeMap<UUID, ConnectionWeakReference> connectionsByID
= new TreeMap<UUID, ConnectionWeakReference>();
/**
* Lock to guard {@link #connectionsByID} against concurrent modification.<br>
* A {@link ReadWriteLock} because the suspected usage pattern is mostly reads, very few writes
* - {@link ReadWriteLock} can do that faster than a regular Lock.<br>
* (A {@link ReentrantReadWriteLock} because thats the only implementation of
* {@link ReadWriteLock}.)
*/
private final ReadWriteLock connectionsByIDLock = new ReentrantReadWriteLock();
/**
* Queue which monitors nulled weak references in {@link #connectionsByID}.<br>
* Monitored in {@link #realRun()}.
*/
private final ReferenceQueue<FCPPluginConnectionImpl> closedConnectionsQueue
= new ReferenceQueue<FCPPluginConnectionImpl>();
/**
* We extend class {@link WeakReference} so we can store the ID of the connection:<br/>
* When using a {@link ReferenceQueue} to get notified about nulled {@link WeakReference}
* values in {@link FCPPluginConnectionTracker#connectionsByID}, we need to remove those values
* from the {@link TreeMap}. For fast removal, we need their key in the map, which is the
* connection ID, so we should store it in the {@link WeakReference}.
*/
static final class ConnectionWeakReference
extends WeakReference<FCPPluginConnectionImpl> {
public final UUID connectionID;
public ConnectionWeakReference(FCPPluginConnectionImpl referent,
ReferenceQueue<FCPPluginConnectionImpl> referenceQueue) {
super(referent, referenceQueue);
connectionID = referent.getID();
}
}
/**
* Stores the {@link FCPPluginConnectionImpl} so in the future it can be obtained by its ID with
* {@link #getConnection(UUID)}.
*
* <b>Must</b> be called for any newly created {@link FCPPluginConnectionImpl} before passing it
* to {@link ServerSideFCPMessageHandler#handlePluginFCPMessage(FCPPluginConnection,
* FCPPluginMessage)}.
*
* Unregistration is not supported and not necessary.
*/
void registerConnection(FCPPluginConnectionImpl connection) {
connectionsByIDLock.writeLock().lock();
try {
// No duplicate checks needed: FCPPluginConnection.getID() is a random UUID.
connectionsByID.put(connection.getID(),
new ConnectionWeakReference(connection, closedConnectionsQueue));
} finally {
connectionsByIDLock.writeLock().unlock();
}
}
/**
* For being indirectly exposed to implementors of server plugins, i.e. implementors of
* {@link ServerSideFCPMessageHandler}.<br/>
* NOT for being used by clients: Clients using a {@link FCPPluginConnection} to connect to a
* server plugin have to keep a reference to the {@link FCPPluginConnection} in memory.
* See {@link PluginRespirator#connectToOtherPlugin(String, ClientSideFCPMessageHandler)}.
* <br/>
* This is necessary because this class only keeps {@link WeakReference}s to the
* {@link FCPPluginConnection} objects. Once they are not referenced by a strong reference
* anymore they will be garbage collected and thus considered as disconnected.<br/>
* The job of keeping the strong references is at the client.<br><br>
*
* ATTENTION:<br>
* The returned FCPPluginConnectionImpl objects class shall not be handed out directly to server
* applications. Instead, only hand out a {@link DefaultSendDirectionAdapter} - which can be
* obtained by {@link FCPPluginConnectionImpl#getDefaultSendDirectionAdapter(SendDirection)}.
* <br>This has two reasons:<br>
* - The send functions which do not require a {@link SendDirection} will always throw an
* exception without an adapter ({@link #send(FCPPluginMessage)} and
* {@link #sendSynchronous(FCPPluginMessage, long)}).<br>
* - Server plugins must not keep a strong reference to the FCPPluginConnectionImpl
* to ensure that the client disconnection mechanism of monitoring garbage collection works.
* The adapter prevents servers from keeping a strong reference by internally only keeping a
* {@link WeakReference} to the FCPPluginConnectionImpl.<br>
*
* @param connectionID
* The value of {@link FCPPluginConnection#getID()} of a client connection which has already
* sent a message to the server plugin via
* {@link ServerSideFCPMessageHandler#handlePluginFCPMessage(FCPPluginConnection,
* FCPPluginMessage)}.
* @return
* The client connection with the given ID, for as long as the client is still connected.
* @throws IOException
* If there has been no connection with the given ID or if the client has disconnected.
*/
public FCPPluginConnectionImpl getConnection(UUID connectionID) throws IOException {
ConnectionWeakReference ref = getConnectionWeakReference(connectionID);
FCPPluginConnectionImpl connection = ref.get();
if(connection == null) {
throw new IOException("Client has closed the connection. "
+ "Connection ID = " + connectionID);
}
return connection;
}
/**
* Same as {@link #getConnection(UUID)} with the only difference of returning a
* {@link WeakReference} to the connection instead of the connection itself.<br>
* <b>Please do read its JavaDoc to understand when to use this!</b>
*/
ConnectionWeakReference getConnectionWeakReference(UUID connectionID)
throws IOException {
connectionsByIDLock.readLock().lock();
try {
ConnectionWeakReference ref = connectionsByID.get(connectionID);
if(ref != null)
return ref;
} finally {
connectionsByIDLock.readLock().unlock();
}
throw new IOException("FCPPluginConnection not found, maybe client has disconnected."
+ " Connection ID: " + connectionID);
}
/**
* You must call {@link #start()} afterwards!
*/
public FCPPluginConnectionTracker() {
super("FCPPluginConnectionTracker Garbage-collector",
NativeThread.PriorityLevel.MIN_PRIORITY.value, true);
setDaemon(true);
}
/**
* Garbage-collection thread: Polls {@link #closedConnectionsQueue} for connections whose
* {@link WeakReference} has been nulled and removes them from the {@link #connectionsByID}
* {@link TreeMap}.<br><br>
*
* Notice: Do not call this function directly. To execute this, call {@link #start()}.
* This function is merely public because this class extends {@link NativeThread}.
*/
@Override
public void realRun() {
while(true) {
try {
ConnectionWeakReference closedConnection
= (ConnectionWeakReference)closedConnectionsQueue.remove();
connectionsByIDLock.writeLock().lock();
try {
ConnectionWeakReference removedFromTree
= connectionsByID.remove(closedConnection.connectionID);
assert(closedConnection == removedFromTree);
if(logMINOR) {
Logger.minor(this, "Garbage-collecting closed connection: " +
"remaining connections = " + connectionsByID.size() +
"; connection ID = " + closedConnection.connectionID);
}
} finally {
connectionsByIDLock.writeLock().unlock();
}
} catch(InterruptedException e) {
// We did setDaemon(true), which causes the JVM to exit even if the thread is still
// running: Daemon threads are force terminated during shutdown.
// Thus, this thread does not need an exit mechanism, it can be an infinite loop. So
// nothing should try to terminate it by InterruptedException. If it does happen
// nevertheless, we honor it by exiting the thread, because interrupt requests
// should never be ignored, but log it as an error.
Logger.error(this,
"Thread interruption requested even though this is a daemon thread!", e);
throw new RuntimeException(e);
} catch(Throwable t) {
Logger.error(this, "Error in thread " + getName(), t);
}
}
}
/** For {@link Logger#registerClass(Class)} */
private static transient volatile boolean logMINOR = false;
static {
Logger.registerClass(FCPPluginConnectionTracker.class);
}
}