From c76402a160f6f1f08aa1b8fcf8c3cdcc53225e0e Mon Sep 17 00:00:00 2001
From: jrandom <jrandom>
Date: Sat, 8 Apr 2006 06:15:43 +0000
Subject: [PATCH] 2006-04-08  jrandom     * Process inbound tunnel requests
 more efficiently     * Proactively drop inbound tunnel requests if the queue
 before we'd       process it in is too long (dynamically adjusted by cpu
 load)     * Adjust the tunnel rejection throttle to reject requeusts when we
 have to       proactively drop too many requests.     * Display the number of
 pending inbound tunnel join requests on the router       console (as the
 "handle backlog")     * Include a few more stats in the default set of graphs

---
 .../net/i2p/router/web/StatSummarizer.java    |  4 +
 .../src/net/i2p/router/web/SummaryHelper.java |  7 ++
 apps/routerconsole/jsp/summary.jsp            |  1 +
 history.txt                                   | 12 ++-
 .../src/net/i2p/router/RouterVersion.java     |  4 +-
 .../net/i2p/router/TunnelManagerFacade.java   |  4 +
 .../i2p/router/tunnel/pool/BuildExecutor.java | 47 ++++++++--
 .../i2p/router/tunnel/pool/BuildHandler.java  | 93 ++++++++++++++++---
 .../router/tunnel/pool/TunnelPoolManager.java | 10 +-
 9 files changed, 152 insertions(+), 30 deletions(-)

diff --git a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java
index aa38562b7c..07fa08fc1d 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java
@@ -45,6 +45,10 @@ public class StatSummarizer implements Runnable {
                                                     ",router.activePeers.60000" +
                                                     ",router.activeSendPeers.60000" +
                                                     ",tunnel.acceptLoad.60000" +
+                                                    ",tunnel.dropLoadProactive.60000" +
+                                                    ",tunnel.buildExploratorySuccess.60000" +
+                                                    ",tunnel.buildExploratoryReject.60000" +
+                                                    ",tunnel.buildExploratoryExpire.60000" +
                                                     ",client.sendAckTime.60000" +
                                                     ",client.dispatchNoACK.60000" +
                                                     ",transport.sendMessageFailureLifetime.60000" +
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 98d0ef6cda..a3de9cc477 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
@@ -493,6 +493,13 @@ public class SummaryHelper {
         return _context.throttle().getTunnelLag() + "ms";
     }
     
+    public String getInboundBacklog() {
+        if (_context == null)
+            return "0";
+        
+        return String.valueOf(_context.tunnelManager().getInboundBuildQueueSize());
+    }
+    
     public boolean updateAvailable() { 
         return NewsFetcher.getInstance(_context).updateAvailable();
     }
diff --git a/apps/routerconsole/jsp/summary.jsp b/apps/routerconsole/jsp/summary.jsp
index 72a5e8f204..38a4d0fc87 100644
--- a/apps/routerconsole/jsp/summary.jsp
+++ b/apps/routerconsole/jsp/summary.jsp
@@ -83,6 +83,7 @@
  <b>Job lag:</b> <jsp:getProperty name="helper" property="jobLag" /><br />
  <b>Message delay:</b> <jsp:getProperty name="helper" property="messageDelay" /><br />
  <b>Tunnel lag:</b> <jsp:getProperty name="helper" property="tunnelLag" /><br />
+ <b>Handle backlog:</b> <jsp:getProperty name="helper" property="inboundBacklog" /><br />
  <hr />
  
 </div>
diff --git a/history.txt b/history.txt
index 29efff5f18..40fa5467dc 100644
--- a/history.txt
+++ b/history.txt
@@ -1,4 +1,14 @@
-$Id: history.txt,v 1.447 2006/04/06 05:33:46 jrandom Exp $
+$Id: history.txt,v 1.448 2006/04/06 20:26:33 jrandom Exp $
+
+2006-04-08  jrandom
+    * Process inbound tunnel requests more efficiently
+    * Proactively drop inbound tunnel requests if the queue before we'd
+      process it in is too long (dynamically adjusted by cpu load)
+    * Adjust the tunnel rejection throttle to reject requeusts when we have to
+      proactively drop too many requests.
+    * Display the number of pending inbound tunnel join requests on the router
+      console (as the "handle backlog")
+    * Include a few more stats in the default set of graphs
 
 2006-04-06  jrandom
     * Fix for a bug in the new irc ping/pong filter (thanks Complication!)
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index 41709cce37..ed3dcfbd95 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -15,9 +15,9 @@ import net.i2p.CoreVersion;
  *
  */
 public class RouterVersion {
-    public final static String ID = "$Revision: 1.387 $ $Date: 2006/04/06 05:33:47 $";
+    public final static String ID = "$Revision: 1.388 $ $Date: 2006/04/06 20:26:33 $";
     public final static String VERSION = "0.6.1.14";
-    public final static long BUILD = 2;
+    public final static long BUILD = 3;
     public static void main(String args[]) {
         System.out.println("I2P Router version: " + VERSION + "-" + BUILD);
         System.out.println("Router ID: " + RouterVersion.ID);
diff --git a/router/java/src/net/i2p/router/TunnelManagerFacade.java b/router/java/src/net/i2p/router/TunnelManagerFacade.java
index 052af69b8a..ad4d1fad57 100644
--- a/router/java/src/net/i2p/router/TunnelManagerFacade.java
+++ b/router/java/src/net/i2p/router/TunnelManagerFacade.java
@@ -56,6 +56,9 @@ public interface TunnelManagerFacade extends Service {
     /** When does the last tunnel we are participating in expire? */
     public long getLastParticipatingExpiration();
     
+    /** count how many inbound tunnel requests we have received but not yet processed */
+    public int getInboundBuildQueueSize();
+    
     /** 
      * the client connected (or updated their settings), so make sure we have
      * the tunnels for them, and whenever necessary, ask them to authorize 
@@ -97,6 +100,7 @@ class DummyTunnelManagerFacade implements TunnelManagerFacade {
     public void setOutboundSettings(TunnelPoolSettings settings) {}
     public void setInboundSettings(Hash client, TunnelPoolSettings settings) {}
     public void setOutboundSettings(Hash client, TunnelPoolSettings settings) {}
+    public int getInboundBuildQueueSize() { return 0; }
     
     public void renderStatusHTML(Writer out) throws IOException {}
     public void restart() {}
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 80e67e824c..9d20b384a6 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
@@ -41,6 +41,7 @@ class BuildExecutor implements Runnable {
         _context.statManager().createRateStat("tunnel.buildClientReject", "How often a client tunnel is rejected", "Tunnels", new long[] { 60*1000, 10*60*1000 });
         _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 });
         _repoll = false;
         _handler = new BuildHandler(ctx, this);
     }
@@ -117,11 +118,19 @@ class BuildExecutor implements Runnable {
         List wanted = new ArrayList(8);
         List pools = new ArrayList(8);
         
-        boolean pendingRemaining = false;
+        int pendingRemaining = 0;
         
+        long loopBegin = 0;
+        long beforeHandleInboundReplies = 0;
+        long afterHandleInboundReplies = 0;
+        long afterBuildZeroHop = 0;
+        long afterBuildReal = 0;
+        long afterHandleInbound = 0;
+                
         while (!_manager.isShutdown()){
+            loopBegin = System.currentTimeMillis();
             try {
-                _repoll = pendingRemaining; // resets repoll to false unless there are inbound requeusts pending
+                _repoll = pendingRemaining > 0; // resets repoll to false unless there are inbound requeusts pending
                 _manager.listPools(pools);
                 for (int i = 0; i < pools.size(); i++) {
                     TunnelPool pool = (TunnelPool)pools.get(i);
@@ -130,7 +139,9 @@ class BuildExecutor implements Runnable {
                         wanted.add(pool);
                 }
 
+                beforeHandleInboundReplies = System.currentTimeMillis();
                 _handler.handleInboundReplies();
+                afterHandleInboundReplies = System.currentTimeMillis();
                 
                 // allowed() also expires timed out requests (for new style requests)
                 int allowed = allowed();
@@ -140,17 +151,22 @@ class BuildExecutor implements Runnable {
 
                 // zero hop ones can run inline
                 allowed = buildZeroHopTunnels(wanted, allowed);
+                afterBuildZeroHop = System.currentTimeMillis();
                 
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Zero hops built, Allowed: " + allowed + " wanted: " + wanted);
 
+                int realBuilt = 0;
                 TunnelManagerFacade mgr = _context.tunnelManager();
                 if ( (mgr == null) || (mgr.selectInboundTunnel() == null) || (mgr.selectOutboundTunnel() == null) ) {
                     // we don't have either inbound or outbound tunnels, so don't bother trying to build
                     // non-zero-hop tunnels
                     synchronized (_currentlyBuilding) {
-                        if (!_repoll)
-                            _currentlyBuilding.wait(5*1000+_context.random().nextInt(5*1000));
+                        if (!_repoll) {
+                            if (_log.shouldLog(Log.DEBUG))
+                                _log.debug("No tunnel to build with (allowed=" + allowed + ", wanted=" + wanted.size() + ", pending=" + pendingRemaining + "), wait for a while");
+                            _currentlyBuilding.wait(1*1000+_context.random().nextInt(1*1000));
+                        }
                     }
                 } else {
                     if ( (allowed > 0) && (wanted.size() > 0) ) {
@@ -173,6 +189,7 @@ class BuildExecutor implements Runnable {
                                     _currentlyBuilding.add(cfg);
                                 }
                                 buildTunnel(pool, cfg);
+                                realBuilt++;
                                 // 0hops are taken care of above, these are nonstandard 0hops
                                 //if (cfg.getLength() <= 1)
                                 //    i--; //0hop, we can keep going, as there's no worry about throttling
@@ -184,13 +201,13 @@ class BuildExecutor implements Runnable {
                             }
                         }
                     } else {
-                        if (_log.shouldLog(Log.DEBUG))
-                            _log.debug("Nothin' doin, wait for a while");
                         try {
                             synchronized (_currentlyBuilding) {
                                 if (!_repoll) {
+                                    if (_log.shouldLog(Log.DEBUG))
+                                        _log.debug("Nothin' doin (allowed=" + allowed + ", wanted=" + wanted.size() + ", pending=" + pendingRemaining + "), wait for a while");
                                     //if (allowed <= 0)
-                                        _currentlyBuilding.wait(_context.random().nextInt(5*1000));
+                                        _currentlyBuilding.wait(_context.random().nextInt(1*1000));
                                     //else // wanted <= 0
                                     //    _currentlyBuilding.wait(_context.random().nextInt(30*1000));
                                 }
@@ -201,8 +218,23 @@ class BuildExecutor implements Runnable {
                     }
                 }
                 
+                afterBuildReal = System.currentTimeMillis();
+                
                 pendingRemaining = _handler.handleInboundRequests();
+                afterHandleInbound = System.currentTimeMillis();
+                
+                if (pendingRemaining > 0)
+                    _context.statManager().addRateData("tunnel.pendingRemaining", pendingRemaining, afterHandleInbound-afterBuildReal);
 
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("build loop complete, tot=" + (afterHandleInbound-loopBegin) + 
+                              " inReply=" + (afterHandleInboundReplies-beforeHandleInboundReplies) +
+                              " zeroHop=" + (afterBuildZeroHop-afterHandleInboundReplies) +
+                              " real=" + (afterBuildReal-afterBuildZeroHop) +
+                              " in=" + (afterHandleInbound-afterBuildReal) + 
+                              " built=" + realBuilt +
+                              " pending=" + pendingRemaining);
+                
                 wanted.clear();
                 pools.clear();
             } catch (Exception e) {
@@ -300,4 +332,5 @@ class BuildExecutor implements Runnable {
     }
     
     List locked_getCurrentlyBuilding() { return _currentlyBuilding; }
+    public int getInboundBuildQueueSize() { return _handler.getInboundBuildQueueSize(); }
 }
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 267c7e74fe..2ce132fae8 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
@@ -6,6 +6,8 @@ import net.i2p.data.i2np.*;
 import net.i2p.router.*;
 import net.i2p.router.tunnel.*;
 import net.i2p.router.peermanager.TunnelHistory;
+import net.i2p.stat.Rate;
+import net.i2p.stat.RateStat;
 import net.i2p.util.Log;
 
 /**
@@ -46,6 +48,7 @@ class BuildHandler {
         _context.statManager().createRateStat("tunnel.dropLoad", "How long we had to wait before finally giving up on an inbound request (period is queue count)?", "Tunnels", new long[] { 60*1000, 10*60*1000 });
         _context.statManager().createRateStat("tunnel.dropLoadDelay", "How long we had to wait before finally giving up on an inbound request?", "Tunnels", new long[] { 60*1000, 10*60*1000 });
         _context.statManager().createRateStat("tunnel.dropLoadBacklog", "How many requests were pending when they were so lagged that we had to drop a new inbound request??", "Tunnels", new long[] { 60*1000, 10*60*1000 });
+        _context.statManager().createRateStat("tunnel.dropLoadProactive", "What the estimated queue time was when we dropped an inbound request (period is num pending)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
         _context.statManager().createRateStat("tunnel.handleRemaining", "How many pending inbound requests were left on the queue after one pass?", "Tunnels", new long[] { 60*1000, 10*60*1000 });
         
         _context.statManager().createRateStat("tunnel.receiveRejectionProbabalistic", "How often we are rejected probabalistically?", "Tunnels", new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l });
@@ -64,10 +67,10 @@ class BuildHandler {
     private static final int NEXT_HOP_LOOKUP_TIMEOUT = 5*1000;
     
     /**
-     * Blocking call to handle a few of the pending inbound requests, returning true if
-     * there are remaining requeusts we skipped over
+     * Blocking call to handle a few of the pending inbound requests, returning how many
+     * requests remain after this pass
      */
-    boolean handleInboundRequests() {
+    int handleInboundRequests() {
         int dropExpired = 0;
         List handled = null;
         synchronized (_inboundBuildMessages) {
@@ -98,7 +101,7 @@ class BuildHandler {
                     
                     // now pull off the oldest requests first (we're doing a tail-drop
                     // when adding)
-                    for (int i = 0; i < toHandle; i++)
+                    for (int i = 0; i < toHandle && _inboundBuildMessages.size() > 0; i++)
                         handled.add(_inboundBuildMessages.remove(0));
                 }
             }
@@ -139,7 +142,7 @@ class BuildHandler {
             int remaining = _inboundBuildMessages.size();
             if (remaining > 0)
                 _context.statManager().addRateData("tunnel.handleRemaining", remaining, 0);
-            return remaining > 0;
+            return remaining;
         }
     }
     
@@ -364,6 +367,30 @@ class BuildHandler {
         }
     }
     
+    /**
+     * If we are dropping lots of requests before even trying to handle them,
+     * I suppose you could call us "overloaded"
+     */
+    private final static int MAX_PROACTIVE_DROPS = 120;
+    
+    private int countProactiveDrops() {
+        int dropped = 0;
+        dropped += countEvents("tunnel.dropLoadProactive", 60*1000);
+        dropped += countEvents("tunnel.dropLoad", 60*1000);
+        dropped += countEvents("tunnel.dropLoadBacklog", 60*1000);
+        dropped += countEvents("tunnel.dropLoadDelay", 60*1000);
+        return dropped;
+    }
+    private int countEvents(String stat, long period) {
+        RateStat rs = _context.statManager().getRate(stat);
+        if (rs != null) {
+            Rate r = rs.getRate(period);
+            if (r != null)
+                return (int)r.getCurrentEventCount();
+        }
+        return 0;
+    }
+    
     private void handleReq(RouterInfo nextPeerInfo, BuildMessageState state, BuildRequestRecord req, Hash nextPeer) {
         long ourId = req.readReceiveTunnelId();
         long nextId = req.readNextTunnelId();
@@ -384,17 +411,21 @@ class BuildHandler {
         //if ( (response == 0) && (_context.random().nextInt(50) <= 1) )
         //    response = TunnelHistory.TUNNEL_REJECT_PROBABALISTIC_REJECT;
         
+        int proactiveDrops = countProactiveDrops();
         long recvDelay = System.currentTimeMillis()-state.recvTime;
-        if ( (response == 0) && (recvDelay > BuildRequestor.REQUEST_TIMEOUT/2) ) {
-            _context.statManager().addRateData("tunnel.rejectOverloaded", recvDelay, recvDelay);
-            response = TunnelHistory.TUNNEL_REJECT_TRANSIENT_OVERLOAD;
+        if ( (response == 0) && ( (recvDelay > BuildRequestor.REQUEST_TIMEOUT/2) || (proactiveDrops > MAX_PROACTIVE_DROPS) ) ) {
+            _context.statManager().addRateData("tunnel.rejectOverloaded", recvDelay, proactiveDrops);
+            if (true || (proactiveDrops < MAX_PROACTIVE_DROPS*2))
+                response = TunnelHistory.TUNNEL_REJECT_TRANSIENT_OVERLOAD;
+            else
+                response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH;
         } else if (response == 0) {
             _context.statManager().addRateData("tunnel.acceptLoad", recvDelay, recvDelay);
         }
         
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Responding to " + state.msg.getUniqueId() + "/" + ourId
-                       + " after " + recvDelay + " with " + response 
+                       + " after " + recvDelay + "/" + proactiveDrops + " with " + response 
                        + " from " + (state.fromHash != null ? state.fromHash.toBase64() : 
                                      state.from != null ? state.from.calculateHash().toBase64() : "tunnel"));
 
@@ -505,6 +536,12 @@ class BuildHandler {
         }
     }
     
+    public int getInboundBuildQueueSize() {
+        synchronized (_inboundBuildMessages) {
+            return _inboundBuildMessages.size();
+        }
+    }
+    
     private static final boolean HANDLE_REPLIES_INLINE = true;
 
     private class TunnelBuildMessageHandlerJobBuilder implements HandlerJobBuilder {
@@ -551,22 +588,26 @@ class BuildHandler {
                     synchronized (_inboundBuildMessages) {
                         boolean removed = false;
                         int dropped = 0;
-                        while (_inboundBuildMessages.size() > 0) {
-                            BuildMessageState cur = (BuildMessageState)_inboundBuildMessages.get(_inboundBuildMessages.size()-1);
+                        for (int i = 0; i < _inboundBuildMessages.size(); i++) {
+                            BuildMessageState cur = (BuildMessageState)_inboundBuildMessages.get(i);
                             long age = System.currentTimeMillis() - cur.recvTime;
                             if (age >= BuildRequestor.REQUEST_TIMEOUT) {
-                                _inboundBuildMessages.remove(_inboundBuildMessages.size()-1);
+                                _inboundBuildMessages.remove(i);
+                                i--;
                                 dropped++;
                                 _context.statManager().addRateData("tunnel.dropLoad", age, _inboundBuildMessages.size());
-                            } else {
-                                break;
                             }
                         }
                         if (dropped > 0) {
                             // if the queue is backlogged, stop adding new messages
                             _context.statManager().addRateData("tunnel.dropLoadBacklog", _inboundBuildMessages.size(), _inboundBuildMessages.size());
                         } else {
-                            _inboundBuildMessages.add(new BuildMessageState(receivedMessage, from, fromHash));
+                            int queueTime = estimateQueueTime(_inboundBuildMessages.size());
+                            if (queueTime > BuildRequestor.REQUEST_TIMEOUT/2) {
+                                _context.statManager().addRateData("tunnel.dropLoadProactive", queueTime, _inboundBuildMessages.size());
+                            } else {
+                                _inboundBuildMessages.add(new BuildMessageState(receivedMessage, from, fromHash));
+                            }
                         }
                     }
                     _exec.repoll();
@@ -576,6 +617,28 @@ class BuildHandler {
         }
     }
     
+    private int estimateQueueTime(int numPendingMessages) {
+        int decryptTime = 200;
+        RateStat rs = _context.statManager().getRate("crypto.elGamal.decrypt");
+        if (rs != null) {
+            Rate r = rs.getRate(60*1000);
+            double avg = 0;
+            if (r != null)
+                avg = r.getAverageValue();
+            if (avg > 0) {
+                decryptTime = (int)avg;
+            } else {
+                avg = rs.getLifetimeAverageValue();
+                if (avg > 0)
+                    decryptTime = (int)avg;
+            }
+        }
+        int estimatedQueueTime = numPendingMessages * decryptTime;
+        estimatedQueueTime *= 2; // lets leave some cpu to spare, 'eh?
+        return estimatedQueueTime;
+    }
+    
+    
     private class TunnelBuildReplyMessageHandlerJobBuilder implements HandlerJobBuilder {
         public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) {
             if (_log.shouldLog(Log.DEBUG))
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 55bf0580c4..0a1578fd31 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
@@ -8,10 +8,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
-import net.i2p.data.DataHelper;
-import net.i2p.data.Destination;
-import net.i2p.data.Hash;
-import net.i2p.data.TunnelId;
+import net.i2p.data.*;
 import net.i2p.data.i2np.*;
 import net.i2p.stat.RateStat;
 import net.i2p.router.*;
@@ -390,6 +387,9 @@ public class TunnelPoolManager implements TunnelManagerFacade {
     void tunnelFailed() { _executor.repoll(); }
     BuildExecutor getExecutor() { return _executor; }
     boolean isShutdown() { return _isShutdown; }
+
+    public int getInboundBuildQueueSize() { return _executor.getInboundBuildQueueSize(); }
+    
     
     public void renderStatusHTML(Writer out) throws IOException {
         out.write("<h2><a name=\"exploratory\">Exploratory tunnels</a> (<a href=\"/configtunnels.jsp#exploratory\">config</a>):</h2>\n");
@@ -499,7 +499,7 @@ public class TunnelPoolManager implements TunnelManagerFacade {
                 if (_context.routerHash().equals(peer))
                     out.write("<td><i>" + peer.toBase64().substring(0,4) + (id == null ? "" : ":" + id) + "</i></td>");
                 else
-                    out.write("<td>" + peer.toBase64().substring(0,4) + (id == null ? "" : ":" + id) + "</td>");
+                    out.write("<td>" + peer.toBase64().substring(0,4) + (id == null ? "" : ":" + id) + "</td>");                
             }
             out.write("</tr>\n");
             
-- 
GitLab