aglinxinyuan opened a new pull request, #7119:
URL: https://github.com/apache/texera/pull/7119

   ### What changes were proposed in this PR?
   
   The same doubling-backoff loop was written three times, and no module could 
share the others' copy: amber's `Utils` sits above `common/workflow-core` and 
`file-service` in the module graph.
   
   New dependency-free **`common/util`** module with `RetryUtil.withBackoff`, 
and both blocking copies now call it:
   
   | Site | Before | After |
   | --- | --- | --- |
   | `LakeFSStorageClient.healthCheck` | private 27-line `retryWithBackoff` | 
`RetryUtil.withBackoff("connect to lake fs server", 5, 200, ...)` |
   | `FileService.awaitDependency` | private 36-line loop | 7-line delegation 
keeping its 6/200 defaults + logger |
   | `Utils.retry` (amber, non-blocking) | unchanged | cross-references the 
blocking sibling |
   
   ```scala
   RetryUtil.withBackoff(description, maxAttempts, initialDelayMillis, onRetry, 
sleep)(operation)
   ```
   
   The util owns everything the copies duplicated: attempt counting, the 
doubling, what counts as transient (`NonFatal`), interrupt fail-fast with the 
interrupt status restored, and the message wording. `description` is a verb 
phrase ("connect to lake fs server"), interpolated into all three messages, so 
LakeFS's give-up text is byte-identical to before. Retries are reported through 
an `onRetry` hook carrying a `RetryAttempt` (attempt, budget, delay, cause, and 
the standard `message`), which callers log with **their own** logger — so a 
LakeFS retry still logs under LakeFS, not under the util.
   
   **Bug fixed on the way in.** LakeFS's `sleep` call sat inside the `catch` 
block, so an interrupt raised *while waiting* escaped raw with the interrupt 
flag left cleared — a `catch` cannot catch what its own body throws:
   
   ```
   Before (LakeFS):  op fails -> catch -> sleep interrupted -> 
InterruptedException escapes, flag cleared
   After  (shared):  op fails -> sleep interrupted -> interrupt restored + 
wrapped, fails fast
   ```
   
   `FileService`'s copy already plugged that hole with an inner `try`; the 
shared util keeps the better behavior for both.
   
   **Why the non-blocking variant stays in amber.** `Utils.retry` needs 
`com.twitter:util-core`, declared only in `amber/build.sbt`. Moving it down 
would put util-core on every service's classpath and force LICENSE-binary 
resyncs, so the pair is: blocking in `common/util`, non-blocking in `amber`, 
each documented in terms of the other. The new module is deliberately 
dependency-free for the same reason — everything depends on it.
   
   **Two retry sites deliberately left alone**, because converting them changes 
behavior rather than just structure:
   
   | Site | Why not now |
   | --- | --- |
   | `URLFetchUtil.getInputStreamFromURL` | Retries 5x with **no** delay and 
returns `Option`. Adopting backoff adds up to ~3 s before a dead URL gives up — 
arguably a fix (it is an unthrottled retry storm today), but a behavior change 
in an operator path. |
   | `PythonProxyClient` Flight connect loop | **Constant** delay, closes the 
Flight client between attempts, and throws `WorkflowRuntimeException` on 
give-up. Needs a delay-multiplier knob plus a give-up type decision. |
   
   ### Any related issues, documentation, discussions?
   
   Closes #7095
   
   Follows #7088, which introduced the non-blocking variant; the consolidation 
was split out of it to keep that fix narrow. Context: the review discussion on 
#7103 (the LakeFS health-check backport) asked for one util rather than a 
fourth copy.
   
   ### How was this PR tested?
   
   `RetryUtilSpec` (new, in `common/util`) is the union of what the two 
hand-rolled loops were tested for, plus what neither covered:
   
   - returns the operation's value on first success without sleeping; retries 
until success with a doubling progression; honors a custom base delay; succeeds 
on the final permitted attempt (the `attempt >= maxAttempts` boundary); gives 
up naming the description and preserving the cause; `maxAttempts = 1` means no 
retry and no sleep;
   - the `onRetry` hook fires once per wait with 1-based attempt numbers and 
never after the last attempt, and its `message` wording is pinned;
   - interrupt during the operation **and** interrupt during a backoff sleep 
both restore the interrupt status and fail fast (the second is the LakeFS hole 
above);
   - a fatal throwable is not retried and passes through unwrapped.
   
   At the call sites: `FileServiceSpec` keeps 8 tests that exercise the 
delegation, its own 6/200 defaults, and the description in its messages (the 4 
that only re-tested util mechanics moved into `RetryUtilSpec`). 
`LakeFSStorageClientSpec`'s 4 retry tests moved out with the loop; its 
remaining 5 pass unchanged. amber's `UtilsSpec` and 
`RegionExecutionManagerSpec` are untouched and still green.
   
   ```bash
   sbt "Util/test" "FileService/testOnly 
org.apache.texera.service.FileServiceSpec" "WorkflowCore/testOnly 
org.apache.texera.amber.core.storage.util.LakeFSStorageClientSpec"
   ```
   
   ```
   [info] Tests: succeeded 10, failed 0, canceled 0, ignored 0, pending 0   
(Util)
   [info] Tests: succeeded 12, failed 0, canceled 0, ignored 0, pending 0   
(FileService)
   [info] Tests: succeeded 5,  failed 0, canceled 0, ignored 0, pending 0   
(WorkflowCore / LakeFS)
   ```
   
   Lint plus test-source compiles for every module the new dependency edge 
touches, and amber's two retry specs:
   
   ```bash
   sbt "Util/scalafixAll --check" "Util/scalafmtCheckAll" 
"WorkflowCore/Test/compile" "WorkflowCore/scalafixAll --check" 
"FileService/Test/compile" "FileService/scalafixAll --check" 
"WorkflowExecutionService/Test/compile" "WorkflowExecutionService/scalafixAll 
--check" "WorkflowExecutionService/testOnly 
org.apache.texera.amber.engine.common.UtilsSpec 
org.apache.texera.amber.engine.architecture.scheduling.RegionExecutionManagerSpec"
   ```
   
   ```
   [success] all scalafix / scalafmt checks and Test/compile tasks
   [info] Tests: succeeded 27, failed 0, canceled 0, ignored 0, pending 0
   ```
   
   ### Was this PR authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code (Opus 5)
   


-- 
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