j1wonpark opened a new pull request, #4272:
URL: https://github.com/apache/amoro/pull/4272

   ## Why are the changes needed?
   
   Close #4271.
   
   This is the second implementation phase of **AIP-5: Dynamic Resource 
Allocation for Optimizer** (#4191), building on the configuration foundation 
merged in #4254. It adds demand-driven **scale-up** for 
dynamic-allocation-enabled groups; scale-down lands in later phases. Everything 
remains opt-in: groups without `dynamic-allocation.enabled = true` are 
unaffected.
   
   **Why a dedicated keeper.** The existing `OptimizerGroupKeeper` checks a 
group at `optimizer-group.min-parallelism-check-interval × consecutive 
attempts` — minutes apart by design, which is right for its floor duty but 
makes the DRA backlog timeouts (`scheduler-backlog-timeout` 1min, 
`sustained-backlog-timeout` 30s) physically unreachable. `OptimizerScaleKeeper` 
reuses the same `AbstractKeeper` infrastructure (HA leader gating, delay queue, 
lifecycle) but each group's scale evaluations run at its own 
`sustained-backlog-timeout`. The legacy keeper skips DRA groups (continuing the 
implicit `isEffectivelyEnabled` pattern from the Phase 1 review) while keeping 
them under watch, so it resumes floor duty if DRA is disabled later.
   
   **Why a "future demand" signal.** Planning is only driven by `pollTask`: 
with zero optimizers nothing polls, planning never runs, and PLANNED tasks 
never materialize — so a task-based signal alone can never wake a 
scaled-to-zero group. PENDING tables are the one signal observable without 
optimizers; they ignite a single probe instance, and once planning materializes 
tasks, the task-based signal takes over.
   
   ## Brief change log
   
   - **`OptimizerProperties` / `DynamicAllocationConfig`**: new 
`dynamic-allocation.executor-parallelism` (default `1`) — the scaling unit is 
one homogeneous K-thread optimizer instance (the Spark executor model). 
Validation: `1 ≤ executor-parallelism ≤ max-parallelism`, and the floor must be 
reachable in K units (`ceil(min/K)×K ≤ max`) — otherwise the group would 
silently sit below its floor forever.
   
   - **`OptimizerScaleKeeper`** (new, in `DefaultOptimizingService`): owns both 
the floor and the demand scaling of DRA groups. Groups are watched on create, 
startup load, and update (covering enabling DRA on an existing group at 
runtime). Per evaluation round:
     - **Floor**: `min-parallelism` deficits are satisfied immediately in 
K-thread units, resetting any demand-phase timers so a recovery does not fire 
through a stale gate.
     - **Immediate demand**: `busy + serviceable PLANNED > effective threads` 
scales out with an exponential ramp (1, 2, 4, 8 instances) clamped to the 
actual need — Spark `ExecutorAllocationManager` semantics, including resetting 
the ramp when the clamp binds or demand clears.
     - **Future demand**: pending tables while every thread is busy add one 
probe instance (pending tables are not quantified demand before planning).
     - Demand must persist for `scheduler-backlog-timeout` before the first 
scale-out; later rounds are spaced by `sustained-backlog-timeout`, so a trickle 
drained between rounds never accumulates. The `max-parallelism` cap always wins.
     - Failure paths: a transient group-read failure retries instead of being 
treated as deletion; an enabled group whose queue is momentarily absent 
(delete/recreate racing the config watcher) is transient too; group deletion 
cleans all keeper state immediately.
   
   - **`PendingRegistrations`** (new): requested-but-unregistered capacity 
counts toward effective threads so a booting pod is not re-requested every 
round. Registration is optimizer-driven and heartbeat expiry only starts after 
it, so entries carry their own boot deadline (3 minutes). A synchronously 
failed request is dropped and retried; a pod whose `requestResource` succeeded 
but whose persistence failed keeps its entry (it will self-register) instead of 
being re-requested as a duplicate.
   
   - **`OptimizingQueue.collectDynamicAllocationLoad()`** (new): snapshots the 
demand side — busy threads (SCHEDULED and ACKED: a thread is occupied from 
assignment, not from ack), quota-mode-aware serviceable PLANNED tasks (a 
proportional quota `≤ 1` counts its whole backlog since scaling raises its 
`ceil(quota × availableCore)` limit; an absolute quota `> 1` counts only free 
slots), and PENDING tables. Table runtimes are copied under `tableLock` via a 
new `SchedulingPolicy.snapshotTableRuntimes()` rather than iterating the 
lock-guarded map from another thread.
   
   - **Planning-bound guard**: idle registered threads while tables wait as 
PENDING and nothing is PLANNED means the bottleneck is the serialized planning 
(`optimizer.max-planning-parallelism`), not thread capacity — scaling out would 
only add idle threads. The keeper warns (once per episode, after the condition 
persists across two evaluations) naming the config to raise, and does not scale.
   
   - **Docs**: the optimizer group property table now documents all 
`dynamic-allocation.*` properties (as promised in the Phase 1 PR for the phase 
that first exposes runtime behavior), marks the flat `min-parallelism` 
deprecated, and recommends `executor-parallelism` 4–8 for Kubernetes groups.
   
   ## Known trade-offs
   
   - **Proportional-quota convergence**: counting a proportional table's whole 
backlog can over-provision toward `E ≈ planned/(1−q)` while the table only 
occupies `ceil(q×E)` threads; the excess is reclaimed by idle-timeout when 
scale-down lands (Phase 4), and `max-parallelism` is the operator's bound until 
then.
   - **Manual optimizer operations**: a pod created via the dashboard scale-out 
endpoint is invisible to the boot-window accounting, so the keeper may briefly 
scale on top of it (bounded; corrects once it registers). Manual operations on 
DRA groups are documented as not recommended.
   - **HA failover**: boot-window accounting is leader-local, so a new leader 
can re-request capacity the old leader already requested within the boot window 
— bounded to one duplicate round and self-correcting on registration.
   - **Boot deadline**: 3 minutes is sized for Kubernetes pods (the AIP's 
primary target); slow container types may exceed it and briefly double-request, 
reclaimed by idle-timeout in Phase 4. Making it configurable is left to a 
follow-up to avoid growing the public config surface here.
   
   ## How was this patch tested?
   
   - [x] Add some test cases that check the changes thoroughly including 
negative and positive cases if possible
     - `TestComputeScaleUp` (15): floor immediacy and cap clamping, 
backlog/sustained timing gates, exponential ramp including 
reset-on-clamp-binding, trickle filtering, cold-start ignition, future-demand 
single-probe semantics, stale-gate reset after floor recovery.
     - `TestDynamicAllocationState` (17): quota-mode-aware serviceable counting 
(including fractional absolute quotas), SCHEDULED-occupies-thread semantics, 
planning-bound predicate.
     - `TestPendingRegistrations` (6): boot-deadline expiry per entry, 
registration/failure removal.
     - `TestDynamicAllocationConfig` (29): executor-parallelism parsing and 
validation including the unreachable-floor rejection.
     - `TestOptimizerScaleKeeper` (4, integration): K-unit floor satisfaction 
(vs. the legacy single deficit-sized instance), boot-window duplicate 
prevention (the legacy path re-requests the full deficit every round), failure 
retry, runtime enablement.
     - `TestOptimizingQueue#testCollectDynamicAllocationLoad`: the 
PENDING-table cold-start signal and the SCHEDULED transition on a real table.
   - [x] Run test locally before making a pull request: full `amoro-common` and 
`amoro-ams` suites pass (the AMS suite takes ~34 min); spotless and checkstyle 
clean.
   
   ## Documentation
   
   - Does this pull request introduce a new feature? (yes)
   - If yes, how is the feature documented? (docs — the optimizer group 
property table in `docs/admin-guides/managing-optimizers.md`; the AIP-5 page 
will be revised alongside this PR to document 
`dynamic-allocation.executor-parallelism`, which replaces the page's incorrect 
reference to `execution-parallel`)
   


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