Skip to content

fix(file-service, v1.2): retry LakeFS health check on startup - #7103

Queued
Yicong-Huang wants to merge 3 commits into
apache:release/v1.2from
Yicong-Huang:backport/5647-retry-lakefs-health-check-on-startup-v1.2
Queued

fix(file-service, v1.2): retry LakeFS health check on startup#7103
Yicong-Huang wants to merge 3 commits into
apache:release/v1.2from
Yicong-Huang:backport/5647-retry-lakefs-health-check-on-startup-v1.2

Conversation

@Yicong-Huang

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

Backport of #5647 to release/v1.2, cherry-picked from 263093a.

Follows the Direct Backport Push convention; opened as a PR (rather than a direct push) as part of a backport-coverage audit for fixes merged to main since early June that were never labeled for backport.

Any related issues, documentation, discussions?

Backport of #5647. Originally linked #5646.

How was this PR tested?

Release-branch CI runs on this PR. The cherry-pick applied cleanly onto release/v1.2; no manual conflict resolution was needed.

Was this PR authored or co-authored using generative AI tooling?

Yes — backport prepared with Claude Code (mechanical cherry-pick; the change itself is #5647 by its original author).

### What changes were proposed in this PR?
- `LakeFSStorageClient.healthCheck()` now retries the LakeFS health
check with **exponential backoff** before failing, so a transient
`Connection reset` during concurrent startup no longer crashes the
file-service.
- Retry policy: 5 attempts with the delay doubling each time, starting
at 200ms (200, 400, 800, 1600ms), capping total wait at ~3s.
- The retry logic is factored into a small, reusable `retryWithBackoff`
helper with an injectable `sleep` function so the backoff can be
unit-tested without real waiting.
- Each failed attempt logs a warning; if LakeFS is still unreachable
after all attempts, startup fails with a clear aggregated error
(preserving the last exception as the cause) so a genuine outage is
still surfaced.
- An `InterruptedException` while waiting restores the thread's
interrupt status and fails fast instead of retrying.
- Added `LakeFSStorageClientSpec` covering immediate success,
retry-then-success with doubling delays, give-up-after-max-attempts
(with cause preserved), and interrupt handling.

### Any related issues, documentation, discussions?
Closes: apache#5646

### How was this PR tested?
- Unit tests: `LakeFSStorageClientSpec` exercises the backoff behavior
(delay sequence, max-attempt failure, interrupt handling) using an
injected `sleep`.
- Reproduce the race: stop LakeFS, start file-service, then start LakeFS
within a few seconds; confirm file-service logs retry warnings and then
starts successfully instead of exiting.
- Regression with LakeFS already up: start file-service, confirm it
binds `:9092` and `curl http://127.0.0.1:9092/api/healthcheck` returns
HTTP 200.
- Compile: run `sbt "FileService/compile"` and expect `[success]`.

### Was this PR authored or co-authored using generative AI tooling?
Co-authored with Claude Opus 4.8 in compliance with ASF

(backported from commit 263093a)
@github-actions

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • No candidates found from git blame history.

@codecov-commenter

codecov-commenter commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.72727% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.62%. Comparing base (b1ac819) to head (4d33d1b).
⚠️ Report is 1 commits behind head on release/v1.2.

Files with missing lines Patch % Lines
.../amber/core/storage/util/LakeFSStorageClient.scala 72.72% 4 Missing and 2 partials ⚠️
Additional details and impacted files
@@                Coverage Diff                 @@
##             release/v1.2    #7103      +/-   ##
==================================================
- Coverage           52.84%   52.62%   -0.22%     
+ Complexity           2525     2520       -5     
==================================================
  Files                1078     1078              
  Lines               42348    42350       +2     
  Branches             4563     4566       +3     
==================================================
- Hits                22379    22288      -91     
- Misses              18656    18743      +87     
- Partials             1313     1319       +6     
Flag Coverage Δ *Carryforward flag
access-control-service 64.35% <ø> (ø)
agent-service 34.36% <ø> (ø) Carriedforward from 5c53cf7
amber 52.90% <72.72%> (+<0.01%) ⬆️
computing-unit-managing-service 1.65% <ø> (ø)
config-service 56.06% <ø> (ø)
file-service 61.72% <ø> (ø)
frontend 47.29% <ø> (ø) Carriedforward from 5c53cf7
pyamber 91.09% <ø> (-1.98%) ⬇️ Carriedforward from 5c53cf7
python 90.88% <ø> (ø) Carriedforward from 5c53cf7
workflow-compiling-service 58.69% <ø> (ø)

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

renovate-bot pushed a commit to renovate-bot/apache-_-texera that referenced this pull request Jul 30, 2026
### 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 a new module rather than `common/workflow-core`**, which was
proposal 1 in apache#7095: `workflow-core` is not reachable from `Auth`,
`ConfigService`, or `AccessControlService`, so hosting it there would
have left future callers in those modules stuck writing their own loop —
the exact thing this PR is meant to stop. `common/config` is reachable
from everything and already carries
`org.apache.texera.amber.util.ConfigParserUtil`, but a retry helper
isn't configuration. A dependency-free module any other module may
depend on avoids both problems, at the cost of one `build.sbt` entry.

**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 the classpath of every service that depends
on the new module 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 dependency-free for the same
reason.

**One behavior change, deliberate.** Both replaced loops caught `case e:
Exception`; the util uses `NonFatal`, for parity with `Utils.retry`.
`NonFatal` is wider: non-fatal `Error`s — `AssertionError`,
`java.io.IOError`, `ServiceConfigurationError` — are now retried and
wrapped rather than propagating immediately. On the S3/LakeFS startup
paths that means such a failure costs the full backoff before surfacing.
`RetryUtilSpec` pins it in both directions (a non-fatal `Error` is
retried; a fatal throwable is not).

**Two retry sites deliberately left alone**, because converting them
changes behavior rather than just structure — tracked with the full
reasoning in apache#7124:

| 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 apache#7095

Follows apache#7088, which introduced the non-blocking variant; the
consolidation was split out of it to keep that fix narrow. Context: the
review discussion on apache#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`, registered in `build.yml`'s
hand-maintained `jacoco` module list so it actually runs) 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, while a
non-fatal `Error` is retried (the `NonFatal`-vs-`Exception` widening
above).

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/jacoco" "FileService/testOnly org.apache.texera.service.FileServiceSpec" "WorkflowCore/testOnly org.apache.texera.amber.core.storage.util.LakeFSStorageClientSpec"
```

```
[info] Tests: succeeded 11, 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)
@Yicong-Huang
Yicong-Huang added this pull request to the merge queue Jul 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 31, 2026
@xuang7
xuang7 added this pull request to the merge queue Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants