-
Notifications
You must be signed in to change notification settings - Fork 8
Run the freestyle TestingBot Tunnel on the build's agent, not the controller #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,12 +4,16 @@ | |
| import com.testingbot.tunnel.Api; | ||
| import com.testingbot.tunnel.App; | ||
| import hudson.model.TaskListener; | ||
| import hudson.remoting.VirtualChannel; | ||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
| import jenkins.security.MasterToSlaveCallable; | ||
|
|
||
| /** | ||
| * Shared TestingBot Tunnel lifecycle helper used by both the freestyle | ||
|
|
@@ -203,4 +207,138 @@ public static void stop(App app, TaskListener listener) { | |
| app.stop(); | ||
| } | ||
| } | ||
|
|
||
| // ------------------------------------------------------------------------------------------ | ||
| // Remote (agent) tunnel lifecycle. | ||
| // | ||
| // The tunnel must boot on the same node where the tests run, so that a test connecting to | ||
| // {@code localhost} reaches the tunnel. Both the freestyle build wrapper and the pipeline step | ||
| // therefore run the tunnel inside a MasterToSlaveCallable on the build node's channel. The live | ||
| // App is kept in a per-JVM static registry (co-located with the tunnel on the agent) so teardown | ||
| // can find and stop it again without serializing the handle back across the channel. | ||
| // ------------------------------------------------------------------------------------------ | ||
|
|
||
| /** Tunnels running in this JVM, keyed by tunnel identifier. Lives in the agent's static space. */ | ||
| private static final ConcurrentHashMap<String, App> RUNNING_TUNNELS = new ConcurrentHashMap<>(); | ||
|
|
||
| /** Monotonic suffix so multiple generated identifiers within one run stay distinct. */ | ||
| private static final AtomicInteger TUNNEL_SEQ = new AtomicInteger(); | ||
|
|
||
| /** | ||
| * Generates a tunnel identifier unique to a build (and to each tunnel within it), so that | ||
| * parallel tunnels on the same node stay isolated. | ||
| */ | ||
| public static String generateTunnelIdentifier(String jobFullName, int buildNumber) { | ||
| return "jenkins-" + jobFullName.replace('/', '-') + "-" + buildNumber + "-" + TUNNEL_SEQ.incrementAndGet(); | ||
| } | ||
|
|
||
| /** | ||
| * Boots a tunnel on the given channel's node and registers it under {@code tunnelIdentifier}. | ||
| * Credentials are passed as already-decrypted strings (only plaintext crosses the encrypted | ||
| * remoting link, never the credential object). | ||
| */ | ||
| public static void startOnChannel(VirtualChannel channel, String key, String secret, String options, | ||
| String tunnelIdentifier, TaskListener listener) throws Exception { | ||
| channel.call(new StartTunnelHandler(key, secret, options, tunnelIdentifier, listener)); | ||
| } | ||
|
|
||
| /** | ||
| * Stops the tunnel registered under {@code tunnelIdentifier} on the given channel's node. | ||
| * Best-effort: a {@code null} channel (offline agent) or a remoting failure is tolerated so a | ||
| * teardown never fails the build; an offline agent is reported to the log. | ||
| */ | ||
| public static void stopOnChannel(VirtualChannel channel, String tunnelIdentifier, TaskListener listener) { | ||
| if (channel == null) { | ||
| RUNNING_TUNNELS.remove(tunnelIdentifier); | ||
| if (listener != null) { | ||
| listener.getLogger().println("[TestingBot] Agent offline: remote teardown of tunnel " | ||
| + tunnelIdentifier + " could not be performed. It may still be running on the agent " | ||
| + "and should stop when the agent process exits (best-effort cleanup)."); | ||
| } | ||
| return; | ||
| } | ||
| try { | ||
| channel.call(new StopTunnelHandler(tunnelIdentifier, listener)); | ||
| } catch (InterruptedException ie) { | ||
| // Restore the interrupt status; teardown stays best-effort and does not rethrow. | ||
| Thread.currentThread().interrupt(); | ||
| if (listener != null) { | ||
| listener.getLogger().println("[TestingBot] Interrupted while stopping tunnel " + tunnelIdentifier); | ||
| } | ||
| } catch (Exception e) { | ||
| // Best-effort, but do not silently discard the failure — record why teardown could not complete. | ||
| if (listener != null) { | ||
| listener.getLogger().println("[TestingBot] Failed to stop tunnel " + tunnelIdentifier + ": " + e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static final class StartTunnelHandler extends MasterToSlaveCallable<Void, Exception> { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
| private final String key; | ||
| private final String secret; | ||
| private final String tunnelOptions; | ||
| private final String tunnelIdentifier; | ||
| private final TaskListener listener; | ||
|
|
||
| StartTunnelHandler(String key, String secret, String tunnelOptions, String tunnelIdentifier, TaskListener listener) { | ||
| this.key = key; | ||
| this.secret = secret; | ||
| this.tunnelOptions = tunnelOptions; | ||
| this.tunnelIdentifier = tunnelIdentifier; | ||
| this.listener = listener; | ||
| } | ||
|
|
||
| @Override | ||
| public Void call() throws Exception { | ||
| App app = new App(); | ||
| // Reserve the identifier before booting: a duplicate identifier is rejected rather than | ||
| // silently overwriting (and leaking) an existing tunnel, and teardown can find and stop | ||
| // the App even while it is still booting. | ||
| App previous = RUNNING_TUNNELS.putIfAbsent(tunnelIdentifier, app); | ||
| if (previous != null) { | ||
| try { | ||
| app.stop(); | ||
| } catch (Exception ignored) { | ||
| // best effort | ||
| } | ||
| throw new IllegalStateException( | ||
| "A TestingBot tunnel with identifier '" + tunnelIdentifier + "' is already running"); | ||
| } | ||
| try { | ||
| start(app, key, secret, tunnelOptions, tunnelIdentifier, listener); | ||
| } catch (Exception e) { | ||
| // Startup failed: remove only our own registry entry (leaving any concurrent owner | ||
| // untouched) and stop the App so we don't leak a half-booted tunnel. | ||
| RUNNING_TUNNELS.remove(tunnelIdentifier, app); | ||
| try { | ||
| app.stop(); | ||
| } catch (Exception ignored) { | ||
| // best effort | ||
| } | ||
| throw e; | ||
| } | ||
| return null; | ||
|
Comment on lines
+293
to
+322
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Reserve registry ownership before booting the tunnel. The tunnel is undiscoverable until Install a collision-aware lifecycle entry before booting, coordinate stop with in-progress startup, and remove it conditionally on failure. 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| private static final class StopTunnelHandler extends MasterToSlaveCallable<Void, Exception> { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
| private final String tunnelIdentifier; | ||
| private final TaskListener listener; | ||
|
|
||
| StopTunnelHandler(String tunnelIdentifier, TaskListener listener) { | ||
| this.tunnelIdentifier = tunnelIdentifier; | ||
| this.listener = listener; | ||
| } | ||
|
|
||
| @Override | ||
| public Void call() { | ||
| App app = RUNNING_TUNNELS.remove(tunnelIdentifier); | ||
| stop(app, listener); | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: testingbot/TestingBot-Jenkins-Plugin
Length of output: 6168
🏁 Script executed:
Repository: testingbot/TestingBot-Jenkins-Plugin
Length of output: 4505
Preserve interruption and report teardown failures.
stopOnChannelswallowsInterruptedExceptionand all remoting failures, so cancellation state is lost and a remote tunnel stop can fail silently. Restore the interrupt flag and log the failure before returning.🤖 Prompt for AI Agents