Skip to content

feat(archive): integrate archive lifecycle into sandbox state machine, operator, and reconciler#1095

Merged
zhangjaycee merged 60 commits into
alibaba:masterfrom
zhangjaycee:feature/archive_lifecycle
Jul 6, 2026
Merged

feat(archive): integrate archive lifecycle into sandbox state machine, operator, and reconciler#1095
zhangjaycee merged 60 commits into
alibaba:masterfrom
zhangjaycee:feature/archive_lifecycle

Conversation

@zhangjaycee

@zhangjaycee zhangjaycee commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

close #1085

1. State Machine Overview

image

6 states: pending / running / stopped / archiving / archived / deleted (final).


2. Timestamp Fields

Field Meaning Set by Cleared by
create_time Sandbox creation time SandboxManager._build_sandbox_info_metadata Never
start_time First time container is alive on_alive (once, idempotent) Never
stop_time Container stop time on_stop Popped by on_restart / on_restore
archive_time Archive completion time on_archive_done Popped by on_archive_failed
delete_time Soft-delete time on_delete Never (final state)

Timeout anchor: state_history

Intermediate-state timeout is computed from state_history — a capped list of {from_state, to_state, event, timestamp} records appended by before_transition. The reconciler calls get_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 against archive_timeout_seconds
  • _reconcile_pending: looks up "pending" entry (only when archive_time is present, indicating a restore-in-progress), compares against restore_timeout_seconds

Key points

  • archive_time is the completion time, not the initiation time. It is written in on_archive_done (ARCHIVING → ARCHIVED), not in on_archive (STOPPED → ARCHIVING).
  • archive_time also serves as a restore-in-progress discriminator. _reconcile_pending uses info.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() and restore_and_start() wrap their work in asyncio.wait_for(coro, timeout=timeout_seconds). On timeout:

  • asyncio.CancelledError propagates to all awaited subprocesses
  • _run_shell_command and _docker_cmd catch CancelledError and proc.kill() the child process
  • For archive: _run_archive then ray.kill(actor) in its finally block
  • For restore: the actor raises TimeoutError, but stays alive (restore actor is long-lived); the reconciler detects the timeout and fires restore_failed

Layer 2: Reconciler timeout (passive detection)

_reconcile_archiving / _reconcile_pending poll periodically, compute elapsed time from state_history, and fire archive_failed / restore_failed when 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:

Storage Format Example
OSS / S3 key {prefix}{sandbox_id}.tar.gz rock-archives/sbx-abc123.tar.gz
ACR image ref {registry}/{namespace}/sandbox_archived:{sandbox_id} rock-acr.cr.aliyuncs.com/sandbox_archive/sandbox_archived:sbx-abc123

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

stopped → archive → archived → restore → pending → running → stopped → archive again

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:

  • Different S3-compatible implementations vary in PUT-overwrite semantics (versioning, ACL inheritance)
  • ACR tag overwrite behavior differs across registry implementations (some require manifest deletion first)
  • Explicit delete-then-write is predictable and easy to debug

The deletion is triggered in on_archive when sandbox_info already contains archive_time (indicating a prior successful archive). Both this and the on_delete cleanup are best-effort — failures are logged as warnings but do not block the main flow.

When Trigger What gets cleaned
on_archive (re-archive) archive_time already present in sandbox_info Old key + old ref
on_delete archive_time present and storage clients available key + ref

Size limits

Two configurable limits prevent archiving excessively large sandboxes:

Config Default Check point
max_image_push_size "16g" After docker commit, before push — checked via docker image inspect
max_dir_upload_size "16g" Before upload_dir — checked via du -sb

When a limit is exceeded, the actor raises RuntimeError and is killed. The _reconcile_archiving scanner detects the ARCHIVING timeout and fires archive_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

lifecycle:
    archive_timeout_seconds: 1800
    restore_timeout_seconds: 1800
    reconcile_interval_seconds: 30
    archive:
      enabled: false
      max_image_push_size: "16g"
      max_dir_upload_size: "16g"
      allowed_keys: ["key-a", "key-b"]  # null = open to all; list = whitelist by X-Key header
      dir_storage:
        type: "oss"                   # oss or s3
        endpoint: ""
        bucket: ""
        access_key_id: ""
        access_key_secret: ""
        region: ""
        prefix: "archive-logs/"       # OSS/S3 key prefix for log archives
      acr:
        registry_url: ""
        username: ""
        password: ""
        namespace: "sandbox_archive"

@zhangjaycee zhangjaycee force-pushed the feature/archive_lifecycle branch from d2c9e3b to d71185a Compare June 11, 2026 12:04
Comment thread rock/admin/core/schema.py Outdated
Comment thread rock/admin/core/schema.py Outdated
Comment thread rock/admin/entrypoints/sandbox_api.py Outdated
Comment thread rock/admin/main.py Outdated
Comment thread rock/admin/main.py Outdated
Comment thread rock/admin/main.py Outdated
Comment thread rock/sandbox/archive/_constants.py Outdated
@zhangjaycee zhangjaycee force-pushed the feature/archive_lifecycle branch from d71185a to 3a100c1 Compare June 17, 2026 07:48
Comment thread rock/sandbox/base_manager.py
Comment thread rock/config.py Outdated
@zhangjaycee zhangjaycee marked this pull request as ready for review June 17, 2026 12:40
@zhangjaycee zhangjaycee force-pushed the feature/archive_lifecycle branch from 46dbec1 to 9142d34 Compare June 17, 2026 12:42
Comment thread rock/sandbox/archive/factory.py Outdated
@zhangjaycee zhangjaycee force-pushed the feature/archive_lifecycle branch 12 times, most recently from a93f442 to 408b2af Compare July 1, 2026 09:27
@zhangjaycee zhangjaycee force-pushed the feature/archive_lifecycle branch 3 times, most recently from 5a9ea12 to 26d1f68 Compare July 5, 2026 04:29
…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.
@zhangjaycee zhangjaycee force-pushed the feature/archive_lifecycle branch from c25d5bd to 2641e4f Compare July 6, 2026 07:42
@zhangjaycee zhangjaycee merged commit b81e3f9 into alibaba: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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Support Sandbox Archive/Restore Lifecycle

2 participants