Skip to content

[AMORO-4198][optimizer] Support graceful shutdown for in-progress tasks#4197

Merged
czy006 merged 6 commits into
apache:masterfrom
j1wonpark:optimizer-graceful-shutdown
Jul 6, 2026
Merged

[AMORO-4198][optimizer] Support graceful shutdown for in-progress tasks#4197
czy006 merged 6 commits into
apache:masterfrom
j1wonpark:optimizer-graceful-shutdown

Conversation

@j1wonpark

@j1wonpark j1wonpark commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Why are the changes needed?

Close #4198.

When an optimizer receives SIGTERM, in-progress tasks are silently dropped and
AMS re-schedules them — doubling work and potentially causing duplicate commits.

Brief change log

  • Optimizer.stopOptimizing(): join executor threads up to --shutdown-timeout-ms
    (default 10 min); keep toucher alive during drain so AMS heartbeats continue
  • OptimizerExecutor.completeTask(): best-effort direct call after shutdown so
    results are not silently dropped
  • OptimizerToucher.stop(): interrupt runner thread to wake it from sleep immediately
  • AbstractOptimizerOperator.waitAShortTime(): preserve interrupt flag
  • OptimizerConfig: new -st / --shutdown-timeout-ms option
  • StandaloneOptimizer / SparkOptimizer: register graceful shutdown hook on
    Hadoop's ShutdownHookManager above FS_CACHE priority with explicit timeout
  • KubernetesOptimizerContainer: exec prefix in container command; derive
    terminationGracePeriodSeconds from shutdown-timeout-ms + 30s buffer
  • optimizer.sh start-foreground: exec $CMDS so Java receives SIGTERM directly

How was this patch tested?

  • Add some test cases that check the changes thoroughly including negative and positive cases if possible

  • Add screenshots for manual tests if appropriate

  • Run test locally before making a pull request

Documentation

  • Does this pull request introduce a new feature? yes
  • If yes, how is the feature documented? JavaDocs / option usage string

Review updates

  • Wired the shutdown-timeout-ms optimizer group property to the -st startup arg and the K8s grace period (single typed source; the CLI re-parser is removed), and documented it. (@czy006 review point 1)
  • Added a graceful drain to the Flink optimizer: FlinkExecutor.close() waits up to the shutdown timeout, capped below Flink's task.cancellation.timeout. (@czy006 review point 2)
  • Hardened the new shutdown paths: safe publication of executor threads, force-interrupt reaching all executors, no busy-spin on stray interrupts, completeTask retrying through a bounded drain window, and no toucher re-registration while draining.

Known limitations (proposed follow-ups)

  • Spark container release path: spark-submit --kill does not extend the driver pod's termination grace period (K8s default 30s applies); operators can work around it via spark-conf.spark.kubernetes.appKillPodDeletionGracePeriod.
  • Local container release path: LocalOptimizerContainer.releaseResource() uses kill -9, which skips JVM shutdown hooks; graceful drain only applies to externally sent SIGTERM.

@j1wonpark j1wonpark changed the title [AMORO][optimizer] Support graceful shutdown for in-progress tasks [AMORO-4198][optimizer] Support graceful shutdown for in-progress tasks Apr 30, 2026
@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 59.09091% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 30.24%. Comparing base (99fcc08) to head (2a6df8f).
⚠️ Report is 12 commits behind head on master.

Files with missing lines Patch % Lines
...a/org/apache/amoro/optimizer/common/Optimizer.java 43.58% 18 Missing and 4 partials ⚠️
...o/server/manager/KubernetesOptimizerContainer.java 77.27% 3 Missing and 2 partials ⚠️
...pache/amoro/optimizer/common/OptimizerToucher.java 69.23% 2 Missing and 2 partials ⚠️
...apache/amoro/optimizer/common/OptimizerConfig.java 40.00% 3 Missing ⚠️
...ache/amoro/optimizer/common/OptimizerExecutor.java 75.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #4197      +/-   ##
============================================
+ Coverage     23.09%   30.24%   +7.14%     
- Complexity     2706     4390    +1684     
============================================
  Files           463      680     +217     
  Lines         42826    55337   +12511     
  Branches       6044     7102    +1058     
============================================
+ Hits           9891    16735    +6844     
- Misses        32076    37337    +5261     
- Partials        859     1265     +406     
Flag Coverage Δ
core 30.24% <59.09%> (?)
trino ?

Flags with carried forward coverage won't be shown. 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:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

On SIGTERM the optimizer flips its stopped flag and returns immediately,
so in-flight task results are silently dropped (completeTask is gated by
isStarted). On K8s this is compounded by `sh -c` swallowing SIGTERM, a
30s default grace period, and Hadoop's FileSystem cache cleanup racing
JVM shutdown hooks.

- Optimizer.stopOptimizing: join executors with a deadline, force
  interrupt only on timeout; keep toucher alive so AMS heartbeats
  continue while tasks drain.
- OptimizerExecutor.completeTask: best-effort direct call after stop so
  the in-flight result still reaches AMS.
- SparkOptimizer / StandaloneOptimizer: register on Hadoop
  ShutdownHookManager (priority above FS_CACHE / SparkContext) with an
  explicit per-hook timeout.
- OptimizerConfig: new -st / --shutdown-timeout-ms (default 600s).
- KubernetesOptimizerContainer: `sh -c 'exec <args>'` and an explicit
  terminationGracePeriodSeconds derived from -st + 30s buffer; user
  podTemplate values are respected.
- optimizer.sh start-foreground: exec $CMDS so java gets PID 1.

Signed-off-by: Jiwon Park <jpark92@outlook.kr>
@j1wonpark
j1wonpark force-pushed the optimizer-graceful-shutdown branch from 3607baa to 2a6df8f Compare May 31, 2026 00:50
@j1wonpark

Copy link
Copy Markdown
Contributor Author

Gentle ping for review 🙏 @zhoujinsong @czy006 — this builds on the master-slave optimizer work you reviewed in #4174 / #3937, and you both merged master in earlier. Just rebased onto latest master as a single clean commit. Key bits: heartbeat/shutdown ordering in Optimizer.stopOptimizing, the best-effort completeTask path after stop, and the ShutdownHookManager priorities. Thanks!

@czy006 czy006 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for your contributor cc @j1wonpark

  • --shutdown-timeout-ms is added as a configurable parameter, but AMS does not append shutdown-timeout-ms from resource/container properties when generating optimizer startup arguments. Although K8s terminationGracePeriodSeconds parses this value from the startup arguments, the current AMS-managed K8s optimizer effectively can only use the default 600s + 30s buffer.
  • Although the Flink optimizer inherits the shared stopOptimizing() / completeTask() changes, it does not register an equivalent drain hook for external termination. I suggest adding this part.

@github-actions github-actions Bot added the type:docs Improvements or additions to documentation label Jul 4, 2026
j1wonpark added 3 commits July 4, 2026 15:46
…rtup arg and K8s grace period

The -st/--shutdown-timeout-ms option was parsed by the optimizer but AMS
never emitted it into the startup args, so the configured shutdown timeout
was unreachable for AMS-spawned optimizers and the K8s grace period was
pinned at the default.

- Emit '-st <value>' in buildOptimizerStartupArgsString when the optimizer
  group sets the shutdown-timeout-ms property (same pattern as -hb), which
  wires all containers sharing the args builder.
- Derive terminationGracePeriodSeconds from the same resource property that
  drives the -st arg via PropertyUtil, instead of re-parsing the rendered
  CLI string, and remove parseShutdownTimeoutMs. Reading from one source
  guarantees the pod's SIGKILL deadline can never fall below the shutdown
  timeout the optimizer JVM actually honors.

Signed-off-by: Jiwon Park <jpark92@outlook.kr>
…llation

FlinkExecutor.close() previously interrupted the executor thread after a
hardcoded 5s join, killing in-progress tasks on job cancellation and
silently ignoring the -st/--shutdown-timeout-ms option on the Flink engine.

- close() now stops the executor first, waits up to the configured
  shutdown timeout for the in-progress task to complete (so its result is
  reported to AMS), and only then force-interrupts.
- The drain absorbs interrupts of the thread running close(): Flink's
  cancellation machinery (TaskCanceler/TaskInterrupter) interrupts the
  task thread to unblock I/O, which must not abort the drain early.
- The drain budget is capped safely below Flink's cancellation watchdog
  (task.cancellation.timeout, default 180s), which would otherwise fail
  the whole TaskManager; operators wanting longer drains must raise both
  settings, as documented.
- Document the shutdown-timeout-ms group property, the -st option for
  Flink/Spark external optimizers, and the interaction with
  task.cancellation.timeout.

Signed-off-by: Jiwon Park <jpark92@outlook.kr>
… survive

Fixes several defects in the graceful-shutdown paths that could still lose
the result of a task that completed during the drain, or leave the process
in a bad state:

- Optimizer.startOptimizing published the executorThreads array before
  filling its elements; a concurrent shutdown hook could observe null
  elements and skip joining executor threads that were already running
  tasks. Build the array fully, then publish, then start the threads.
- The force-interrupt pass in stopOptimizing interleaved interrupt with
  per-thread 1s joins: when the stopping thread was itself interrupted
  (e.g. the shutdown-hook watchdog cancelling a timed-out hook), join()
  threw instantly and executor threads after the first were never
  interrupted; the per-thread joins also overshot the deadline by up to N
  seconds. Interrupt all survivors first, then await them against one
  shared deadline, restoring the caller's interrupt status only after
  cleanup completes.
- waitAShortTime restored the interrupt flag unconditionally, but no
  caller loop ever clears it: a stray interrupt while still running turned
  every subsequent sleep into an instant return, busy-spinning the
  poll/retry loops. Preserve the flag only once the operator is stopped.
- completeTask checked isStarted() once at entry and fell back to a single
  best-effort call after stop, so a result finishing during shutdown was
  dropped on a stop/report race or one transient AMS error. It now retries
  through callAuthenticatedAmsWithDrain: a bounded window anchored at the
  moment shutdown is observed, reusing the existing transient-error and
  auth handling, aborted early by the shutdown force-interrupt. The mock
  AMS gains transient-failure injection to cover this.
- The toucher stays alive during the drain for heartbeats, but scale-down
  unregisters the optimizer before the pod receives SIGTERM; the resulting
  auth error made the toucher re-register a ghost optimizer and rotate the
  executors' token so their final completeTask was rejected. Drain mode
  now skips re-registration once the token becomes invalid.
- The graceful-shutdown test timed stop() with fixed sleeps against the
  poll cadence (~0.5s margins on CI); slow test tasks now signal a latch
  when execute() begins so the test stops the optimizer deterministically.

Signed-off-by: Jiwon Park <jpark92@outlook.kr>
@j1wonpark
j1wonpark force-pushed the optimizer-graceful-shutdown branch from 4551aff to b4e5ae3 Compare July 4, 2026 06:56
…link drain

Flink cancels the FlinkToucher source before downstream operators, so
heartbeats stop the moment the drain in FlinkExecutor.close() begins. AMS
then expires the optimizer after optimizer.heart-beat-timeout (default
1 min) and, in the same sweep, unregisters it and resets its in-flight
tasks — so any task finishing later than that lost its result even though
the drain was still waiting, capping the effective drain window at the
heartbeat timeout instead of the configured shutdown timeout.

The drain loop now sends a best-effort touch with the existing token
between join slices, at the configured heartbeat interval. Touching keeps
the registration and the task assignment alive for exactly as long as the
drain runs; it never re-registers (consistent with the drain-mode
toucher), and once close() returns the touches stop and AMS cleans up
through the normal expiration path.

Signed-off-by: Jiwon Park <jpark92@outlook.kr>
@j1wonpark

Copy link
Copy Markdown
Contributor Author

Thanks for the review @czy006! Both points are addressed:

1. Fixed in 99367db — the optimizer group property shutdown-timeout-ms is now appended as -st in the startup args (same pattern as -hb), and the K8s terminationGracePeriodSeconds is derived from the same property instead of re-parsing the CLI string, so the two can never diverge. Documented in managing-optimizers.md.

2. Added in 21d75deFlinkExecutor.close() now drains the in-flight task up to the shutdown timeout (capped below task.cancellation.timeout to avoid failing the TaskManager) before force-interrupting. Since Flink cancels the toucher source before the operators, the drain also keeps the registration alive by touching AMS with the existing token at the heartbeat interval (e34d2bc) — otherwise AMS would expire the optimizer after optimizer.heart-beat-timeout and reset the in-flight task mid-drain. It only touches, never re-registers, so once close() returns AMS cleans up through the normal expiration path.

I also fixed several races I found in the new shutdown paths (b4e5ae3).

@j1wonpark
j1wonpark requested a review from czy006 July 4, 2026 12:05

@czy006 czy006 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@czy006
czy006 merged commit 89d27bc into apache:master Jul 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module:ams-optimizer AMS optimizer module module:ams-server Ams server module module:common type:build type:docs Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Improvement]: Support graceful shutdown for in-progress optimizer tasks

3 participants