8000 docs(architecture): document jobset and workunit state mapping and sync by AntonTyb · Pull Request #1411 · google/xmc · GitHub
[go: up one dir, main page]

Skip to content
  • docs(architecture): document jobset and workunit state mapping and sync - #1411

    Open
    AntonTyb wants to merge 3 commits into
    mainfrom
    docs/jobset-workunit-states
    Open

    AntonTyb wants to merge 3 commits into
    mainfrom
    docs/jobset-workunit-states

    Conversation

    @AntonTyb
    @AntonTyb AntonTyb commented Oct 26, 2026 •
    Copy link
    Copy Markdown
    Collaborator

    Summary

    Adds a technical architecture document (docs/architecture/jobset_and_work_unit_states.md) specifying how Kubernetes JobSet, WorkUnitExecution, and WorkUnit states relate, map, synchronize, and transition across the XMC control and execution planes. Updates the root and architecture documentation indices to cross-reference the new guide.

    Motivation & Context

    XMC tracks execution state across three distinct layers—cluster JobSet/Workload resources, numbered WorkUnitExecution attempts (WorkUnitExecutionState slot activity + WorkUnitStatus.WorkUnitState), and parent WorkUnit / Experiment projections—synchronized asynchronously through Redis task streams, the Actuator, Reconciler controllers, UpdateBatcher, and synchronous deletion finalizers. Consolidating the exact evaluation priority order, storage monotonicity/transition guards, field-level conflict resolution, and end-to-end event sequences in one reference makes debugging and extending workload lifecycle behavior straightforward without reverse-engineering multiple binaries.

    Key Changes

    • Add docs/architecture/jobset_and_work_unit_states.md covering:
      • Three-layer state model and Cloud Spanner physical storage split (WorkUnits inline ACTIVE execution vs. WorkUnitExecutions interleaved INACTIVE history, plus current PostgreSQL DDL/repository status).
      • Reference tables for WorkUnitExecutionState, all 13 WorkUnitStatus.WorkUnitState values, writer ownership, execution-terminal semantics vs. RestartWorkUnit restartability, and observed Kubernetes/JobSet/Kueue/Pod signals.
      • Priority-ordered 9-rule JobSet → WorkUnitState mapping decision tree, control-plane-only states, DESCHEDULING proto vs. RUNNING → SCHEDULING preemption behavior, and execution writer UpdateMask paths and conflict resolution.
      • Storage transition guards in ShouldUpdateWorkUnitExecutionState / MergeWorkUnitExecution (EID fencing, forward/backward rules, StateUpdateTime monotonicity with STOPPING skew bypass, partial merges, and automatic reset/retention side effects).
      • Dual-loop synchronization architecture (Intent Loop, Observation Loop with UpdateBatcher latest-wins slot indexing, and synchronous jobset.xmanager.cloud/finalizer bypass) plus end-to-end walkthroughs and sequence diagrams for create, preemption/eviction, stop, restart, and global scheduling pause/resume.
    • Link docs/architecture/jobset_and_work_unit_states.md in docs/architecture/overview.md, docs/architecture/execution_model.md, and README.md.

    Assumptions

    • Included navigation cross-links in docs/architecture/overview.md, docs/architecture/execution_model.md, and README.md alongside docs/architecture/jobset_and_work_unit_states.md — repository documentation alignment guidelines require keeping architecture index tables synchronized when adding new docs — if a strictly single-file PR is preferred, the three index link additions can be reverted without affecting the new document.

    Verification & Testing

    • Commands Executed: ./.githooks/pre-commit, python3 ./tools/run_affected_tests.py --dry-run, python3 ./tools/analyze_diff_size.py --base-ref origin/main...
    • Coverage & Test Status: Documentation-only change (zero production or test code modified); pre-commit hooks, Markdown relative link/heading/fence validation, legacy import ratchet, and nightly examples coverage checks all passed cleanly.

    Related Issues / Bugs

    N/A

    TAG=agy
    CONV=76617b8d-6898-4554-b6b1-758e3ba90593

    Add a dedicated architecture reference document (`docs/architecture/jobset_and_work_unit_states.md`) detailing how Kubernetes `JobSet`, Kueue `Workload`, and owned `Pod` signals map to XMC `WorkUnitExecution` and `WorkUnit` states, how state transitions are guarded in storage, how the intent and observation loops synchronize state, and the end-to-end sequences of events across lifecycle transitions.
    
    Also link the new reference from the architecture overview, execution model, and root README documentation index.
    
    TAG=agy
    CONV=76617b8d-6898-4554-b6b1-758e3ba90593
    
    Change-Id: I798bca6d7469f2c3c5acee33a6136e8ce41f699f
    @gemini-code-assist gemini-code-assist Bot left a comment
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    Code Review

    This pull request introduces a comprehensive new documentation file, jobset_and_work_unit_states.md, detailing the mapping, transition guards, and synchronization mechanics between Kubernetes JobSet states and XMC WorkUnit states. It also updates existing documentation files (README.md, execution_model.md, and overview.md) to reference this new guide. The feedback highlights a broken relative link in the new document that should be updated to point to the correct Spanner storage path.

    Every call to [`UpdateWorkUnitExecutions`](../../internal/workunit/updateworkunitexecutions.go)
    runs inside a read-write database transaction (`RunInTransaction`) and evaluates
    [`ShouldUpdateWorkUnitExecutionState`](../../internal/storage/state/state.go) and
    [`MergeWorkUnitExecution`](../../internal/storage/data/workunitexecutions.go) before persisting
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    medium

    The relative link to MergeWorkUnitExecution points to ../../internal/storage/data/workunitexecutions.go, but the actual Spanner storage data files are located under internal/storage/spanner/data/. Please update the path to ../../internal/storage/spanner/data/workunitexecutions.go to ensure the link is correct and consistent with the references on lines 51-53.

    Suggested change
    [`MergeWorkUnitExecution`](../../internal/storage/data/workunitexecutions.go) before persisting
    [`MergeWorkUnitExecution`](../../internal/storage/spanner/data/workunitexecutions.go) before persisting

    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    Keeping ../../internal/storage/data/workunitexecutions.go as written (Push Back — Technical Verification):

    MergeWorkUnitExecution is defined in the database-agnostic package internal/storage/data/workunitexecutions.go (line 34, package data), which is shared across storage backends (UpdateWorkUnitExecutions in internal/workunit/updateworkunitexecutions.go calls data.MergeWorkUnitExecution). By contrast, internal/storage/spanner/data/workunitexecutions.go (package spannerdata, referenced on lines 51–53) handles Spanner row-to-proto scanning (WorkUnitExecutionRowToProto / WorkUnitCurrentExecutionRowToProto) and does not define MergeWorkUnitExecution.

    @AntonTyb
    AntonTyb marked this pull request as ready for review October 26, 2026 07:50
    janzak-dev
    janzak-dev previously approved these changes Oct 26, 2026
    Comment on lines +223 to +225
    Kueue sets `JobSet.spec.suspend = true` and terminates active pods; as `totalReady` and
    `totalActive` drop to `0`, `JobSetReconciler` transitions the execution directly from `RUNNING` (or
    `STARTING`) back to `SCHEDULING` via Rule 9 on the **same `eid` and same `JobSet`**.
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Actionable] docs/architecture/jobset_and_work_unit_states.md:223-225
    Observation: The documentation states that preemption always drops totalReady and totalActive to 0 and transitions the execution to SCHEDULING via Rule 9. However, ToWorkUnitState() evaluates Rule 8b (totalSucceeded > 0) before Rule 9, meaning if a workload had successful pods prior to preemption, it remains in RUNNING.
    Risk / Rule: Accuracy of edge cases and boundary conditions in architecture documentation.
    Suggestion:

    Suggested change
    Kueue sets `JobSet.spec.suspend = true` and terminates active pods; as `totalReady` and
    `totalActive` drop to `0`, `JobSetReconciler` transitions the execution directly from `RUNNING` (or
    `STARTING`) back to `SCHEDULING` via Rule 9 on the **same `eid` and same `JobSet`**.
    Kueue sets `JobSet.spec.suspend = true` and terminates active pods; as `totalReady` and
    `totalActive` drop to `0` (with `Failed == 0` and `Succeeded == 0`), `JobSetReconciler` transitions
    the execution directly from `RUNNING` (or `STARTING`) back to `SCHEDULING` via Rule 9 on the **same
    `eid` and same `JobSet`**. (If the evicted workload already had successes, it remains `RUNNING` via Rule 8b).

    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    Addressed in 53c84bb: qualified the preemption mapping in §3.2, §4.1, and §6.2 to note that Rule 9 (SCHEDULING) applies when Failed == 0 && Succeeded == 0, whereas evicted workloads with prior completed jobs stay RUNNING via Rule 8b (or PARTIALLY_FAILED via Rule 8a when totalFailed > 0 without target SuccessPolicy satisfied).

    - Cluster-backed execution: `{INITIALIZING | SCHEDULING | STARTING | RUNNING | PARTIALLY_FAILED | RESTARTING} → STOPPING → STOPPED`
    - Paused shortcut (no `JobSet` on cluster): `SCHEDULING_PAUSED → STOPPED` (state committed directly in ESS; `Delete` task still closes `Operation.done = true` upon `NotFound`)

    ```mermaid
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    This diagram is currently broken

    Image

    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    fixed. GitHub seems to get confused when there is a ; in description

    github-actions[bot]
    github-actions Bot previously approved these changes Oct 26, 2026
    @github-actions github-actions Bot left a comment
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    🤖 Incremental PR Approval

    PR author is a code owner and changes since last approval (2 lines) are within the 50-line grace delta.

    Preserving prior approval by @janzak-dev on commit 6e34de9.

    @AntonTyb
    AntonTyb requested a review from janzak-dev October 26, 2026 08:43
    janzak-dev
    janzak-dev previously approved these changes Oct 26, 2026

    | Writer Component | Batcher Source | Mutates `status.state`? | Masked Fields Written |
    | :--- | :--- | :---: | :--- |
    | **ESS** (`Create` / `Stop` / `Restart`) | Direct transaction | **Yes** | `INITIALIZING`, `STOPPING`, `STOPPED` (only from `SCHEDULING_PAUSED`), `RESTARTING` (on `eid+1`). |
    Copy link
    8BD0 Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Actionable] ESS Create and Restart write full execution rows. They never call the masked UpdateWorkUnitExecutions (cmd/experiment_state_server/server/utils.go:99-115, restartworkunitexecutions.go:99-147). Only Stop uses a mask, ["status.state"] (batchstopworkunits.go:163-172). ESS also writes status.system_message on enqueue errors (utils.go:173).

    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    Addressed in 53c84bb: split the ESS entry in §3.3 into full-row writes (CreateWorkUnit via InsertWorkUnit and RestartWorkUnit via SaveWorkUnitExecution + UpdateCurrentWorkUnitExecution) versus masked UpdateWorkUnitExecutions calls (Stop with ["status.state"] and handleEnqueueActionError with ["status.system_message"] and ForceStatusMessageUpdate: true).

    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    Thanks. The split between full-row writes for Create/Restart and masked writes for Stop is correct now. One new inaccuracy came in with the fix:

    • handleEnqueueActionError passes ForceStatusMessageUpdate: false, not true (cmd/experiment_state_server/server/utils.go:189-193), and no production caller sets it to true.
    • It's also only reached from the Stop paths. Create and BatchRestart enqueue failures write a FATAL message by calling updateWorkUnitExecutionStatusMessage directly (createworkunit.go:164, batchrestartworkunits.go:142).

    Suggested wording: "Enqueue failures write ["status.system_message"] (no force): via handleEnqueueActionError on Stop, and via updateWorkUnitExecutionStatusMessage on Create/BatchRestart."

    ESS->>Redis: Publish trigger on xmc-scheduling-resume-stream

    ESS->>DB: Resumption worker reads PausedTasks batches
    ESS->>ESS: Filter out missing WUs, curEID > taskEID, or state != SCHEDULING_PAUSED
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Actionable] The resume filter has no state != SCHEDULING_PAUSED check. It skips a task when (a) the WU has a newer EID (> taskEID, or > taskEID+1 for Restart), (b) the WU is COMPLETED/STOPPED/STOPPING/FAILED, or (c) it's an Update that is stale or the WU is still paused. Missing WUs drop out through the SQL JOIN (filter.go:46-110). Also L697: PausedTasks rows are soft-marked via MarkProcessed (setting ResolutionStatus/ProcessedTime), not deleted.

    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    Addressed in 53c84bb: updated §6.5 to reflect the JOIN WorkUnits lookup for missing work units, the exact ShouldSkipTask skip conditions (newer curEID, COMPLETED/STOPPED/STOPPING/FAILED, and stale or still-paused Update tasks), and soft-marking via PausedTasks.MarkProcessed (ResolutionStatus, ProcessedTime, ResumptionRunId, 90-day TTL).

    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    Thanks, the JOIN, ShouldSkipTask and MarkProcessed parts are correct now. One error remains: the PausedTasks TTL is 7 days after ProcessedTime, not 90 days. Sources:

    • internal/storage/spanner/schema/ess-schema.sql:624: TIMESTAMP_ADD(ProcessedTime, INTERVAL 7 DAY)
    • the row-deletion policy at :628
    • migration 013_pause_scheduling.up.sql:73
    • the Postgres schema (INTERVAL '7 days')

    Please change L795 to PausedTasks.MarkProcessed (ResolutionStatus, ProcessedTime, ResumptionRunId; row TTL 7d after ProcessedTime).

    | `JobSet` | `metadata.deletionTimestamp` & `metadata.finalizers` | Non-nil timestamp + `jobset.xmanager.cloud/finalizer` | Triggers synchronous terminal state persistence (`STOPPED`, unless `Completed`/`Failed` is already `True`) before finalizer removal. |
    | `JobSet` | `spec.suspend` | `true` / `false` | Set `true` on creation; flipped `false` by Kueue upon quota admission and flipped back to `true` on Kueue eviction/preemption. |
    | `JobSet` | `status.conditions` | `Completed` (`JobSetCompleted`), `Failed` (`JobSetFailed`), `Suspended` (`JobSetSuspended`) | `Completed=True` → `COMPLETED`; `Failed=True` → `FAILED`. `Suspended=False` `LastTransitionTime` anchors the startup pod error timer. |
    | `JobSet` | `status.replicatedJobsStatus[]` | Summed across jobs into `JobTotals`: `Ready`, `Active`, `Succeeded`, `Failed`, `Replicas` | Drives `SCHEDULING`, `STARTING`, `RUNNING`, and `PARTIALLY_FAILED`, and populates `WorkUnitStatus.detailed_status.job_statuses`. |
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Nit] Bundled nits (one comment): L144 the field is detailed_status.jobset_status, not job_statuses, and Replicas comes from the spec. L146/L164/L539 the image-pull check covers all regular containers (init containers skipped), and the 2m is measured from admission. L147 Kueue Admitted/Evicted are never read; the inputs are QuotaReserved=False/Inadmissible, status.clusterName and podSetAssignments. L148 the field is positionInClusterQueue (stored +1). L36-40/L128 the experiment rollup also depends on launch_state. L166 ERROR messages defer to an existing message of the same severity. L238 the Inadmissible logic is in workload/reconciler.go. L314 the EID fence is a silent continue, not logged. L331 the function is the unexported restartWorkUnitExecutions. L334 a status.state mask without state_update_time fails the whole batch with InvalidArgument. L366 complete_time is time.Now(), not the transaction timestamp. L491 the constant is maxStoppedWriteRetryAge. L494 permanent errors also include FailedPrecondition and PermissionDenied. L451 done=true is also written on failure (task goes to the DLQ). L466 "3-level coalescing" is really latest-wins per slot inside a 3-level index.

    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    Addressed all 15 items in 53c84bb: (1) detailed_status.jobset_status and Replicas from spec, (2) startup image-pull check across all regular containers (skipping initContainers) after >= 2m from admission (JobSetSuspended=False), (3) Kueue inputs (QuotaReserved=False/Inadmissible, status.clusterName, podSetAssignments), (4) positionInClusterQueue (stored +1), (5) launch_state in UpdateExperimentActiveState (§1 & §2.2), (6) DeferToExistingMessage on ERROR messages, (7) workload/reconciler.go link in §3.3, (8) silent continue on EID fence, (9) unexported restartWorkUnitExecutions, (10) InvalidArgument batch failure when status.state omits state_update_time, (11) complete_time set via time.Now(), (12) maxStoppedWriteRetryAge, (13) all 4 permanent error codes (NotFound, InvalidArgument, FailedPrecondition, PermissionDenied), (14) Operations.done = true with error on failure and DLQ xmc-tasks-dlq, and (15) UpdateBatcher latest-wins replacement per (xid, wid, source) slot in its 3-level index.

    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    Thanks, 14 of the 15 nits now look correct. A few new small issues came in with 53c84bb1:

    1. L526 (wrong name): the dead-letter stream is xmc-dlq-stream (RedisDLQ, internal/kubernetes/common/constants/constants.go:119, wired into the actuator consumer at cmd/actuator/prod.go:116), not xmc-tasks-dlq.
    2. L507: Create does run preconditionsValidator.ValidateJobSetManifest (cmd/actuator/k8s_client/jobset.go:71,93). It only skips the EID and last-edit guards. Suggest "Create skips the EID/last-edit guards" instead of "skips preconditions_validator".
    3. L380-382: only a nil state_update_time with a status.state mask fails the batch with InvalidArgument (internal/storage/data/workunitexecutions.go:199). A non-nil zero value drops the state change silently (internal/storage/state/state.go:267).
    4. L748: k8sEid == N reads like a precondition. The actual guard is k8sEid <= N (preconditions_validator.go:209) plus last-edit ordering, the same as §6.3 L692. Suggest Delete old JobSet (Preconditions: UID, k8sEid <= N, last-edit).
    5. L155 (optional): Kueue's admission webhook only suspends at creation. Unsuspending on admission and re-suspending on eviction are done by Kueue's job reconciler (jobframework), not the webhook.

    @VLukiantsev
    Copy link
    Copy Markdown
    Collaborator

    [Priority: Actionable] Request: run XMC Iterative Reflection on the authoring session

    A fact-check of this doc against origin/main (47ecc2a) — every concrete claim traced to code — found ~57 discrepancies (5 Blocker, 23 Actionable, 29 Nit) against ~150 verified-true claims. The inline comments above cover them individually. Several are non-existent symbols rather than imprecise wording, e.g.:

    • metadata.job_manifest mask path (L237) — no such field anywhere
    • ResumeWorkloadsScheduling() RPC (L690) — doesn't exist; resume is Health Service ResumeScheduling
    • GetWorkUnitExecution RPC (L99), max_concurrent_flushes (L475), StatusHistories table (L376), finalizerMaxRetryDuration (L491)
    • Behavioural claims contradicting code: pause handler marking Operation done=true (L687), spec.suspend=true on creation (L142/L528), "Durable Intent First" for Stop (L436), single-tx Create (L523)

    Since this doc was agent-authored (TAG=agy, CONV=76617b8d-6898-4554-b6b1-758e3ba90593), could you please run the XMC Iterative Reflection skill on that session to root-cause why these were produced (e.g., claims inferred from naming/older docs instead of read from code, missing per-claim code-citation step for architecture docs) and propose a rule/skill fix — for example requiring a path:line citation for every concrete symbol, RPC, field, or config knob in architecture docs, plus a verification pass before PR creation? Please link the reflection outcome here.

    | :--- | :--- | :---: | :--- |
    | **ESS** (`Create` / `Stop` / `Restart`) | Direct transaction | **Yes** | `INITIALIZING`, `STOPPING`, `STOPPED` (only from `SCHEDULING_PAUSED`), `RESTARTING` (on `eid+1`). |
    | **Actuator** (`TaskPauseHandler`) | Direct transaction | **Yes** | `status.state = SCHEDULING_PAUSED`, `status.state_update_time` (only when global scheduling is paused). |
    | **Actuator** (`WorkUnitTaskHandler`) | Direct (`Operations` repo) | **No** | Marks `Operations.done = true` (or error); never writes `RUNNING` or `STOPPED`. |
    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    [Priority: Actionable] WorkUnitTaskHandler does not exist in the codebase (cmd/actuator/workunittaskhandler defines Handler, which has no storage.Storage dependency). Operations updates (done, response/error on Create/Delete/Restart) are written by k8sclient.Client.updateOperation (cmd/actuator/k8s_client/client.go:291-318), not via UpdateWorkUnitExecutions. Consider moving this to a note below the table or referencing k8sclient.Client.updateOperation.

    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    Addressed in 53c84bb: removed WorkUnitTaskHandler and added a note under §3.3 clarifying that workunittaskhandler.Handler has no WorkUnitExecution client and that k8sclient.Client.updateOperation updates Operations (done, response/error) directly.

    ### 5.3 Synchronous Finalizer Bypass on `JobSet` Deletion

    Every XMC `JobSet` is created with finalizer `jobset.xmanager.cloud/finalizer`
    ([`k8s_client/jobset.go`](../../cmd/actuator/k8s_client/jobset.go)). When `deletionTimestamp != nil`,
    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    [Priority: Nit] jobset.xmanager.cloud/finalizer (constants.JobSetFinalizer) is attached in buildBaseJobSet inside cmd/actuator/k8s_client/jobset_builder.go:270 rather than k8s_client/jobset.go.

    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    Addressed in 53c84bb: updated the §5.3 finalizer reference to buildBaseJobSet in cmd/actuator/k8s_client/jobset_builder.go.

    | Writer Component | Batcher Source | Mutates `status.state`? | Masked Fields Written |
    | :--- | :--- | :---: | :--- |
    | **ESS** (`Create` / `Stop` / `Restart`) | Direct transaction | **Yes** | `INITIALIZING`, `STOPPING`, `STOPPED` (only from `SCHEDULING_PAUSED`), `RESTARTING` (on `eid+1`). |
    | **Actuator** (`TaskPauseHandler`) | Direct transaction | **Yes** | `status.state = SCHEDULING_PAUSED`, `status.state_update_time` (only when global scheduling is paused). |
    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    [Priority: Actionable] TaskPauseHandler (cmd/actuator/pausescheduling/pause_handler.go:208-221) masks ["status.state", "status.system_message"] (writing an INFO system message alongside SCHEDULING_PAUSED, with state_update_time carried implicitly by status.state), and runs this execution update only on Create and Restart (paused Update tasks only write PausedTasks). Also mention status.system_message among the shared mask paths above.

    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    Addressed in 53c84bb: updated §3.3 to list ["status.state", "status.system_message"] for TaskPauseHandler (executed on Create and Restart, while paused Update tasks only write PausedTasks) and included status.system_message among the shared mask paths.

    3. `newState` is in `WorkUnitStatusRetainingStates` (`{STOPPED, FAILED}`), preserving the diagnostic
    error or user log context that preceded failure or stop.
    - **Audit & Message Tables**: Any change to `WorkUnitExecution.status` appends a snapshot row to
    `StatusHistories` ([`internal/workunit/history.go`](../../internal/workunit/history.go)), and new
    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    [Priority: Nit] recordStatusMessages (internal/workunit/history.go:129-140) mirrors only system_message and user_message (excluding scheduling_information.message) on any accepted execution change—even without a state transition—and Experiment.status.message_severity_counts tracks only the single highest-severity {system_message, user_message} per work unit (GetBestMessageFromStatus).

    Copy link
    Copy Markdown
    Collaborator Author

    Choose a reason for hiding this comment

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

    Addressed in 53c84bb: clarified in §4.3 that recordStatusMessages mirrors only system_message and user_message on any accepted execution change (even when oldState == newState) and that message_severity_counts tallies each work unit's single highest-severity message via GetBestMessageFromStatus.

    … doc
    
    Align `docs/architecture/jobset_and_work_unit_states.md` with exact Go,
    Protobuf, and SQL contracts across all open review threads and self-review
    verification findings:
    
    - Scope inline/interleaved `WorkUnitExecutions`, stored generated
      `ExecutionState`, and read-time `ACTIVE`/`INACTIVE` stamping to Cloud
      Spanner, noting PostgreSQL DDL and `Unimplemented` repository status.
    - Remove non-existent `GetWorkUnitExecution`, `WorkUnitTaskHandler`,
      `metadata.job_manifest`, `status.state_update_time` mask path,
      `StatusHistories`, `ResumeWorkloadsScheduling`, `max_concurrent_flushes`,
      and `finalizerMaxRetryDuration` references, replacing them with exact
      symbols, flags, and RPCs.
    - Clarify `JobSet.spec.suspend` creation/webhook ownership, startup pod
      diagnostics container scope and admission timer anchor, Kueue watched
      fields (`QuotaReserved`, `clusterName`, `podSetAssignments`,
      `positionInClusterQueue`), preemption Rule 8b/8a vs. Rule 9 edge cases,
      shared `UpdateMask` conflict resolution, `ShouldResetStatusMessage`
      replacement semantics, `WorkUnitStatusHistory` vs. `StatusMessages`
      triggers, `UpdateBatcher` latest-wins slot indexing, `Stop` vs.
      `Create`/`Restart` persistence ordering, `Delete`/`Restart` precondition
      guards, `STOPPING` + `Completed`/`Failed` finalizer interaction, and
      Health Service `ResumeScheduling` / `PausedTasks.MarkProcessed` flows.
    
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    Comment link: #1411 (comment)
    
    TAG=agy
    CONV=76617b8d-6898-4554-b6b1-758e3ba90593
    
    Change-Id: If32c87bbb2b0290eabdb0bf379acb14c98c136f6
    | Writer Component | Write Path / Batcher Source | Mutates `status.state`? | Fields / `UpdateMask` Paths Written |
    | :--- | :--- | :---: | :--- |
    | **ESS** (`Create` / `Restart`) | Direct full-row write (not `UpdateWorkUnitExecutions`) | **Yes** | `InsertWorkUnit` writes `INITIALIZING` on `eid=1` ([`server/utils.go`](../../cmd/experiment_state_server/server/utils.go)); `SaveWorkUnitExecution` + `UpdateCurrentWorkUnitExecution` archive `eid=N` and write `RESTARTING` on `eid=N+1` ([`restartworkunitexecutions.go`](../../cmd/experiment_state_server/server/restartworkunitexecutions.go)). |
    | **ESS** (`Stop` / enqueue error) | Direct `UpdateWorkUnitExecutions` | **Yes** (`Stop` only) | `StopWorkUnit` / `BatchStopWorkUnits` masks `["status.state"]` (`STOPPING`, or `STOPPED` from `SCHEDULING_PAUSED`, [`batchstopworkunits.go`](../../cmd/experiment_state_server/server/batchstopworkunits.go)); Redis enqueue failure (`handleEnqueueActionError`, [`server/utils.go`](../../cmd/experiment_state_server/server/utils.go)) masks `["status.system_message"]` (`ForceStatusMessageUpdate: true`). |
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Actionable] docs/architecture/jobset_and_work_unit_states.md:260
    Observation: The ESS (Stop / enqueue error) row states that handleEnqueueActionError sets ForceStatusMessageUpdate: true and implies it is the sole enqueue-failure writer. In cmd/experiment_state_server/server/utils.go:160-196, updateWorkUnitExecutionStatusMessage sets ForceStatusMessageUpdate: forceUpdate and handleEnqueueActionError passes false (called by StopWorkUnit, BatchStopWorkUnits, and RestartWorkUnit), while CreateWorkUnit (createworkunit.go:164-168) and BatchRestartWorkUnits (batchrestartworkunits.go:142-146) call updateWorkUnitExecutionStatusMessage(..., false) directly.
    Risk / Rule: Documentation Alignment (.agents/rules/languages/markdown.md — Accurate Contract Representation).
    Suggestion:

    Suggested change
    | **ESS** (`Stop` / enqueue error) | Direct `UpdateWorkUnitExecutions` | **Yes** (`Stop` only) | `StopWorkUnit` / `BatchStopWorkUnits` masks `["status.state"]` (`STOPPING`, or `STOPPED` from `SCHEDULING_PAUSED`, [`batchstopworkunits.go`](../../cmd/experiment_state_server/server/batchstopworkunits.go)); Redis enqueue failure (`handleEnqueueActionError`, [`server/utils.go`](../../cmd/experiment_state_server/server/utils.go)) masks `["status.system_message"]` (`ForceStatusMessageUpdate: true`). |
    | **ESS** (`Stop` / enqueue error) | Direct `UpdateWorkUnitExecutions` | **Yes** (`Stop` only) | `StopWorkUnit` / `BatchStopWorkUnits` masks `["status.state"]` (`STOPPING`, or `STOPPED` from `SCHEDULING_PAUSED`, [`batchstopworkunits.go`](../../cmd/experiment_state_server/server/batchstopworkunits.go)); Redis enqueue failures (`updateWorkUnitExecutionStatusMessage` / `handleEnqueueActionError`, [`server/utils.go`](../../cmd/experiment_state_server/server/utils.go)) mask `["status.system_message"]` (`ForceStatusMessageUpdate: false`). |

    Comment on lines +380 to +387
    - Whenever `"status.state"` is included in `UpdateMask`,
    `newExecution.status.state_update_time` must be non-nil and valid — omitting it fails the
    **entire batch transaction** with `InvalidArgument` in `MergeWorkUnitExecution`. When present,
    it must satisfy `newTime >= currentTime` (`!currentTime.After(newTime)`).
    - **Exception (`WorkUnitTimeIgnoredStates = {STOPPING}`)**: `STOPPING` bypasses the timestamp
    ordering check (though a valid `state_update_time` is still required) so a user `StopWorkUnit`
    request on ESS is never rejected if a GKE Reconciler node's clock was slightly ahead when
    writing `RUNNING`.
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Actionable] docs/architecture/jobset_and_work_unit_states.md:380-387
    Observation: Section 4.2 item 5 states that any missing or invalid state_update_time fails the entire batch transaction with InvalidArgument. In internal/storage/data/workunitexecutions.go:197-202, mergeWorkUnitExecutionStatusPath only returns InvalidArgument when state_update_time == nil; a non-nil zero timestamp (AsTime().IsZero()) causes ShouldUpdateWorkUnitExecutionState (internal/storage/state/state.go:267-270) to return false, nil, silently dropping the state change without failing the batch transaction.
    Risk / Rule: Documentation Alignment (.agents/rules/languages/markdown.md — Accurate Contract Representation).
    Suggestion:

    Suggested change
    - Whenever `"status.state"` is included in `UpdateMask`,
    `newExecution.status.state_update_time` must be non-nil and valid — omitting it fails the
    **entire batch transaction** with `InvalidArgument` in `MergeWorkUnitExecution`. When present,
    it must satisfy `newTime >= currentTime` (`!currentTime.After(newTime)`).
    - **Exception (`WorkUnitTimeIgnoredStates = {STOPPING}`)**: `STOPPING` bypasses the timestamp
    ordering check (though a valid `state_update_time` is still required) so a user `StopWorkUnit`
    request on ESS is never rejected if a GKE Reconciler node's clock was slightly ahead when
    writing `RUNNING`.
    - Whenever `"status.state"` is included in `UpdateMask`, a `nil`
    `newExecution.status.state_update_time` fails the **entire batch transaction** with
    `InvalidArgument` in `MergeWorkUnitExecution`, whereas a non-nil zero timestamp
    (`AsTime().IsZero()`) causes `ShouldUpdateWorkUnitExecutionState` to return `false, nil`
    (dropping the state change without failing the batch). When non-zero, it must satisfy
    `newTime >= currentTime` (`!currentTime.After(newTime)`).
    - **Exception (`WorkUnitTimeIgnoredStates = {STOPPING}`)**: `STOPPING` bypasses the timestamp
    ordering check (though a non-nil, non-zero `state_update_time` is still required) so a user
    `StopWorkUnit` request on ESS is never rejected if a GKE Reconciler node's clock was slightly
    ahead when writing `RUNNING`.

    Comment on lines +438 to +443
    - **Parent Experiment Aggregation (on `state` change)**: `DenormalizeWorkUnitStatus` decrements
    `oldState` and increments `newState` in `Experiment.status.child_state_counts`.
    `UpdateExperimentActiveState` recomputes `Experiment.status.active_state` (`ACTIVE` while
    `LaunchState == LAUNCHING` or at least one work unit is non-terminal; `INACTIVE` once
    `LaunchState == FAILED` or all child work units are in `{STOPPED, COMPLETED, FAILED}`, setting
    `Experiment.complete_time`).
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Clarity] docs/architecture/jobset_and_work_unit_states.md:438-443
    Observation: Section 4.3 attributes setting Experiment.complete_time to UpdateExperimentActiveState (including when LaunchState == FAILED). In internal/storage/state/state.go:174-193, UpdateExperimentActiveState only updates exp.Status.ActiveState; Experiment.complete_time is set in updateExperimentFromExecutions (internal/workunit/updateworkunitexecutions.go:255-264) when an execution update transitions active_state from ACTIVE to INACTIVE (whereas UpdateExperimentLaunchState in internal/experiment/updateexperimentlaunchstate.go:66-91 does not set complete_time).
    Risk / Rule: Documentation Alignment (.agents/rules/languages/markdown.md — Accurate Contract Representation).
    Suggestion:

    Suggested change
    - **Parent Experiment Aggregation (on `state` change)**: `DenormalizeWorkUnitStatus` decrements
    `oldState` and increments `newState` in `Experiment.status.child_state_counts`.
    `UpdateExperimentActiveState` recomputes `Experiment.status.active_state` (`ACTIVE` while
    `LaunchState == LAUNCHING` or at least one work unit is non-terminal; `INACTIVE` once
    `LaunchState == FAILED` or all child work units are in `{STOPPED, COMPLETED, FAILED}`, setting
    `Experiment.complete_time`).
    - **Parent Experiment Aggregation (on `state` change)**: `DenormalizeWorkUnitStatus` decrements
    `oldState` and increments `newState` in `Experiment.status.child_state_counts` and calls
    `UpdateExperimentActiveState` to recompute `Experiment.status.active_state` (`ACTIVE` while
    `LaunchState == LAUNCHING` or at least one work unit is non-terminal; `INACTIVE` once
    `LaunchState == FAILED` or all child work units are in `{STOPPED, COMPLETED, FAILED}`). When
    `updateExperimentFromExecutions` observes `active_state` transition from `ACTIVE` to `INACTIVE`,
    it sets `Experiment.complete_time` to `timestamppb.New(time.Now())`.

    Comment on lines +507 to +518
    - **`Create` (`AlreadyExists` verification)**: `Create` skips `preconditions_validator` and
    submits the `JobSet` directly; if Kubernetes returns `AlreadyExists`, the Actuator invokes
    `verifyDuplicateJobSet` ([`k8s_client/client.go`](../../cmd/actuator/k8s_client/client.go)) and
    treats a matching `JobSet` as an idempotent success.
    - **`Delete` and `Restart` (`xmc.google.com/execution-id` & `xmc.google.com/last-edit` guards)**:
    Evaluated by
    [`preconditions_validator`](../../cmd/actuator/k8s_client/preconditions_validator/preconditions_validator.go)
    **only on `Delete` and `Restart`**. If `k8sEid > targetEid`, the task is skipped as superseded;
    `last-edit` rejects tasks whose `operationTime` predates the `JobSet`'s `xmc.google.com/last-edit`
    label. On `Restart`, `targetEid` is the **outgoing `eid = N`** (while `N + 1` travels inside
    `payload.WorkUnit`): if `k8sEid == N`, the Actuator deletes `eid=N` and creates `eid=N+1`; on a
    redelivered retry after `eid=N+1` exists, `k8sEid (N+1) > targetEid (N)` skips idempotently.
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Actionable] docs/architecture/jobset_and_work_unit_states.md:507-518
    Observation: Section 5.1 step 2 states that Create skips preconditions_validator and that Restart deletes the old JobSet when k8sEid == N. In cmd/actuator/k8s_client/jobset.go:71,87-98, createJobSet still runs c.preconditionsValidator.ValidateJobSetManifest(jobSetUnstructured) (hostNetwork exclusive-topology validation) and only skips the live-cluster EID and last-edit guards (IsActuationRequestValid / IsExecutionIDAligned), while Restart (preconditions_validator.go:209) permits deletion whenever k8sEid <= N.
    Risk / Rule: Documentation Alignment (.agents/rules/languages/markdown.md — Accurate Contract Representation).
    Suggestion:

    Suggested change
    - **`Create` (`AlreadyExists` verification)**: `Create` skips `preconditions_validator` and
    submits the `JobSet` directly; if Kubernetes returns `AlreadyExists`, the Actuator invokes
    `verifyDuplicateJobSet` ([`k8s_client/client.go`](../../cmd/actuator/k8s_client/client.go)) and
    treats a matching `JobSet` as an idempotent success.
    - **`Delete` and `Restart` (`xmc.google.com/execution-id` & `xmc.google.com/last-edit` guards)**:
    Evaluated by
    [`preconditions_validator`](../../cmd/actuator/k8s_client/preconditions_validator/preconditions_validator.go)
    **only on `Delete` and `Restart`**. If `k8sEid > targetEid`, the task is skipped as superseded;
    `last-edit` rejects tasks whose `operationTime` predates the `JobSet`'s `xmc.google.com/last-edit`
    label. On `Restart`, `targetEid` is the **outgoing `eid = N`** (while `N + 1` travels inside
    `payload.WorkUnit`): if `k8sEid == N`, the Actuator deletes `eid=N` and creates `eid=N+1`; on a
    redelivered retry after `eid=N+1` exists, `k8sEid (N+1) > targetEid (N)` skips idempotently.
    - **`Create` (manifest validation & `AlreadyExists` verification)**: `Create` validates the
    constructed manifest via `preconditionsValidator.ValidateJobSetManifest` (`hostNetwork`
    exclusive-topology check) while skipping the live-cluster EID and `last-edit` guards; if
    Kubernetes returns `AlreadyExists`, the Actuator invokes `verifyDuplicateJobSet`
    ([`k8s_client/client.go`](../../cmd/actuator/k8s_client/client.go)) and treats a matching
    `JobSet` as an idempotent success.
    - **`Delete` and `Restart` (`xmc.google.com/execution-id` & `xmc.google.com/last-edit` guards)**:
    `IsActuationRequestValid` and `IsExecutionIDAligned` in
    [`preconditions_validator`](../../cmd/actuator/k8s_client/preconditions_validator/preconditions_validator.go)
    run **only on `Delete` and `Restart`**. If `k8sEid > targetEid`, the task is skipped as superseded;
    `last-edit` rejects tasks whose `operationTime` predates the `JobSet`'s `xmc.google.com/last-edit`
    label. On `Restart`, `targetEid` is the **outgoing `eid = N`** (while `N + 1` travels inside
    `payload.WorkUnit`): if `k8sEid <= N`, the Actuator deletes the old `JobSet` and creates `eid=N+1`; on a
    redelivered retry after `eid=N+1` exists, `k8sEid (N+1) > targetEid (N)` skips idempotently.

    Comment on lines +525 to +526
    `error` on failure—and the consumer `XACK`s the message (routing exhausted/failed tasks to
    dead-letter stream `xmc-tasks-dlq`). Cluster execution states (`SCHEDULING`, `STARTING`,
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Actionable] docs/architecture/jobset_and_work_unit_states.md:525-526
    Observation: Section 5.1 step 3 refers to the Redis dead-letter stream as xmc-tasks-dlq, whereas constants.RedisDLQ wired into redisclient.NewConsumer (internal/kubernetes/common/constants/constants.go:119, cmd/actuator/prod.go:123) is xmc-dlq-stream.
    Risk / Rule: Documentation Alignment (.agents/rules/languages/markdown.md — Accurate Contract Representation).
    Suggestion:

    Suggested change
    `error` on failure—and the consumer `XACK`s the message (routing exhausted/failed tasks to
    dead-letter stream `xmc-tasks-dlq`). Cluster execution states (`SCHEDULING`, `STARTING`,
    `error` on failure—and the consumer `XACK`s the message (routing exhausted/failed tasks to
    dead-letter stream `xmc-dlq-stream`). Cluster execution states (`SCHEDULING`, `STARTING`,

    ESS-->>Client: Operation (done=false)

    Redis->>Act: XREADGROUP Restart task (target eid=N)
    Act->>K8s: Delete old JobSet (k8sEid == N, UID precondition)
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Clarity] docs/architecture/jobset_and_work_unit_states.md:748
    Observation: In the §6.4 sequence diagram, step 8 writes k8sEid == N, UID precondition. validateAndDeleteJobSetForRestart and deleteJobSet (cmd/actuator/k8s_client/jobset.go:123-143,217-238, preconditions_validator.go:209) enforce k8sEid <= N alongside last-edit ordering and the UID precondition, matching §6.3 line 692.
    Risk / Rule: Documentation Alignment (.agents/rules/languages/markdown.md — Accurate Contract Representation).
    Suggestion:

    Suggested change
    Act->>K8s: Delete old JobSet (k8sEid == N, UID precondition)
    Act->>K8s: Delete old JobSet (Preconditions: UID, k8sEid <= N, last-edit)

    ESS->>DB: Read unprocessed PausedTasks joined with WorkUnits (drops missing WUs)
    ESS->>ESS: ShouldSkipTask skips newer curEID, COMPLETED/STOPPED/STOPPING/FAILED, or stale/paused Update
    ESS->>Redis: Re-sign fresh JWT/OIDC and XADD non-skipped tasks to xmc-tasks-stream
    ESS->>DB: PausedTasks.MarkProcessed (ResolutionStatus, ProcessedTime, ResumptionRunId, 90d TTL)
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    [Priority: Actionable] docs/architecture/jobset_and_work_unit_states.md:795
    Observation: The §6.5 sequence diagram states that PausedTasks.MarkProcessed sets a 90d TTL. Both internal/storage/spanner/schema/ess-schema.sql:623-628 and internal/storage/postgres/schema/ess-schema.sql:359-360 define PausedTasks.ExpirationTime as ProcessedTime + 7 days (INTERVAL 7 DAY / INTERVAL '7 days'), whereas 90 days is the TTL for Operations.
    Risk / Rule: Documentation Alignment (.agents/rules/languages/markdown.md — Accurate Contract Representation).
    Suggestion:

    Suggested change
    ESS->>DB: PausedTasks.MarkProcessed (ResolutionStatus, ProcessedTime, ResumptionRunId, 90d TTL)
    ESS->>DB: PausedTasks.MarkProcessed (ResolutionStatus, ProcessedTime, ResumptionRunId, 7d TTL)

    @VLukiantsev VLukiantsev left a comment
    Copy link
    Copy Markdown
    Collaborator

    Choose a reason for hiding this comment

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

    Please address the comments

    This branch has not been deployed

    No deployments
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    None yet

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    3 participants

    0