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:
+73
-43
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user