diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..49a62ee5ba4a59e7697cd1d8f84c6ed06fa7e772
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,31 @@
+Prerequisites to build from source:
+	Java SDK (preferably Sun) 1.5.0 or higher (1.6 recommended)
+	Apache Ant 1.7.0 or higher
+
+To build:
+	ant pkg
+	Run 'ant' with no arguments to see other build options.
+	See http://www.i2p2.de/download.html for installation instructions.
+
+Documentation:
+	http://www.i2p2.de/
+	API: run 'ant javadoc' then start at build/javadoc/index.html
+
+Latest release:
+	http://www.i2p2.de/download.html
+
+To get development branch from source control:
+	http://www.i2p2.de/newdevelopers.html
+
+FAQ:
+	http://www.i2p2.de/faq.html
+
+Need help?
+	IRC irc.freenode.net #i2p
+	http://forum.i2p2.de/
+
+Licenses:
+	http://www.i2p2.de/licenses.html
+	Also http://localhost:7657/help.jsp
+	Also see licenses for the individual bundled apps in apps/*
+
diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
index f014580ec5f5e4728e6d933fdc3f65fef630357a..26ed5860f07c30dab1d811ea2970d8bb3b34621c 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
@@ -24,6 +24,7 @@ import net.i2p.data.Destination;
 import net.i2p.data.Hash;
 import net.i2p.util.EepGet;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -183,7 +184,7 @@ public class I2PSnarkUtil {
             synchronized (_shitlist) {
                 _shitlist.add(dest);
             }
-            SimpleTimer.getInstance().addEvent(new Unshitlist(dest), 10*60*1000);
+            SimpleScheduler.getInstance().addEvent(new Unshitlist(dest), 10*60*1000);
             throw new IOException("Unable to reach the peer " + peer + ": " + ie.getMessage());
         }
     }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
index 8fed9577ae0d82d0e78177f5362a9c4104866627..1a53c342f5d1c48493d440793ac796a955335b3b 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
@@ -28,6 +28,7 @@ import java.util.List;
 
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 class PeerConnectionOut implements Runnable
@@ -215,7 +216,7 @@ class PeerConnectionOut implements Runnable
   private void addMessage(Message m)
   {
     if (m.type == Message.PIECE)
-      SimpleTimer.getInstance().addEvent(new RemoveTooSlow(m), SEND_TIMEOUT);
+      SimpleScheduler.getInstance().addEvent(new RemoveTooSlow(m), SEND_TIMEOUT);
     synchronized(sendQueue)
       {
         sendQueue.add(m);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
index 1b4feee75f5c8d6b1997d0984624c41b9a054505..054b58262b3ed013db52214738a6f76766cad0e5 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
@@ -60,7 +60,7 @@ class PeerState
   // If we have te resend outstanding requests (true after we got choked).
   private boolean resend = false;
 
-  private final static int MAX_PIPELINE = 2;               // this is for outbound requests
+  private final static int MAX_PIPELINE = 3;               // this is for outbound requests
   private final static int MAX_PIPELINE_BYTES = 128*1024;  // this is for inbound requests
   public final static int PARTSIZE = 32*1024; // Snark was 16K, i2p-bt uses 64KB
   private final static int MAX_PARTSIZE = 64*1024; // Don't let anybody request more than this
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index 7e5cd962f013aa8c8415a83858e3b72fcd912ee9..7b62ace8435a92fa3930300ef709b3df5b199247 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -141,7 +141,7 @@ public class SnarkManager implements Snark.CompleteListener {
         if (!_config.containsKey(PROP_I2CP_PORT))
             _config.setProperty(PROP_I2CP_PORT, "7654");
         if (!_config.containsKey(PROP_I2CP_OPTS))
-            _config.setProperty(PROP_I2CP_OPTS, "inbound.length=2 inbound.lengthVariance=0 outbound.length=2 outbound.lengthVariance=0");
+            _config.setProperty(PROP_I2CP_OPTS, "inbound.length=2 inbound.lengthVariance=0 outbound.length=2 outbound.lengthVariance=0 inbound.quantity=3 outbound.quantity=3");
         if (!_config.containsKey(PROP_EEP_HOST))
             _config.setProperty(PROP_EEP_HOST, "localhost");
         if (!_config.containsKey(PROP_EEP_PORT))
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
index c7ee93d7d35200e2ffb2b4afc4e321abab496a71..c791ad2fb1150eb872c8181e715d23cc028b72da 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -580,7 +580,7 @@ public class I2PSnarkServlet extends HttpServlet {
                 else if ("AUZV".equals(ch) || "AkZV".equals(ch) || "A0ZV".equals(ch))
                     client = "Robert";
                 else
-                    client = "Unknown";
+                    client = "Unknown (" + ch + ')';
                 out.write("<font size=-1>" + client + "</font>&nbsp;&nbsp;<tt>" + peer.toString().substring(5, 9) + "</tt>");
                 if (showDebug)
                     out.write(" inactive " + (peer.getInactiveTime() / 1000) + "s");
diff --git a/apps/i2ptunnel/java/build.xml b/apps/i2ptunnel/java/build.xml
index 635cad2e44e735eba9a77aaf970c089c91e09380..564f6fc4b85b3e75aad91dcf58ec90e882e88291 100644
--- a/apps/i2ptunnel/java/build.xml
+++ b/apps/i2ptunnel/java/build.xml
@@ -42,7 +42,7 @@
     </target>
     <target name="war" depends="precompilejsp"> 
         <war destfile="build/i2ptunnel.war" webxml="../jsp/web-out.xml"
-             basedir="../jsp/" excludes="web.xml, *.java, *.jsp">
+             basedir="../jsp/" excludes="web.xml, **/*.java, *.jsp">
         </war>
     </target>
     <target name="precompilejsp" unless="precompilejsp.uptodate">
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
index d6e5bf9f93339c6f08bf4e1047d3b5643137ebeb..38311eaf1de2c7175a41530e9844b7c6462d74ed 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
@@ -27,6 +27,7 @@ import net.i2p.data.Destination;
 import net.i2p.util.EventDispatcher;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runnable {
@@ -401,7 +402,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
         }
         
         if (_maxWaitTime > 0)
-            SimpleTimer.getInstance().addEvent(new CloseEvent(s), _maxWaitTime);
+            SimpleScheduler.getInstance().addEvent(new CloseEvent(s), _maxWaitTime);
 
         synchronized (_waitingSockets) {
             _waitingSockets.add(s);
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
index 252d4e1aa8e3b55ff6228e473829a9fdc15dfb7c..38c50f2661fdfa57ee0b3eb519c88a1a7abcb445 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
@@ -361,24 +361,24 @@ public class SOCKS5Server extends SOCKSServer {
     /*
      * Some namespaces to enclose SOCKS protocol codes
      */
-    private class Method {
+    private static class Method {
         private static final int NO_AUTH_REQUIRED = 0x00;
         private static final int NO_ACCEPTABLE_METHODS = 0xff;
     }
 
-    private class AddressType {
+    private static class AddressType {
         private static final int IPV4 = 0x01;
         private static final int DOMAINNAME = 0x03;
         private static final int IPV6 = 0x04;
     }
 
-    private class Command {
+    private static class Command {
         private static final int CONNECT = 0x01;
         private static final int BIND = 0x02;
         private static final int UDP_ASSOCIATE = 0x03;
     }
 
-    private class Reply {
+    private static class Reply {
         private static final int SUCCEEDED = 0x00;
         private static final int GENERAL_SOCKS_SERVER_FAILURE = 0x01;
         private static final int CONNECTION_NOT_ALLOWED_BY_RULESET = 0x02;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigAdvancedHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigAdvancedHelper.java
index c901948604b90739b5fd17da2a33d791f30c34d6..3ab6354a6362fba67c97d14e470e41dab077264f 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigAdvancedHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigAdvancedHelper.java
@@ -6,22 +6,7 @@ import java.util.TreeSet;
 
 import net.i2p.router.RouterContext;
 
-public class ConfigAdvancedHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-    
+public class ConfigAdvancedHelper extends HelperBase {
     public ConfigAdvancedHelper() {}
     
     public String getSettings() {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java
index d6441736a7ded763d0d73ea96e5b9b59a2b67581..2bee435335d43ac409cc748ca73ec9cdd9c7389b 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java
@@ -9,22 +9,7 @@ import java.util.TreeSet;
 import net.i2p.router.RouterContext;
 import net.i2p.router.startup.ClientAppConfig;
 
-public class ConfigClientsHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-
+public class ConfigClientsHelper extends HelperBase {
     public ConfigClientsHelper() {}
     
     public String getForm1() {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java
index 48bc15068e9767b0af4029d33d2a25d7b719bdec..85c8ee42314830c32a04582ccd8b7ba4ce20f42b 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java
@@ -6,22 +6,7 @@ import java.io.OutputStreamWriter;
 
 import net.i2p.router.RouterContext;
 
-public class ConfigKeyringHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-
+public class ConfigKeyringHelper extends HelperBase {
     public ConfigKeyringHelper() {}
     
     public String getSummary() {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java
index 07acb0849ed0f9d52cadde105398218a21103658..635d2e5445d3385f02dd6cc50f27f5bf027d0a47 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java
@@ -6,22 +6,7 @@ import java.util.TreeSet;
 
 import net.i2p.router.RouterContext;
 
-public class ConfigLoggingHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-    
+public class ConfigLoggingHelper extends HelperBase {
     public ConfigLoggingHelper() {}
     
     public String getLogFilePattern() {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java
index d355e9d61c2026d1454e2180e66a771fa6382590..9beeb33cf500b7183f0a9fd8237e74b3e0c5b5e8 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java
@@ -10,22 +10,7 @@ import net.i2p.router.transport.udp.UDPAddress;
 import net.i2p.router.transport.udp.UDPTransport;
 import net.i2p.time.Timestamper;
 
-public class ConfigNetHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-
+public class ConfigNetHelper extends HelperBase {
     public ConfigNetHelper() {}
     
     /** copied from various private components */
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHelper.java
index 63fc1f5e512e22528e47db7f15fe6a34ab6c1e40..662a078b8140fb066c63658e29357a779ee1e194 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHelper.java
@@ -4,25 +4,9 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 
-import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
 
-public class ConfigPeerHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-
+public class ConfigPeerHelper extends HelperBase {
     public ConfigPeerHelper() {}
     
     public String getBlocklistSummary() {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHelper.java
index 925fce79acadf7054b828cd960f84051c3ec7c0a..3af4ffafb5f9c0e519c3f99c01371b3b8672d801 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHelper.java
@@ -15,8 +15,7 @@ import net.i2p.stat.RateStat;
 import net.i2p.stat.StatManager;
 import net.i2p.util.Log;
 
-public class ConfigStatsHelper {
-    private RouterContext _context;
+public class ConfigStatsHelper extends HelperBase {
     private Log _log;
     private String _filter;
     private Set _filters;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java
index 98141771b71a18987eeecee4bf56c4181760925d..e21f9d9ce77305168ae8690bae51cfc370dc1cd4 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java
@@ -8,22 +8,7 @@ import net.i2p.data.Destination;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelPoolSettings;
 
-public class ConfigTunnelsHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-
+public class ConfigTunnelsHelper extends HelperBase {
     public ConfigTunnelsHelper() {}
     
     
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
index 68e2ec5b9d377c68e8edc8720917578f813e0ea6..818b748a7b534323f3f88418971257a4dee91648 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
@@ -51,7 +51,7 @@ public class ConfigUpdateHandler extends FormHandler {
                 if ( (_updatePolicy == null) || (!_updatePolicy.equals("notify")) )
                     addFormNotice("Update available, attempting to download now");
                 else
-                    addFormNotice("Update available, click link on left to download");
+                    addFormNotice("Update available, click button on left to download");
             } else
                 addFormNotice("No update available");
         }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java
index 0ecaca4f01a9fd8c152f10053f87647e4558975b..d0d2437994d567cafbf58106fa0f6f795888ce14 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java
@@ -4,22 +4,7 @@ import net.i2p.crypto.TrustedUpdate;
 import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
 
-public class ConfigUpdateHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-
+public class ConfigUpdateHelper extends HelperBase {
     public ConfigUpdateHelper() {}
     
     public boolean updateAvailable() {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java
index edacfaa41793da3f00b0fce3373758b7d05fefc0..ce29250b9fbdad83dca8d8c825fe88baa3435cf8 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java
@@ -6,25 +6,11 @@ import java.util.Locale;
 import net.i2p.router.RouterContext;
 import net.i2p.util.FileUtil;
 
-public class ContentHelper {
+public class ContentHelper extends HelperBase {
     private String _page;
     private int _maxLines;
     private boolean _startAtBeginning;
     private String _lang;
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
     
     public ContentHelper() {}
     
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java
index 658caa1a309d8ee27841bb9c91b16e3b4f6a2903..16ce7337d29b4a64f6c9baa0d1cb731864945596 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java
@@ -11,27 +11,12 @@ import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
 import net.i2p.stat.Rate;
 
-public class GraphHelper {
-    private RouterContext _context;
-    private Writer _out;
+public class GraphHelper extends HelperBase {
     private int _periodCount;
     private boolean _showEvents;
     private int _width;
     private int _height;
     private int _refreshDelaySeconds;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
     
     public GraphHelper() {
         _periodCount = 60; // SummaryListener.PERIODS;
@@ -41,7 +26,6 @@ public class GraphHelper {
         _refreshDelaySeconds = 60;
     }
     
-    public void setOut(Writer out) { _out = out; }
     public void setPeriodCount(String str) { 
         try { _periodCount = Integer.parseInt(str); } catch (NumberFormatException nfe) {}
     }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java b/apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..db5aa9ba249f9f7f35a3ac62ae53c214f0797732
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java
@@ -0,0 +1,29 @@
+package net.i2p.router.web;
+
+import java.io.Writer;
+
+import net.i2p.router.RouterContext;
+
+/**
+ * Base helper
+ */
+public abstract class HelperBase {
+    protected RouterContext _context;
+    protected Writer _out;
+
+    /**
+     * Configure this bean to query a particular router context
+     *
+     * @param contextId begging few characters of the routerHash, or null to pick
+     *                  the first one we come across.
+     */
+    public void setContextId(String contextId) {
+        try {
+            _context = ContextHelper.getContext(contextId);
+        } catch (Throwable t) {
+            t.printStackTrace();
+        }
+    }
+
+    public void setWriter(Writer out) { _out = out; }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/JobQueueHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/JobQueueHelper.java
index a56cce19afd03d31813b815168b5552cab0441cc..cf8ed23520e12ce2d4c85209533e543e96263260 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/JobQueueHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/JobQueueHelper.java
@@ -7,27 +7,9 @@ import java.io.Writer;
 
 import net.i2p.router.RouterContext;
 
-public class JobQueueHelper {
-    private RouterContext _context;
-    private Writer _out;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-    
+public class JobQueueHelper extends HelperBase {
     public JobQueueHelper() {}
     
-    public void setWriter(Writer writer) { _out = writer; }
-    
     public String getJobQueueSummary() {
         try {
             if (_out != null) {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
index 67d2fc38c6a3a59edce8f75cdcb589f1354efecc..e1fce8f3ecc2ecc763823fd922ea4e2aca8a982a 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
@@ -5,22 +5,7 @@ import java.util.List;
 import net.i2p.router.RouterContext;
 import net.i2p.util.FileUtil;
 
-public class LogsHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-    
+public class LogsHelper extends HelperBase {
     public LogsHelper() {}
     
     public String getLogs() {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java
index a4b2125e3a8266c5183823be7d398194069610d3..2d50379f3402dac022d290471573521a11d2e79a 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java
@@ -6,22 +6,8 @@ import java.util.Map;
 
 import net.i2p.router.RouterContext;
 
-public class NavHelper {
+public class NavHelper extends HelperBase {
     private static Map _apps = new HashMap();
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
     
     public NavHelper() {}
     
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
index 114579a27c854690343ae8ba66bdb42e0552bc81..a3280ac446c4af4ad3f29c1633103762e1f73a6e 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
@@ -7,29 +7,12 @@ import java.io.Writer;
 
 import net.i2p.router.RouterContext;
 
-public class NetDbHelper {
-    private RouterContext _context;
-    private Writer _out;
+public class NetDbHelper extends HelperBase {
     private String _routerPrefix;
     private boolean _full = false;
-
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
     
     public NetDbHelper() {}
     
-    public void setWriter(Writer writer) { _out = writer; }
     public void setRouter(String r) { _routerPrefix = r; }
     public void setFull(String f) { _full = "1".equals(f); };
     
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NoticeHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NoticeHelper.java
index cd656e9e941a9b14625f5358c62428c47b666856..d5ce2b0d96ebe6de763ced489480a36428397eb8 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NoticeHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NoticeHelper.java
@@ -7,22 +7,7 @@ import net.i2p.router.RouterContext;
  * Simple helper to query the appropriate router for data necessary to render
  * any emergency notices 
  */
-public class NoticeHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-    
+public class NoticeHelper extends HelperBase {
     public String getSystemNotice() {
         if (true) return ""; // moved to the left hand nav
         if (_context.router().gracefulShutdownInProgress()) {
@@ -35,4 +20,4 @@ public class NoticeHelper {
             return "";
         }
     }
-}
\ No newline at end of file
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java
index 556367e27bcc16a58dd8a37672b65fd1d98477e7..6237183abf5fb9f035c7887be5711825babf6653 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java
@@ -8,29 +8,9 @@ import java.io.Writer;
 import net.i2p.router.RouterContext;
 import net.i2p.router.admin.StatsGenerator;
 
-public class OldConsoleHelper {
-    private RouterContext _context;
-    private Writer _out;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-    
+public class OldConsoleHelper extends HelperBase {
     public OldConsoleHelper() {}
     
-    public void setWriter(Writer writer) { 
-        _out = writer; 
-    }
-    
     public String getConsole() {
         try {
             if (_out != null) {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PeerHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/PeerHelper.java
index e5561fe1fe61166bf45fcb4fe7a325ca8fd672c5..2504067ac3a484ccf35aa28c853810acc97f5139 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PeerHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PeerHelper.java
@@ -5,28 +5,12 @@ import java.io.Writer;
 
 import net.i2p.router.RouterContext;
 
-public class PeerHelper {
-    private RouterContext _context;
-    private Writer _out;
+public class PeerHelper extends HelperBase {
     private int _sortFlags;
     private String _urlBase;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
     
     public PeerHelper() {}
     
-    public void setOut(Writer out) { _out = out; }
     public void setSort(String flags) {
         if (flags != null) {
             try {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java
index 4db1010a570d160d296da60c1cf02d907d10a195..702a63e50013d14fefd1f1a468f2253eaf9c965d 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java
@@ -6,22 +6,7 @@ import java.io.OutputStreamWriter;
 
 import net.i2p.router.RouterContext;
 
-public class ProfilesHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-    
+public class ProfilesHelper extends HelperBase {
     public ProfilesHelper() {}
     
     public String getProfileSummary() {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/StatHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/StatHelper.java
index 8b67d2622e2cea63ec565cf81a8c9b9ee36f99bb..ce6fefd5d677ab2ea860aa4d783f437d6092e5b2 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/StatHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/StatHelper.java
@@ -11,12 +11,10 @@ import net.i2p.router.RouterContext;
  * uuuugly.  dump the peer profile data if given a peer.
  *
  */
-public class StatHelper {
+public class StatHelper extends HelperBase {
     private String _peer;
-    private Writer _writer;
     
     public void setPeer(String peer) { _peer = peer; }
-    public void setWriter(Writer writer) { _writer = writer; }
     
     public String getProfile() { 
         RouterContext ctx = (RouterContext)net.i2p.router.RouterContext.listContexts().get(0);
@@ -25,10 +23,10 @@ public class StatHelper {
             Hash peer = (Hash)iter.next();
             if (peer.toBase64().startsWith(_peer)) {
                 try {
-                    WriterOutputStream wos = new WriterOutputStream(_writer);
+                    WriterOutputStream wos = new WriterOutputStream(_out);
                     ctx.profileOrganizer().exportProfile(peer, wos);
                     wos.flush();
-                    _writer.flush();
+                    _out.flush();
                     return "";
                 } catch (Exception e) {
                     e.printStackTrace();
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
index ad8e7135d5496dcd93fa768ea6457cae71f96baa..2e56e858bcbbc928f6324df3085b2274cf63c924 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
@@ -25,22 +25,7 @@ import net.i2p.stat.RateStat;
  * Simple helper to query the appropriate router for data necessary to render
  * the summary sections on the router console.  
  */
-public class SummaryHelper {
-    private RouterContext _context;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-    
+public class SummaryHelper extends HelperBase {
     /**
      * Retrieve the shortened 4 character ident for the router located within
      * the current JVM at the given context.
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/TunnelHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/TunnelHelper.java
index 4d4eba76b79ef5d466c1da2d569426c03868bd32..3cd8a96e3d03d99694679394274195758a3bed12 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/TunnelHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/TunnelHelper.java
@@ -7,27 +7,9 @@ import java.io.Writer;
 
 import net.i2p.router.RouterContext;
 
-public class TunnelHelper {
-    private RouterContext _context;
-    private Writer _out;
-    /**
-     * Configure this bean to query a particular router context
-     *
-     * @param contextId begging few characters of the routerHash, or null to pick
-     *                  the first one we come across.
-     */
-    public void setContextId(String contextId) {
-        try {
-            _context = ContextHelper.getContext(contextId);
-        } catch (Throwable t) {
-            t.printStackTrace();
-        }
-    }
-    
+public class TunnelHelper extends HelperBase {
     public TunnelHelper() {}
     
-    public void setWriter(Writer writer) { _out = writer; }
-    
     public String getTunnelSummary() {
         try {
             if (_out != null) {
diff --git a/apps/routerconsole/jsp/graphs.jsp b/apps/routerconsole/jsp/graphs.jsp
index 422bf19d67e0212ba3d4112fca7b9cb308c2d745..06807f397f3e8a6e0917b6987cf7153e18243d59 100644
--- a/apps/routerconsole/jsp/graphs.jsp
+++ b/apps/routerconsole/jsp/graphs.jsp
@@ -14,7 +14,7 @@
  <jsp:useBean class="net.i2p.router.web.GraphHelper" id="graphHelper" scope="request" />
  <jsp:setProperty name="graphHelper" property="*" />
  <jsp:setProperty name="graphHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
- <jsp:setProperty name="graphHelper" property="out" value="<%=out%>" />
+ <jsp:setProperty name="graphHelper" property="writer" value="<%=out%>" />
  <jsp:getProperty name="graphHelper" property="images" />
  <jsp:getProperty name="graphHelper" property="form" />
 </div>
diff --git a/apps/routerconsole/jsp/peers.jsp b/apps/routerconsole/jsp/peers.jsp
index a537af634ad40e5520569ce781e8e3ce157a17c4..d3b941a34b408fb718cb3344c8944e9400e2b5d8 100644
--- a/apps/routerconsole/jsp/peers.jsp
+++ b/apps/routerconsole/jsp/peers.jsp
@@ -13,7 +13,7 @@
 <div class="main" id="main">
  <jsp:useBean class="net.i2p.router.web.PeerHelper" id="peerHelper" scope="request" />
  <jsp:setProperty name="peerHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
- <jsp:setProperty name="peerHelper" property="out" value="<%=out%>" />
+ <jsp:setProperty name="peerHelper" property="writer" value="<%=out%>" />
  <jsp:setProperty name="peerHelper" property="urlBase" value="peers.jsp" />
  <jsp:setProperty name="peerHelper" property="sort" value="<%=request.getParameter("sort") != null ? request.getParameter("sort") : ""%>" />
  <jsp:getProperty name="peerHelper" property="peerSummary" />
diff --git a/apps/routerconsole/jsp/summary.jsp b/apps/routerconsole/jsp/summary.jsp
index 687ccf15ad9682d28f3400cb13099cad9dc4bd11..48f3b4fefd9e2c9dcd153cdb2d7f555105da2b46 100644
--- a/apps/routerconsole/jsp/summary.jsp
+++ b/apps/routerconsole/jsp/summary.jsp
@@ -63,11 +63,9 @@
             if (prev != null) System.setProperty("net.i2p.router.web.ReseedHandler.noncePrev", prev);
             System.setProperty("net.i2p.router.web.ReseedHandler.nonce", nonce+"");
             String uri = request.getRequestURI();
-            if (uri.indexOf('?') > 0)
-                uri = uri + "&reseedNonce=" + nonce;
-            else
-                uri = uri + "?reseedNonce=" + nonce;
-            out.print(" <a href=\"" + uri + "\">reseed</a><br />");
+            out.print("<p><form action=\"" + uri + "\" method=\"GET\">\n");
+            out.print("<input type=\"hidden\" name=\"reseedNonce\" value=\"" + nonce + "\" >\n");
+            out.print("<button type=\"submit\" >Reseed</button></form></p>\n");
         }
     }
     // If a new reseed ain't running, and the last reseed had errors, show error message
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
index 431540d46c51086a2f988152dcc6aa871fd7bea6..e493124d05c19fb4a223e34cae8a44325016dee3 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
@@ -12,6 +12,7 @@ import net.i2p.client.I2PSession;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -247,7 +248,7 @@ public class Connection {
     void sendReset() {
         if (_disconnectScheduledOn < 0) {
             _disconnectScheduledOn = _context.clock().now();
-            SimpleTimer.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT);
+            SimpleScheduler.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT);
         }
         long now = _context.clock().now();
         if (_resetSentOn + 10*1000 > now) return; // don't send resets too fast
@@ -461,7 +462,7 @@ public class Connection {
     void resetReceived() {
         if (_disconnectScheduledOn < 0) {
             _disconnectScheduledOn = _context.clock().now();
-            SimpleTimer.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT);
+            SimpleScheduler.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT);
         }
         _resetReceived = true;
         MessageOutputStream mos = _outputStream;
@@ -509,7 +510,7 @@ public class Connection {
         if (removeFromConMgr) {
             if (_disconnectScheduledOn < 0) {
                 _disconnectScheduledOn = _context.clock().now();
-                SimpleTimer.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT);
+                SimpleScheduler.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT);
             }
         }
         _connected = false;
@@ -708,7 +709,7 @@ public class Connection {
         _closeSentOn = when;
         if (_disconnectScheduledOn < 0) {
             _disconnectScheduledOn = _context.clock().now();
-            SimpleTimer.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT);
+            SimpleScheduler.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT);
         }
     }
     public long getCloseReceivedOn() { return _closeReceivedOn; }
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java
index 7d1d4827f84adcd47228da17ed492e88569ed980..a123708e4a515efd022d81ee12546620e62cd952 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java
@@ -5,6 +5,7 @@ import java.util.List;
 
 import net.i2p.I2PAppContext;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -54,7 +55,7 @@ class ConnectionHandler {
         }
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Receive new SYN: " + packet + ": timeout in " + _acceptTimeout);
-        RetransmissionTimer.getInstance().addEvent(new TimeoutSyn(packet), _acceptTimeout);
+        SimpleScheduler.getInstance().addEvent(new TimeoutSyn(packet), _acceptTimeout);
         synchronized (_synQueue) {
             _synQueue.add(packet);
             _synQueue.notifyAll();
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
index 7c445f0380140cb12199061d579da758cf260aa3..f7b245cb83fb0748f3d37e805ed1c2bd345b70b9 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
@@ -7,6 +7,7 @@ import net.i2p.I2PException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -168,7 +169,7 @@ public class ConnectionPacketHandler {
                 // take note of congestion
                 if (_log.shouldLog(Log.WARN))
                     _log.warn("congestion.. dup " + packet);
-                RetransmissionTimer.getInstance().addEvent(new AckDup(con), con.getOptions().getSendAckDelay());
+                SimpleScheduler.getInstance().addEvent(new AckDup(con), con.getOptions().getSendAckDelay());
                 //con.setNextSendTime(_context.clock().now() + con.getOptions().getSendAckDelay());
                 //fastAck = true;
             } else {
diff --git a/apps/susidns/src/build.xml b/apps/susidns/src/build.xml
index f31340954374ca54773b7196f1a3908ae6d9d28d..d3f5f1662c72284c6677379b447a60970f436753 100644
--- a/apps/susidns/src/build.xml
+++ b/apps/susidns/src/build.xml
@@ -63,12 +63,10 @@
         	<fileset dir=".">
         		<include name="WEB-INF/**/*.class"/>
         		<include name="WEB-INF/lib/*.jar"/>
-        		<include name="${src}/**/*.java"/>
         		<include name="jsp/*.jsp"/>
         		<include name="images/*.png"/>
         		<include name="css.css"/>
         		<include name="index.html"/>
-        		<include name="build.xml"/>
         		<include name="WEB-INF/web-template.xml"/>
         		<include name="WEB-INF/web-out.xml"/>
         		<include name="WEB-INF/classes/${project}.properties"/>
diff --git a/apps/susimail/build.xml b/apps/susimail/build.xml
index 2bdc4f164ed51c51cec87717b6c93d69d2576024..abf2a88cf71c2829a9ca9beeada5ad658f02e7f7 100644
--- a/apps/susimail/build.xml
+++ b/apps/susimail/build.xml
@@ -19,7 +19,7 @@
     <target name="jar" depends="compile, war" />
     <target name="war" depends="compile">
         <war destfile="susimail.war" webxml="src/WEB-INF/web.xml"
-             basedir="src/" excludes="WEB-INF/web.xml">
+             basedir="src/" excludes="WEB-INF/web.xml LICENSE src/**/*">
         </war>
     </target>
     <target name="javadoc">
diff --git a/apps/systray/java/src/net/i2p/apps/systray/SysTray.java b/apps/systray/java/src/net/i2p/apps/systray/SysTray.java
index 380c5b172091bdf92187d7e2d04ad203284f200d..4a635fd08092d23ba9ac385a4e9cc338982fbc4f 100644
--- a/apps/systray/java/src/net/i2p/apps/systray/SysTray.java
+++ b/apps/systray/java/src/net/i2p/apps/systray/SysTray.java
@@ -11,6 +11,7 @@ package net.i2p.apps.systray;
 
 import java.awt.Frame;
 
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 import snoozesoft.systray4j.SysTrayMenu;
 import snoozesoft.systray4j.SysTrayMenuEvent;
@@ -60,14 +61,13 @@ public class SysTray implements SysTrayMenuListener {
     private SysTray() {
         _sysTrayMenuIcon.addSysTrayMenuListener(this);
         createSysTrayMenu();
-        SimpleTimer.getInstance().addEvent(new RefreshDisplayEvent(), REFRESH_DISPLAY_FREQUENCY);
+        SimpleScheduler.getInstance().addPeriodicEvent(new RefreshDisplayEvent(), REFRESH_DISPLAY_FREQUENCY);
     }
     
     private static final long REFRESH_DISPLAY_FREQUENCY = 30*1000;
     private class RefreshDisplayEvent implements SimpleTimer.TimedEvent {
         public void timeReached() {
             refreshDisplay();
-            SimpleTimer.getInstance().addEvent(RefreshDisplayEvent.this, REFRESH_DISPLAY_FREQUENCY);
         }
     }
 
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java
index a57957107a8d29380cee2ce7be4c7428ab331dd0..d4ff7360a73e48ac8b91607f0c790dd115ac18b9 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl.java
@@ -40,6 +40,7 @@ import net.i2p.data.i2cp.MessagePayloadMessage;
 import net.i2p.data.i2cp.SessionId;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -369,7 +370,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
             if (_log.shouldLog(Log.INFO))
                 _log.info(getPrefix() + "Notified availability for session " + _sessionId + ", message " + id);
         }
-        SimpleTimer.getInstance().addEvent(new VerifyUsage(mid), 30*1000);
+        SimpleScheduler.getInstance().addEvent(new VerifyUsage(mid), 30*1000);
     }
     private class VerifyUsage implements SimpleTimer.TimedEvent {
         private Long _msgId;
diff --git a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java
index 1b160f8dd7cf4042bf29a21f81b6a8277290a4e9..0d71677a970a2f199a9a262dfe915528914ad9c4 100644
--- a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java
+++ b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java
@@ -24,6 +24,7 @@ import net.i2p.data.PublicKey;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SessionTag;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -70,7 +71,7 @@ class TransientSessionKeyManager extends SessionKeyManager {
         _inboundTagSets = new HashMap(1024);
         context.statManager().createRateStat("crypto.sessionTagsExpired", "How many tags/sessions are expired?", "Encryption", new long[] { 10*60*1000, 60*60*1000, 3*60*60*1000 });
         context.statManager().createRateStat("crypto.sessionTagsRemaining", "How many tags/sessions are remaining after a cleanup?", "Encryption", new long[] { 10*60*1000, 60*60*1000, 3*60*60*1000 });
-        SimpleTimer.getInstance().addEvent(new CleanupEvent(), 60*1000);
+        SimpleScheduler.getInstance().addPeriodicEvent(new CleanupEvent(), 60*1000);
     }
     private TransientSessionKeyManager() { this(null); }
     
@@ -80,7 +81,6 @@ class TransientSessionKeyManager extends SessionKeyManager {
             int expired = aggressiveExpire();
             long expireTime = _context.clock().now() - beforeExpire;
             _context.statManager().addRateData("crypto.sessionTagsExpired", expired, expireTime);
-            SimpleTimer.getInstance().addEvent(CleanupEvent.this, 60*1000);
         }
     }
 
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index 4a074f17cf9bdaefe2b59a8b90f16a48651a20a5..53e32a347e85d42c584e7bd822e87524fd17e64f 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -344,8 +344,9 @@ public class DataHelper {
 
         long rv = 0;
         for (int i = 0; i < numBytes; i++) {
-            long cur = rawStream.read() & 0xFF;
+            long cur = rawStream.read();
             if (cur == -1) throw new DataFormatException("Not enough bytes for the field");
+            cur &= 0xFF;
             // we loop until we find a nonzero byte (or we reach the end)
             if (cur != 0) {
                 // ok, data found, now iterate through it to fill the rv
@@ -355,9 +356,10 @@ public class DataHelper {
                     cur = cur << shiftAmount;
                     rv += cur;
                     if (j + 1 < remaining) {
-                        cur = rawStream.read() & 0xFF;
+                        cur = rawStream.read();
                         if (cur == -1)
                             throw new DataFormatException("Not enough bytes for the field");
+                        cur &= 0xFF;
                     }
                 }
                 break;
diff --git a/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java b/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java
index 2cd630db618c166c95afa4c2db5254953d7b765c..b5fca013d78cb4c8c22e31225666228843b8ad9c 100644
--- a/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java
@@ -156,7 +156,7 @@ public class RequestLeaseSetMessage extends I2CPMessageImpl {
         return buf.toString();
     }
 
-    private class TunnelEndpoint {
+    private static class TunnelEndpoint {
         private Hash _router;
         private TunnelId _tunnelId;
 
@@ -186,4 +186,4 @@ public class RequestLeaseSetMessage extends I2CPMessageImpl {
             _tunnelId = tunnelId;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/core/java/src/net/i2p/util/ByteCache.java b/core/java/src/net/i2p/util/ByteCache.java
index aadc721aa447143fa0d90c2d6c10f5def65e7703..4bd3da6eff517abfed251ba57deb9374d2d5126c 100644
--- a/core/java/src/net/i2p/util/ByteCache.java
+++ b/core/java/src/net/i2p/util/ByteCache.java
@@ -55,7 +55,7 @@ public final class ByteCache {
         _maxCached = maxCachedEntries;
         _entrySize = entrySize;
         _lastOverflow = -1;
-        SimpleTimer.getInstance().addEvent(new Cleanup(), CLEANUP_FREQUENCY);
+        SimpleScheduler.getInstance().addPeriodicEvent(new Cleanup(), CLEANUP_FREQUENCY);
         _log = I2PAppContext.getGlobalContext().logManager().getLog(ByteCache.class);
     }
     
@@ -120,7 +120,6 @@ public final class ByteCache {
                         _log.debug("Removing " + toRemove + " cached entries of size " + _entrySize);
                 }
             }
-            SimpleTimer.getInstance().addEvent(Cleanup.this, CLEANUP_FREQUENCY);
         }
     }
 }
diff --git a/core/java/src/net/i2p/util/SimpleScheduler.java b/core/java/src/net/i2p/util/SimpleScheduler.java
new file mode 100644
index 0000000000000000000000000000000000000000..91415102c473c56b5c4cce900a9d9b3d96c0ee52
--- /dev/null
+++ b/core/java/src/net/i2p/util/SimpleScheduler.java
@@ -0,0 +1,164 @@
+package net.i2p.util;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ThreadFactory;
+
+import net.i2p.I2PAppContext;
+
+/**
+ * Simple event scheduler - toss an event on the queue and it gets fired at the
+ * appropriate time.  The method that is fired however should NOT block (otherwise
+ * they b0rk the timer).
+ *
+ * This is like SimpleScheduler but addEvent() for an existing event adds a second
+ * job. Events cannot be cancelled or rescheduled.
+ *
+ * For events that cannot or will not be cancelled or rescheduled -
+ * for example, a call such as:
+ *       SimpleTimer.getInstance().addEvent(new FooEvent(bar), timeoutMs);
+ * use SimpleScheduler instead to reduce lock contention in SimpleTimer...
+ *
+ * For periodic events, use addPeriodicEvent(). Unlike SimpleTimer,
+ * uncaught Exceptions will not prevent subsequent executions.
+ *
+ * @author zzz
+ */
+public class SimpleScheduler {
+    private static final SimpleScheduler _instance = new SimpleScheduler();
+    public static SimpleScheduler getInstance() { return _instance; }
+    private static final int THREADS = 4;
+    private I2PAppContext _context;
+    private Log _log;
+    private ScheduledThreadPoolExecutor _executor;
+    private String _name;
+    private int _count;
+
+    protected SimpleScheduler() { this("SimpleScheduler"); }
+    protected SimpleScheduler(String name) {
+        _context = I2PAppContext.getGlobalContext();
+        _log = _context.logManager().getLog(SimpleScheduler.class);
+        _name = name;
+        _count = 0;
+        _executor = new ScheduledThreadPoolExecutor(THREADS, new CustomThreadFactory());
+    }
+    
+    /**
+     * Removes the SimpleScheduler.
+     */
+    public void stop() {
+        _executor.shutdownNow();
+    }
+
+    /**
+     * Queue up the given event to be fired no sooner than timeoutMs from now.
+     *
+     * @param event
+     * @param timeoutMs 
+     */
+    public void addEvent(SimpleTimer.TimedEvent event, long timeoutMs) {
+        if (event == null)
+            throw new IllegalArgumentException("addEvent null");
+        RunnableEvent re = new RunnableEvent(event, timeoutMs);
+        re.schedule();
+    }
+    
+    public void addPeriodicEvent(SimpleTimer.TimedEvent event, long timeoutMs) {
+        addPeriodicEvent(event, timeoutMs, timeoutMs);
+    }
+    
+    /**
+     * Queue up the given event to be fired after initialDelay and every
+     * timeoutMs thereafter. The TimedEvent must not do its own rescheduling.
+     * As all Exceptions are caught in run(), these will not prevent
+     * subsequent executions (unlike SimpleTimer, where the TimedEvent does
+     * its own rescheduling)
+     *
+     * @param event
+     * @param initialDelay (ms)
+     * @param timeoutMs 
+     */
+    public void addPeriodicEvent(SimpleTimer.TimedEvent event, long initialDelay, long timeoutMs) {
+        if (event == null)
+            throw new IllegalArgumentException("addEvent null");
+        RunnableEvent re = new PeriodicRunnableEvent(event, initialDelay, timeoutMs);
+        re.schedule();
+    }
+    
+    private class CustomThreadFactory implements ThreadFactory {
+        public Thread newThread(Runnable r) {
+            Thread rv = Executors.defaultThreadFactory().newThread(r);
+            rv.setName(_name +  ' ' + (++_count) + '/' + THREADS);
+            rv.setDaemon(true);
+            return rv;
+        }
+    }
+
+    /**
+     * Same as SimpleTimer.TimedEvent but use run() instead of timeReached(), and remembers the time
+     */
+    private class RunnableEvent implements Runnable {
+        protected SimpleTimer.TimedEvent _timedEvent;
+        protected long _scheduled;
+
+        public RunnableEvent(SimpleTimer.TimedEvent t, long timeoutMs) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Creating w/ delay " + timeoutMs + " : " + t);
+            _timedEvent = t;
+            _scheduled = timeoutMs + System.currentTimeMillis();
+        }
+        public void schedule() {
+            _executor.schedule(this, _scheduled - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
+        }
+        public void run() {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Running: " + _timedEvent);
+            long before = System.currentTimeMillis();
+            if (_log.shouldLog(Log.WARN) && before < _scheduled - 100)
+                _log.warn(_name + " wtf, early execution " + (_scheduled - before) + ": " + _timedEvent);
+            else if (_log.shouldLog(Log.WARN) && before > _scheduled + 1000)
+                _log.warn(" wtf, late execution " + (before - _scheduled) + ": " + _timedEvent + debug());
+            try {
+                _timedEvent.timeReached();
+            } catch (Throwable t) {
+                _log.log(Log.CRIT, _name + " wtf, event borked: " + _timedEvent, t);
+            }
+            long time = System.currentTimeMillis() - before;
+            if (time > 1000 && _log.shouldLog(Log.WARN))
+                _log.warn(_name + " wtf, event execution took " + time + ": " + _timedEvent);
+            long completed = _executor.getCompletedTaskCount();
+            if (_log.shouldLog(Log.INFO) && completed % 250  == 0)
+                _log.info(debug());
+        }
+    }
+
+    /** Run every timeoutMs. TimedEvent must not do its own reschedule via addEvent() */
+    private class PeriodicRunnableEvent extends RunnableEvent {
+        private long _timeoutMs;
+        private long _initialDelay;
+        public PeriodicRunnableEvent(SimpleTimer.TimedEvent t, long initialDelay, long timeoutMs) {
+            super(t, timeoutMs);
+            _initialDelay = initialDelay;
+            _timeoutMs = timeoutMs;
+            _scheduled = initialDelay + System.currentTimeMillis();
+        }
+        public void schedule() {
+            _executor.scheduleWithFixedDelay(this, _initialDelay, _timeoutMs, TimeUnit.MILLISECONDS);
+        }
+        public void run() {
+            super.run();
+            _scheduled = _timeoutMs + System.currentTimeMillis();
+        }
+    }
+
+    private String debug() {
+        return
+            " Pool: " + _name +
+            " Active: " + _executor.getActiveCount() + '/' + _executor.getPoolSize() +
+            " Completed: " + _executor.getCompletedTaskCount() +
+            " Queued: " + _executor.getQueue().size();
+    }
+}
+
diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java
index 5f686c19230f5f0b82bc837e80761b6f723c77f8..1c50eaa65ff0245aeb1fa37cc0a7afde14646717 100644
--- a/router/java/src/net/i2p/router/Blocklist.java
+++ b/router/java/src/net/i2p/router/Blocklist.java
@@ -256,7 +256,7 @@ public class Blocklist {
         }
     }
 
-    private class Entry {
+    private static class Entry {
         String comment;
         byte ip1[];
         byte ip2[];
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index f7342413afdcb0413d1d13c5962ad9451e181a75..033678924c63475de3ae6ac0d3083fc6dbdffb71 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -43,6 +43,7 @@ import net.i2p.stat.StatManager;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -257,7 +258,7 @@ public class Router {
         _context.inNetMessagePool().startup();
         startupQueue();
         //_context.jobQueue().addJob(new CoalesceStatsJob(_context));
-        SimpleTimer.getInstance().addEvent(new CoalesceStatsEvent(_context), 0);
+        SimpleScheduler.getInstance().addPeriodicEvent(new CoalesceStatsEvent(_context), 20*1000);
         _context.jobQueue().addJob(new UpdateRoutingKeyModifierJob(_context));
         warmupCrypto();
         _sessionKeyPersistenceHelper.startup();
@@ -346,7 +347,7 @@ public class Router {
             if (blockingRebuild)
                 r.timeReached();
             else
-                SimpleTimer.getInstance().addEvent(r, 0);
+                SimpleScheduler.getInstance().addEvent(r, 0);
         } catch (DataFormatException dfe) {
             _log.log(Log.CRIT, "Internal error - unable to sign our own address?!", dfe);
         }
@@ -1261,8 +1262,6 @@ class CoalesceStatsEvent implements SimpleTimer.TimedEvent {
                 getContext().statManager().addRateData("bw.sendBps", (long)KBps, 60*1000);
             }
         }
-        
-        SimpleTimer.getInstance().addEvent(this, 20*1000);
     }
 }
 
diff --git a/router/java/src/net/i2p/router/Shitlist.java b/router/java/src/net/i2p/router/Shitlist.java
index 7d86926cfabaaa4c741795c04ab07c323fc2a1bd..2005366c26356b2455a33f6ffd56ae35b77c7e7d 100644
--- a/router/java/src/net/i2p/router/Shitlist.java
+++ b/router/java/src/net/i2p/router/Shitlist.java
@@ -36,7 +36,7 @@ public class Shitlist {
     private RouterContext _context;
     private Map _entries;
     
-    private class Entry {
+    private static class Entry {
         /** when it should expire, per the i2p clock */
         long expireOn;
         /** why they were shitlisted */
diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
index 133ad142c8b040c415676c5fe9caff8b0f3a1896..189568eadd166bedad2fd3d00d37f0209395aeaa 100644
--- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
@@ -38,6 +38,7 @@ import net.i2p.router.RouterContext;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
 import net.i2p.util.RandomSource;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -419,7 +420,7 @@ public class ClientConnectionRunner {
                     // theirs is newer
                 } else {
                     // ours is newer, so wait a few secs and retry
-                    SimpleTimer.getInstance().addEvent(new Rerequest(set, expirationTime, onCreateJob, onFailedJob), 3*1000);
+                    SimpleScheduler.getInstance().addEvent(new Rerequest(set, expirationTime, onCreateJob, onFailedJob), 3*1000);
                 }
                 // fire onCreated?
                 return; // already requesting
diff --git a/router/java/src/net/i2p/router/peermanager/PeerManager.java b/router/java/src/net/i2p/router/peermanager/PeerManager.java
index b2b16a00df9dda6812696caa977cbdb9077ff0c0..1c265ee6768e49bb6f22fffb7f54000d26f144fc 100644
--- a/router/java/src/net/i2p/router/peermanager/PeerManager.java
+++ b/router/java/src/net/i2p/router/peermanager/PeerManager.java
@@ -24,6 +24,7 @@ import net.i2p.router.PeerSelectionCriteria;
 import net.i2p.router.RouterContext;
 import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -50,7 +51,7 @@ class PeerManager {
             _peersByCapability[i] = new ArrayList(64);
         loadProfiles();
         ////_context.jobQueue().addJob(new EvaluateProfilesJob(_context));
-        SimpleTimer.getInstance().addEvent(new Reorg(), 0);
+        SimpleScheduler.getInstance().addPeriodicEvent(new Reorg(), 0, 30*1000);
         //_context.jobQueue().addJob(new PersistProfilesJob(_context, this));
     }
     
@@ -60,8 +61,6 @@ class PeerManager {
                 _organizer.reorganize(true);
             } catch (Throwable t) {
                 _log.log(Log.CRIT, "Error evaluating profiles", t);
-            } finally {
-                SimpleTimer.getInstance().addEvent(Reorg.this, 30*1000);
             }
         }
     }
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d19c62498fedae7de6426558925dacd3f6739ff
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java
@@ -0,0 +1,89 @@
+package net.i2p.router.transport.ntcp;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ThreadFactory;
+
+import net.i2p.I2PAppContext;
+import net.i2p.router.OutNetMessage;
+import net.i2p.util.Log;
+
+/**
+ * Previously, NTCP was using SimpleTimer with a delay of 0, which
+ * was a real abuse.
+ *
+ * Here we use the non-scheduled, lockless ThreadPoolExecutor with
+ * a fixed pool size and an unbounded queue.
+ *
+ * The old implementation was having problems with lock contention;
+ * this should work a lot better - and not clog up the SimpleTimer queue.
+ *
+ * @author zzz
+ */
+public class NTCPSendFinisher {
+    private static final int THREADS = 4;
+    private I2PAppContext _context;
+    private NTCPTransport _transport;
+    private Log _log;
+    private int _count;
+    private ThreadPoolExecutor _executor;
+
+    public NTCPSendFinisher(I2PAppContext context, NTCPTransport transport) {
+        _context = context;
+        _log = _context.logManager().getLog(NTCPSendFinisher.class);
+        _transport = transport;
+    }
+    
+    public void start() {
+        _count = 0;
+        _executor = new CustomThreadPoolExecutor();
+    }
+
+    public void stop() {
+        _executor.shutdownNow();
+    }
+
+    public void add(OutNetMessage msg) {
+        _executor.execute(new RunnableEvent(msg));
+    }
+    
+    // not really needed for now but in case we want to add some hooks like afterExecute()
+    private class CustomThreadPoolExecutor extends ThreadPoolExecutor {
+        public CustomThreadPoolExecutor() {
+             // use unbounded queue, so maximumPoolSize and keepAliveTime have no effect
+             super(THREADS, THREADS, 1000, TimeUnit.MILLISECONDS,
+                   new LinkedBlockingQueue(), new CustomThreadFactory());
+        }
+    }
+
+    private class CustomThreadFactory implements ThreadFactory {
+        public Thread newThread(Runnable r) {
+            Thread rv = Executors.defaultThreadFactory().newThread(r);
+            rv.setName("NTCPSendFinisher " + (++_count) + '/' + THREADS);
+            rv.setDaemon(true);
+            return rv;
+        }
+    }
+
+    /**
+     * Call afterSend() for the message
+     */
+    private class RunnableEvent implements Runnable {
+        private OutNetMessage _msg;
+
+        public RunnableEvent(OutNetMessage msg) {
+            _msg = msg;
+        }
+
+        public void run() {
+            try {
+                _transport.afterSend(_msg, true, false, _msg.getSendTime());
+            } catch (Throwable t) {
+                _log.log(Log.CRIT, " wtf, afterSend borked", t);
+            }
+        }
+    }
+}
+
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
index 4b5573917eb7f0c6afba5d4ecf22f015216476cd..c23245bae41edab88fb81c2903697474fb314b41 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
@@ -27,7 +27,6 @@ import net.i2p.router.transport.Transport;
 import net.i2p.router.transport.TransportBid;
 import net.i2p.router.transport.TransportImpl;
 import net.i2p.util.Log;
-import net.i2p.util.SimpleTimer;
 
 /**
  *
@@ -50,7 +49,7 @@ public class NTCPTransport extends TransportImpl {
     private List _establishing;
 
     private List _sent;
-    private SendFinisher _finisher;
+    private NTCPSendFinisher _finisher;
     
     public NTCPTransport(RouterContext ctx) {
         super(ctx);
@@ -124,7 +123,7 @@ public class NTCPTransport extends TransportImpl {
         _conByIdent = new HashMap(64);
         
         _sent = new ArrayList(4);
-        _finisher = new SendFinisher();
+        _finisher = new NTCPSendFinisher(ctx, this);
         
         _pumper = new EventPumper(ctx, this);
         _reader = new Reader(ctx);
@@ -310,27 +309,8 @@ public class NTCPTransport extends TransportImpl {
         return countActivePeers() < getMaxConnections() * 4 / 5;
     }
 
+    /** queue up afterSend call, which can take some time w/ jobs, etc */
     void sendComplete(OutNetMessage msg) { _finisher.add(msg); }
-    /** async afterSend call, which can take some time w/ jobs, etc */
-    private class SendFinisher implements SimpleTimer.TimedEvent {
-        public void add(OutNetMessage msg) {
-            synchronized (_sent) { _sent.add(msg); }
-            SimpleTimer.getInstance().addEvent(SendFinisher.this, 0);
-        }
-        public void timeReached() {
-            int pending = 0;
-            OutNetMessage msg = null;
-            synchronized (_sent) {
-                pending = _sent.size()-1;
-                if (pending >= 0)
-                    msg = (OutNetMessage)_sent.remove(0);
-            }
-            if (msg != null)
-                afterSend(msg, true, false, msg.getSendTime());
-            if (pending > 0)
-                SimpleTimer.getInstance().addEvent(SendFinisher.this, 0);
-        }
-    }
 
     private boolean isEstablished(RouterIdentity peer) {
         return isEstablished(peer.calculateHash());
@@ -412,6 +392,7 @@ public class NTCPTransport extends TransportImpl {
     
     public RouterAddress startListening() {
         if (_log.shouldLog(Log.DEBUG)) _log.debug("Starting ntcp transport listening");
+        _finisher.start();
         _pumper.startPumping();
         
         _reader.startReading(NUM_CONCURRENT_READERS);
@@ -423,6 +404,7 @@ public class NTCPTransport extends TransportImpl {
 
     public RouterAddress restartListening(RouterAddress addr) {
         if (_log.shouldLog(Log.DEBUG)) _log.debug("Restarting ntcp transport listening");
+        _finisher.start();
         _pumper.startPumping();
         
         _reader.startReading(NUM_CONCURRENT_READERS);
@@ -551,6 +533,7 @@ public class NTCPTransport extends TransportImpl {
         _pumper.stopPumping();
         _writer.stopWriting();
         _reader.stopReading();
+        _finisher.stop();
         Map cons = null;
         synchronized (_conLock) {
             cons = new HashMap(_conByIdent);
diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
index 6ab159408cc876fe16d1f877bb508a3ad8c4c5bb..896fe1ce435d26f84ba2752db5d04adcf0743b0a 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -22,6 +22,7 @@ import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -184,7 +185,7 @@ public class EstablishmentManager {
                                                        msg.getTarget().getIdentity(), 
                                                        new SessionKey(addr.getIntroKey()), addr);
                     _outboundStates.put(to, state);
-                    SimpleTimer.getInstance().addEvent(new Expire(to, state), 10*1000);
+                    SimpleScheduler.getInstance().addEvent(new Expire(to, state), 10*1000);
                 }
             }
             if (state != null) {
@@ -394,7 +395,7 @@ public class EstablishmentManager {
                                                msg.getTarget().getIdentity(), 
                                                new SessionKey(addr.getIntroKey()), addr);
             _outboundStates.put(to, qstate);
-            SimpleTimer.getInstance().addEvent(new Expire(to, qstate), 10*1000);
+            SimpleScheduler.getInstance().addEvent(new Expire(to, qstate), 10*1000);
 
             for (int i = 0; i < queued.size(); i++) {
                 OutNetMessage m = (OutNetMessage)queued.get(i);
@@ -477,7 +478,7 @@ public class EstablishmentManager {
         dsm.setMessageExpiration(_context.clock().now()+10*1000);
         dsm.setMessageId(_context.random().nextLong(I2NPMessage.MAX_ID_VALUE));
         _transport.send(dsm, peer);
-        SimpleTimer.getInstance().addEvent(new PublishToNewInbound(peer), 0);
+        SimpleScheduler.getInstance().addEvent(new PublishToNewInbound(peer), 0);
     }
     private class PublishToNewInbound implements SimpleTimer.TimedEvent {
         private PeerState _peer;
@@ -629,7 +630,7 @@ public class EstablishmentManager {
                 }
             }
         }
-        SimpleTimer.getInstance().addEvent(new FailIntroduction(state, nonce), INTRO_ATTEMPT_TIMEOUT);
+        SimpleScheduler.getInstance().addEvent(new FailIntroduction(state, nonce), INTRO_ATTEMPT_TIMEOUT);
         state.setIntroNonce(nonce);
         _context.statManager().addRateData("udp.sendIntroRelayRequest", 1, 0);
         UDPPacket requests[] = _builder.buildRelayRequest(_transport, state, _transport.getIntroKey());
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
index 7aa3c2fa164ae4afb773729ed6bd6d6f1115a28b..35c5511be4be4bc994ced8a0f24ba4408ad3b039 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
@@ -15,6 +15,7 @@ import net.i2p.data.SessionKey;
 import net.i2p.router.CommSystemFacade;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -79,7 +80,7 @@ class PeerTestManager {
         
         sendTestToBob();
         
-        SimpleTimer.getInstance().addEvent(new ContinueTest(), RESEND_TIMEOUT);
+        SimpleScheduler.getInstance().addEvent(new ContinueTest(), RESEND_TIMEOUT);
     }
     
     private class ContinueTest implements SimpleTimer.TimedEvent {
@@ -103,7 +104,7 @@ class PeerTestManager {
                     // second message from Charlie yet
                     sendTestToCharlie();
                 }
-                SimpleTimer.getInstance().addEvent(ContinueTest.this, RESEND_TIMEOUT);
+                SimpleScheduler.getInstance().addEvent(ContinueTest.this, RESEND_TIMEOUT);
             }
         }
     }
@@ -430,7 +431,7 @@ class PeerTestManager {
                 synchronized (_activeTests) {
                     _activeTests.put(new Long(nonce), state);
                 }
-                SimpleTimer.getInstance().addEvent(new RemoveTest(nonce), MAX_CHARLIE_LIFETIME);
+                SimpleScheduler.getInstance().addEvent(new RemoveTest(nonce), MAX_CHARLIE_LIFETIME);
             }
 
             UDPPacket packet = _packetBuilder.buildPeerTestToBob(bobIP, from.getPort(), aliceIP, alicePort, aliceIntroKey, nonce, state.getBobCipherKey(), state.getBobMACKey());
@@ -511,7 +512,7 @@ class PeerTestManager {
                 synchronized (_activeTests) {
                     _activeTests.put(new Long(nonce), state);
                 }
-                SimpleTimer.getInstance().addEvent(new RemoveTest(nonce), MAX_CHARLIE_LIFETIME);
+                SimpleScheduler.getInstance().addEvent(new RemoveTest(nonce), MAX_CHARLIE_LIFETIME);
             }
             
             UDPPacket packet = _packetBuilder.buildPeerTestToCharlie(aliceIP, from.getPort(), aliceIntroKey, nonce, 
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java
index 10876a0e7df37873efa12100487a0f4bb66f799b..3535484c9f4d8df1f13b6e546d96b7f13e8443de 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java
@@ -9,6 +9,7 @@ import net.i2p.router.RouterContext;
 import net.i2p.router.transport.FIFOBandwidthLimiter;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -115,7 +116,7 @@ public class UDPReceiver {
             long delay = ARTIFICIAL_DELAY_BASE + _context.random().nextInt(ARTIFICIAL_DELAY);
             if (_log.shouldLog(Log.INFO))
                 _log.info("Delay packet " + packet + " for " + delay);
-            SimpleTimer.getInstance().addEvent(new ArtificiallyDelayedReceive(packet), delay);
+            SimpleScheduler.getInstance().addEvent(new ArtificiallyDelayedReceive(packet), delay);
             return -1;
         }
         
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
index 03714d7ef2c53ba64d76badb57b8693c8040eee8..e5185defaf28f50b3975416dbf8d658a4ac981dc 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -33,6 +33,7 @@ import net.i2p.router.transport.Transport;
 import net.i2p.router.transport.TransportBid;
 import net.i2p.router.transport.TransportImpl;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -631,7 +632,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                     }
                     if (added) {
                         _context.statManager().addRateData("udp.dropPeerDroplist", droplistSize, 0);
-                        SimpleTimer.getInstance().addEvent(new RemoveDropList(remote), DROPLIST_PERIOD);
+                        SimpleScheduler.getInstance().addEvent(new RemoveDropList(remote), DROPLIST_PERIOD);
                     }
                 }
                 markUnreachable(peerHash);
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
index 390fe888dcc2ca829b974db6543edb9d1db55bdc..3a84f48105159cd1905df7334d2492c64c89946a 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
@@ -46,6 +46,7 @@ class BuildExecutor implements Runnable {
         _context.statManager().createRateStat("tunnel.buildRequestTime", "How long it takes to build a tunnel request", "Tunnels", new long[] { 60*1000, 10*60*1000 });
         _context.statManager().createRateStat("tunnel.buildRequestZeroHopTime", "How long it takes to build a zero hop tunnel", "Tunnels", new long[] { 60*1000, 10*60*1000 });
         _context.statManager().createRateStat("tunnel.pendingRemaining", "How many inbound requests are pending after a pass (period is how long the pass takes)?", "Tunnels", new long[] { 60*1000, 10*60*1000 });
+        _context.statManager().createRateStat("tunnel.buildFailFirstHop", "How often we fail to build a OB tunnel because we can't contact the first hop", "Tunnels", new long[] { 60*1000, 10*60*1000 });
 
         // Get stat manager, get recognized bandwidth tiers
         StatManager statMgr = _context.statManager();
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
index 3c2a9dd204af3018fedf8bb54df98434f393e2bf..f6b4d3478682a3c24bdc9cb2d50408eb4a772ae0 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
@@ -61,6 +61,7 @@ class BuildHandler {
 
         _context.statManager().createRateStat("tunnel.decryptRequestTime", "How long it takes to decrypt a new tunnel build request", "Tunnels", new long[] { 60*1000, 10*60*1000 });
         _context.statManager().createRateStat("tunnel.rejectTimeout", "How often we reject a tunnel because we can't find the next hop", "Tunnels", new long[] { 60*1000, 10*60*1000 });
+        _context.statManager().createRateStat("tunnel.rejectTimeout2", "How often we fail a tunnel because we can't contact the next hop", "Tunnels", new long[] { 60*1000, 10*60*1000 });
 
         _context.statManager().createRateStat("tunnel.rejectOverloaded", "How long we had to wait before processing the request (when it was rejected)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
         _context.statManager().createRateStat("tunnel.acceptLoad", "Delay before processing the accepted request", "Tunnels", new long[] { 60*1000, 10*60*1000 });
@@ -413,7 +414,7 @@ class BuildHandler {
         }
     }
 
-    private class TimeoutReq extends JobImpl {
+    private static class TimeoutReq extends JobImpl {
         private BuildMessageState _state;
         private BuildRequestRecord _req;
         private Hash _nextPeer;
@@ -425,10 +426,12 @@ class BuildHandler {
         }
         public String getName() { return "Timeout looking for next peer for tunnel join"; }
         public void runJob() {
-            getContext().statManager().addRateData("tunnel.rejectTimeout", 1, 1);
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Request " + _state.msg.getUniqueId() 
-                          + " could no be satisfied, as the next peer could not be found: " + _nextPeer.toBase64());
+            getContext().statManager().addRateData("tunnel.rejectTimeout", 1, 0);
+            // logging commented out so class can be static
+            //if (_log.shouldLog(Log.WARN))
+            //    _log.warn("Request " + _state.msg.getUniqueId() 
+            //              + " could no be satisfied, as the next peer could not be found: " + _nextPeer.toBase64());
+
             // ???  should we blame the peer here?   getContext().profileManager().tunnelTimedOut(_nextPeer);
             getContext().messageHistory().tunnelRejected(_state.fromHash, new TunnelId(_req.readReceiveTunnelId()), _nextPeer, 
                                                          "rejected because we couldn't find " + _nextPeer.toBase64() + ": " +
@@ -516,8 +519,9 @@ class BuildHandler {
                        + " from " + (state.fromHash != null ? state.fromHash.toBase64() : 
                                      state.from != null ? state.from.calculateHash().toBase64() : "tunnel"));
 
+        HopConfig cfg = null;
         if (response == 0) {
-            HopConfig cfg = new HopConfig();
+            cfg = new HopConfig();
             cfg.setCreation(_context.clock().now());
             cfg.setExpiration(_context.clock().now() + 10*60*1000);
             cfg.setIVKey(req.readIVKey());
@@ -593,6 +597,8 @@ class BuildHandler {
             msg.setExpiration(state.msg.getMessageExpiration());
             msg.setPriority(300);
             msg.setTarget(nextPeerInfo);
+            if (response == 0)
+                msg.setOnFailedSendJob(new TunnelBuildNextHopFailJob(_context, cfg));
             _context.outNetMessagePool().add(msg);
         } else {
             // send it to the reply tunnel on the reply peer within a new TunnelBuildReplyMessage
@@ -619,6 +625,8 @@ class BuildHandler {
                 outMsg.setMessage(m);
                 outMsg.setPriority(300);
                 outMsg.setTarget(nextPeerInfo);
+                if (response == 0)
+                    outMsg.setOnFailedSendJob(new TunnelBuildNextHopFailJob(_context, cfg));
                 _context.outNetMessagePool().add(outMsg);
             }
         }
@@ -762,7 +770,7 @@ class BuildHandler {
     }
     
     /** normal inbound requests from other people */
-    private class BuildMessageState {
+    private static class BuildMessageState {
         TunnelBuildMessage msg;
         RouterIdentity from;
         Hash fromHash;
@@ -775,7 +783,7 @@ class BuildHandler {
         }
     }
     /** replies for outbound tunnels that we have created */
-    private class BuildReplyMessageState {
+    private static class BuildReplyMessageState {
         TunnelBuildReplyMessage msg;
         long recvTime;
         public BuildReplyMessageState(I2NPMessage m) {
@@ -784,7 +792,7 @@ class BuildHandler {
         }
     }
     /** replies for inbound tunnels we have created */
-    private class BuildEndMessageState {
+    private static class BuildEndMessageState {
         TunnelBuildMessage msg;
         PooledTunnelCreatorConfig cfg;
         long recvTime;
@@ -796,15 +804,35 @@ class BuildHandler {
     }
 
     // noop
-    private class TunnelBuildMessageHandlerJob extends JobImpl {
+    private static class TunnelBuildMessageHandlerJob extends JobImpl {
         private TunnelBuildMessageHandlerJob(RouterContext ctx) { super(ctx); }
         public void runJob() {}
         public String getName() { return "Receive tunnel build message"; }
     }
     // noop
-    private class TunnelBuildReplyMessageHandlerJob extends JobImpl {
+    private static class TunnelBuildReplyMessageHandlerJob extends JobImpl {
         private TunnelBuildReplyMessageHandlerJob(RouterContext ctx) { super(ctx); }
         public void runJob() {}
         public String getName() { return "Receive tunnel build reply message"; }
     }
+
+    /**
+     *  Remove the participating tunnel if we can't contact the next hop
+     *  Not strictly necessary, as the entry doesn't use that much space,
+     *  but it affects capacity calculations
+     */
+    private static class TunnelBuildNextHopFailJob extends JobImpl {
+        HopConfig _cfg;
+        private TunnelBuildNextHopFailJob(RouterContext ctx, HopConfig cfg) {
+            super(ctx);
+            _cfg = cfg;
+        }
+        public String getName() { return "Timeout contacting next peer for tunnel join"; }
+        public void runJob() {
+            getContext().tunnelDispatcher().remove(_cfg);
+            getContext().statManager().addRateData("tunnel.rejectTimeout2", 1, 0);
+            // static, no _log
+            //_log.error("Cant contact next hop for " + _cfg);
+        }
+    }
 }
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
index a4917772fa5adfeba5a055b6620036d52aad8fb3..21325be85189c68045f0405a60f127decb6db6ec 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
@@ -12,6 +12,7 @@ import net.i2p.data.RouterInfo;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.data.i2np.TunnelBuildMessage;
+import net.i2p.router.JobImpl;
 import net.i2p.router.OutNetMessage;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelInfo;
@@ -136,6 +137,7 @@ class BuildRequestor {
                 return;
             }
             outMsg.setTarget(peer);
+            outMsg.setOnFailedSendJob(new TunnelBuildFirstHopFailJob(ctx, pool, cfg, exec));
             ctx.outNetMessagePool().add(outMsg);
         }
         if (log.shouldLog(Log.DEBUG))
@@ -213,4 +215,33 @@ class BuildRequestor {
         ctx.jobQueue().addJob(expireJob);
         // can it get much easier?
     }
+
+    /**
+     *  Do two important things if we can't get the build msg to the
+     *  first hop on an outbound tunnel -
+     *  - Call buildComplete() so we can get started on the next build
+     *    without waiting for the full expire time
+     *  - Blame the first hop in the profile
+     *  Most likely to happen on an exploratory tunnel, obviously.
+     *  Can't do this for inbound tunnels since the msg goes out an expl. tunnel.
+     */
+    private static class TunnelBuildFirstHopFailJob extends JobImpl {
+        TunnelPool _pool;
+        PooledTunnelCreatorConfig _cfg;
+        BuildExecutor _exec;
+        private TunnelBuildFirstHopFailJob(RouterContext ctx, TunnelPool pool, PooledTunnelCreatorConfig cfg, BuildExecutor exec) {
+            super(ctx);
+            _cfg = cfg;
+            _exec = exec;
+            _pool = pool;
+        }
+        public String getName() { return "Timeout contacting first peer for OB tunnel"; }
+        public void runJob() {
+            _exec.buildComplete(_cfg, _pool);
+            getContext().profileManager().tunnelTimedOut(_cfg.getPeer(1));
+            getContext().statManager().addRateData("tunnel.buildFailFirstHop", 1, 0);
+            // static, no _log
+            //System.err.println("Cant contact first hop for " + _cfg);
+        }
+    }
 }
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
index 43120d0b08039616e0485c3a856e17c100ef166b..c6b1f5b9a9925e356e0ac713e6e3bea14780d7d5 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
@@ -376,7 +376,7 @@ public class TunnelPoolManager implements TunnelManagerFacade {
         _context.jobQueue().addJob(new BootstrapPool(_context, _outboundExploratory));
     }
     
-    private class BootstrapPool extends JobImpl {
+    private static class BootstrapPool extends JobImpl {
         private TunnelPool _pool;
         public BootstrapPool(RouterContext ctx, TunnelPool pool) {
             super(ctx);