Commit Graph
59 Commits
Author SHA1 Message Date
tao.chenandtao.chen ba874579d3 refactor(common.storage): drop sync storage abstraction and example_usage
The async-only direction was already the only one used in production:
* create_storage never accepted mode=sync; build_storage_config always
  emitted mode=async; zero callers referenced StorageBackend / SyncData
  / S3StorageBackend.sync / LocalStorageBackend.sync anywhere.
* Drop the parallel sync base class, the sync concrete classes in
  backends/local.py and backends/s3.py, and the boto3 dependency.
* Drop example_usage.py (zero importers; demonstration code, not part
  of the public surface).
* Rename LocalAsyncStorageBackend -> LocalStorageBackend,
  S3AsyncStorageBackend -> S3StorageBackend to reflect the single
  remaining class per type.
* Tighten create_storage: any mode=... key now raises StorageConfigError
  with the new pointer (settings.storage_backend controls behavior).
* Cleanup call sites: schedule.application.service.build_object_store
  no longer passes mode=async to create_storage.
* Cosmetic touch-ups in backend/services/storage.py and
  common/config.py docstrings where they still said "boto3" instead of
  "S3 client".

Public API surface preserved: AsyncStorageBackend / ObjectMeta /
create_storage / build_storage_config / register_backend all keep
their names and call signatures. backend tests: 136 passed.
2026-09-02 10:10:41 +08:00
tao.chenandtao.chen 5ed3aa4300 chore: update docstring 2026-09-02 10:10:41 +08:00
d91fb669e9 docs(schedule): record the deliberate ExecutionResult dataclass upgrade
Review (2026-08-21) found that stage 1 promoted ExecutionResult from a
plain class to @dataclass(frozen=True) along the way. No caller mutates
or compares these objects by identity, so the only externally visible
change is structured log output. User opted to keep the upgrade.

- domain/execution.py module docstring: explicit note that the frozen +
  value-equality form is a deliberate enhancement, not a behavioral
  accident
- CLAUDE.md "Schedule service layering" lesson: add a "don't silently
  upgrade dataclass-ness during a structural-only refactor" note so
  future refactors copy class definitions verbatim unless they intend
  to tighten semantics explicitly

No code change; tests still 29 green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-02 10:10:41 +08:00
d196b237a7 refactor(schedule): move orchestrator to application/ + review fixes (stage 8)
Follow-up to the layered refactor (review-driven):

- Move scheduling/orchestrator.py -> application/orchestrator.py
  (orchestrator is application-level coordination, not a cron-trigger
  primitive; matches the intended target tree)
- Migrate orchestrator re-exports from scheduling/__init__.py to
  application/__init__.py; scheduling/ now exposes only CronScheduler
- Rewrite imports + 5 mock.patch string targets in test_janitor.py and
  the orchestrator import in test_layering.py
- Update docstring refs in application/service.py + execution/worker.py
- Add 4 runner smoke tests (test_layering.py): _limited_log under-limit /
  empty-sentinel / above-MAX_LOG_BYTES truncation; execute_artifact
  rejects unsupported script_type with ValueError
- infrastructure/__init__.py re-exports SchedulerStorageClient so
  ``from schedule.infrastructure import SchedulerStorageClient`` is a
  stable top-level surface
- CLAUDE.md engineering note: extend the commit trail to 7476c27 and
  note the stage-8 orchestrator placement

Zero behavior change; schedule/pyproject.toml untouched. 29 tests green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-02 10:10:41 +08:00
ff03a79938 test(schedule): add layer-boundary smoke tests (stage 7)
Pin the new five-layer contract so a later refactor can't silently break
an import surface or lifecycle:

- domain: ExecutionResult defaults, terminal/failed state-set invariants,
  naive_utc normalization (import-side-effect-free)
- infrastructure.storage: SchedulerStorageClient base64 upload via
  httpx.MockTransport (no live server; base_url required for relative URL)
- scheduling: CronScheduler start/close lifecycle, global trigger cleared
- application: SchedulerService wires cron + orchestrator + worker, handler
  dispatch table points at the wired NodeExecutor
- execution: schedule.notebook_runner shim re-exports the real main

25 schedule tests green; schedule/pyproject.toml untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-02 10:10:41 +08:00
b6799a8a4d refactor(schedule): cleanup flat files + document layering (stage 6)
- Delete the six orphaned flat modules (context/executor/orchestrator/
  scheduler/storage_client/worker) — all import sites already point at the
  layered packages; keep notebook_runner.py as the compatibility shim
- Clear __pycache__; fix stale docstring module refs in surviving files
- Subpackage __init__.py files re-export public symbols per layer
  (CronScheduler / DispatchOrchestrator / SchedulerService / NodeExecutor /
  SchedulerStorageClient / ExecutionResult / TERMINAL_NODE_STATES ...)
- CLAUDE.md engineering notes: add "Schedule service layering" section
- Zero behavior change; schedule/pyproject.toml untouched

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-02 10:10:41 +08:00
67a458c491 refactor(schedule): extract application/ layer (stage 5)
- Move service.py -> application/service.py (SchedulerService class name
  unchanged; build_object_store / build_storage_http_client move along)
- main.py imports schedule.application.service
- Rewrite worker.py's lazy `from schedule.service import build_object_store`
  and test_worker.py's mock patch string targets — same class of bug as the
  test_janitor patch strings (silent no-op until the old file is deleted)
- Delete flat service.py (orphaned; only docstring refs remain in
  orchestrator, cleaned up in stage 6)
- Zero behavior change; schedule/pyproject.toml untouched

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-02 10:10:41 +08:00
27d5dfaeb8 refactor(schedule): extract execution/ + runners in layered refactor (stage 4)
- Move worker.py -> execution/worker.py, executor.py -> execution/executor.py
  (byte-identical copies; import sites updated)
- Merge old execution.py + notebook_runner.py into
  execution/runners/notebook.py: subprocess CLI (main/emit_outputs) plus the
  in-process helpers (_execute_notebook/_execute_python/execute_artifact)
- schedule/notebook_runner.py becomes a compatibility shim so
  `python -m schedule.notebook_runner` (the worker's stable -m string) still works
- Delete flat execution.py (shadowed by the new execution/ package)
- Zero behavior change; schedule/pyproject.toml untouched

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-02 10:10:41 +08:00
48b060dba9 refactor(schedule): extract scheduling/ layer (scheduler + orchestrator)
Stage 3 of the layered refactor. Relocate the two scheduling components
into their own package so that domain / application / scheduling /
execution / infrastructure boundaries actually exist on disk.

- Add schedule/src/schedule/scheduling/__init__.py
- Move scheduler.py (232 lines) -> scheduling/scheduler.py
  (byte-identical via diff; CronScheduler class name unchanged)
- Move orchestrator.py (946 lines) -> scheduling/orchestrator.py
  (byte-identical via diff; DispatchOrchestrator + event constants
  unchanged; NOT further split this round, per plan)
- service.py lines 39-40: import paths rewritten to the new module
- tests/test_janitor.py: rewrite the import + 5 patch() string targets

  The 5 patch() targets ("schedule.orchestrator.session_scope" x3,
  "schedule.orchestrator.asyncio.sleep" x2) were NOT caught by the
  import-line grep — they patch module attributes at runtime and would
  have become dead no-ops after the move (and would hard-raise once
  the old module is deleted in stage 6). Rewriting them to
  "schedule.scheduling.orchestrator.*" keeps the janitor tests meaningfully
  exercising the new module.

- old flat scheduler.py / orchestrator.py left on disk; stage 6 deletes
  them once all layers are extracted.

Validation:
- uv run --package schedule pytest schedule/tests -q: 18 passed
- uv run python -m compileall schedule/src: zero errors
- grep 'from schedule.(scheduler|orchestrator)\\b' (old paths): 0 matches
- grep '"schedule.orchestrator.' (old patch targets): 0 matches
- main.py / worker.py / domain/ / infrastructure/ / pyproject.toml
  byte-identical to HEAD

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-02 10:10:41 +08:00
8675868bdc refactor(schedule): extract infrastructure/storage/ layer
Stage 2 of the layered refactor. Move the storage HTTP client one
package deeper so that infrastructure code lives under a dedicated
namespace.

- Add schedule/src/schedule/infrastructure/__init__.py
- Add schedule/src/schedule/infrastructure/storage/__init__.py
- Add schedule/src/schedule/infrastructure/storage/client.py
  (verbatim copy of old schedule/src/schedule/storage_client.py,
  byte-identical via diff — 2682 bytes)
- main.py line 17: import path rewrite to the new module
  (only consumer — service.py and worker.py take storage_client as
  an `Any` constructor param and never imported the class)
- old schedule/src/schedule/storage_client.py left on disk; stage 6
  deletes it once all layers are extracted.

Validation:
- uv run --package schedule pytest schedule/tests -q: 18 passed
- uv run python -m compileall schedule/src: zero errors
- grep 'from schedule.storage_client' (old path): 0 matches
- service.py and worker.py byte-identical to HEAD
- main.py / pyproject.toml / tests/ unchanged apart from the 1 import line

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-02 10:10:41 +08:00
53ad6718ed refactor(schedule): add domain/ package files
Add the three files that complete stage 1 of the layered refactor:
- schedule/src/schedule/domain/__init__.py (empty package marker)
- schedule/src/schedule/domain/context.py
  (TERMINAL_NODE_STATES / FAILED_NODE_STATES / TERMINAL_RUN_STATES / naive_utc —
  pure types, no I/O)
- schedule/src/schedule/domain/execution.py
  (ExecutionResult dataclass, frozen=True)

The corresponding import-path rewrites in worker.py / orchestrator.py /
scheduler.py / execution.py were already landed in cdcfcb2 (the prior
commit on this branch). This commit only adds the missing domain/
package files those imports point at.

Validation:
- uv run --package schedule pytest schedule/tests -q: 18 passed
- uv run python -m compileall schedule/src: zero errors
- from schedule.domain.context / schedule.domain.execution importable
- main.py / pyproject.toml / tests/ unchanged

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-02 10:10:41 +08:00
tao.chenandtao.chen 9f7b6ba18c feat(audit): skip audit log for excluded health/root paths
健康检查与根路径(/health/live、/health/ready、/api/v1/health、
/、/health/storage)没有用户、没业务动作,每秒被 K8s/LB
探针刷一次只会灌进无意义噪音。命中排除集即跳过审计行;
诊断日志(method/path/status/ms 走 stderr)照常打,对容器
运维排错仍有用。

* settings.audit_excluded_paths: list[str] 默认覆盖 5 条
  基础设施路径,env AUDIT_EXCLUDED_PATHS 用逗号分隔
  (pydantic NoDecode + field_validator 兼容 str/list)
* main.py 模块级 _AUDIT_EXCLUDED = frozenset(...),
  access_log 的 success/exception 两条审计行各加守卫
  诊断无条件打
* 测试用 _AccessLogReplica 复刻 access_log 契约(不 import
  真实 main.py),新增 4 个 case:排除根路径、排除 /health/live、
  不排除路径照写审计、自定义排除集

顺带 schedule 模块:ExecutionResult 与 context 已迁到
schedule.domain.*(execution.py / orchestrator.py /
scheduler.py / worker.py),调用点跟进;schedule 自身
18 个测试在改前改后均通过。
2026-09-02 10:10:41 +08:00
tao.chen b600c6810b fix: P0-5 — upload-status rollback, streaming copy (LOCAL only), user re-verify, honest lock
B1: `_mark_upload_failed_and_raise` now commits on a separate session
  - Helper takes `request + upload_id`, opens a fresh session from
    `request.app.state.session_factory` and commits there before raising.
  - Closes the named-lock connection-pool leak Codex flagged: the old
    "commit-on-the-same-session" implementation could return the
    GET_LOCK connection to the pool before the enclosing
    `finally: release_named_lock` ran, leaking `mp:<hash>` for up to
    `pool_recycle` and re-opening the same-key upload race.
  - Same helper now used by `create_server_object_payload`'s put-failure
    branch — two failure paths have identical semantics.

B2: streaming copy for soft-delete + restore (`get_stream() + put()`)
  - LOCAL backend: zero-copy (aiofiles stream write). OOM fixed.
  - S3 backend: still OOMs on multi-GB objects — `put()` materializes
    the async iter via `b"".join(chunks)`. Multipart `put` is a
    follow-up; do NOT claim "OOM fixed on production" since production
    defaults to S3.

C1: worker re-verifies `Users.status='active' AND is_deleted=0`
  - `_assert_user_active` called from `_execution_context` after
    resolving `triggered_by`; skips `SYSTEM_CRON_USER_ID`.
  - `USER_DISABLED` error_code goes into the `NODE_FINISHED_EVENT`
    outbox payload — `schedule_node_runs` has no `error_code` column,
    the row only carries the `message` text. Docstrings corrected to
    say so explicitly (previous docstring falsely promised row-level
    observability).

F1: honest browser-local file lock
  - `api.ts` `acquireFileLock/heartbeatFileLock/releaseFileLock/
    releaseFileLockOnUnload` are now no-ops with comments stating they
    never call the network.
  - `scriptWorkspaceStore` dropped `tickHeartbeats`; `tickCleanup`
    simplified to just clear cache.
  - `useEditSessionLifecycle` dropped its 15s heartbeat `setInterval`.
  - `ScriptWorkspace.tsx` renders `.local-lock-banner` info bar when
    `isEditing`. Two tabs may still silently last-write — banner is the
    only guard (acceptable disclosure-only tradeoff).

Dead code: deleted the duplicate `upload_bytes_to_session` in
`backend/src/backend/storage_api.py`. The `services.storage` import
is now the only source of the function; `create_upload_record`'s
docstring updated to point at `backend.resources`.
2026-08-20 12:07:52 +08:00
tao.chenandClaude Fable 5 4acfbb162f fix: P0-4 schedule node janitor + runtime reaper/lock invariants
S2: schedule worker add janitor task that force-terminals node_runs
whose deadline (timeout_seconds + retry_count*retry_interval + 120s
slack from started_at) has passed. Closes the gap where outbox retry
exhaustion (5 tries, capped 30s backoff) marked the *event* failed
but left the *node_run* stuck in queued/running forever. Re-reads the
row under FOR UPDATE before writing so a worker that races us to a
real terminal state is not overwritten; idempotency key uses
:timed_out variant so the :finished path cannot collide.

R1: extract _reap_once() from _reap_loop for testability; in the
dead-process branch, re-verify (process.pid, started_at) against the
live JUPYTER_PROCESSES entry before del. A start_workspace that
replaced the dead record mid-cycle used to have its new entry
silently erased by the reaper's stale snapshot — leaked the port.

R2: delete _drop_workspace_lock and its two call sites
(stop_workspace tail, get_workspace 404 path). Popping the lock
object after release breaks mutual exclusion for any coroutine still
holding the old reference while a fresh caller gets a new lock
object — same ws_id can race two starts. The dict is bounded by the
number of workspaces so the leak is negligible; invariant lives on
WORKSPACE_LOCKS in a comment.

Tests:
- schedule/tests/test_janitor.py — 8 tests covering normal kill /
  healthy-skip / worker-race / never-started / multi-row batch /
  cancellation propagation / per-iteration self-heal
- runtime/tests/test_process.py — 7 tests covering reaper identity
  match / replacement-skip / alive-preserved + lock
  same-object / concurrent-serialize / survives-stop /
  helper-removed guard

uv run --package schedule pytest schedule/tests → 14 passed
uv run --package runtime pytest runtime/tests  →  7 passed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 10:49:47 +08:00
tao.chen e5633cc95a fix: artifact_bucket 2026-08-20 10:26:44 +08:00
Winnie 3b0229f018 修复失败策略运行逻辑 2026-08-18 11:10:10 +08:00
Winnie ff700d8db4 fix: stabilize cron scheduling and run history 2026-08-17 18:55:31 +08:00
tao.chenandClaude Fable 5 dfe3f0b118 fix(security): P0-1 — port exposure + service-token auth on /internal/* + jupyter RPC
The fix lands in three concentric layers, all backed by a single
INTERNAL_SERVICE_TOKEN shared secret so we have one mechanism
instead of three:

1. docker-compose: drop the backend.ports: 8891:8000 and
   runtime.ports: 8892:8000 mappings. Nginx is the only host
   ingress again (architecture §2.2).
2. /internal/v1/*: the storage control plane had six endpoints, five
   of which were dead code (frontend already migrated to
   /api/v1/data-resources/* with JWT; schedule only ever called
   POST /internal/v1/objects). Delete the dead routes, mount the
   one survivor with Depends(require_internal_service) that
   compares the X-Internal-Service-Token header against
   settings.internal_service_token with secrets.compare_digest.
3. POST /api/v1/jupyter on the runtime container: previously open
   inside the Docker network. Same token mechanism — backend's
   runtime_http_client now carries the header, runtime's
   handle_jupyter_action requires the same header. /api/v1/health
   stays open for the Nginx and compose healthchecks.

The schedule worker was already configured to call
POST /internal/v1/objects; build_storage_http_client now
sets the token header so its existing call site keeps working
without changes.

Files touched:
  backend/src/backend/storage_api.py   # 5 dead routes deleted + token guard
  backend/src/backend/main.py          # runtime_http_client header
  runtime/src/runtime/main.py          # require_internal_service Depends
  common/src/common/config.py          # internal_service_token setting
  schedule/src/schedule/service.py     # httpx client header
  docker-compose.yml                   # ports dropped, INTERNAL_SERVICE_TOKEN env
  .env.example                         # INTERNAL_SERVICE_TOKEN placeholder
  API.md / README.md / DEVELOP.md      # §9 trimmed to 1 endpoint

Verified:
  compileall -> 0 errors
  pytest backend/tests -> 37 passed
  in-process ASGI smoke:
    POST /internal/v1/objects no/wrong/correct token -> 401/401/200
    POST /api/v1/jupyter   no/wrong/correct token -> 401/401/200
    5 deleted internal routes -> 404
  docker compose config (with env) -> OK

P0-1 still has one open sub-item (rclone RC --rc-no-auth) that
the user has explicitly deferred; not touched here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 16:48:46 +08:00
tao.chen 6258cf5d12 fix: offline deploy 2026-08-17 15:59:19 +08:00
tao.chen 0974881fbb fix: offline deploy 2026-08-17 15:30:21 +08:00
tao.chen 25e563dcaa update: ruff check --fix 2026-08-14 19:51:58 +08:00
tao.chen 341cc77a79 update: Dockerfile 2026-08-14 16:02:19 +08:00
tao.chen 2014d59681 update: Dockerfile 2026-08-14 15:55:00 +08:00
tao.chen af9466c4e6 update: UV_HTTP_TIMEOUT 2026-08-14 15:27:16 +08:00
tao.chen a378ea5352 feat: add pytest in dev 2026-08-12 12:21:48 +08:00
tao.chen 6c6e7e6500 update: python script run in python intercept 2026-08-11 16:20:40 +08:00
tao.chen fe7f1a744e feat: schedule node add python version 2026-08-11 15:56:05 +08:00
tao.chen a97aad3179 fix: schedule error 2026-08-07 19:59:04 +08:00
tao.chen 6ff2127de7 update: add logger info 2026-08-07 19:19:25 +08:00
tao.chen a220afdb0b update: schedule add multi python 2026-08-06 10:26:07 +08:00
tao.chen b9a028a1db update: multi python version 2026-08-06 10:03:58 +08:00
tao.chen f4cabe6e20 update: remove storage 2026-08-05 13:41:13 +08:00
tao.chen 4b2a67ae5d 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.
2026-08-05 13:08:32 +08:00
tao.chen 87382d7bda refactor: update Dockerfile 2026-08-04 18:39:30 +08:00
Winnie f3cc83c5fa Merge branch 'develop' of http://8.153.151.51:8888/team_group/model-develop into develop
# Conflicts:
#	schedule/src/schedule/orchestrator.py
#	schedule/src/schedule/worker.py
2026-08-04 14:41:22 +08:00
Winnie a2deae2f22 修复调度运行失败 2026-08-04 14:09:03 +08:00
tao.chen 1cf2eecbc9 chore: frontend and schedule module 2026-08-04 13:58:19 +08:00
tao.chen 4919fe0909 update: create file 2026-08-04 12:40:38 +08:00
tao.chen 1bd7da384c feat: add loguru 2026-08-03 20:45:00 +08:00
Winnie d7bd88335c merge: integrate feat/auth into develop 2026-08-03 17:44:00 +08:00
Winnie 4ac8485d3e Merge branch 'develop' of http://8.153.151.51:8888/team_group/model-develop into develop 2026-07-31 19:17:24 +08:00
Winnie 49ee2c0a4a feat: 完善模型平台相关功能 2026-07-31 19:10:37 +08:00
tao.chen 838cbfdc16 chore: else bug 2026-07-31 18:39:44 +08:00
tao.chen fb073c6f99 feat: auth 2026-07-31 17:22:25 +08:00
tao.chen 1994937349 chore: fix dockefile 2026-07-31 15:52:49 +08:00
tao.chen d31fde6abc chore: fix debian source 2026-07-31 15:48:16 +08:00
tao.chen ba96f4f52a chore: update debian source 2026-07-31 15:45:17 +08:00
tao.chenandClaude 9fc886a55e perf: decouple notebook execution and tune pools
schedule:
- New _execution_loop runs alongside _database_event_loop. It claims
  job.node.execute rows, sets a 30-min lease on available_at, then
  dispatches each as asyncio.create_task under a Semaphore(N).
  Polling loop is back to sub-millisecond turnaround for
  schedule.run.requested and job.node.finished. Long notebook
  execution no longer blocks DAG advance events.
- _process_pending_events filters by event_type IN
  ('schedule.run.requested', 'job.node.finished'); the executor
  loop owns job.node.execute exclusively.
- _process_outbox_event builds a plain dict envelope before
  handler dispatch; the previous ORM-row handoff risked
  DetachedInstanceError once the outer session closed.
- _sync_once uses get_job + reschedule_job for existing job ids
  instead of add_job(replace_existing=True). Each cron schedule
  no longer removed-and-readded every 5s.
- service.py threads settings.schedule_execution_concurrency into
  the orchestrator (default 4).

common:
- create_async_engine gets explicit pool_size=10, max_overflow=20,
  pool_recycle=1800. No more relying on SQLAlchemy defaults.
- New schedule_execution_concurrency setting.

runtime:
- scan_workspaces: add missing 'import os' (NameError on startup)
  and switch to asyncio.gather bounded by Semaphore(4) so N
  workspaces start in parallel instead of sequentially.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 15:07:57 +08:00
tao.chenandClaude b0f93d976f build: switch services to gunicorn with per-service config
Replace uvicorn with gunicorn + UvicornWorker in backend, runtime,
and schedule Dockerfiles. New gunicorn.conf.py per service exposes
bind/workers/timeout/graceful_timeout as GUNICORN_* env knobs.

Notable details:
- schedule: timeout=0 (disables gunicorn worker heartbeat) so long
  notebook execution isn't killed by gunicorn's silent-worker kill.
- All three configs: drop dead threads=4 setting; UvicornWorker is
  async and ignores threads.
- Pin gunicorn>=26.0.0 in each pyproject; uv.lock regenerated.
- Drop stale 'COPY contracts ./contracts' from runtime and schedule
  Dockerfiles (contracts dir was deleted in an earlier refactor;
  builds would have failed).
- backend Dockerfile: switch to uv sync layout matching runtime/
  schedule; add build deps for native wheels.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 15:07:45 +08:00
tao.chen f288c90d37 refactor: remove local fs, add config class to common pacakge 2026-07-31 12:46:31 +08:00