8000 feat(ess,reconciler): decouple artifact lifecycle management into ESS by janzak-dev · Pull Request #1440 · google/xmc · GitHub
[go: up one dir, main page]

Skip to content

feat(ess,reconciler): decouple artifact lifecycle management into ESS - #1440

Draft
janzak-dev wants to merge 15 commits into
mainfrom
feat/artifact-lifecycle
Draft

janzak-dev wants to merge 15 commits into
mainfrom
feat/artifact-lifecycle

Conversation

@janzak-dev
@janzak-dev janzak-dev commented Oct 27, 2026 •
Copy link
Copy Markdown
Collaborator

Summary

Decouples work-unit artifact lifecycle management (ManagedXprof / Hypercompute MLRun synchronization) from the Kubernetes JobSetReconciler into the Experiment State Server (ESS) via an event-driven Redis notification stream and a pluggable LifecycleRule registry, gated behind the artifact_lifecycle_enabled (XMC_ARTIFACT_LIFECYCLE_ENABLED, default false) feature flag for safe dual-path rollout.

Motivation & Context

Previously, JobSetReconciler directly queried Spanner for ManagedXprof artifacts and called the Hypercompute Cluster Director API on every reconciliation loop, coupling cluster-level controller reconciliation with external artifact API calls and lacking transactional concurrency protection across work unit restarts. Furthermore, GetArtifact and ListArtifacts dynamically constructed xprof_url on every read rather than persisting it on the Artifact row.

This change implements the approved Artifact Lifecycle architecture behind XMC_ARTIFACT_LIFECYCLE_ENABLED:

  1. Selective Notification Filtering (xmc.google.com/notify-when): When enabled, ESS computes the union of execution states requested by attached LifecycleRule implementations and stamps xmc.google.com/notify-when onto the Kubernetes JobSet (both at creation time and dynamically via Actuator when artifacts are attached to running work units).
  2. Edge-Triggered Reconciler Redis Events: UpdateBatcher records committed (OldState, NewState) transitions from Spanner and publishes ArtifactLifecycleEvent messages to Redis stream xmc-artifact-lifecycle-stream only when a state transition (or newly patched notify-when annotation) matches the JobSet's notify-when filter.
  3. ESS LifecycleRule Registry & Fenced Persistence: ESS consumes ArtifactLifecycleEvent tasks, creates/updates Hypercompute MLRun resources with runPhase and etag monotonicity fencing, and persists xprof_url onto the Spanner Artifact row inside a read-write transaction fenced on event.EID == CurrentWorkUnitExecution.EID.
  4. Synchronous Superseded Execution Cleanup: RestartWorkUnit invokes OnExecutionSuperseded for the superseded execution ID before scheduling the new execution.
  5. Feature-Flagged Coexistence (XMC_ARTIFACT_LIFECYCLE_ENABLED): When XMC_ARTIFACT_LIFECYCLE_ENABLED is false (default), Reconciler continues running legacy syncMLRun and ESS leaves its LifecycleRule registry empty and falls back to enrichManagedXprof; when true, ESS owns the ManagedXprof lifecycle end-to-end and Reconciler skips legacy syncMLRun in favor of edge-triggered Redis notifications.

Key Changes

  • internal/featureflags: Added ArtifactLifecycleFlagName (XMC_ARTIFACT_LIFECYCLE_ENABLED, default false) and Flags.ArtifactLifecycleEnabled.
  • internal/hypercomputeclient: Relocated from cmd/reconciler and added runPhase terminal-state monotonicity fencing plus etag preservation on UpdateMLRun. It is now the single source of truth for MLRun identity hashing (ConstructMLRunID), labels, IsAlreadyExists, console/response URL handling, and NewControlPlaneClientFromConfig (with impersonation), shared by ESS and the Reconciler.
  • internal/kubernetes/common/notifywhen: Owns the xmc.google.com/notify-when wire format (Encode used by ESS, Matches used by the Reconciler).
  • internal/task_queue/updatemask: Exported update-mask path constants shared by the ESS enqueue client and the Actuator.
  • internal/storage: Added transactional ArtifactTxRepository (FetchArtifact, UpdateArtifact) to storage.Transaction across Spanner and PostgreSQL. WorkUnitExecutionUpdate carries no notification metadata.
  • api/protos/task_queue/artifact_lifecycle_event.proto: New ArtifactLifecycleEvent / JobSetRuntimeContext messages for the Reconciler → ESS stream (same proto package as task.proto).
  • cmd/experiment_state_server/artifactlifecycle: LifecycleRule registry, ManagedXprofRule, EID transaction fencing (fencing.go), paginated work-unit artifact listing, Redis stream consumer (xmc-artifact-lifecycle-stream) and worker.
  • cmd/experiment_state_server/server: Lifecycle hooks (artifactlifecycle_hooks.go: notify-when computation, late-attach sync, single superseded-execution finalizer used by RestartWorkUnit and BatchRestartWorkUnits). GetArtifact/ListArtifacts return persisted xprof_url with fallback to enrichManagedXprof. MLRun project/location/environment are resolved once at startup; ESS uses its own service account (no impersonation). Each execution's MLRun URL is recorded on WorkUnitExecution.metadata.mlrun_url; restarts close exactly the superseded execution's MLRun (failures are logged and never fail the restart). Hypercompute errors are returned so Redis redelivers the event. Resumption recomputes notify-when when replaying Create/Restart tasks. With the flag off, the artifact read path matches main.
  • cmd/actuator: notify_when on create/restart and a mask-driven UpdateWorkUnit patch; a JobSet NotFound is retried only for the notify-when patch (global retry classification unchanged).
  • cmd/reconciler: Committed (OldState, NewState) transitions from UpdateWorkUnitExecutionsWithTransitions; notify-when predicate watching and edge detection carried in a reconciler-side LifecycleHints envelope; shared.RedisLifecycleNotifier built and closed in the manager composition root; notify-when tracking keyed by JobSet UID and execution ID; publishes only from UpdateBatcher on committed state changes or newly added notify-when (no deletion-time publish); legacy syncMLRun (including metadata.mlrun_url) kept when the flag is off.
  • deploy/helm & deploy/ctk_v2: featureFlags.artifact_lifecycle_enabled, Redis settings for the Reconciler, and a shared top-level mlRun: values block rendered into both the ESS and Reconciler ConfigMaps. With the flag off, the Reconciler needs no Redis address or secret. CTK grants roles/hypercomputecluster.editor and enables the Hypercompute API when the flag is on.

Assumptions

  • MLRun console links follow the existing production format (kubernetes/aiml/run/{location}/{id}), and the API response URL is read from the MLRun resource's top-level url field. Previously persisted /runs/{id} URLs are still parsed.
  • notify-when matching is exact and dot-separated, as ESS always writes the canonical encoding.

Verification & Testing

  • Commands Executed:
    • ./.githooks/pre-commit
    • bazel test //internal/featureflags/... //internal/hypercomputeclient/... //internal/storage/... //internal/workunit/... //cmd/experiment_state_server/... //cmd/actuator/... //cmd/reconciler/... //tools/release/... //test/helm/...
  • Coverage & Test Status:
    • All 118 affected Bazel unit and integration test targets pass (118/118 PASSED), plus Helm chart tests (test/helm, including new mlrun_config_test.py).
    • Validated end-to-end on a live GKE development environment across three scenarios (on an earlier revision; to be re-run on the current head):
      1. Scenario A (Initial ManagedXprof Artifact): Verified xmc.google.com/notify-when: STARTING.RUNNING.COMPLETED.FAILED.STOPPED stamped on JobSet, Reconciler Redis event publication, ESS MLRun creation, and xprof_url persistence/readback on the Spanner Artifact row.
      2. Scenario B (Late-Attached ManagedXprof Artifact): Verified live JobSet annotation patching via CreateArtifact -> UpdateNotifyWhenAction -> Actuator.UpdateWorkUnit, Reconciler ForceNotify edge detection, and xprof_url population.
      3. Scenario C (RestartWorkUnit Superseding & Fencing): Verified synchronous OnExecutionSuperseded invocation for EID 1, transactional fencing rejecting stale EID 1 updates (eventEID=1, curEID=2), and clean transition to EID 2.

Related Issues / Bugs

Bug: 578366632

TAG=agy
CONV=e7c98da6-30d1-49fd-960a-981e85cfd781

@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 an artifact lifecycle notification framework that publishes state transitions of Kubernetes JobSets to Redis and processes them in the Experiment State Server (ESS) to manage external Hypercompute MLRun resources. It updates the task queue API, Kubernetes client, reconciler, and ESS handlers to propagate and synchronize the notify-when annotation. Feedback on the changes identifies a missing import alias for the task_queue package in createartifact.go that will cause a compilation error.

xidmsgpb "xmanager_cloud/api/protos/xid"
"xmanager_cloud/internal/grpcerr"
"xmanager_cloud/internal/logging"
"xmanager_cloud/internal/task_queue"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The import of xmanager_cloud/internal/task_queue is missing the client alias, which is required because the package is referenced as client on line 207. This will cause a compilation error. Please add the client alias to the import.

Suggested change
"xmanager_cloud/internal/task_queue"
client "xmanager_cloud/internal/task_queue"

…, and reuse standard modules

TAG=agy

CONV=e7c98da6-30d1-49fd-960a-981e85cfd781
- Scope NotFound retries to the notify-when JobSet patch only; restore
  global retry classification. Dispatch JobSet updates on the field mask
  via new internal/task_queue/updatemask constants.
- Add internal/kubernetes/common/notifywhen as the single owner of the
  notify-when annotation format (Encode in ESS, Matches in Reconciler).
- Move RedisLifecycleNotifier to cmd/reconciler/shared, build and close
  the Redis publisher in the reconciler composition root, and share one
  PublishLifecycleEvent between the batcher and deletion path.
- Keep internal/storage free of reconciler notification hints by carrying
  them in a reconciler-side LifecycleHints envelope.
- Consolidate MLRun identity, label, error and console URL helpers in
  internal/hypercomputeclient, with a shared client constructor; resolve
  MLRun project/location once in ESS and drop invented fallback IDs.
- Move lifecycle glue into server/artifactlifecycle_hooks.go with a single
  superseded-execution finalizer and paginated artifact listing.
- Split ArtifactLifecycleEvent into artifact_lifecycle_event.proto.
- Add a shared Helm mlRun values block for ESS and Reconciler, plus docs.

TAG=agy
- ManagedXprofRule returns Hypercompute errors so Redis redelivers the
  event; never persists a constructed URL after a failed create.
- ESS records each execution's MLRun URL on
  WorkUnitExecution.metadata.mlrun_url; restarts close exactly the
  superseded execution's MLRun (failures are logged, restart succeeds);
  MLRuns created by late events for superseded executions are closed.
- Reconciler publishes only on committed state changes or newly added
  notify-when; removes the deletion-path publish. notify-when tracking
  is keyed by JobSet UID and execution ID.
- Resumption recomputes notify-when when replaying Create/Restart tasks.
- With the feature flag off: artifact read path, ListArtifacts calls,
  and Reconciler Redis config/secret match main.
- ESS calls Hypercompute with its own service account (no impersonation);
  CTK grants the Hypercompute role when the feature flag is on.

TAG=agy
An MLRun deleted outside XManager has nothing left to close, so the
terminal, superseded and superseded-guard close paths log it at info and
succeed instead of retrying until the dead-letter queue. The active path
still creates the run.

TAG=agy
ESS sent artifacts keyed "xprof_gcs_path", which the control plane
rejects, and its request diverged from the legacy reconciler's. Build
the request in one place (hypercomputeclient.BuildManagedXprofCreateOptions)
for both producers: artifacts {"gcs_path": folder}, the xprof tool, GKE
JobSet workload details with the full cluster path and creation time,
and the legacy gating (GCS folder, display name and run group required;
display name and run group fall back to the ManagedXprof artifact).

TAG=agy

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.

1 participant

0