feat(archive): integrate archive lifecycle into sandbox state machine, operator, and reconciler#1095
Merged
zhangjaycee merged 60 commits intoJul 6, 2026
Conversation
d2c9e3b to
d71185a
Compare
StephenRi
reviewed
Jun 17, 2026
StephenRi
reviewed
Jun 17, 2026
StephenRi
reviewed
Jun 17, 2026
StephenRi
reviewed
Jun 17, 2026
StephenRi
reviewed
Jun 17, 2026
StephenRi
reviewed
Jun 17, 2026
StephenRi
reviewed
Jun 17, 2026
d71185a to
3a100c1
Compare
StephenRi
reviewed
Jun 17, 2026
StephenRi
reviewed
Jun 17, 2026
46dbec1 to
9142d34
Compare
StephenRi
reviewed
Jun 18, 2026
a93f442 to
408b2af
Compare
StephenRi
approved these changes
Jul 2, 2026
5a9ea12 to
26d1f68
Compare
…or state advance - Change _do_archive to upload dir to OSS before pushing image to ACR, with proper rollback if image push fails after dir upload. - Change _try_advance_archiving to advance on image presence alone, since dir upload is skipped when ROCK_LOGGING_PATH is not set.
…tions Add info-level logging to delete/exists operations across all storage backends (OSS, S3, Registry V2) and archive cleanup in statemachine.
…achine fallback Change default from "archive-logs/" to "rock-archives/" so it matches the hardcoded fallback in sandbox_statemachine on_delete/on_archive.
Write credentials directly to DOCKER_CONFIG/config.json instead of shelling out to docker login, removing the dependency on the login subcommand.
…-reload Nacos update previously used flat setattr on top-level fields, which replaced entire nested dataclass objects (e.g. ArchiveConfig) even when only a subset of fields was provided — wiping out YAML-only fields like dir_storage and acr credentials. Introduce _merge_dataclass() to recursively merge only the keys present in the Nacos payload.
Some registries (ACR) reject docker push when the tag already exists. Add a delete call before push_from_local to allow re-archiving the same sandbox. Update test to match current upload order (dir first).
…tion
Replace asyncio.create_task/_run_archive/ray.kill pattern with fire-and-
forget actor.archive.remote() + ray.actor.exit_actor(). Archive actor now
writes progress to /data/service_status/{id}.json (image_archive and
log_archive phases) and self-exits on completion.
_try_advance_archiving reads phases via operator.get_remote_status instead
of polling image_storage.exists(ref), making archive detection symmetric
with _try_advance_pending's alive detection.
- Remove pin_to_host_ip constraint from start_restore so Ray freely schedules the restore actor based on resource availability. - After actor creation, fetch its host_ip and persist to meta store so get_status/try_advance_pending can reach the correct worker. - Add DockerUtil.detect_storage_opt_support() check in restart_from_image to gracefully degrade when the target node does not support --storage-opt (aligned with start() behavior). - Add remove_container_force before restart_from_image to clean up possible leftover containers on the new node.
… timestamps PersistedServiceStatus now reads the existing JSON file on set_sandbox_id and merges old phases (e.g. image_pull/docker_run) so archive phases don't overwrite startup history. PhaseStatus gains started_at/completed_at fields populated automatically by update_status.
Read the precise container-ready timestamp from service_status phases instead of using admin-side clock (which lags by one poll interval). Falls back to admin timestamp if phases data is unavailable.
- _init_archive_storage: remove enabled check so storage clients are ready when the feature is dynamically enabled; downgrade missing credentials from RuntimeError to warning - archive endpoint: add enabled check before is_allowed - update restful API tests accordingly
The integration tests instantiate SandboxActor directly (no Ray), so exit_actor() raises RuntimeError. Mock it as no-op to let the full cycle test run to completion including cleanup.
archive() swallows exceptions (fire-and-forget design), so the rollback test must call _do_archive() to get the propagated error.
Archive only works with DockerDeployment, so the isinstance check and fallback to generic restart() are unreachable.
Reference ArchiveDirStorageConfig.prefix and ArchiveAcrConfig.namespace instead of duplicating "rock-archives/" and "sandbox_archive" literals.
on_alive now pops archive_time so a subsequent plain restart is not misclassified as a restore-in-progress by _reconcile_pending.
SandboxTable.get already merges the JSONB status column into the returned dict via _merge_status_blob, so the duplicate hoisting in SandboxMetaStore.get was a no-op.
…detachment Both restore_and_start and restart now catch exceptions and call ray.actor.exit_actor() so a stale detached actor doesn't linger. The manager-side reconcile loop will move the sandbox back to ARCHIVED (restore) or STOPPED (restart).
Replace silent `except Exception: pass` with a warning log so auth/network failures are visible while still tolerating a benign 404 when the tag has never been pushed before.
ServiceStatus.update_status() was using datetime.now(timezone.utc) while all other sandbox timestamps (create_time, stop_time, etc.) use get_iso8601_timestamp() with ROCK_TIME_ZONE. This caused start_time (derived from phases.docker_run.completed_at) to be UTC while create_time was +08:00, confusing downstream consumers.
Clarify intent: this long-interval scanner handles automatic state transitions (expired → STOPPED), not generic background checks.
… time Add _auto_archive_stopped to _auto_transition: STOPPED sandboxes whose stop_time exceeds auto_archive_after_sec (default 3600s) are automatically archived. Set auto_archive_after_sec=0 to disable. Rename _check_job_background → _auto_transition to reflect expanded scope.
_auto_archive_stopped called operator.supports_archive() which is not defined on any Operator subclass, causing AttributeError once auto_archive_after_seconds > 0. The existing storage-config check plus the operator's own start_archive rejection are sufficient.
Move _cleanup_kata_disk() from _stop() to delete() so the disk image survives a stop→restart cycle. Remove the NotImplementedError guard in restart(). Add _prepare_kata_disk() + volume mount in restart_from_image so archive→restore also works for kata containers. Note: DinD state inside the kata disk is lost on archive (the .img is host-mounted, not part of the container layer). A fresh disk is prepared on restore.
…imeouts and auto_transition - Rename ArchiveAcrConfig → ArchiveRegistryConfig, acr → registry throughout - Move archive_timeout_seconds / restore_timeout_seconds under ArchiveConfig - Group auto_transition fields into AutoTransitionConfig (interval_seconds, auto_archive_seconds, auto_delete_seconds, auto_clear_seconds) - Update all references in manager, statemachine, actor, API, and tests
…e and auto_archive Avoid full-table scans by limiting candidate queries to 1000 rows ordered by stop_time. Add composite indexes on (state, stop_time) and (state, start_time).
Drop 8 indexes with no active query callers (namespace, experiment_id, cluster_name, host_ip, host_name, create_user_gray_flag, state+start_time). Retain user_id, state, image, and the (state, stop_time) composite index.
c25d5bd to
2641e4f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
close #1085
1. State Machine Overview
6 states:
pending/running/stopped/archiving/archived/deleted(final).2. Timestamp Fields
create_timeSandboxManager._build_sandbox_info_metadatastart_timeon_alive(once, idempotent)stop_timeon_stopon_restart/on_restorearchive_timeon_archive_doneon_archive_faileddelete_timeon_deleteTimeout anchor:
state_historyIntermediate-state timeout is computed from
state_history— a capped list of{from_state, to_state, event, timestamp}records appended bybefore_transition. The reconciler callsget_current_state_started_at(state_history, target_state)to find the timestamp of the most recent transition into the current state._reconcile_archiving: looks up"archiving"entry, compares elapsed time againstarchive_timeout_seconds_reconcile_pending: looks up"pending"entry (only whenarchive_timeis present, indicating a restore-in-progress), compares againstrestore_timeout_secondsKey points
archive_timeis the completion time, not the initiation time. It is written inon_archive_done(ARCHIVING → ARCHIVED), not inon_archive(STOPPED → ARCHIVING).archive_timealso serves as a restore-in-progress discriminator._reconcile_pendingusesinfo.get("archive_time")to distinguish a fresh-start PENDING sandbox from one that is restoring from ARCHIVED.3. Timeout Enforcement
Two layers of timeout enforcement:
Layer 1: Actor-internal timeout (active kill)
SandboxActor.archive()andrestore_and_start()wrap their work inasyncio.wait_for(coro, timeout=timeout_seconds). On timeout:asyncio.CancelledErrorpropagates to all awaited subprocesses_run_shell_commandand_docker_cmdcatchCancelledErrorandproc.kill()the child process_run_archivethenray.kill(actor)in itsfinallyblockTimeoutError, but stays alive (restore actor is long-lived); the reconciler detects the timeout and firesrestore_failedLayer 2: Reconciler timeout (passive detection)
_reconcile_archiving/_reconcile_pendingpoll periodically, compute elapsed time fromstate_history, and firearchive_failed/restore_failedwhen the configured timeout is exceeded. This is the safety net in case the actor dies without reporting.4. Constraints and Design Choices
Single archive per sandbox
Each sandbox has at most one archive — multi-version snapshots are not supported. Archive artifacts use deterministic names based solely on
sandbox_id:{prefix}{sandbox_id}.tar.gzrock-archives/sbx-abc123.tar.gz{registry}/{namespace}/sandbox_archived:{sandbox_id}rock-acr.cr.aliyuncs.com/sandbox_archive/sandbox_archived:sbx-abc123This means any component can compute the storage location from
sandbox_id+ config alone, without reading extra fields from the database.Re-archive: explicit delete before write
A sandbox may go through multiple archive/restore cycles:
On the second archive, the previous key/ref still exist in remote storage (restore downloads but does not delete the remote copy). We handle this by explicitly deleting the old artifacts before starting a new archive — we do not rely on OSS/ACR implicit overwrite behavior.
Rationale:
The deletion is triggered in
on_archivewhensandbox_infoalready containsarchive_time(indicating a prior successful archive). Both this and theon_deletecleanup are best-effort — failures are logged as warnings but do not block the main flow.on_archive(re-archive)archive_timealready present insandbox_infoon_deletearchive_timepresent and storage clients availableSize limits
Two configurable limits prevent archiving excessively large sandboxes:
max_image_push_size"16g"docker commit, beforepush— checked viadocker image inspectmax_dir_upload_size"16g"upload_dir— checked viadu -sbWhen a limit is exceeded, the actor raises
RuntimeErrorand is killed. The_reconcile_archivingscanner detects the ARCHIVING timeout and firesarchive_failed, rolling the sandbox back to STOPPED.Note: the dir size check happens after the image has already been pushed. If the dir exceeds the limit, the image is rolled back (
image_storage.delete) before the error propagates.5. Configuration