You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by be...@apache.org on 2026/07/26 12:36:27 UTC

(tapestry-5) 03/03: JettyRunner allow for re-bind attempts

This is an automated email from the ASF dual-hosted git repository.

benweidig pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit 6712f46ab52a63409e4997f603cc815b8f36b096
Author: Ben Weidig <be...@netzgut.net>
AuthorDate: Sun Jul 26 14:35:34 2026 +0200

    JettyRunner allow for re-bind attempts
    
    To reduce failing tests due to test-unrelated issues like a port not
    being free, the JettyRunner now tries 5 times with a 500ms delay to
    reacquire the port.
    
    This isn't bullet proof, but should provide a little more leeway in case
    a previous integration test app is still holding on to a port.
---
 .../org/apache/tapestry5/test/JettyRunner.java     | 82 +++++++++++++++++++++-
 1 file changed, 80 insertions(+), 2 deletions(-)

diff --git a/tapestry-runner/src/main/java/org/apache/tapestry5/test/JettyRunner.java b/tapestry-runner/src/main/java/org/apache/tapestry5/test/JettyRunner.java
index 0a15cf63e..ffe04043a 100644
--- a/tapestry-runner/src/main/java/org/apache/tapestry5/test/JettyRunner.java
+++ b/tapestry-runner/src/main/java/org/apache/tapestry5/test/JettyRunner.java
@@ -1,4 +1,4 @@
-// Copyright 2009, 2010, 2011, 2013 The Apache Software Foundation
+// Copyright 2009, 2010, 2011, 2013, 20026 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -22,12 +22,29 @@ import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.webapp.WebAppContext;
 
 import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
 
 /**
  * Launches an instance of Jetty.
  */
 public class JettyRunner implements ServletContainerRunner
 {
+    /**
+     * Number of attempts made to acquire the server's ports before giving up.
+     *
+     * @since 5.10
+     */
+    private static final int BIND_ATTEMPTS = 5;
+
+    /**
+     * Delay, in milliseconds, between attempts to acquire the server's ports.
+     *
+     * @since 5.10
+     */
+    private static final long BIND_RETRY_DELAY = 500;
+
     private Server jettyServer;
 
     private String description;
@@ -36,6 +53,8 @@ public class JettyRunner implements ServletContainerRunner
 
     private int sslPort;
 
+    private boolean sslEnabled;
+
     public JettyRunner()
     {
         // un-configured runner
@@ -65,7 +84,9 @@ public class JettyRunner implements ServletContainerRunner
         // SSL support
         File keystoreFile = new File(TapestryRunnerConstants.MODULE_BASE_DIR, "src/test/conf/keystore");
 
-        if (keystoreFile.exists())
+        sslEnabled = keystoreFile.exists();
+
+        if (sslEnabled)
         {
             SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
             sslContextFactory.setKeyStorePath(keystoreFile.getAbsolutePath());
@@ -91,6 +112,13 @@ public class JettyRunner implements ServletContainerRunner
 
     public void start() throws Exception
     {
+        awaitPortAvailable(port);
+
+        if (sslEnabled)
+        {
+            awaitPortAvailable(sslPort);
+        }
+
         jettyServer.start();
     }
 
@@ -106,6 +134,10 @@ public class JettyRunner implements ServletContainerRunner
         {
             // Stop immediately and not gracefully.
             jettyServer.stop();
+
+            // Release the connectors and the thread pool, rather than leaving them to be
+            // collected at some later point.
+            jettyServer.destroy();
         } catch (Exception ex)
         {
             throw new RuntimeException("Error stopping Jetty instance: " + ex.toString(), ex);
@@ -114,6 +146,52 @@ public class JettyRunner implements ServletContainerRunner
         System.out.println("Jetty instance has stopped.");
     }
 
+    /**
+     * Waits, for a bounded amount of time, until the given port can be acquired.
+     *
+     * <p>A port might not always be available the instant the server holding it is stopped,
+     * so a suite that runs several test applications in sequence inside a single JVM could
+     * otherwise fail with "Failed to bind" IOException.
+     *
+     * @param portToAcquire
+     *         the port the server is about to bind
+     * @throws IOException
+     *         if the port is still held after {@link #BIND_ATTEMPTS} attempts
+     * @since 5.10
+     */
+    private static void awaitPortAvailable(int portToAcquire) throws IOException, InterruptedException
+    {
+        IOException failure = null;
+
+        for (int attempt = 0; attempt < BIND_ATTEMPTS; attempt++)
+        {
+            if (attempt > 0)
+            {
+                Thread.sleep(BIND_RETRY_DELAY);
+            }
+
+            // Jetty's connectors set SO_REUSEADDR, so this probe has to as well for the result to
+            // mean the same thing.
+            try (ServerSocket probe = new ServerSocket())
+            {
+                probe.setReuseAddress(true);
+                probe.bind(new InetSocketAddress(portToAcquire));
+
+                return;
+            } catch (IOException ex)
+            {
+                failure = ex;
+
+                System.out.printf("Port %d is not available yet (attempt %d of %d).%n",
+                        portToAcquire, attempt + 1, BIND_ATTEMPTS);
+            }
+        }
+
+        throw new IOException(String.format(
+                "Port %d is still in use after %d attempts over %d ms; a previous server instance, or another process, has not released it.",
+                portToAcquire, BIND_ATTEMPTS, (BIND_ATTEMPTS - 1) * BIND_RETRY_DELAY), failure);
+    }
+
     public Server getServer()
     {
         return jettyServer;