Skip to content

perf(storage): scheduled-task point operations rewrite the full catalog #4954

Description

@Totoro-qaq

What happened

Updating one scheduled task reads and rewrites every scheduled task and pending fire claim. Binding an execution, advancing a native fire state, and settling a fire use the same path. Even a due scan with no changes deletes and reinserts both tables.

At main 492ff80f0f4abf3a917ce8f2c41b32fa9e3fc60c, SqliteScheduledTaskStore.mutate() loads the complete state before write() deletes both tables and reinserts every record. get(taskId) also reads both tables before finding its target.

A local reproduction using the real owner lease, store API and SQLite connection produced the following counts. Each unrelated task also has one pending fire claim; measurements exclude fixture creation.

Unrelated tasks / claims JSON records returned by get(target) Rows changed by snooze(target) Rows changed by an unchanged claimNextDue()
0 / 0 1 2 2
32 / 32 65 130 130
256 / 256 513 1,026 1,026

Returned JSON records measure materialization, not all internal SQLite row visits. Changed rows are measured with total_changes() on the actual store connection and include deletes plus inserts. These are work counts, not latency or WAL-volume measurements.

Expected: point operations locate the affected task/claim through existing keys and update only relevant rows. Settlement updates the current task and removes its claim in one transaction. A due scan with no state changes performs no DML.

How to reproduce

  1. Build source at 492ff80f0f4abf3a917ce8f2c41b32fa9e3fc60c.
  2. Resolve a fresh interactive storage root, acquire its owner, and open openInteractiveScheduledTaskStoreForWrite(owner.lease).
  3. Through store.create(), create a target task and 0, 32, or 256 unrelated tasks scheduled for the future. Use store.claimNow() to create one pending claim for each unrelated task.
  4. Acquire the observation lease through runWithStorageRootLease() using its canonical root path, so observation shares the store's actual connection.
  5. After setup, measure get(target.id), snooze(target.id, 1000, now), and claimNextDue(now). Keep now fixed before the target is due and before any task expires. Record returned JSON rows and the connection's total_changes() difference around each operation.

The unchanged poll returns { claim: null, expired: [] } but still produces the writes shown above. Inspecting the executed statements confirms the unqualified deletes and full reinserts.

Environment

  • Source commit reproduced: 492ff80f0f4abf3a917ce8f2c41b32fa9e3fc60c.
  • Surface: Runtime Host / SQLite scheduled-task storage, exercised through its owner-protected store API.
  • Reproduction environment: macOS 26.6.2; results confirmed with Node v22.23.1 and v24.18.0; npm 11.19.0 for builds.
  • No external provider or timing-dependent sleep is required. Business fixtures are created through the store API.

Logs, screenshots, or additional context

This tracks the scheduled-task whole-collection rewrite item from Discussion #4876.

Source evidence: point get, full-state read and rewrite, and existing primary/unique keys.

Proposed scope: keyed task/claim reads and transactional row operations for point mutations; due discovery writes only newly expired tasks and at most one new claim. Preserve claim uniqueness, execution identity, expiry ordering, native-delivery state and restart recovery.

Full bounded-read treatment of due discovery, catalog listing, and the Host's schedule/residency refresh remains separate work. The Host's create limit is not a storage-layer bound. This issue does not claim to close all read-path items in #4876, and does not require a schema or protocol change.

Prepared with Codex; submitted with contributor approval.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions