docs: align with new storage architecture (s3 + local + server-proxied PUT)

All operator- and developer-facing docs updated to reflect:

  - The unified AsyncStorageBackend abstraction (s3 + local backends).
  - The STORAGE_BACKEND toggle ("s3" default, "local" for dev /
    single-node / air-gapped deployments).
  - The 4-purpose-bucket layout (workspace / version / run_log / trash)
    in both modes — 4 separate S3 buckets in s3 mode, 4 subdirectories
    of LOCAL_STORAGE_BASE_DIR in local mode.
  - The S3_* env var naming (was RUSTFS_*).
  - The server-proxied upload flow (was browser-direct presign-PUT):
    POST /internal/v1/uploads → PUT /internal/v1/uploads/{id} with
    raw bytes → server calls backend.put().
  - The factory helpers workspaces_root() (runtime's view of the
    workspace bucket on disk) and rclone_remote_spec() (s3-mode mount
    source).
  - The "two settings describing the same thing" cleanup: the deleted
    settings.workspace_root, settings.workspaces_root, and
    settings.remote_bucket fields.

Files touched:
  - API.md (§5 data-resource upload flow, §9 storage control plane,
    §10 readiness example)
  - ARCHITECTURE.md (storage layer diagram)
  - CLAUDE.md (architecture description + volume-preservation note)
  - DEVELOP.md (settings list, Storage section, "Wire a new bucket"
    how-to, dev-export example, troubleshooting network hint)
  - README.md (architecture diagram, container table, quick-start
    credentials note, tear-down note, Storage layout section)
  - REFACTOR_NOTES.md (final container list with s3 explanation)
  - backend/README.md (storage backend description)
  - migrations/data/README.md (step 11/12 record mentioning object
    storage)

A handful of historical "RustFS" mentions are intentionally retained
where they name a specific S3-compatible product (e.g. as an example
in REFACTOR_NOTES.md's container list) or document the pre-2026
abstraction name (DEVELOP.md Storage section).
This commit is contained in:
tao.chen
2026-08-05 13:13:20 +08:00
parent 4e290bd80a
commit 309b657d35
8 changed files with 195 additions and 112 deletions
+73 -43
View File
@@ -12,7 +12,7 @@ common/ Pure-Python shared library
db/ SQLAlchemy 2.0 async engine, session_scope, Base
db/models/ 26 tables in 9 domain files (zero FK, zero relationship)
scheduler/ build_sqlalchemy_jobstore (delayed import)
storage/ RustFSObjectStore + StorageClient + Pydantic schemas
storage/ AsyncStorageBackend abstraction (s3 + local impls) + Pydantic schemas
eventing.py add_outbox_event / utcnow / event_time
service_app.py /health/ready TCP probe, /api/v1/health
schemas.py StrictModel base
@@ -21,14 +21,14 @@ common/ Pure-Python shared library
backend/ Public FastAPI service + internal /internal/v1/* sub-app
main.py lifespan + route registration
jupyter.py /api/v1/auth/jupyter — the ONLY auth entry
scripts.py CRUD for scripts/notebooks (workspace_fs=rustfs)
scripts.py CRUD for scripts/notebooks (object storage via AsyncStorageBackend)
schedules.py DAG CRUD: schedules, nodes, edges
schedule_runs.py Trigger / list / get runs
schedule_schemas.py Pydantic request/response models
admin.py Admin endpoints
resources.py Misc data resources
storage_api.py /internal/v1/* (sub-app merged into main)
storage_client.py HTTP client for the storage sub-app
storage_client.py Stub (HTTP client removed post-migration; rewrite pending)
schedule_client.py Placeholder module (was the HTTP-push executor client)
runtime_client.py Self-contained httpx wrapper for the runtime
jupyter.py auth_request handler
@@ -41,7 +41,7 @@ schedule/ Schedule Executor (DAG worker)
worker.py NodeExecutor (notebook / python execution)
service.py SchedulerService facade (composes the three)
main.py Lifespan + FastAPI app
storage_client.py SchedulerStorageClient (subclass of common StorageClient)
storage_client.py Stub (SchedulerStorageClient rewrite pending — use AsyncStorageBackend directly)
execution.py execute_artifact (notebook + python paths)
notebook_runner.py Subprocess entry point (nbclient)
@@ -68,16 +68,17 @@ All env vars go through one place: `common/src/common/config.py`.
from common.config import settings
settings.database_url # str
settings.rustfs_endpoint # str (full URL, e.g. "http://rustfs:9000")
settings.rustfs_access_key # str
settings.rustfs_secret_key # str
settings.rustfs_workspace_bucket
settings.rustfs_version_bucket
settings.rustfs_run_log_bucket
settings.jwt_secret # HS256 secret for the auth_request handler
settings.workspace_root # schedule subprocess cwd; backend ignores
settings.workspaces_root # runtime rclone FUSE mount point
settings.remote_bucket # rclone remote spec (e.g. "rustfs:workspaces")
settings.storage_backend # str: "s3" (default) or "local"
settings.local_storage_base_dir # str: root dir for storage data (default "/data"); see "Storage" below for per-mode derivation
settings.s3_endpoint # str (full URL, e.g. "http://s3:9000"; s3 mode only)
settings.s3_access_key # str (s3 mode only)
settings.s3_secret_key # str (s3 mode only)
settings.s3_workspace_bucket # str (s3 mode only)
settings.s3_version_bucket # str (s3 mode only)
settings.s3_run_log_bucket # str (s3 mode only)
settings.s3_trash_bucket # str (s3 mode only)
settings.s3_trash_retention_days # int (s3 mode only)
settings.jwt_secret # HS256 secret for the auth_request handler
settings.backend_api_url # schedule → backend HTTP base
settings.runtime_api_url # backend → runtime HTTP base
settings.public_base_url # runtime public base URL
@@ -125,12 +126,31 @@ grep -rnE 'os\.(environ\[?["\x27][A-Z_]+|getenv\(["\x27][A-Z_]+)' --include="*.p
### Storage
- All object bytes go to **RustFS** via boto3.
- Use `StorageClient` (HTTP) or `RustFSObjectStore` (direct) — never
the local filesystem.
- `bucket_name` is one of: `RUSTFS_WORKSPACE_BUCKET` (default
`workspaces`), `RUSTFS_VERSION_BUCKET` (default `versions`,
reserved), `RUSTFS_RUN_LOG_BUCKET` (default `run-logs`, reserved).
- All object bytes go through `common.storage.AsyncStorageBackend`,
created by `create_storage(config)` from `common.storage.factory`.
- Two backends are registered: `local` (filesystem, local mode) and
`s3` (S3-compatible service, s3 mode). Selection is per-deployment
via `settings.storage_backend` (`"s3"` default, `"local"` for
dev / single-node / air-gapped).
- The factory helper `build_storage_config(bucket_name)` returns the
right `create_storage` kwargs for each of the 4 purpose buckets
(`workspace`, `version`, `run_log`, `trash`). Use it in lifespan code;
route handlers don't see the difference.
- Bucket resolution from `usage_type` is in **one place**
(`backend/storage_api.py:resolve_bucket`); route handlers only know
about `app.state.object_stores[bucket_name]`.
- The runtime's view of the workspace bucket on disk is exposed by
`common.storage.workspaces_root()`:
- `s3` mode: `${settings.local_storage_base_dir}/workspaces`
(default `/data/workspaces`, the rclone FUSE mount target).
- `local` mode: `${settings.local_storage_base_dir}/workspace`
(default `/data/workspace`, a subdir of the shared local-storage
volume).
`settings.local_storage_base_dir` is the **only** path setting; the
helper handles the per-mode suffix. Don't read `settings.workspaces_root`
or any other path setting directly in runtime code — use this helper.
- The pre-2026 abstraction (`RustFSObjectStore` / `common.storage.client`
/ `StorageClient` HTTP wrapper) is gone. Don't reintroduce it.
### Auth
@@ -202,9 +222,13 @@ cd frontend && pnpm install && cd ..
```bash
# Backend (terminal 1)
export DATABASE_URL="mysql+asyncmy://model_platform:model_platform@127.0.0.1:3306/model_platform?charset=utf8mb4"
export RUSTFS_ACCESS_KEY=modelplatform
export RUSTFS_SECRET_KEY=modelplatformsecret
export RUSTFS_ENDPOINT=http://127.0.0.1:9000
export STORAGE_BACKEND=s3
export S3_ACCESS_KEY=modelplatform
export S3_SECRET_KEY=modelplatformsecret
export S3_ENDPOINT=http://127.0.0.1:9000
# Or for local mode:
# export STORAGE_BACKEND=local
# export LOCAL_STORAGE_BASE_DIR=/data
uv run --frozen --package backend uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload
# Schedule Executor (terminal 2)
@@ -246,7 +270,7 @@ PYTHONPATH="backend/src:common/src" uv run --frozen --package backend python -c
from backend.main import app
from common.config import settings
print('backend:', len(app.routes), 'routes')
print('settings ok:', settings.rustfs_endpoint)
print('settings ok:', settings.s3_endpoint)
"
```
@@ -284,36 +308,42 @@ See "Adding a new env var" above.
uv run --frozen --package backend alembic upgrade head
```
### Wire a new RustFS bucket
### Wire a new storage bucket
The current three buckets are wired in `backend/storage_api.py:resolve_bucket`:
The current 4 buckets are wired in `backend/storage_api.py:resolve_bucket`:
```python
BUCKET_FOR_USAGE: dict[str, str] = {
"working_copy": settings.rustfs_workspace_bucket,
"public_script": settings.rustfs_workspace_bucket,
"data_resource": settings.rustfs_workspace_bucket,
"snapshot": settings.rustfs_workspace_bucket,
"version_artifact": settings.rustfs_version_bucket,
"run_log": settings.rustfs_run_log_bucket,
"run_result": settings.rustfs_run_log_bucket,
"working_copy": settings.s3_workspace_bucket,
"public_script": settings.s3_workspace_bucket,
"data_resource": settings.s3_workspace_bucket,
"snapshot": settings.s3_workspace_bucket,
"version_artifact": settings.s3_version_bucket,
"run_log": settings.s3_run_log_bucket,
"run_result": settings.s3_run_log_bucket,
}
```
To add a fourth bucket:
The constant `PURPOSE_BUCKETS = ("workspace", "version", "run_log", "trash")`
in `common.storage.factory` enumerates the four backends built in the
backend lifespan. To add a fifth bucket:
1. Add the env var to `Settings`:
1. Add the env var to `Settings` (s3 mode only):
```python
rustfs_<feature>_bucket: str = Field(default="<feature>", description="...")
s3_<feature>_bucket: str = Field(default="<feature>", description="...")
```
2. Add to `.env.example` with a one-line comment.
3. Extend the `Literal` in `common/storage/schemas.py` (in
`ServerObjectRequest.usage_type`, `CreateUploadRequest.usage_type`,
`CompleteUploadRequest.usage_type`) to include the new value.
4. Add an entry in `BUCKET_FOR_USAGE` mapping the new `usage_type` to
3. Append `"<feature>"` to the `PURPOSE_BUCKETS` tuple in
`common/storage/factory.py`. `build_storage_config("<feature>")`
will then automatically read `settings.s3_<feature>_bucket` (s3
mode) or use `<local_storage_base_dir>/<feature>` (local mode).
4. Extend the `Literal` in `common/storage/schemas.py` (in
`CreateUploadRequest.usage_type`, `ServerObjectRequest.usage_type`)
to include the new value.
5. Add an entry in `BUCKET_FOR_USAGE` mapping the new `usage_type` to
the new bucket env var.
5. Add the bucket to the `ensure_bucket` loop in
`backend/main.py` lifespan.
6. Pre-create the bucket (s3 mode) or subdirectory (local mode) in the
deployment. The backend no longer auto-creates buckets.
A workspace's `artifact_bucket` column (when non-null) overrides the
default for that workspace, regardless of `usage_type`.
@@ -354,7 +384,7 @@ contexts. Add `greenlet>=3.0.0` to `common/pyproject.toml` and
Either MySQL isn't running, or the network namespace doesn't allow
`mysql:3306` resolution. Inside the Docker network, services reach
each other by service name (`mysql`, `backend`, `runtime`,
`schedule`, `rustfs`).
`schedule`, `s3`).
### Jupyter routing 401s