DanielLeens commented on PR #11641:
URL: https://github.com/apache/seatunnel/pull/11641#issuecomment-5340845595
A note up front: this is my own PR, so this is a self-review and cannot
count as an independent approval — GitHub blocks self-review, so this is posted
as a plain comment. There is no new commit since @davidzollo's `APPROVED`
review on this exact head (`be6b805`), so this pass is not a re-review of new
code. What changed is a CI fact: my own last comment on this thread said the
required `Build` check was `cancelled` and that I'd triggered a rerun to get a
real signal. That rerun has since completed, and the picture is different from
what I described, so I'm correcting it here rather than leaving stale guidance
on the thread. I also independently re-derived every source-level claim below
from the current head rather than carrying anything over from the previous 16
rounds.
# What Problem Does This PR Solve?
- **User pain point:** `MetricsApiTest#metricsApiTest` intermittently failed
CI with `Expected status code <200> but was <500>` on `GET /metrics`, observed
on at least five unrelated PRs (#10874, #10958, #10973, #11060, #11382) on both
JDK 8 and JDK 11 `unit-test` lanes, blocking merges that had nothing to do with
metrics.
- **Fix approach:** replace the single, immediate assertion with a bounded
(60s, 1s interval, explicit zero poll-delay) Awaitility poll around a new
`assertMetricsExposed()` helper, add a per-request 5s connect/socket timeout,
convert transport failures into retryable `AssertionError`s instead of letting
them abort the poll, and attach the response body to the failure message so a
genuine failure is diagnosable from CI logs instead of a bare status code.
- **One-sentence summary:** the test encoded an implicit "the metrics
endpoint is fully warm the instant the member reaches STARTED" guarantee the
engine never promised, and this PR removes that false assumption without
weakening any of the original assertions.
# 1. Code Change Review
## 1.1 Core Logic Analysis
Single file, test-only change:
`seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/metrics/MetricsApiTest.java`
(verified via `gh pr diff --name-only` — no other file is touched despite the
branch being 28 commits behind `dev`).
Before (on `dev`):
```java
given().get("http://localhost:8080" + RestConstant.REST_URL_METRICS)
.then()
.statusCode(200)
.body(containsString("process_start_time_seconds"))
.body(containsString("engine_state_store_local_owned_entries"))
.body(containsString("engine_state_store_checkpoint_monitor_jobs"));
```
After (current head, `MetricsApiTest.java:103-107`):
```java
Awaitility.await()
.pollDelay(0, TimeUnit.SECONDS)
.pollInterval(1, TimeUnit.SECONDS)
.atMost(READY_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.untilAsserted(MetricsApiTest::assertMetricsExposed);
```
with `assertMetricsExposed()` (`:127-156`) wrapping the GET in a 5s
connect/socket timeout, rethrowing transport failures as `AssertionError`
(`:142-146`), and asserting status + all three original metric families with a
truncated-body failure message (`:158-178`).
**Root-cause diagnosis, re-verified against source today rather than trusted
from the PR description:**
- `SeaTunnelServer.init()` starts the Jetty listener synchronously:
`jettyService.createJettyServer()` is called directly inside `init()`
(`SeaTunnelServer.java:187-189`), and `JettyService.createJettyServer()` calls
`server.start()` synchronously (`JettyService.java:260`). So the HTTP listener
is already accepting connections by the time `@BeforeAll` returns.
- `CoordinatorService` becomes active on a separate, asynchronous path.
`SeaTunnelServer.getCoordinatorService()` (`SeaTunnelServer.java:286-318`)
throws `SeaTunnelEngineRetryableException` if the coordinator isn't active yet
even after its own internal wait/retry.
- `ExceptionHandlingFilter.doFilter()` catches any exception escaping the
servlet chain and maps it to HTTP 500, and — this is the detail I actually got
wrong in an earlier round and corrected on Aug 13 — the body is not just
`e.getMessage()`, it's the full stack trace:
`errorResponse.setMessage(ExceptionUtils.getStackTrace(e))`
(`ExceptionHandlingFilter.java:66`). So a collector that touches not-yet-wired
coordinator state during that narrow window produces a 500 whose body carries
the originating stack trace — which the pre-change test discarded entirely,
since Rest Assured's `statusCode(200)` only reports the code.
Given that, the fix is correctly targeted: it doesn't paper over the race
with a fixed sleep, it re-checks the real endpoint on every iteration (a
genuinely broken endpoint still fails, just after up to 60s instead of
instantly), and it makes the eventual failure carry the diagnostic that was
previously thrown away.
**Key Findings**
- The normal test-execution path reaches the changed logic directly — this
is the entire body of `metricsApiTest()`, not a defensive/recovery branch.
- The scenario covered is exactly the observed CI flake: a scrape landing in
the STARTED-but-coordinator-not-yet-active window.
- This is a precise fix, not a workaround: it removes an incorrect timing
assumption rather than hiding the symptom, and the actual production-side
readiness gap (a real Prometheus scraper hitting the same window) is tracked
separately rather than silently dropped — I confirmed
`https://github.com/apache/seatunnel/issues/11846` exists, is open, and its
title matches the diagnosed gap.
- No remaining code-level blind spot from the last 16 rounds of discussion:
the two issues the author's own self-review originally flagged (no retry on
non-`AssertionError` transport failures; implicit 1s first-poll delay) are both
resolved at this head, and I re-verified both mechanisms directly in the
current file rather than taking the changelog at its word.
## 1.2 Compatibility Impact
**Fully compatible.** Test-only change; no production code, configuration
option, default value, protocol, or serialization format is touched. `gh pr
diff --name-only` confirms the diff is confined to this one test file.
## 1.3 Performance / Side-Effect Analysis
- Healthy path: `pollDelay(0, TimeUnit.SECONDS)` (`:104`) makes the first
request fire immediately — Awaitility's fixed-interval strategy otherwise
defaults the poll delay to the poll interval when not set explicitly, so this
line is load-bearing, not decorative.
- Broken-endpoint path: failure detection moves from instant to bounded at
60s, which only costs time on a run that was going to fail anyway, and is well
inside the `unit-test` job's overall timeout.
- Stalled-connection path: the 5s `http.connection.timeout` /
`http.socket.timeout` (`:135-140`) bounds each individual attempt, so one hung
TCP read can't silently consume the entire 60s budget on a single blocked
evaluation.
- Resource release: `@AfterAll` still calls `instance.shutdown()`
unconditionally (`:110-115`); the poll itself introduces no new resource (no
executor, no background thread — `untilAsserted` blocks the calling test thread
synchronously).
- Log volume: the missing-metric path truncates the body via
`truncateForLogging` (`:170-178`) so a large Prometheus exposition payload
doesn't flood CI output across up to 60 retried assertions; the 500 path
deliberately keeps the body untruncated at `:152` because that's where the
stack-trace diagnostic actually lives.
## 1.4 Error Handling and Logging
No new formal issue at this head. All issues raised across the review
history (blanket `ignoreExceptions()` retrying unrelated `Throwable`s, no
per-request timeout, implicit poll delay, unbounded body logging) are resolved
in the current implementation, and I re-verified each fix directly against the
file rather than trusting the changelog. One item remains open, carried forward
unchanged from earlier rounds:
**Issue 1: Hardcoded `localhost:8080`**
- **Location:** `MetricsApiTest.java:46-47` (URL constant) and `:80` (port
set in `before()`).
- **Problem description:** the metrics URL is built against a fixed port
rather than one derived from the actually-bound port or an ephemeral port.
- **Potential risk:** if anything else on a CI runner holds 8080,
`server.start()` fails or the test could hit a foreign server. This is
pre-existing on `dev` and not introduced or worsened by this diff; with the
timeout/diagnostic fixes now in place, a collision at least fails with a
connection error inside the 5s per-request bound instead of an opaque 60s
timeout.
- **Best improvement:** a follow-up PR moving this test onto `HttpConfig`'s
dynamic-port mode.
- **Severity:** Low
- **Raised by another reviewer:** Yes — raised independently across multiple
earlier rounds (the author's own self-review and @SEZ9); not resolved by design
since it's out of scope for a targeted stability fix, and I agree with that
scoping decision.
# 2. Code Quality Assessment
## 2.1 Coding Standards
Import grouping/ordering follows project convention; the now-unused static
import (`org.hamcrest.Matchers.containsString`) is correctly removed. Every new
constant (`METRICS_URL`, `READY_TIMEOUT_SECONDS`, `REQUEST_TIMEOUT_MILLIS`,
`MAX_LOGGED_BODY_CHARS`) carries a Javadoc explaining its purpose and
constraints (`:49-71`), and both new private methods (`assertMetricsExposed`,
`assertContains`, `truncateForLogging`) are documented or short enough to be
self-evident. The inline rationale comment above the poll (`:87-102`) explains
the race, why `pollDelay(0)` is explicit, and why transport failures are
rethrown instead of swallowed — this is exactly the kind of "why, not what"
comment this protocol asks for on non-trivial test logic.
## 2.2 Test Coverage and Test Stability
Coverage is unchanged in scope: same endpoint, same 200 expectation, same
three metric-family checks, all retained verbatim inside the polled assertion
(`:153-155`). What changed is when the test gives up and what a failure reports
— the test is not weakened by this PR.
**Mandatory test-stability analysis (Section 5.10.2), re-derived from the
current file:**
- No hard wait: the mechanism is a genuine condition-driven poll
(`Awaitility.await()...untilAsserted(...)`, `:103-107`) that re-evaluates the
real HTTP call and all three assertions every iteration, not a fixed-duration
sleep followed by one assertion.
- Non-deterministic-timing dependency removed, not added: the pre-change
test's single immediate GET is exactly the "assert before the event has
actually happened" anti-pattern this section warns about; the poll replaces
that assumption with a bounded, re-checked condition.
- Transport-exception blind spot closed: `assertMetricsExposed()` catches
`Exception` from the HTTP call and rethrows as `AssertionError` with the
original exception as cause (`:142-146`), so `untilAsserted` (which by contract
only retries `AssertionError`) now retries connection-level transients under
the same 60s bound instead of aborting the poll on first occurrence — this was
the exact gap the author's own self-review flagged in the first round, and it
is closed here without reintroducing the earlier `ignoreExceptions()` version's
problem of also swallowing genuinely non-transient exceptions (a real bug in
the helper would still surface immediately as itself, not get silently retried
for 60s, since only the explicit HTTP-call `try/catch` is narrowed to
`Exception`, not the assertions below it).
- No resource leak: `@AfterAll` unconditionally shuts down the instance
(`:110-115`); no new executor, thread, or connection pool is introduced.
- No log-flooding risk: truncation on the missing-metric path (`:170-178`)
bounds the message size across up to 60 retried assertions.
- Hardcoded port remains (Issue 1 above) — a real but pre-existing,
Low-severity, non-blocking anti-pattern, not newly introduced.
- Live evidence at this exact head (`be6b805`, fork run `32092429934`):
`unit-test (8, ubuntu-latest)`, `unit-test (11, ubuntu-latest)`, `unit-test (8,
windows-latest)`, and `unit-test (11, windows-latest)` — the only lanes that
execute this test — all report `conclusion: success`.
**Stability rating: Stable.** No remaining flaky-test anti-pattern was found
in the current implementation; the one open item (Issue 1, hardcoded port) is a
pre-existing, low-risk, non-blocking carryover rather than something this diff
introduces or worsens, and it does not warrant a "Risk present" rating on its
own given the timeout/diagnostic hardening already in place around it.
## 2.3 Documentation Updates
Not applicable — no user-facing behavior, config, or API changed, so no
`docs/en` / `docs/zh` update is required. In-code documentation (constant
Javadoc, method Javadoc, rationale comment) is present and, as far as I can
re-verify today, accurate.
# 3. Architectural Soundness
## 3.1 Elegance of the Solution
**Precise fix.** It targets the actual defect (an unstated timing
assumption) rather than hardening every collector's check-then-act readiness
guard on the production side, which would be a much larger change for a
CI-stability goal. The production-side gap is correctly split out into a
tracked follow-up issue instead of being conflated with this test fix.
## 3.2 Maintainability
Good. The response-body-attached failure message makes the next genuine
failure self-diagnosing from CI logs alone — the property the old test lacked
and the actual reason these failures went undiagnosed for months. The rationale
comment should deter a future "simplify this back to one GET" regression.
## 3.3 Extensibility
The `assertMetricsExposed` / `assertContains` split makes adding another
metric-family assertion a one-line change. If other REST-layer tests in this
module race the same coordinator-wiring window, this shape is small enough to
lift into a shared test utility later — premature to do so for one test.
## 3.4 Historical-Version Compatibility
Not applicable in the strict sense (no shipped
artifact/protocol/serialization change). Worth noting: unlike the `dev`
version, this test no longer silently depends on today's exact startup timing,
so it stays correct if the coordinator-wiring window changes shape in a future
engine version.
# 4. Issue Summary
| No. | Issue | Location | Severity |
|-----|-------|----------|----------|
| 1 | Hardcoded `localhost:8080` (pre-existing on `dev`, kept by this PR) |
`MetricsApiTest.java:46-47,80` | Low, non-blocking |
No blocking issues found in the code at this head.
# 5. Merge Recommendation
### Conclusion: Ready to merge after fixes
1. **Blockers — must be fixed**
- Not a code blocker. The required `Build` check on this head (`be6b805`)
currently reports `failure`, and that needs to turn green before this can
merge, but it is not evidence of a defect in this diff. Diagnosis below.
2. **Recommended fixes — non-blocking**
- Issue 1 (hardcoded port): worth a small follow-up PR moving this and
similarly-shaped REST tests onto dynamic-port mode. Predates this diff and
isn't this PR's job to fix.
**CI diagnosis (correcting my own Aug 18 03:33 UTC comment on this
thread):** I previously said the apache-side `Build` check showed `cancelled`
and that I'd triggered a rerun to get a real signal. That rerun (fork run
`32092429934`) has since completed with `conclusion: failure`, so the situation
has changed and I want to be precise about what that failure actually is rather
than leave the old "wait for the rerun" guidance standing:
- The four `unit-test` jobs — `unit-test (8, ubuntu-latest)`, `unit-test
(11, ubuntu-latest)`, `unit-test (8, windows-latest)`, `unit-test (11,
windows-latest)` — the only jobs that execute `MetricsApiTest` — all report
`conclusion: success`.
- The jobs that actually failed/cancelled are `all-connectors-it-1 (8,
ubuntu-latest)` (failure), `all-connectors-it-1 (11, ubuntu-latest)`
(cancelled), `paimon-connector-it (8, ubuntu-latest)` (cancelled), and
`jdbc-connectors-it-ddl (11, ubuntu-latest)` (failure) — all connector-v2 IT
suites in modules this PR does not touch (the diff is one file under
`seatunnel-engine-server/src/test`).
- This branch is `behind_by=28` against `dev`, and I found concrete evidence
the `all-connectors-it-1` failure is a known, already-fixed-on-`dev` issue that
simply postdates this branch's last `dev` merge: `dev` commit `449db4ef`
("[Test][E2E] Retry Couchbase container startup and capture server logs")
landed at `2026-08-18T05:44:39Z`, about three hours *after* this fork's CI run
started (`2026-08-18T02:36:08Z`) — the Couchbase-container-bootstrap flake it
fixes is exactly the kind of failure that kills `all-connectors-it-1` shards.
Separately, `dev` commit `5b358f8b` ("[Fix][CI] Add fail-fast: false to
backend.yml matrix jobs") landed today (`2026-08-19T07:01:00Z`) and explains
why sibling jobs in the same matrix show `cancelled` rather than their own real
result once one job in the group fails. I did not find a specific dev-side fix
for the `jdbc-connectors-it-ddl` failure, so I can't claim the same direct
evidence there, but it's in an unrelated module either way.
- So: please sync with the latest `dev` and rerun CI. Given the evidence
above, that's a real fix for at least the `all-connectors-it-1`/cancellation
pattern, not just a generic "try again." If `jdbc-connectors-it-ddl` still
fails after the sync, that would need its own triage, but it would not be
evidence against this PR's diff.
**Response to the existing review state:** @davidzollo's `APPROVED` review
on this exact head independently re-verified the `awaitility-4.2.0` internals
(poll-delay default, `AssertionCondition` message handling) against bytecode. I
agree with that conclusion — I re-derived the same claims today from the source
files directly (`SeaTunnelServer.java`, `JettyService.java`,
`ExceptionHandlingFilter.java`) rather than re-trusting the earlier rounds, and
they hold up. `reviewDecision` on the PR already shows `APPROVED`; the only
thing standing between this and mergeable is the CI gate described above, not
any remaining code-side concern from my side.
Overall assessment: the diagnosis is backed by source on both the production
race and the response-body claim, the fix removes a real timing assumption
without weakening a single original assertion, the diagnostic value (full
response body on failure) is the part with lasting worth, and live CI already
confirms the exact previously-flaking lanes are green at this head. Once this
branch is synced to current `dev` and the `Build` check comes back green on the
actually-relevant lanes, I don't see anything left to block merge.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]