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:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user