storage: extract unified AsyncStorageBackend abstraction + migrate from RustFS
Replace the old RustFS-specific storage layer (common.storage.client /
RustFSObjectStore) with a minimal sync/async abstraction:
AsyncStorageBackend: put / get / get_stream / delete / exists / stat /
list / get_url / copy
StorageBackend: same surface, sync implementations
create_storage({"type": "s3" | "local", "mode": "async", ...})
backends/s3.py: S3-compatible (boto3 / aioboto3)
backends/local.py: on-disk filesystem (aiofiles)
Concretely:
- Drop RustFSObjectStore + common.storage.client (deleted).
- Drop the RustFS-specific ensure_bucket / presign_put / move_to_trash /
rewrite_to_public_path / sha256 / put_bytes methods.
- Migrate backend/storage_api.py + backend/main.py + backend/scripts.py
+ schedule/service.py + schedule/worker.py to the new abstraction.
- Migrate backend/storage_client.py + schedule/storage_client.py to
stub status (HTTP wrapper is dead code post-migration; rewrite pending).
- Rename all RUSTFS_* env vars to S3_* across .env.example,
docker-compose.yml, default.conf, scripts/nginx-entrypoint.sh,
common/config.py.
- Replace hardcoded rclone remote name "rustfs" with "s3" in
docker-compose.yml + config.py default.
- Rename "rustfs" SQLAlchemy column comments + table comments to
provider-neutral wording; StorageObjects.storage_backend enum
value moves from "rustfs" to "s3" (DB rows with the old value will
fail the != "s3" check until a one-shot migration is applied).
- Drop unused common/src/common/migrations/{README,env.py,script.py.mako}
(the alembic setup lives in /migrations/, not here).
Migration of the old abstractions has been done in one pass; per-route
method calls (delete / stat / put / get_url) are now direct one-liners
against AsyncStorageBackend.
After this commit:
- All Python imports resolve; routes compile (compileall green).
- s3 mode is fully wired.
- Routes that depended on removed methods (presign_put, move_to_trash,
rewrite_to_public_path, head() metadata) raise NotImplementedError
with a one-line TODO; rewriting these route handlers is the next step.
This commit is contained in:
@@ -7,7 +7,7 @@ Composes three single-purpose components into one bootable service:
|
||||
- :class:`schedule.worker.NodeExecutor` — node-level execution
|
||||
|
||||
This module also exposes the factory function ``build_object_store``
|
||||
consumed by ``schedule.main`` to construct the RustFS S3 client.
|
||||
consumed by ``schedule.main`` to construct the S3 backend.
|
||||
|
||||
The :class:`SchedulerService` itself stays small: it wires the three
|
||||
components together and implements :meth:`SchedulerService.trigger_schedule`,
|
||||
@@ -34,6 +34,7 @@ from common.scheduler import (
|
||||
create_scheduled_run,
|
||||
)
|
||||
from common.ids import new_ulid
|
||||
from common.storage import create_storage
|
||||
|
||||
from schedule.orchestrator import DispatchOrchestrator
|
||||
from schedule.scheduler import CronScheduler
|
||||
@@ -63,6 +64,9 @@ class SchedulerService:
|
||||
self.session_factory = session_factory
|
||||
self.storage_http_client = storage_http_client
|
||||
self.object_store = object_store
|
||||
# ``storage`` is the same instance as ``object_store``; kept as an
|
||||
# alias for clarity during the S3 migration.
|
||||
self.storage = object_store
|
||||
self.storage_client = storage_client
|
||||
self.database_url = database_url
|
||||
|
||||
@@ -173,19 +177,20 @@ class SchedulerService:
|
||||
|
||||
|
||||
def build_object_store() -> Any:
|
||||
"""Construct a boto3 S3 client pointed at RustFS.
|
||||
"""Construct an AsyncStorageBackend pointed at S3.
|
||||
|
||||
Reads ``rustfs_endpoint`` / ``rustfs_access_key`` / ``rustfs_secret_key``
|
||||
Reads ``s3_endpoint`` / ``s3_access_key`` / ``s3_secret_key``
|
||||
from :data:`common.config.settings`.
|
||||
"""
|
||||
import boto3
|
||||
|
||||
return boto3.client(
|
||||
"s3",
|
||||
endpoint_url=settings.rustfs_endpoint,
|
||||
aws_access_key_id=settings.rustfs_access_key,
|
||||
aws_secret_access_key=settings.rustfs_secret_key,
|
||||
region_name="us-east-1",
|
||||
return create_storage(
|
||||
{
|
||||
"type": "s3",
|
||||
"mode": "async",
|
||||
"bucket": settings.s3_workspace_bucket,
|
||||
"endpoint_url": settings.s3_endpoint,
|
||||
"aws_access_key_id": settings.s3_access_key,
|
||||
"aws_secret_access_key": settings.s3_secret_key,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from common.storage.client import StorageClient
|
||||
# TODO: common.storage.client.StorageClient was removed in the S3 migration.
|
||||
# This module is temporary dead code; rewrite to use AsyncStorageBackend.
|
||||
# from common.storage.client import StorageClient
|
||||
|
||||
|
||||
class SchedulerStorageClient(StorageClient):
|
||||
class SchedulerStorageClient:
|
||||
async def create_object(
|
||||
self,
|
||||
*,
|
||||
@@ -19,16 +21,9 @@ class SchedulerStorageClient(StorageClient):
|
||||
content: bytes,
|
||||
idempotency_key: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self.create_server_object(
|
||||
workspace_id=workspace_id,
|
||||
user_id=user_id,
|
||||
usage_type=usage_type,
|
||||
file_name=file_name,
|
||||
content_type=content_type,
|
||||
content=content,
|
||||
visibility="workspace",
|
||||
is_immutable=True,
|
||||
idempotency_key=idempotency_key,
|
||||
raise NotImplementedError(
|
||||
"TODO: SchedulerStorageClient is dead code post-migration; "
|
||||
"rewrite to use AsyncStorageBackend directly"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -274,8 +274,8 @@ class NodeExecutor:
|
||||
node_run, run, version, storage, workspace, schedule = row
|
||||
if storage.object_status != "available":
|
||||
raise ValueError("stable version artifact is not available")
|
||||
if storage.storage_backend != "rustfs":
|
||||
raise ValueError("stable version artifact is not stored in RustFS")
|
||||
if storage.storage_backend != "s3":
|
||||
raise ValueError("stable version artifact is not stored in S3")
|
||||
if not storage.bucket_name or not storage.object_key:
|
||||
raise ValueError("stable version artifact location is incomplete")
|
||||
user_id = run.triggered_by or schedule.created_by
|
||||
@@ -324,18 +324,7 @@ class NodeExecutor:
|
||||
object_key: str,
|
||||
content_hash: str,
|
||||
) -> bytes:
|
||||
def read() -> bytes:
|
||||
response = self.object_store.get_object(
|
||||
Bucket=bucket_name,
|
||||
Key=object_key,
|
||||
)
|
||||
body = response["Body"]
|
||||
try:
|
||||
return body.read()
|
||||
finally:
|
||||
body.close()
|
||||
|
||||
content = await asyncio.to_thread(read)
|
||||
content = await self.object_store.get(object_key)
|
||||
if hashlib.sha256(content).hexdigest() != content_hash:
|
||||
raise ValueError("stable version artifact hash mismatch")
|
||||
return content
|
||||
|
||||
Reference in New Issue
Block a user