feat: add durable asset synchronization worker
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# T07 durable asset synchronization
|
||||
|
||||
Status: prototype implementation. This record describes the local, disposable
|
||||
queue and does not claim production readiness.
|
||||
|
||||
## Boundaries
|
||||
|
||||
`POST /api/sync` authenticates the application session and only enqueues
|
||||
server-derived asset jobs. It does not refresh OAuth credentials or call ESI.
|
||||
Partial enqueue results carry per-character safe skip reasons; an all-failed
|
||||
queue response is not reported as `202`. The assets and chat surfaces poll
|
||||
`/api/sync/status?job=<id>`, expose per-character queued/terminal state and
|
||||
cancellation, and rerun the report only after every queued job succeeds.
|
||||
`POST /api/sync/cancel` fences a job for its owning user. The Convex bridge
|
||||
checks the session proof, character ownership, active grant and exact dataset
|
||||
scope again when enqueueing and claiming.
|
||||
|
||||
Jobs are user-owned `asset_sync` rows. The server derives the dedupe key
|
||||
`user:character:assets`; only `queued` and `running` rows deduplicate. A claim
|
||||
captures grant revision, authorization epoch, credential version and scopes.
|
||||
Claims have random lease tokens and a ten-minute default lease, renewed every
|
||||
minute by the worker. Expired claims are recovered by the next claim
|
||||
transaction. Selection is bounded to 100 due rows and prefers users with no
|
||||
running work before due/creation order, providing per-user fairness without a
|
||||
second scheduler.
|
||||
|
||||
Prototype budgets are conservative and explicit (worker lease/poll values are environment-configurable): five attempts per job,
|
||||
250 assets per staged chunk, 4,096 chunks, 50,000 ESI rows, at most eight
|
||||
active jobs per user, a ten-minute sync lease, and a 24-hour maximum retry
|
||||
delay. ESI request/page/response limits are
|
||||
owned by the T06 transport. Queue backpressure is visible as a bounded failed
|
||||
job, never a silently truncated generation.
|
||||
|
||||
## Fencing and delivery
|
||||
|
||||
A worker starts a generation only through the job lease, stages through a
|
||||
job-token mutation, and commits success or failure through a mutation that
|
||||
rechecks the lease, job fence, character authorization and generation fence.
|
||||
Success and failure therefore cannot mutate state after cancellation, scope
|
||||
loss, credential rotation, quarantine or lease expiry. Staging is contiguous;
|
||||
identical duplicate chunk delivery is idempotent while a changed duplicate is
|
||||
rejected. A successful commit is idempotent if a response is lost. Failed
|
||||
snapshots preserve the previous complete generation.
|
||||
|
||||
`isSingleton` is validated by the T06 ESI parser and is carried unchanged in
|
||||
every stored asset row; absent legacy values remain unknown.
|
||||
|
||||
## Refresh coordination
|
||||
|
||||
Refresh leases are durable per user/character rows, not process maps. The
|
||||
worker decrypts a grant only inside the dedicated worker, performs exactly one
|
||||
remote rotating-token refresh, then commits the replacement with the expected
|
||||
credential version. The committed credential version/revision/epoch are the
|
||||
only authority values used for ESI and publication. A lost, timed-out, or
|
||||
otherwise uncertain refresh result is never replayed: the character becomes
|
||||
`reconnect_required`, credentials are removed, active work is quarantined, and
|
||||
all private datasets are blocked until explicit reconnect.
|
||||
|
||||
The worker receives only its dedicated `CONVEX_SYNC_WORKER_SECRET`,
|
||||
token-encryption, EVE OAuth and ESI user-agent credentials. Worker mutations do
|
||||
not accept the general `CONVEX_BRIDGE_SECRET`; login/session/tenant operations
|
||||
remain on the web-only bridge. It does not receive application JWT signing
|
||||
keys, JEV keys, reference paths/artifacts, or deployment-admin credentials. The
|
||||
web process no longer performs request-bound refresh or asset paging. The image
|
||||
build bundles `workers/sync.ts` to `build/sync-worker.js`, so the runtime never
|
||||
executes a raw `$lib`-aliased TypeScript entry. Compose starts that artifact
|
||||
through `scripts/run-sync-worker.ts`; the launcher rejects bridge, admin,
|
||||
JWT-signing and JEV credentials even when `bun run sync:worker` is invoked
|
||||
directly. Start it with `bun run secrets:run --worker -- docker compose --profile worker up -d
|
||||
--build sync-worker`, never with the general web/deployment secret modes. The Compose
|
||||
service is explicitly profiled so plain `docker compose up` cannot launch a worker
|
||||
with empty credentials.
|
||||
|
||||
## Cooldowns and recovery
|
||||
|
||||
Provider cooldowns are persisted in `syncCooldowns`, keyed by character
|
||||
context and shared provider group. Asset jobs carry the provider group, and
|
||||
claim selection plus the worker both honor the group's eligible-at timestamp
|
||||
before ESI work. ESI remains the single transport retry/cooldown
|
||||
implementation. No public-feed scheduler is introduced by this task.
|
||||
|
||||
Retention, migrations, production quotas, remote revocation, reference
|
||||
extraction and public market orchestration remain deferred as required by the
|
||||
parallel-worktree plan.
|
||||
Reference in New Issue
Block a user