DanielLeens commented on PR #11641:
URL: https://github.com/apache/seatunnel/pull/11641#issuecomment-5342884245

   A note up front, same as every prior pass on this thread: this is my own PR, 
so this is a self-review and cannot count as an independent approval — GitHub 
blocks self-approval, so it is posted as a plain comment. There is no new 
commit since my last pass (10:28 UTC today) or since @davidzollo's `APPROVED` 
review, both on this exact head (`be6b805`), so the code-level analysis below 
is a verification pass, not a review of new code. What has changed is CI: my 
own 10:28 UTC comment reported the `Build` check at `conclusion: failure` 
(unrelated `all-connectors-it-1` / `jdbc-connectors-it-ddl` IT shards) and 
recommended a `dev` sync + rerun. That has since resolved on its own — the 
required `Build` check on this exact head now reports `pass` (53m32s, full run, 
not a sanity-only short-circuit). I'm correcting the record rather than leaving 
stale "wait for sync" guidance standing.
   
   I also independently re-derived the core technical claims below by reading 
the actual engine and Awaitility library source rather than trusting the prior 
16 rounds' text, and found one genuinely new piece of evidence worth adding 
(see 1.3).
   
   # 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.
   - **Fix approach:** replace the single, immediate assertion with a bounded 
(60s cap, 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.
   - **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; this PR removes that false assumption without weakening 
a single original assertion.
   
   # 1. Code Change Review
   
   ## 1.1 Core Logic Analysis
   
   Single file, test-only: 
`seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/metrics/MetricsApiTest.java`
 (+118/-7 against `dev`, confirmed via `git diff dev...HEAD --stat` on the 
mirror).
   
   Before (`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 (`MetricsApiTest.java:103-107`):
   ```java
   Awaitility.await()
           .pollDelay(0, TimeUnit.SECONDS)
           .pollInterval(1, TimeUnit.SECONDS)
           .atMost(READY_TIMEOUT_SECONDS, TimeUnit.SECONDS)
           .untilAsserted(MetricsApiTest::assertMetricsExposed);
   ```
   
   **Root-cause diagnosis, re-verified against source at this head, not taken 
on faith:**
   - `SeaTunnelServer.init()` calls `startMaster()` and then starts Jetty 
synchronously later in the same method (`SeaTunnelServer.java:188-191` → 
`jettyService.createJettyServer()`), so the HTTP listener is accepting 
connections by the time `@BeforeAll`'s `createHazelcastInstance()` returns, 
while `CoordinatorService` activation is asynchronous.
   - `SeaTunnelServer.getCoordinatorService()` (`SeaTunnelServer.java:286-318`) 
retries 3×500ms internally and, if still not active, throws 
`SeaTunnelEngineRetryableException("Can not get coordinator service from an 
active master node.")` (line 313-314).
   - `JobMetricExports.collect()` guards with `if (isMaster() && 
isCoordinatorReady())`, where `isCoordinatorReady()` is a non-blocking 
`getServer().isCoordinatorActive()` check — a genuine check-then-act window 
against the subsequent `getCoordinatorService()` call.
   - `MetricsServlet.doGet()` drives `TextFormat.writeFormat(contentType, 
stringWriter, collectorRegistry.metricFamilySamples())`, iterating every 
collector in one pass with no per-collector isolation, so one throwing 
collector aborts the whole scrape.
   - `ExceptionHandlingFilter.doFilter()` catches any escaping `Exception` and, 
at `handleException()`, sets 
`errorResponse.setMessage(ExceptionUtils.getStackTrace(e))` — the full stack 
trace, confirming the 500 body genuinely carries diagnosable information, which 
is what makes attaching it to the assertion message worthwhile.
   
   This is a real, reachable race on the normal CI path (the test runs on every 
PR), not a CI artifact, and the fix targets it directly rather than papering 
over the symptom.
   
   **Is this "fix the race" or "loosen the assertion"?** Line-by-line against 
the pre-PR single-shot assertion: same status-code expectation, same three 
`contains` checks, same strings, re-evaluated on *every* poll iteration, not 
once. No threshold widened, no substring dropped, no `Thread.sleep`-and-hope. A 
server still returning 500 (or missing a metric family) after 60s still fails 
the test — `untilAsserted` re-throws a `ConditionTimeoutException` carrying the 
last `AssertionError`'s message, so a genuinely broken endpoint does not pass.
   
   ## 1.2 Compatibility Impact
   
   **Fully compatible.** Test-only change; no production code, config option, 
default value, protocol, or serialization format is touched.
   
   ## 1.3 Performance / Side-Effect Analysis
   
   - Healthy path: `pollDelay(0, TimeUnit.SECONDS)` makes the first request 
fire immediately. I checked this claim against the actual `awaitility-4.2.0` 
sources (not the changelog text): `ConditionFactory.pollInterval(long, 
TimeUnit)` calls `definePollDelay(pollDelay, fixedPollInterval)`, which — when 
`pollDelay` was never explicitly set — defaults it to the poll interval for a 
`FixedPollInterval`. So without the explicit `pollDelay(0, ...)` line, the 
first GET really would be pushed out by ~1s; the line is load-bearing, not 
decorative.
   - Stalled-connection path, one piece of evidence not previously on this 
thread: `ConditionAwaiter.await()` runs each poll on a 
`Executors.newSingleThreadExecutor(...)`-backed executor 
(`InternalExecutorServiceFactory.create`), and that factory uses the JDK 
default `ThreadFactory` — i.e. **non-daemon** threads. The awaiting thread does 
correctly bound the *overall* wait via 
`getUninterruptibly(currentConditionEvaluation, maxWaitTimeForThisCondition)`, 
and on timeout calls `currentConditionEvaluation.cancel(true)` / 
`shutdownNow()`. But a plain blocking socket read (Apache HttpClient's 
synchronous transport, as used here via RestAssured) does not respond to 
`Thread.interrupt()`. So absent `REQUEST_TIMEOUT_MILLIS`, a request that truly 
hangs (TCP connected, server never responds, no RST) would leave a 
**non-daemon** evaluation thread blocked on the socket read past `atMost`'s 
expiry — not just "wastes the retry budget" as the in-code comment says, but a 
thread that can outlive th
 e test method and, in the worst case, keep the Surefire JVM fork from exiting 
cleanly. `REQUEST_TIMEOUT_MILLIS = 5_000` closes this precisely, and I'd call 
it a more concrete justification for the constant than the current Javadoc 
states, not a gap in the PR.
   - Broken-endpoint path: failure detection moves from instant to bounded at 
60s — well inside the `unit-test` job's overall timeout, and only paid when the 
test was going to fail anyway.
   - Resource release: `@AfterAll` still calls `instance.shutdown()` 
unconditionally; no new executor/thread/connection pool is introduced by the 
test code itself.
   - Log volume: `truncateForLogging` bounds the missing-metric-path message to 
4KB across up to 60 retried assertions; the 500 path deliberately keeps the 
full body (that's where the stack trace lives).
   
   ## 1.4 Error Handling and Logging
   
   No new formal issue at this head — every issue raised across 16 prior rounds 
(blanket `ignoreExceptions()` retrying unrelated `Throwable`s, missing 
per-request timeout, implicit 1s poll delay, unbounded body logging) is 
resolved in the current implementation. One item remains open, carried forward 
unchanged and correctly scoped out of this PR:
   
   **Issue 1: Hardcoded `localhost:8080`**
   - **Location:** `MetricsApiTest.java:46-47` (URL constant), `:80` (port set 
in `before()`).
   - **Problem description:** the metrics URL is built against a fixed port 
rather than an actually-bound or ephemeral one. Pre-existing on `dev`, not 
introduced by this diff.
   - **Potential risk:** if another process on a CI runner holds 8080, 
`server.start()` fails or the test could hit a foreign server. With the 
timeout/diagnostic hardening now in place, a collision at least fails fast 
(within the 5s per-request bound) with a connection error instead of an opaque 
60s timeout.
   - **Best improvement:** follow-up PR moving this test (and similarly-shaped 
REST tests) onto `HttpConfig`'s dynamic-port mode.
   - **Severity:** Low, non-blocking.
   - **Raised by another reviewer:** Yes — raised independently by the author's 
own self-review and by @SEZ9 across multiple earlier rounds; +1, still agree 
it's correctly out of scope for a targeted stability fix.
   
   # 2. Code Quality Assessment
   
   ## 2.1 Coding Standards
   
   Import grouping/ordering follows project convention; the now-unused 
`org.hamcrest.Matchers.containsString` static import is correctly removed. 
Every new constant carries Javadoc explaining its purpose and constraints, and 
the new private methods are documented. The inline rationale comment above the 
poll explains the race and the design choices (`pollDelay(0)`, 
rethrow-as-`AssertionError` instead of `ignoreExceptions()`) — "why," not 
"what."
   
   ## 2.2 Test Coverage and Test Stability
   
   Coverage unchanged in scope: same endpoint, same 200 expectation, same three 
metric-family checks, retained verbatim inside the polled assertion.
   
   **Mandatory flaky-test analysis, re-derived at this head:**
   - No hard sleep anywhere — `Awaitility.await()...untilAsserted(...)` is a 
genuine condition-driven poll that re-issues the real HTTP call and re-checks 
all three assertions every iteration, not a fixed-duration sleep followed by a 
single assertion.
   - Non-deterministic-timing dependency removed, not added — the pre-change 
test's single immediate GET was exactly the "assert before the event has 
happened" anti-pattern; the poll replaces it with a bounded, re-checked 
condition.
   - Transport-exception blind spot closed correctly and narrowly: 
`assertMetricsExposed()` wraps only the HTTP call itself in `try { ... } catch 
(Exception e) { throw new AssertionError(...) }` (`:129-146`) — a real bug 
elsewhere in the helper (e.g. in `assertContains`) still surfaces as its own 
`AssertionError` immediately via the normal JUnit assertion path, not silently 
retried behind a broad `ignoreExceptions()`. One narrow, non-blocking residual: 
`response.getBody().asString()` (`:148`) sits *outside* the try/catch, so if 
reading an already-received response body ever threw a checked or unchecked 
exception, it would propagate un-converted and abort the poll early via 
Awaitility's `catch (Throwable) -> CheckedExceptionRethrower.safeRethrow`. In 
practice RestAssured buffers the body eagerly during `.get(...)`, so this is a 
theoretical edge, not a reproducible one — noting it for completeness rather 
than as a blocking finding.
   - No resource leak: `@AfterAll` unconditionally shuts down the instance; no 
new executor/thread is introduced by the test's own code (see 1.3 for why the 
*library's* executor threading model still matters here).
   - No log-flooding risk: truncation on the missing-metric path bounds message 
size across up to 60 retried assertions.
   - Live evidence at this exact head (`be6b805`): fork CI's `unit-test (8, 
ubuntu-latest)`, `unit-test (11, ubuntu-latest)`, `unit-test (8, 
windows-latest)`, `unit-test (11, windows-latest)` — the only lanes that 
execute this test — and the apache-side required `Build` check are all green as 
of this pass (`gh pr checks`: `Build pass, 53m32s`).
   
   **Stability rating: Stable.** No flaky-test anti-pattern remains in the 
current implementation. The one open item (Issue 1, hardcoded port) is a 
pre-existing, low-risk, non-blocking carryover, not something this diff 
introduces or worsens.
   
   ## 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 is present and 
accurate.
   
   # 3. Architectural Soundness
   
   ## 3.1 Elegance of the Solution
   **Precise fix.** Targets the actual defect (an unstated timing assumption) 
instead of 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 into a tracked follow-up (#11846) 
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 across five blocked PRs. 
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; the same await-with-diagnostic shape 
could be lifted into a shared test utility if other REST-layer tests hit the 
same coordinator-wiring window.
   
   ## 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
   
   1. **Blockers — must be fixed:** none. `reviewDecision` on the PR is already 
`APPROVED` (@davidzollo and @nzw921rx), the required `Build` check on this 
exact head (`be6b805`) is green (`pass`, 53m32s — a full run, not a 
short-circuited sanity pass), and the four `unit-test` lanes that actually 
execute `MetricsApiTest` all report success. My own 10:28 UTC comment on this 
thread, reporting `Build` at `failure` due to unrelated `all-connectors-it-1` / 
`jdbc-connectors-it-ddl` IT shards, is now stale — that run has since been 
superseded by a green one on the same head, so the "sync with dev and rerun" 
recommendation in that comment no longer applies.
   
   2. **Recommended fixes — non-blocking:**
      - Issue 1 (hardcoded port): small follow-up PR moving this and 
similarly-shaped REST tests onto dynamic-port mode.
   
   Overall assessment: the diagnosis is backed by source on both the production 
race and the response-body claim, re-verified independently at this head 
against the engine sources and against the actual `awaitility-4.2.0` library 
sources (not just prior review text). 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 CI now confirms 
the exact previously-flaking lanes are green at this head. 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]

Reply via email to