Merge branch 'optimize-by-taochen' into develop

# Conflicts:
#	.env.example
#	backend/src/backend/main.py
#	common/src/common/db/models.py
#	common/src/common/eventing.py
#	docker-compose.yml
#	runtime/Dockerfile
#	runtime/pyproject.toml
#	runtime/src/runtime/main.py
#	schedule/src/schedule/main.py
#	schedule/src/schedule/service.py
This commit is contained in:
tao.chen
2026-07-31 09:29:03 +08:00
59 changed files with 3016 additions and 4423 deletions
+9 -38
View File
@@ -1,52 +1,35 @@
from __future__ import annotations
import os
import secrets
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any, AsyncIterator
from fastapi import Depends, Header, HTTPException, Request, status
from common.db import create_database_engine, create_session_factory
from common.service_app import create_service_app
from schedule.service import (
SchedulerService,
build_object_store,
build_redis_client,
build_storage_http_client,
)
from schedule.storage_client import SchedulerStorageClient
def verify_internal_service(
x_service_token: str = Header(alias="X-Service-Token"),
) -> None:
expected = os.environ.get("INTERNAL_SERVICE_TOKEN", "")
if not expected or not secrets.compare_digest(expected, x_service_token):
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
"invalid internal service identity",
)
@asynccontextmanager
async def lifespan(app: Any) -> AsyncIterator[None]:
database_url = os.environ["DATABASE_URL"]
engine = create_database_engine(database_url)
engine = create_database_engine(os.environ["DATABASE_URL"])
session_factory = create_session_factory(engine)
backend_http_client = build_storage_http_client()
redis = build_redis_client()
storage_http_client = build_storage_http_client()
service = SchedulerService(
session_factory=session_factory,
redis=redis,
object_store=build_object_store(),
storage_client=SchedulerStorageClient(
backend_http_client,
os.environ["INTERNAL_SERVICE_TOKEN"],
),
backend_http_client=backend_http_client,
storage_client=SchedulerStorageClient(storage_http_client),
workspace_root=Path(
os.getenv("WORKSPACE_ROOT", "/workspace/workspaces")
),
database_url=database_url,
)
app.state.scheduler_service = service
await service.start()
@@ -54,24 +37,12 @@ async def lifespan(app: Any) -> AsyncIterator[None]:
yield
finally:
await service.close()
await backend_http_client.aclose()
await storage_http_client.aclose()
await redis.aclose()
await engine.dispose()
app = create_service_app(
os.getenv("SERVICE_NAME", "schedule-executor"),
os.getenv("SERVICE_NAME", "scheduler-worker"),
lifespan=lifespan,
)
@app.post(
"/internal/v1/runs/{run_id}/dispatch",
dependencies=[Depends(verify_internal_service)],
)
async def dispatch_run(run_id: str, request: Request) -> dict[str, Any]:
processed = await request.app.state.scheduler_service.dispatch_run(run_id)
return {
"status": "accepted",
"run_id": run_id,
"processed_events": processed,
}
+2 -14
View File
@@ -14,7 +14,6 @@ from zoneinfo import ZoneInfo
import boto3
import httpx
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from sqlalchemy import select
@@ -30,11 +29,7 @@ from common.db.models import (
Versions,
Workspaces,
)
from common.eventing import add_outbox_event, event_time, utcnow
from common.ids import new_ulid
from common.db.session import session_scope
from schedule.execution import ExecutionResult, execute_artifact
from schedule.storage_client import SchedulerStorageClient
from common.scheduler import build_sqlalchemy_jobstore
LOGGER = logging.getLogger(__name__)
@@ -56,10 +51,6 @@ TERMINAL_RUN_STATES = {
_ACTIVE_SERVICE: "SchedulerService | None" = None
def _sync_database_url(value: str) -> str:
return value.replace("mysql+asyncmy://", "mysql+pymysql://", 1)
def _naive_utc(value: datetime | None) -> datetime | None:
if value is None:
return None
@@ -96,10 +87,7 @@ class SchedulerService:
self.dispatch_lock = asyncio.Lock()
self.scheduler = AsyncIOScheduler(
jobstores={
"default": SQLAlchemyJobStore(
url=_sync_database_url(database_url),
tablename="apscheduler_jobs",
)
"default": build_sqlalchemy_jobstore(database_url)
},
timezone=UTC,
)
+17 -23
View File
@@ -1,16 +1,13 @@
"""Schedule-specific storage client built on the shared ``StorageClient``."""
from __future__ import annotations
import base64
from typing import Any
import httpx
from common.storage.client import StorageClient
class SchedulerStorageClient:
def __init__(self, client: httpx.AsyncClient, service_token: str) -> None:
self.client = client
self.headers = {"X-Service-Token": service_token}
class SchedulerStorageClient(StorageClient):
async def create_object(
self,
*,
@@ -22,20 +19,17 @@ class SchedulerStorageClient:
content: bytes,
idempotency_key: str,
) -> dict[str, Any]:
response = await self.client.post(
"/internal/v1/objects",
headers=self.headers,
json={
"workspace_id": workspace_id,
"user_id": user_id,
"usage_type": usage_type,
"file_name": file_name,
"content_type": content_type,
"content_base64": base64.b64encode(content).decode("ascii"),
"visibility": "workspace",
"is_immutable": True,
"idempotency_key": idempotency_key,
},
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,
)
response.raise_for_status()
return response.json()["data"]
__all__ = ["SchedulerStorageClient"]