river-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From tho...@apache.org
Subject svn commit: r1137621 [2/2] - in /river/jtsk/tags/2.2.0: ./ asm/ qa/ qa/doc/ src-doc/static/ src-doc/static/release-notes/ src/com/sun/jini/config/ src/com/sun/jini/jeri/internal/mux/ src/com/sun/jini/jeri/internal/runtime/ src/manifest/jsk-resources/ME...
Date Mon, 20 Jun 2011 13:09:24 GMT
Modified: river/jtsk/tags/2.2.0/src/com/sun/jini/jeri/internal/runtime/ObjectTable.java
URL: http://svn.apache.org/viewvc/river/jtsk/tags/2.2.0/src/com/sun/jini/jeri/internal/runtime/ObjectTable.java?rev=1137621&r1=1137620&r2=1137621&view=diff
==============================================================================
--- river/jtsk/tags/2.2.0/src/com/sun/jini/jeri/internal/runtime/ObjectTable.java (original)
+++ river/jtsk/tags/2.2.0/src/com/sun/jini/jeri/internal/runtime/ObjectTable.java Mon Jun
20 13:09:22 2011
@@ -19,41 +19,19 @@
 package com.sun.jini.jeri.internal.runtime;
 
 import com.sun.jini.jeri.internal.runtime.ImplRefManager.ImplRef;
-import com.sun.jini.logging.Levels;
 import com.sun.jini.thread.NewThreadAction;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.ObjectInputStream;
-import java.io.OutputStream;
-import java.lang.reflect.Method;
 import java.rmi.Remote;
 import java.rmi.server.ExportException;
 import java.rmi.server.Unreferenced;
 import java.security.AccessController;
-import java.security.PrivilegedAction;
-import java.security.PrivilegedExceptionAction;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-import net.jini.export.ServerContext;
 import net.jini.id.Uuid;
-import net.jini.id.UuidFactory;
-import net.jini.io.MarshalInputStream;
-import net.jini.io.UnsupportedConstraintException;
-import net.jini.jeri.BasicInvocationDispatcher;
-import net.jini.jeri.InvocationDispatcher;
-import net.jini.jeri.InboundRequest;
 import net.jini.jeri.RequestDispatcher;
-import net.jini.jeri.ServerCapabilities;
-import net.jini.core.constraint.InvocationConstraints;
 import net.jini.security.Security;
 import net.jini.security.SecurityContext;
 
@@ -67,66 +45,48 @@ final class ObjectTable {
     private static final Logger logger =
 	Logger.getLogger("net.jini.jeri.BasicJeriExporter");
 
-    private static final Collection dgcDispatcherMethods = new ArrayList(2);
-    static {
-	Method[] methods = DgcServer.class.getMethods();
-	for (int i = 0; i < methods.length; i++) {
-	    final Method m = methods[i];
-	    AccessController.doPrivileged(new PrivilegedAction() {
-		public Object run() {
-		    m.setAccessible(true);
-		    return null;
-		}
-	    });
-	    dgcDispatcherMethods.add(m);
-	}
-    }
-
-    private static final ServerCapabilities dgcServerCapabilities =
-	new ServerCapabilities() {
-	    public InvocationConstraints checkConstraints(
-		InvocationConstraints constraints)
-		throws UnsupportedConstraintException
-	    {
-		assert constraints.equals(InvocationConstraints.EMPTY);
-		return InvocationConstraints.EMPTY;
-	    }
-	};
-
     /**
      * lock to serialize request dispatcher reservation per export, so
      * that a partial export will not cause another export to fail
      * unnecessarily
      **/
-    private final Object requestDispatchersLock = new Object();
+    private final Object requestDispatchersLock;
 
     /** table of references to impls exported with DGC */
-    private final ImplRefManager implRefManager = new ImplRefManager();
-
-    /** lock guarding keepAliveCount and keeper */
-    private final Object keepAliveLock = new Object();
+    private final ImplRefManager implRefManager;
 
     /** number of objects exported with keepAlive == true */
-    private int keepAliveCount = 0;
-
-    /** thread to keep VM alive while keepAliveCount > 0 */
-    private Thread keeper = null;
+    private final JvmLifeSupport keepAliveCount;
 
     /** maps client ID to Lease (lock guards leaseChecker too) */
-    private final Map leaseTable = new HashMap();
+    private final ConcurrentMap<Uuid,Lease> leaseTable;
 
     /** thread to check for expired leases */
-    private Thread leaseChecker = null;
-
-    ObjectTable() { }
+    private Thread leaseChecker;
+    
+    /** thread guard */
+    private Boolean running;
+
+    ObjectTable() { 
+        requestDispatchersLock = new Object();
+        implRefManager = new ImplRefManager();
+        keepAliveCount = new JvmLifeSupport();
+        leaseTable = new ConcurrentHashMap<Uuid,Lease>(256);//Plenty of capacity to
reduce resizing.
+        leaseChecker = null;
+        running = Boolean.FALSE;
+    }
 
     RequestDispatcher createRequestDispatcher(Unreferenced unrefCallback) {
-	return new RD(unrefCallback);
+	return new DgcRequestDispatcher(unrefCallback, this);
     }
 
     boolean isReferenced(RequestDispatcher requestDispatcher) {
 	return getRD(requestDispatcher).isReferenced();
     }
+    
+    DgcServer getDgcServer(DgcRequestDispatcher dgdRD){
+        return new DgcServerImpl(dgdRD);
+    }
 
     Target export(Remote impl,
 		  RequestDispatcher[] requestDispatchers,
@@ -135,21 +95,31 @@ final class ObjectTable {
 		  Uuid id)
         throws ExportException
     {
-	RD[] rds = new RD[requestDispatchers.length];
+	DgcRequestDispatcher[] rds = new DgcRequestDispatcher[requestDispatchers.length];
 	for (int i = 0; i < requestDispatchers.length; i++) {
 	    rds[i] = getRD(requestDispatchers[i]);
 	}
-
-	return new Target(impl, id, rds, allowDGC, keepAlive);
+        SecurityContext securityContext = Security.getContext();
+        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
+        Target t = null;
+        t = new Target(id, rds, allowDGC, keepAlive,this,
+                securityContext, ccl, keepAliveCount);
+        synchronized (requestDispatchersLock){
+            t.procRequestDispatchers();
+        }
+        ImplRef implRef = implRefManager.getImplRef(impl, t);
+        t.setImplRef(implRef);
+        t.setExported();
+	return t;
     }
 
-    private RD getRD(RequestDispatcher requestDispatcher) {
+    private DgcRequestDispatcher getRD(RequestDispatcher requestDispatcher) {
 	/*
 	 * The following cast will throw a ClassCastException if we were
 	 * passed a RequestDispatcher that was not returned by this class's
 	 * createRequestDispatcher method:
 	 */
-	RD rd = (RD) requestDispatcher;
+	DgcRequestDispatcher rd = (DgcRequestDispatcher) requestDispatcher;
 	if (!rd.forTable(this)) {
 	    throw new IllegalArgumentException(
 		"request dispatcher for different object table");
@@ -157,813 +127,245 @@ final class ObjectTable {
 	return rd;
     }
 
-    /**
-     * Increments the count of objects exported with keepAlive true,
-     * starting a non-daemon thread if necessary.
-     **/
-    private void incrementKeepAliveCount() {
-	synchronized (keepAliveLock) {
-	    keepAliveCount++;
-
-	    if (keeper == null) {
-		keeper = (Thread) AccessController.doPrivileged(
-		    new NewThreadAction(new Runnable() {
-			public void run() {
-			    try {
-				while (true) {
-				    Thread.sleep(Long.MAX_VALUE);
-				}
-			    } catch (InterruptedException e) {
-				// pass away if interrupted
-			    }
-			}
-		    }, "KeepAlive", false));
-		keeper.start();
-	    }
-	}
-    }
-
-    /**
-     * Decrements the count of objects exported with keepAlive true,
-     * stopping the non-daemon thread if decremented to zero.
-     **/
-    private void decrementKeepAliveCount() {
-	synchronized (keepAliveLock) {
-	    keepAliveCount--;
-
-	    if (keepAliveCount == 0) {
-		assert keeper != null;
-		AccessController.doPrivileged(new PrivilegedAction() {
-		    public Object run() {
-			keeper.interrupt();
-			return null;
-		    }
-		});
-		keeper = null;
-	    }
-	}
-    }
-
-    /**
-     * A Target is returned by the export method to represent the object
-     * exported to this ObjectTable.  It can be used to unexport the
-     * exported object.
-     */
-    final class Target {
-
-	private final ImplRef implRef;
-	private final Uuid id;
-	private final RD[] requestDispatchers;
-	private final boolean allowDGC;
-	private final boolean keepAlive;
-	private final SecurityContext securityContext;
-	private final ClassLoader ccl;
-
-	/** lock guarding all mutable instance state (below) */
-	private final Object lock = new Object();
-	private InvocationDispatcher invocationDispatcher;
-	private boolean exported = false;
-	private int callsInProgress = 0;
-	private final Set referencedSet;
-	private final Map sequenceTable;
-
-	Target(Remote impl,
-	       Uuid id,
-	       RD[] requestDispatchers,
-	       boolean allowDGC,
-	       boolean keepAlive)
-	    throws ExportException
-	{
-	    this.id = id;
-	    this.requestDispatchers = requestDispatchers;
-	    this.allowDGC = allowDGC;
-	    this.keepAlive = keepAlive;
-
-	    securityContext = Security.getContext();
-	    ccl = Thread.currentThread().getContextClassLoader();
-
-	    synchronized (requestDispatchersLock) {
-		boolean success = false;
-		int i = 0;
-		try {
-		    for (i = 0; i < requestDispatchers.length; i++) {
-			requestDispatchers[i].put(this);
-		    }
-		    success = true;
-		} finally {
-		    if (!success) {
-			for (int j = 0; j < i; j++) {
-			    requestDispatchers[i].remove(this, false);
-			}
-		    }
-		}
-	    }
-
-	    implRef = implRefManager.getImplRef(impl, this);
-
-	    if (allowDGC) {
-		referencedSet = new HashSet(3);
-		sequenceTable = new HashMap(3);
-	    } else {
-		referencedSet = null;
-		sequenceTable = null;
-	    }
-
-	    if (keepAlive) {
-		incrementKeepAliveCount();
-	    }
-
-	    synchronized (lock) {
-		exported = true;
-	    }
-	}
-
-	void setInvocationDispatcher(InvocationDispatcher id) {
-	    assert id != null;
-	    synchronized (lock) {
-		assert invocationDispatcher == null;
-		invocationDispatcher = id;
-	    }
-	}
-
-	boolean unexport(boolean force) {
-	    synchronized (lock) {
-		if (!exported) {
-		    return true;
-		}
-		if (!force && callsInProgress > 0) {
-		    return false;
-		}
-		exported = false;
-
-		if (keepAlive && callsInProgress == 0) {
-		    decrementKeepAliveCount();
-		}
-
-		if (allowDGC) {
-		    if (!referencedSet.isEmpty()) {
-			for (Iterator i = referencedSet.iterator();
-			     i.hasNext();)
-			{
-			    Uuid clientID = (Uuid) i.next();
-			    unregisterTarget(this, clientID);
-			}
-			referencedSet.clear();
-		    }
-		    sequenceTable.clear();
-		}
-	    }
-
-	    implRef.release(this);
-
-	    for (int i = 0; i < requestDispatchers.length; i++) {
-		requestDispatchers[i].remove(this, false);
-	    }
-	    return true;
-	}
-
-	void collect() {
-	    synchronized (lock) {
-		if (!exported) {
-		    return;
-		}
-
-		if (logger.isLoggable(Level.FINE)) {
-		    logger.log(Level.FINE,
-			"garbage collection of object with id {0}", id);
-		}
-
-		exported = false;
-
-		if (keepAlive && callsInProgress == 0) {
-		    decrementKeepAliveCount();
-		}
-
-		if (allowDGC) {
-		    assert referencedSet.isEmpty();
-		    sequenceTable.clear();
-		}
-	    }
-
-	    for (int i = 0; i < requestDispatchers.length; i++) {
-		requestDispatchers[i].remove(this, true);
-	    }
-	}
-
-	Uuid getObjectIdentifier() {
-	    return id;
-	}
-
-	// used by ImplRef for invoking Unreferenced.unreferenced
-	boolean getEnableDGC() {
-	    return allowDGC;
-	}
-
-	// used by ImplRef for invoking Unreferenced.unreferenced
-	SecurityContext getSecurityContext() {
-	    return securityContext;
-	}
-
-	// used by ImplRef for invoking Unreferenced.unreferenced
-	ClassLoader getContextClassLoader() {
-	    return ccl;
-	}
-
-	void referenced(Uuid clientID, long sequenceNum) {
-	    if (!allowDGC) {
-		return;	// ignore if DGC not enabled for this object
-	    }
-
-	    synchronized (lock) {
-		if (!exported) {
-		    return;
-		}
-
-		if (logger.isLoggable(Level.FINEST)) {
-		    logger.log(Level.FINEST,
-			"this={0}, clientID={1}, sequenceNum={2}",
-			new Object[] {
-			    this, clientID, new Long(sequenceNum)
-			});
-		}
-
-		/*
-		 * Check current sequence number against the last
-		 * recorded sequence number for the client.  If the
-		 * current value is lower, then this is a "late dirty
-		 * call", which should not be processed.  Otherwise,
-		 * update the last recorded sequence number.
-		 */
-		SequenceEntry entry =
-		    (SequenceEntry) sequenceTable.get(clientID);
-		if (entry == null) {
-		    // no record: must assume this is not a late dirty call
-		    entry = new SequenceEntry(sequenceNum);
-		    sequenceTable.put(clientID, entry);
-		} else if (sequenceNum < entry.sequenceNum) {
-		    return;	// late dirty call: ignore
-		} else {
-		    entry.sequenceNum = sequenceNum;
-		}
-
-		if (!referencedSet.contains(clientID)) {
-		    if (referencedSet.isEmpty()) {
-			Remote impl = implRef.getImpl();
-			if (impl == null) {
-			    return;	// too late if impl was collected
-			}
-			implRef.pin(this);
-		    }
-		    referencedSet.add(clientID);
-
-		    registerTarget(this, clientID);
-		}
-	    }
-	}
-
-	void unreferenced(Uuid clientID, long sequenceNum, boolean strong) {
-	    if (!allowDGC) {
-		return;	// ignore if DGC not enabled for this object
-	    }
-
-	    synchronized (lock) {
-		if (!exported) {
-		    return;
-		}
-
-		if (logger.isLoggable(Level.FINEST)) {
-		    logger.log(Level.FINEST,
-			"this={0}, clientID={1}, sequenceNum={2}, strong={3}",
-			new Object[] {
-			    this, clientID, new Long(sequenceNum),
-			    Boolean.valueOf(strong)
-			});
-		}
-
-		/*
-		 * Check current sequence number against the last
-		 * recorded sequence number for the client.  If the
-		 * current value is lower, then this is a "late clean
-		 * call", which should not be processed.  Otherwise:
-		 * if this is for a strong clean call, then update the
-		 * last recorded sequence number; if no strong clean
-		 * call has been processed for this client, discard
-		 * its sequence number record.
-		 */
-		SequenceEntry entry =
-		    (SequenceEntry) sequenceTable.get(clientID);
-		if (entry == null) {
-		    // no record: must assume this is not a late clean call
-		    if (strong) {
-			entry = new SequenceEntry(sequenceNum);
-			sequenceTable.put(clientID, entry);
-			entry.keep = true;
-		    }
-		} else if (sequenceNum < entry.sequenceNum) {
-		    return;	// late clean call: ignore
-		} else if (strong) {
-		    entry.sequenceNum = sequenceNum;
-		    entry.keep = true;	// strong clean: retain sequence number
-		} else if (!entry.keep) {
-		    sequenceTable.remove(clientID);
-		}
-
-		unregisterTarget(this, clientID);
-
-		if (referencedSet.remove(clientID) &&
-		    referencedSet.isEmpty())
-		{
-		    implRef.unpin(this);
-		}
-	    }
-	}
-
-	void leaseExpired(Uuid clientID) {
-	    assert allowDGC;
-
-	    synchronized (lock) {
-		if (!exported) {
-		    return;
-		}
-
-		if (logger.isLoggable(Level.FINEST)) {
-		    logger.log(Level.FINEST,
-			"this={0}, clientID={1}",
-			new Object[] { this, clientID });
-		}
-
-		SequenceEntry entry =
-		    (SequenceEntry) sequenceTable.get(clientID);
-		if (entry != null && !entry.keep) {
-		    /*
-		     * REMIND: We could be removing the sequence number
-		     * for a more recent lease, thus allowing a "late
-		     * clean call" to be inappropriately processed?
-		     * (See 4848840 Comments.)
-		     */
-		    sequenceTable.remove(clientID);
-		}
-
-		if (referencedSet.remove(clientID) &&
-		    referencedSet.isEmpty())
-		{
-		    implRef.unpin(this);
-		}
-	    }
-	}
-
-	void dispatch(InboundRequest request)
-	    throws IOException, NoSuchObject
-	{
-	    InvocationDispatcher id;
-	    synchronized (lock) {
-		if (!exported || invocationDispatcher == null) {
-		    if (logger.isLoggable(Level.FINEST)) {
-			logger.log(Level.FINEST,
-			    "this={0}, not exported", this);
-		    }
-		    throw new NoSuchObject();
-		}
-		id = invocationDispatcher; // save for reference outside lock
-		callsInProgress++;
-	    }
-	    try {
-		Remote impl = implRef.getImpl();
-		if (impl == null) {
-		    if (logger.isLoggable(Level.FINEST)) {
-			logger.log(Level.FINEST,
-			    "this={0}, garbage collected", this);
-		    }
-		    throw new NoSuchObject();
-		}
-
-		dispatch(request, id, impl);
-
-	    } finally {
-		synchronized (lock) {
-		    assert callsInProgress > 0;
-		    callsInProgress--;
-
-		    if (keepAlive && !exported && callsInProgress == 0) {
-			decrementKeepAliveCount();
-		    }
-		}
-	    }
-	}
-
-	private void dispatch(final InboundRequest request,
-			      final InvocationDispatcher id,
-			      final Remote impl)
-	    throws IOException, NoSuchObject
-	{
-	    Thread t = Thread.currentThread();
-	    ClassLoader savedCcl = t.getContextClassLoader();
-	    try {
-		if (ccl != savedCcl) {
-		    t.setContextClassLoader(ccl);
-		}
-		AccessController.doPrivileged(securityContext.wrap(
-		    new PrivilegedExceptionAction() {
-			public Object run() throws IOException {
-			    dispatch0(request, id, impl);
-			    return null;
-			}
-		    }), securityContext.getAccessControlContext());
-			    
-	    } catch (java.security.PrivilegedActionException e) {
-		throw (IOException) e.getException();
-	    } finally {
-		if (ccl != savedCcl || savedCcl != t.getContextClassLoader()) {
-		    t.setContextClassLoader(savedCcl);
-		}
-	    }
-	}
-
-	private void dispatch0(final InboundRequest request,
-			       final InvocationDispatcher id,
-			       final Remote impl)
-	    throws IOException
-	{
-	    request.checkPermissions();
-
-	    OutputStream out = request.getResponseOutputStream();
-	    out.write(Jeri.OBJECT_HERE);
-
-	    final Collection context = new ArrayList(5);
-	    request.populateContext(context);
-
-	    ServerContext.doWithServerContext(new Runnable() {
-		public void run() {
-		    id.dispatch(impl, request, context);
-		}
-	    }, Collections.unmodifiableCollection(context));
-	}
-
-	public String toString() {	// for logging
-	    return "Target@" + Integer.toHexString(hashCode()) +
-		"[" + id + "]";
-	}
-    }
-
-    private static final class SequenceEntry {
-	long sequenceNum;
-	boolean keep;
-
-	SequenceEntry(long sequenceNum) {
-	    this.sequenceNum = sequenceNum;
-	}
-    }
-
     void registerTarget(Target target, Uuid clientID) {
-	synchronized (leaseTable) {
-	    Lease lease = (Lease) leaseTable.get(clientID);
-	    if (lease == null) {
-		target.leaseExpired(clientID);
-	    } else {
-		synchronized (lease.notifySet) {
-		    lease.notifySet.add(target);
-		}
-	    }
-	}
+        Lease lease = leaseTable.get(clientID);
+        if (lease == null) {
+            target.leaseExpired(clientID);
+        } else {
+            boolean added = lease.add(target);
+            if ( added == false){
+                // lease has been locked because it has expired
+                // prior to removal
+                target.leaseExpired(clientID);
+            }
+        }
     }
 
     void unregisterTarget(Target target, Uuid clientID) {
-	synchronized (leaseTable) {
-	    Lease lease = (Lease) leaseTable.get(clientID);
-	    if (lease != null) {
-		synchronized (lease.notifySet) {
-		    lease.notifySet.remove(target);
-		}
-	    }
-	}
-    }
-
-    /**
-     * RequestDispatcher implementation.
-     **/
-    private class RD implements RequestDispatcher {
-
-	private final Unreferenced unrefCallback;
-
-	private final Map idTable = new HashMap();
-	private int dgcEnabledCount = 0;	// guarded by idTable lock
-
-	private final InvocationDispatcher dgcDispatcher;
-	private final DgcServerImpl dgcServerImpl;
-
-	RD(Unreferenced unrefCallback) {
-	    this.unrefCallback = unrefCallback;
-	    try {
-		dgcDispatcher =
-		    new BasicInvocationDispatcher(
-			dgcDispatcherMethods, dgcServerCapabilities,
-			null, null, this.getClass().getClassLoader())
-		    {
-			protected ObjectInputStream createMarshalInputStream(
-			    Object impl,
-			    InboundRequest request,
-			    boolean integrity,
-			    Collection context)
-			    throws IOException
-			{
-			    ClassLoader loader = getClassLoader();
-			    return new MarshalInputStream(
-				request.getRequestInputStream(),
-				loader, integrity, loader,
-				Collections.unmodifiableCollection(context));
-			    // useStreamCodebases() not invoked
-			}
-		    };
-	    } catch (ExportException e) {
-		throw new AssertionError();
-	    }
-	    dgcServerImpl = new DgcServerImpl();
-	}
-
-	boolean forTable(ObjectTable table) {
-	    return ObjectTable.this == table;
-	}
-
-	boolean isReferenced() {
-	    synchronized (idTable) {
-		return !idTable.isEmpty();
-	    }
-	}
-
-	Target get(Uuid id) {
-	    synchronized (idTable) {
-		return (Target) idTable.get(id);
-	    }
-	}
-
-	void put(Target target) throws ExportException {
-	    synchronized (idTable) {
-		Uuid id = target.getObjectIdentifier();
-		if (id.equals(Jeri.DGC_ID)) {
-		    throw new ExportException(
-			"object identifier reserved for DGC");
-		}
-		if (idTable.containsKey(id)) {
-		    throw new ExportException(
-			"object identifier already in use");
-		}
-		idTable.put(id, target);
-		if (target.getEnableDGC()) {
-		    dgcEnabledCount++;
-		}
-	    }
-	}
-
-	void remove(Target target, boolean gc) {
-	    boolean empty = false;
-	    synchronized (idTable) {
-		Uuid id = target.getObjectIdentifier();
-		assert idTable.get(id) == target;
-		idTable.remove(id);
-		if (target.getEnableDGC()) {
-		    dgcEnabledCount--;
-		    assert dgcEnabledCount >= 0;
-		}
-
-		if (idTable.isEmpty()) {
-		    empty = true;
-		}
-	    }
-
-	    if (gc && empty) {
-		/*
-		 * We have to be careful to make this callback without holding
-		 * the lock for idTable, because the callback implementation
-		 * will likely be code that calls this object's isReferenced
-		 * method in its own synchronized block.
-		 */
-		unrefCallback.unreferenced();
-	    }
-	}
-
-	private boolean hasDgcEnabledTargets() {
-	    synchronized (idTable) {
-		return dgcEnabledCount > 0;
-	    }
-	}
-
-	public void dispatch(InboundRequest request) {
-	    try {
-		InputStream in = request.getRequestInputStream();
-		Uuid id = UuidFactory.read(in);
-
-		if (logger.isLoggable(Level.FINEST)) {
-		    logger.log(Level.FINEST, "id={0}", id);
-		}
-
-		try {
-		    /*
-		     * The DGC object identifier is hardwired here,
-		     * rather than install it in idTable; this
-		     * eliminates the need to worry about not counting
-		     * the DGC server as an exported object in the
-		     * table, and it doesn't need all of the machinery
-		     * that Target provides.
-		     */
-		    if (id.equals(Jeri.DGC_ID)) {
-			dispatchDgcRequest(request);
-			return;
-		    }
-
-		    Target target = (Target) get(id);
-		    if (target == null) {
-			logger.log(Level.FINEST, "id not in table");
-			throw new NoSuchObject();
-		    }
-		    target.dispatch(request);
-
-		} catch (NoSuchObject e) {
-		    in.close();
-		    OutputStream out = request.getResponseOutputStream();
-		    out.write(Jeri.NO_SUCH_OBJECT);
-		    out.close();
-
-		    if (logger.isLoggable(Levels.FAILED)) {
-			logger.log(Levels.FAILED, "no such object: {0}", id);
-		    }
-		}
-	    } catch (IOException e) {
-		request.abort();
-
-		if (logger.isLoggable(Levels.FAILED)) {
-		    logger.log(Levels.FAILED,
-			       "I/O exception dispatching request", e);
-		}
-	    }
-	}
-
-	private void dispatchDgcRequest(final InboundRequest request)
-	    throws IOException, NoSuchObject
-	{
-	    if (!hasDgcEnabledTargets()) {
-		logger.log(Level.FINEST, "no DGC-enabled targets");
-		throw new NoSuchObject();
-	    }
-
-	    OutputStream out = request.getResponseOutputStream();
-	    out.write(Jeri.OBJECT_HERE);
-
-	    final Collection context = new ArrayList(5);
-	    request.populateContext(context);
-
-	    ServerContext.doWithServerContext(new Runnable() {
-		public void run() {
-		    dgcDispatcher.dispatch(dgcServerImpl, request, context);
-		}
-	    }, Collections.unmodifiableCollection(context));
-	}
-
-	private class DgcServerImpl implements DgcServer {
-
-	    public long dirty(Uuid clientID,
-			      long sequenceNum,
-			      Uuid[] ids)
-	    {
-		if (logger.isLoggable(Level.FINEST)) {
-		    logger.log(Level.FINEST,
-			"clientID={0}, sequenceNum={1}, ids={2}",
-			new Object[] {
-			    clientID, new Long(sequenceNum), Arrays.asList(ids)
-			});
-		}
-
-		long duration = Jeri.leaseValue;
-
-		synchronized (leaseTable) {
-		    Lease lease = (Lease) leaseTable.get(clientID);
-		    if (lease == null) {
-			leaseTable.put(clientID,
-				       new Lease(clientID, duration));
-			if (leaseChecker == null) {
-			    leaseChecker =
-				(Thread) AccessController.doPrivileged(
-				    new NewThreadAction(new LeaseChecker(),
-					"DGC Lease Checker", true));
-			    leaseChecker.start();
-			}
-		    } else {
-			lease.renew(duration);
-		    }
-		}
-
-		for (int i = 0; i < ids.length; i++) {
-		    Target target = get(ids[i]);
-		    if (target != null) {
-			target.referenced(clientID, sequenceNum);
-		    }
-		}
-
-		return duration;
-	    }
-
-	    public void clean(Uuid clientID,
-			      long sequenceNum,
-			      Uuid[] ids,
-			      boolean strong)
-	    {
-		if (logger.isLoggable(Level.FINEST)) {
-		    logger.log(Level.FINEST,
-			"clientID={0}, sequenceNum={1}, ids={2}, strong={3}",
-			new Object[] {
-			    clientID, new Long(sequenceNum),
-			    Arrays.asList(ids), Boolean.valueOf(strong)
-			});
-		}
-
-		for (int i = 0; i < ids.length; i++) {
-		    Target target = get(ids[i]);
-		    if (target != null) {
-			target.unreferenced(clientID, sequenceNum, strong);
-		    }
-		}
-	    }
-	}
+        Lease lease = leaseTable.get(clientID);
+        if (lease != null) {
+            lease.remove(target);
+        }
+    }
+
+    
+
+    private class DgcServerImpl implements DgcServer {
+        private final DgcRequestDispatcher dgcRequestDispatcher;
+        
+        DgcServerImpl(DgcRequestDispatcher dgcRequestDispatcher){
+            this.dgcRequestDispatcher = dgcRequestDispatcher;
+        }
+
+        public long dirty(Uuid clientID,
+                          long sequenceNum,
+                          Uuid[] ids)
+        {
+            if (logger.isLoggable(Level.FINEST)) {
+                logger.log(Level.FINEST,
+                    "clientID={0}, sequenceNum={1}, ids={2}",
+                    new Object[] {
+                        clientID, new Long(sequenceNum), Arrays.asList(ids)
+                    });
+            }
+
+            long duration = Jeri.leaseValue;
+
+            Lease lease = leaseTable.get(clientID);
+            if (lease == null) {
+                lease = new Lease(clientID, duration);
+                Lease existed = leaseTable.putIfAbsent(clientID,lease);
+                if (existed != null){
+                    assert clientID.equals(existed.getClientID());
+                    boolean renewed = existed.renew(duration);
+                    if (!renewed){
+                        /* The probability of getting here is low,
+                         * it indicates a lease with an extremely short 
+                         * expiry and a very small lease table.
+                         */               
+                        if (logger.isLoggable(Level.WARNING)) {
+                            logger.log(Level.WARNING,
+                                "Problem with lease table, try a longer " +
+                                "lease duration clientID={0}, " +
+                                "sequenceNum={1}, ids={2}",
+                                new Object[] {
+                                    clientID, new Long(sequenceNum), Arrays.asList(ids)
+                                });
+                        }                          
+                    }
+                }
+            } else {
+                assert clientID.equals(lease.getClientID());
+                boolean renewed = lease.renew(duration);
+                if (!renewed){
+                    // Indicates an expired lease in the table.  A lease
+                    // always becomes expired prior to removal, it is 
+                    // never removed prior to expiry, in case it is
+                    // renewed by another thread, which would risk a renewed
+                    // lease being removed from the table.  
+                    // An expired lease must be replaced.
+                    leaseTable.remove(clientID, lease); // Another thread could remove it
first.
+                    lease = new Lease(clientID, duration);
+                    Lease existed = leaseTable.putIfAbsent(clientID, lease);
+                    if (existed != null){
+                        lease = existed;
+                        assert clientID.equals(lease.getClientID());
+                        renewed = lease.renew(duration);
+                        if (!renewed){
+                            /* The probability of getting here is low,
+                             * it indicates a lease of extremely short 
+                             * duration and a very small lease table.
+                             */               
+                            if (logger.isLoggable(Level.WARNING)) {
+                                logger.log(Level.WARNING,
+                                    "Problem with lease table, try a longer " +
+                                    "lease duration clientID={0}, " +
+                                    "sequenceNum={1}, ids={2}",
+                                    new Object[] {
+                                        clientID, new Long(sequenceNum), Arrays.asList(ids)
+                                    });
+                            }                          
+                        }  
+                    }
+                }
+            }
+            /* FIXED: River-142: 
+             * In the server-side DGC implementation's thread that check's for
+             * lease expirations 
+             * (com.sun.jini.jeri.internal.runtime.ObjectTable.LeaseChecker.run),
+             * it checks for them while synchronized on the overall lease table,
+             * but it delays notifying the expired leases' individual registered
+             * Targets about the expirations until after it has released the
+             * lease table lock. This approach was taken from the 
+             * JRMP implementation, which is that way because of the fix 
+             * for 4118056 (a previous deadlock bug-- but now, I'm thinking 
+             * that the JRMP implementation has this bug too).
+             *
+             * The problem seems to be that after releasing the lease table 
+             * lock, it is possible for another lease renewal/request to 
+             * come in (from the same DGC client and for the same remote object)
+             * that would then be invalidated by the subsequent Target 
+             * notification made by the lease expiration check thread-- and 
+             * thus the client's lease renewal (for that remote object) will 
+             * be forgotten. It would appear that the synchronization approach 
+             * here needs to be reconsidered.
+             * 
+             * ( Comments note: )
+             * In addition to the basic problem of the expired-then-renewed 
+             * client being removed from the referenced set, there is also 
+             * the problem of the sequence table entry being forgotten-- which 
+             * prevents detection of a "late clean call".
+             * Normally, late clean calls are not a problem because sequence 
+             * numbers are retained while the client is in the referenced set 
+             * (and there is no such thing as a "strong dirty"). 
+             * But in this case, with the following order of events on 
+             * the server side:
+             *
+             *   1. dirty, seqNo=2
+             *   2. (lease expiration)
+             *   3. clean, seqNo=1
+             *
+             * The primary bug here is that the first two events will leave 
+             * the client missing from the referenced set. But the secondary 
+             * bug is that even if that's fixed, with the sequence number 
+             * forgotten, the third event (the "late clean call") will still 
+             * cause the client to be removed from the referenced set.
+             * 
+             * FIX:
+             * This issue has been fixed by making the Lease responsible for
+             * it's own state, which is protected internally by synchronization.
+             * The lease checker passes the time to the Lease, which checks 
+             * itself and notifies the Targets in the event of expiry.
+             * 
+             * Because the notification is not delayed, the client id and
+             * sequence number will not be not be removed by the LeaseChecker
+             * thread after being updated by the second dirty call (when
+             * the lock was cleared as described).
+             * 
+             * The client id and sequence number are added to the Target sequence
+             * table by the second dirty call, after the Lease removes
+             * them immediately upon expiry being invoked by the LeaseChecker.
+             * 
+             * Then because the late clean call sequence number is less than the 
+             * second dirty call and exists, it is correctly recognised.
+             */
+            synchronized (running){
+                if (!running) {
+                    leaseChecker =
+                        (Thread) AccessController.doPrivileged(
+                            new NewThreadAction(new LeaseChecker(),
+                                "DGC Lease Checker", true));
+                    leaseChecker.start();
+                }
+            }
+            for (int i = 0; i < ids.length; i++) {
+                Target target = dgcRequestDispatcher.get(ids[i]);
+                if (target != null) {
+                    target.referenced(clientID, sequenceNum);
+                }
+            }
+            return duration;
+        }
+
+        public void clean(Uuid clientID,
+                          long sequenceNum,
+                          Uuid[] ids,
+                          boolean strong)
+        {
+            if (logger.isLoggable(Level.FINEST)) {
+                logger.log(Level.FINEST,
+                    "clientID={0}, sequenceNum={1}, ids={2}, strong={3}",
+                    new Object[] {
+                        clientID, new Long(sequenceNum),
+                        Arrays.asList(ids), Boolean.valueOf(strong)
+                    });
+            }
+
+            for (int i = 0; i < ids.length; i++) {
+                Target target = dgcRequestDispatcher.get(ids[i]);
+                if (target != null) {
+                    target.unreferenced(clientID, sequenceNum, strong);
+                }
+            }
+        }
     }
 
     private class LeaseChecker implements Runnable {
 
 	public void run() {
 	    boolean done = false;
-	    do {
-		try {
-		    Thread.sleep(Jeri.leaseCheckInterval);
-		} catch (InterruptedException e) {
-		    // REMIND: shouldn't happen, OK to ignore?
-		}
-
-		long now = System.currentTimeMillis();
-
-		Collection expiredLeases = new ArrayList();
-
-		synchronized (leaseTable) {
-		    for (Iterator i = leaseTable.values().iterator();
-			 i.hasNext();)
-		    {
-			Lease lease = (Lease) i.next();
-			if (lease.hasExpired(now)) {
-			    expiredLeases.add(lease);
-			    i.remove();
-			}
-		    }
-
-		    if (leaseTable.isEmpty()) {
-			leaseChecker = null;
-			done = true;
-		    }
-		}
-
-		if (expiredLeases.isEmpty()) {
-		    continue;
-		}
-
-		for (Iterator i = expiredLeases.iterator(); i.hasNext();) {
-		    Lease lease = (Lease) i.next();
-		    if (lease.notifySet.isEmpty()) {
-			continue;
-		    }
-
-		    for (Iterator i2 = lease.notifySet.iterator();
-			 i2.hasNext();)
-		    {
-			Target target = (Target) i2.next();
-			target.leaseExpired(lease.getClientID());
-		    }
-		}
-	    } while (!done);
-	}
-    }
-
-    private static class Lease {
-
-	private final Uuid clientID;
-	final Set notifySet = new HashSet(3);	// guarded?
-	private long expiration;		// guarded by leaseTable lock
-
-	Lease(Uuid clientID, long duration) {
-	    this.clientID = clientID;
-	    expiration = System.currentTimeMillis() + duration;
-	}
-
-	Uuid getClientID() {
-	    return clientID;
-	}
-
-	void renew(long duration) {
-	    long newExpiration = System.currentTimeMillis() + duration;
-	    if (newExpiration > expiration) {
-		expiration = newExpiration;
-	    }
-	}
-
-	boolean hasExpired(long now) {
-	    return expiration < now;
+            try {
+                do {
+                    Thread.sleep(Jeri.leaseCheckInterval);
+                    long now = System.currentTimeMillis();		
+                    for (Iterator i = leaseTable.values().iterator();
+                         i.hasNext();)
+                    {
+                        Lease lease = (Lease) i.next();
+                        boolean expired = lease.notifyIfExpired(now);
+                        if (expired) {			    
+                            i.remove();
+                        }
+                    }
+                    if (leaseTable.isEmpty()) {
+                        done = true;
+                    }		
+                } while (!done);
+            } catch (InterruptedException e) {
+                // REMIND: shouldn't happen, OK to ignore?
+                // No, restore the interrupted status
+                 Thread.currentThread().interrupt();
+            } finally {
+                // This is always executed and returns the lease checker
+                // to the non running state, such that if the application
+                // has not exited, another thread will be started eventually.
+                synchronized (running){
+                    leaseChecker = null;
+                    running = Boolean.FALSE;               
+                }
+            }
 	}
     }
 
-    private static class NoSuchObject extends Exception { }
+    static class NoSuchObject extends Exception { }
 }

Modified: river/jtsk/tags/2.2.0/src/manifest/jsk-resources/META-INF/services/net.jini.config.Configuration
URL: http://svn.apache.org/viewvc/river/jtsk/tags/2.2.0/src/manifest/jsk-resources/META-INF/services/net.jini.config.Configuration?rev=1137621&r1=1137620&r2=1137621&view=diff
==============================================================================
--- river/jtsk/tags/2.2.0/src/manifest/jsk-resources/META-INF/services/net.jini.config.Configuration
(original)
+++ river/jtsk/tags/2.2.0/src/manifest/jsk-resources/META-INF/services/net.jini.config.Configuration
Mon Jun 20 13:09:22 2011
@@ -15,4 +15,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #*/
-com.sun.jini.config.GroovyConfig
+net.jini.config.GroovyConfig

Modified: river/jtsk/tags/2.2.0/src/net/jini/config/package.html
URL: http://svn.apache.org/viewvc/river/jtsk/tags/2.2.0/src/net/jini/config/package.html?rev=1137621&r1=1137620&r2=1137621&view=diff
==============================================================================
--- river/jtsk/tags/2.2.0/src/net/jini/config/package.html (original)
+++ river/jtsk/tags/2.2.0/src/net/jini/config/package.html Mon Jun 20 13:09:22 2011
@@ -59,6 +59,10 @@ The {@link net.jini.config.EmptyConfigur
 instance of this class to simplify handling cases where no configuration
 is specified rather than, for example, checking for a <code>null</code>
 configuration. <p>
+    
+The net.jini.config.GroovyConfig class is a new configuration provider,
+it uses Groovy configuration objects, this is potentially the most
+powerful configuration mechanism. <p>
 
 <a name="Usage"><h2>Using Configuration</h2></a>
 

Modified: river/jtsk/tags/2.2.0/src/net/jini/jeri/connection/ConnectionManager.java
URL: http://svn.apache.org/viewvc/river/jtsk/tags/2.2.0/src/net/jini/jeri/connection/ConnectionManager.java?rev=1137621&r1=1137620&r2=1137621&view=diff
==============================================================================
--- river/jtsk/tags/2.2.0/src/net/jini/jeri/connection/ConnectionManager.java (original)
+++ river/jtsk/tags/2.2.0/src/net/jini/jeri/connection/ConnectionManager.java Mon Jun 20 13:09:22
2011
@@ -106,6 +106,10 @@ import net.jini.jeri.OutboundRequestIter
  * milliseconds to leave idle client-side connections around before
  * closing them. The default value is 15000 milliseconds (15 seconds).
  *
+ * <li><code>com.sun.jini.jeri.handshakeTimeout</code> - Time in
+ * milliseconds for client-side connections to wait for the server to
+ * acknowledge an opening handshake. The default value is 15000 milliseconds (15 seconds).
+ *
  * </ul>
  **/
 public final class ConnectionManager {
@@ -113,10 +117,17 @@ public final class ConnectionManager {
      * How long to leave idle muxes around before closing them.
      */
     private static final long TIMEOUT =
-	((Long) AccessController.doPrivileged(new GetLongAction(
+	( (Long) AccessController.doPrivileged(new GetLongAction(
 		"com.sun.jini.jeri.connectionTimeout", 
 		15000))).longValue();
     /**
+     * How long to wait for a server to respond to an initial client message.
+     */
+    private static final long HANDSHAKE_TIMEOUT =
+	((Long) AccessController.doPrivileged(new GetLongAction(
+		"com.sun.jini.jeri.handshakeTimeout", 
+		15000))).longValue();
+    /**
      * ConnectionManager logger.
      */
     private static final Logger logger =
@@ -278,6 +289,7 @@ public final class ConnectionManager {
 	try {
 	    mux = (c.getChannel() == null) ?
 		new OutboundMux(c) : new OutboundMux(c, true);
+                mux.setStartTimeout(HANDSHAKE_TIMEOUT);
 	} finally {
 	    if (mux == null) {
 		try {

Modified: river/jtsk/tags/2.2.0/src/net/jini/loader/pref/PreferredClassProvider.java
URL: http://svn.apache.org/viewvc/river/jtsk/tags/2.2.0/src/net/jini/loader/pref/PreferredClassProvider.java?rev=1137621&r1=1137620&r2=1137621&view=diff
==============================================================================
--- river/jtsk/tags/2.2.0/src/net/jini/loader/pref/PreferredClassProvider.java (original)
+++ river/jtsk/tags/2.2.0/src/net/jini/loader/pref/PreferredClassProvider.java Mon Jun 20
13:09:22 2011
@@ -43,6 +43,8 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 import java.util.StringTokenizer;
 import java.util.WeakHashMap;
@@ -274,7 +276,7 @@ public class PreferredClassProvider exte
      * references, so this table does not prevent loaders from being
      * garbage collected.
      */
-    private final Map loaderTable = new HashMap();
+    private final Map<LoaderKey,LoaderEntryHolder> loaderTable = new HashMap<LoaderKey,LoaderEntryHolder>();
 
     /** reference queue for cleared class loader entries */
     private final ReferenceQueue refQueue = new ReferenceQueue();
@@ -1556,31 +1558,55 @@ public class PreferredClassProvider exte
 	 * }
 	 */
 
+	/*
+	 * Take this opportunity to remove from the table entries
+	 * whose weak references have been cleared.
+	 */
+	List<LoaderKey> toRemove = new LinkedList<LoaderKey>();
+	Object ref;
+	while ((ref = refQueue.poll()) != null) {
+	    if (ref instanceof LoaderKey)
+		toRemove.add((LoaderKey) ref);
+	    else if (ref instanceof LoaderEntry) {
+		LoaderEntry entry = (LoaderEntry) ref;
+		if (!entry.removed)	// ignore entries removed below
+		    toRemove.add(entry.key);
+	    }
+	}
+
+	LoaderKey key = new LoaderKey(urls, parent);
+	LoaderEntryHolder holder;
 	synchronized (loaderTable) {
-	    /*
-	     * Take this opportunity to remove from the table entries
-	     * whose weak references have been cleared.
-	     */
-	    Object ref;
-	    while ((ref = refQueue.poll()) != null) {
-		if (ref instanceof LoaderKey) {
-		    LoaderKey key = (LoaderKey) ref;
-		    loaderTable.remove(key);
-		} else if (ref instanceof LoaderEntry) {
-		    LoaderEntry entry = (LoaderEntry) ref;
-		    if (!entry.removed) {	// ignore entries removed below
-			loaderTable.remove(entry.key);
-		    }
-		}
+	    if (!toRemove.isEmpty()) {
+		for (LoaderKey oldKey : toRemove)
+		    loaderTable.remove(oldKey);
+		toRemove.clear();
 	    }
 
 	    /*
 	     * Look up the codebase URL path and parent class loader pair
 	     * in the table of RMI class loaders.
 	     */
-	    LoaderKey key = new LoaderKey(urls, parent);
-	    LoaderEntry entry = (LoaderEntry) loaderTable.get(key);
+	    holder = loaderTable.get(key);
+	    if (null == holder) {
+		holder = new LoaderEntryHolder();
+		loaderTable.put(key, holder);
+	    }
+	}
 
+	/*
+	 * Four possible cases:
+	 *   1) this is our first time creating this classloader
+	 *      - holder.entry is null, need to make a new entry and a new loader
+	 *   2) we made this classloader before, but it was garbage collected a long while ago
+	 *      - identical to case #1 and it was reaped by the toRemove code above
+	 *   3) we made this classloader before, and it was garbage collected recently
+	 *      - holder.entry is non-null, but holder.entry.get() is null, very similar to case
#1
+	 *   4) we made this classloader before, and it's still alive (CACHE HIT)
+	 *      - just return it
+	 */
+	synchronized (holder) {
+	    LoaderEntry entry = holder.entry;
 	    ClassLoader loader;
 	    if (entry == null ||
 		(loader = (ClassLoader) entry.get()) == null)
@@ -1593,7 +1619,6 @@ public class PreferredClassProvider exte
 		 * from the weak reference queue.
 		 */
 		if (entry != null) {
-		    loaderTable.remove(key);
 		    entry.removed = true;
 		}
 
@@ -1623,7 +1648,7 @@ public class PreferredClassProvider exte
 		 * weak reference and store it in the table with the key.
 		 */
 		entry = new LoaderEntry(key, loader);
-		loaderTable.put(key, entry);
+		holder.entry = entry;
 	    }
 	    return loader;
 	}
@@ -1737,6 +1762,9 @@ public class PreferredClassProvider exte
 	    this.key = key;
 	}
     }
+    private class LoaderEntryHolder {
+	public LoaderEntry entry;
+    }
 
     private static ClassLoader getClassLoader(final Class c) {
 	return (ClassLoader) AccessController.doPrivileged(

Modified: river/jtsk/tags/2.2.0/test/src/com/sun/jini/outrigger/FastListTest.java
URL: http://svn.apache.org/viewvc/river/jtsk/tags/2.2.0/test/src/com/sun/jini/outrigger/FastListTest.java?rev=1137621&r1=1137620&r2=1137621&view=diff
==============================================================================
--- river/jtsk/tags/2.2.0/test/src/com/sun/jini/outrigger/FastListTest.java (original)
+++ river/jtsk/tags/2.2.0/test/src/com/sun/jini/outrigger/FastListTest.java Mon Jun 20 13:09:22
2011
@@ -29,7 +29,6 @@ public class FastListTest {
     public void initialize() {
         testee = new FastList<TestNode>();
         rawTestee = new Iterable<TestNode>() {
-            @Override
             public Iterator<TestNode> iterator() {
                 return testee.rawIterator();
             }
@@ -181,7 +180,6 @@ public class FastListTest {
     private List<Thread> getAddThreads(final List<List<TestNode>> elements,
             final CyclicBarrier barrier) {
         NodeListRunnableFactory factory = new NodeListRunnableFactory() {
-            @Override
             public Runnable getRunnable(final List<TestNode> nodes) {
                 return new Runnable() {
                     public void run() {
@@ -207,7 +205,6 @@ public class FastListTest {
     private List<Thread> getRemoveThreads(final List<List<TestNode>> elements,
             final CyclicBarrier barrier) {
         NodeListRunnableFactory factory = new NodeListRunnableFactory() {
-            @Override
             public Runnable getRunnable(final List<TestNode> nodes) {
                 return new Runnable() {
                     public void run() {
@@ -233,7 +230,6 @@ public class FastListTest {
     private List<Thread> getRemoveAllThreads(final List<List<TestNode>>
elements,
             final CyclicBarrier barrier, final int tries) {
         NodeListRunnableFactory factory = new NodeListRunnableFactory() {
-            @Override
             public Runnable getRunnable(final List<TestNode> nodes) {
                 return new Runnable() {
                     public void run() {



Mime
View raw message