Skip to content

Run the freestyle TestingBot Tunnel on the build's agent, not the controller#29

Merged
jochen-testingbot merged 2 commits into
build-report-embedfrom
node-agent-support
Jul 16, 2026
Merged

Run the freestyle TestingBot Tunnel on the build's agent, not the controller#29
jochen-testingbot merged 2 commits into
build-report-embedfrom
node-agent-support

Conversation

@jochen-testingbot

@jochen-testingbot jochen-testingbot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

Fixes distributed-build support: the freestyle TestingBot Tunnel now runs on the build's agent, not the Jenkins controller.

Problem

TestingBotBuildWrapper.setUp constructed new App() and booted the tunnel directly in the controller JVM — the Launcher (which holds the agent channel) was never used. For a build running on an agent, the tunnel came up on the controller, so a test connecting to localhost never reached it. The pipeline testingbotTunnel step already handled this correctly by booting the tunnel inside a MasterToSlaveCallable on the build node's channel.

Change

Extract the pipeline step's proven mechanism into TunnelManager so both entry points share one implementation and one registry:

  • TunnelManager.startOnChannel(...) / stopOnChannel(...) — boot/stop the tunnel on a given node's VirtualChannel, keeping the live App in a per-JVM RUNNING_TUNNELS map co-located with the tunnel (so teardown finds it without shipping the handle back).
  • TunnelManager.generateTunnelIdentifier(job, buildNumber) — unique per build/tunnel.
  • TestingBotTunnelStep now delegates to these (its private callables/registry removed); observable behavior unchanged.
  • TestingBotBuildWrapper runs the tunnel via launcher.getChannel(), stores the channel + identifier for tearDown, exposes TESTINGBOT_TUNNEL_IDENTIFIER (so parallel freestyle builds on one node stay isolated), and falls back to a controller-local tunnel only when no channel is available.

Validation

  • mvn -DskipTests verify (SpotBugs on) — clean.
  • hpi:run script console: the relocated MasterToSlaveCallable serializes cleanly (crosses to the agent); testingbotTunnel step + freestyle wrapper descriptors both register; freestyle useTunnel config round-trips across reload.

Note

Stacked on #28 (build-report-embed) to avoid conflicts in TestingBotBuildWrapper.setUp. Retarget to master once #28 merges.

Summary by CodeRabbit

  • New Features

    • TestingBot tunnels can now run directly on build agent nodes when available.
    • Builds automatically receive tunnel status and a unique tunnel identifier through environment variables.
    • Tunnel identifiers are generated per build when none is provided.
  • Bug Fixes

    • Added fallback tunnel startup on the controller when no agent connection is available.
    • Failed tunnel startups now fail the build clearly and clean up partially started tunnels.
    • Tunnel shutdown is handled reliably for both connected and unavailable agents.

TestingBotBuildWrapper.setUp booted the tunnel with new App() directly in
the controller JVM (the Launcher was unused), so for builds on an agent the
tunnel ran on the wrong node and localhost traffic from the tests never
reached it. The pipeline testingbotTunnel step already did this correctly by
running the tunnel inside a MasterToSlaveCallable on the build node's channel.

Extract that mechanism into TunnelManager (startOnChannel/stopOnChannel, a
shared RUNNING_TUNNELS registry, and generateTunnelIdentifier) and use it from
both the pipeline step (now delegating) and the freestyle wrapper. The wrapper
runs the tunnel via launcher.getChannel(), stores the channel + identifier for
teardown, exposes TESTINGBOT_TUNNEL_IDENTIFIER (so parallel freestyle builds on
one node stay isolated), and falls back to a controller-local tunnel only when
no channel is available. The pipeline step's observable behavior is unchanged.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cb98be96-edbd-43f8-b583-7d12d897a0c9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/testingbot/TestingBotBuildWrapper.java`:
- Line 118: Update TestingBotBuildEnvironment construction in the surrounding
wrapper method to persist stable node identity instead of the original
launcher.getChannel(). In tearDown, resolve the current agent Computer and
obtain its live channel via Computer.getChannel() before performing tunnel
cleanup, while preserving the existing cleanup behavior when no channel is
available.

In `@src/main/java/testingbot/TunnelManager.java`:
- Around line 284-299: Update the tunnel startup flow in the callable’s call
method to reserve tunnelIdentifier in the running-tunnel registry before
invoking start, reject collisions without overwriting an existing entry, and
coordinate cleanup so stop waits for or safely handles in-progress startup. On
startup failure, remove only the entry owned by this tunnel and stop the created
App as needed; preserve the registry ownership through successful startup.
- Around line 260-264: Update stopOnChannel’s exception handling around
StopTunnelHandler so InterruptedException restores the thread’s interrupted
status, while other teardown exceptions are logged with relevant failure details
before returning. Preserve the existing best-effort behavior without silently
discarding remoting failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 11b51091-2b66-41db-9c51-bee420a1b5cb

📥 Commits

Reviewing files that changed from the base of the PR and between cce2a98 and fa8f208.

📒 Files selected for processing (3)
  • src/main/java/testingbot/TestingBotBuildWrapper.java
  • src/main/java/testingbot/TunnelManager.java
  • src/main/java/testingbot/pipeline/TestingBotTunnelStep.java

TunnelManager.stopOnChannel(channel, tunnelIdentifier, listener);
throw new IOException("Failed to start TestingBot tunnel", ex);
}
return new TestingBotBuildEnvironment(credentials, buildId, tunnelIdentifier, channel, null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show file with line numbers around the cited regions
sed -n '1,280p' src/main/java/testingbot/TestingBotBuildWrapper.java | nl -ba | sed -n '1,280p'

# Search for tunnel tracking and channel resolution
rg -n "RUNNING_TUNNELS|getChannel\\(|VirtualChannel|Computer" src/main/java/testingbot/TestingBotBuildWrapper.java src/main/java -S

Repository: testingbot/TestingBot-Jenkins-Plugin

Length of output: 214


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant file with line numbers using awk (nl is unavailable here)
awk '{printf "%5d  %s\n", NR, $0}' src/main/java/testingbot/TestingBotBuildWrapper.java | sed -n '1,280p'

# Search for channel and tunnel bookkeeping references
rg -n "RUNNING_TUNNELS|getChannel\\(|VirtualChannel|Computer" src/main/java/testingbot/TestingBotBuildWrapper.java src/main/java -S

Repository: testingbot/TestingBot-Jenkins-Plugin

Length of output: 16213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the tunnel manager methods around start/stop on channel and RUNNING_TUNNELS usage
awk '{printf "%5d  %s\n", NR, $0}' src/main/java/testingbot/TunnelManager.java | sed -n '220,340p'

# Inspect the pipeline step's channel handling for comparison
awk '{printf "%5d  %s\n", NR, $0}' src/main/java/testingbot/pipeline/TestingBotTunnelStep.java | sed -n '190,260p'

Repository: testingbot/TestingBot-Jenkins-Plugin

Length of output: 8054


Resolve the current agent channel in tearDown. Storing launcher.getChannel() in TestingBotBuildEnvironment ties cleanup to the original remoting connection; if the agent reconnects, teardown can no longer reach the live JVM and the tunnel can remain in RUNNING_TUNNELS until the agent exits. Persist stable node identity and resolve Computer.getChannel() during teardown instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/testingbot/TestingBotBuildWrapper.java` at line 118, Update
TestingBotBuildEnvironment construction in the surrounding wrapper method to
persist stable node identity instead of the original launcher.getChannel(). In
tearDown, resolve the current agent Computer and obtain its live channel via
Computer.getChannel() before performing tunnel cleanup, while preserving the
existing cleanup behavior when no channel is available.

Comment on lines +260 to +264
try {
channel.call(new StopTunnelHandler(tunnelIdentifier, listener));
} catch (Exception ignored) {
// best effort
}

Copy link
Copy Markdown

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:

#!/bin/bash
set -euo pipefail

# Locate the relevant file and surrounding methods
git ls-files src/main/java/testingbot/TunnelManager.java
ast-grep outline src/main/java/testingbot/TunnelManager.java --view expanded || true

# Show the target section with line numbers
sed -n '220,320p' src/main/java/testingbot/TunnelManager.java

# Search for teardown/logging helpers and interrupt handling in the file
rg -n "logTeardownFailure|InterruptedException|best effort|StopTunnelHandler|stopOnChannel|interrupt\\(" src/main/java/testingbot/TunnelManager.java

Repository: testingbot/TestingBot-Jenkins-Plugin

Length of output: 6168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find call sites and related teardown logic
rg -n "stopOnChannel\\(|stop\\(App app, TaskListener listener\\)|channel.call\\(new StopTunnelHandler|RUNNING_TUNNELS.remove\\(" src/main/java src/test/java

# Show the public stop method and nearby context
sed -n '190,250p' src/main/java/testingbot/TunnelManager.java

Repository: testingbot/TestingBot-Jenkins-Plugin

Length of output: 4505


Preserve interruption and report teardown failures. stopOnChannel swallows InterruptedException and 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/testingbot/TunnelManager.java` around lines 260 - 264, Update
stopOnChannel’s exception handling around StopTunnelHandler so
InterruptedException restores the thread’s interrupted status, while other
teardown exceptions are logged with relevant failure details before returning.
Preserve the existing best-effort behavior without silently discarding remoting
failures.

Comment on lines +284 to +299
@Override
public Void call() throws Exception {
App app = new App();
try {
start(app, key, secret, tunnelOptions, tunnelIdentifier, listener);
} catch (Exception e) {
// Startup failed after the App was created; stop it so we don't leak a half-booted tunnel.
try {
app.stop();
} catch (Exception ignored) {
// best effort
}
throw e;
}
RUNNING_TUNNELS.put(tunnelIdentifier, app);
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 start finishes. If channel.call is interrupted while the remote callable continues, cleanup can remove nothing before line 298 subsequently registers a live tunnel. The unconditional put also overwrites an existing user-supplied identifier, making later teardown stop the wrong instance.

Install a collision-aware lifecycle entry before booting, coordinate stop with in-progress startup, and remove it conditionally on failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/testingbot/TunnelManager.java` around lines 284 - 299, Update
the tunnel startup flow in the callable’s call method to reserve
tunnelIdentifier in the running-tunnel registry before invoking start, reject
collisions without overwriting an existing entry, and coordinate cleanup so stop
waits for or safely handles in-progress startup. On startup failure, remove only
the entry owned by this tunnel and stop the created App as needed; preserve the
registry ownership through successful startup.

- TestingBotBuildWrapper no longer stores the setUp-time channel; tearDown
  re-resolves the build node's live channel via build.getBuiltOn().toComputer()
  so a mid-build agent reconnect can't leave teardown holding a stale channel.
  The controller-local fallback still stops its own App.
- TunnelManager.stopOnChannel restores the interrupt status on
  InterruptedException and logs other remoting failures instead of silently
  swallowing them, while staying best-effort (no rethrow).
- The start callable reserves the tunnel identifier (putIfAbsent) before booting:
  a duplicate identifier is rejected instead of overwriting/leaking an existing
  tunnel, teardown can find an App even mid-boot, and a startup failure removes
  only this tunnel's own entry.
@jochen-testingbot jochen-testingbot merged commit e2d4eb5 into build-report-embed Jul 16, 2026
5 checks passed
jochen-testingbot added a commit that referenced this pull request Jul 16, 2026
PRs #29 and #30 were merged into the build-report-embed branch, but that branch
had already been squash-merged to master as #28 beforehand, so their code never
reached master. This brings the stranded changes forward, on top of the current
master (which already has #28 and the #31 inline-media feature):

- #29: run the freestyle tunnel on the build's agent (TunnelManager.startOnChannel/
  stopOnChannel + shared registry; wrapper + testingbotTunnel step delegate to it).
- #30: TestingBotChecksPublisher (testingbotChecks) publishing results as a GitHub
  check, plus the checks-api + display-url-api dependencies and docs.

No behavior change versus the already-reviewed #29/#30; only rebased onto master.
The inline-media (#31) files are left untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant