Skip to content

(MOT-4372) fix(database): bound query history stored via state::update - #732

Open
andersonleal wants to merge 3 commits into
mainfrom
fix/database-state-cap
Open

(MOT-4372) fix(database): bound query history stored via state::update#732
andersonleal wants to merge 3 commits into
mainfrom
fix/database-state-cap

Conversation

@andersonleal

@andersonleal andersonleal commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes MOT-4372

Problem

The database worker records every console query into the state worker (scope database, key history:{db}) via an uncapped state::update append. On a live stack history:primary reached ~8.4MB. state::update echoes the full old and new value in its response and emits an event carrying both again (state/src/functions.rs), and it is exempt from state.max_value_bytes (which guards state::set only) — so each one-line append re-served the whole blob ~4x over the engine⇄worker WebSocket. Past the transport's per-message cap the connection resets, the state SDK reconnect-loops, state::* stays unregistered, and the engine spams Function not found: state::update. The only existing trim ran in the database::history read handler, which nothing calls (the console keeps its own localStorage history), so it never fired. History shipped this way in #645.

Fix

  • record() now does read → append → trim_to_capsstate::set full replace, still fire-and-forget: a state failure never fails or delays the user's query, and state::set is the size-guarded path.
  • New WorkerConfig knobs (hot-reloaded, read per write; whichever cap hits first, oldest entries dropped; 0 disables recording):
    • history_max_entries — default 200
    • history_max_bytes — default 262144 (256KB; worst-case set echo ≈ 3×256KB stays under the transport's 1MiB per-message cap)
  • Self-healing for already-poisoned values: a per-db reset flag makes the write after a failed read skip the read and replace the stored value wholesale — recovery never depends on round-tripping the blob that caused the outage. With a pre-seeded ~8MB history:primary, the next history write replaces it.
  • trim_to_caps uses exact compact-JSON byte accounting (no re-serialization loop); a unit test locks the formula against serde_json to the byte. A single entry larger than the byte cap yields an empty list rather than a panic.
  • The dead read-path trim and the append_ops helper are deleted; database::history keeps its behavior (newest first, default 50).
  • No history_store_results knob: history was already metadata-only (SQL truncated to 4000 chars, verb, timing, row count — never result rows) and stays that way, now documented in the README.

Tests

  • New unit tests: entry-cap trim (oldest dropped), byte-cap trim, exact byte-accounting formula, oversized single entry, no-op within caps, reset-flag semantics, config defaults/overrides, schema descriptions.
  • cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, cargo test all green (385 tests); e2e schema fixture regenerated.
  • Query-execution resilience to history failure is structural (detached spawn) and covered by the existing integration suite, which runs with recording off.

Manual verification

On a live stack under heavy console use: redis-cli HSTRLEN state:database history:primary stays under 262144; state worker shows no reconnect loop; engine stops logging Function not found: state::update. Related engine-side reconnect issue (secondary, after disconnect): engine#1993.

Follow-up

acp/src/session.rs append_history grows sessions:{id}:history unbounded via the same pattern — tracked separately (noted in MOT-4372).

Summary by CodeRabbit

  • New Features

    • Added configurable per-database query-history limits for maximum entries and storage size.
    • Limits apply immediately and automatically remove the oldest entries when exceeded.
    • Set the entry limit to zero to disable query-history recording.
    • Added defaults of 200 entries and 256 KiB, with settings available in configuration and schema examples.
  • Bug Fixes

    • Improved handling of oversized, truncated, or unreadable history data during recording.
  • Documentation

    • Expanded guidance on query-history storage, limits, defaults, and eviction behavior.

Oversized history:primary blobs (~8MB) reset the state worker WebSocket
and leave state::* unregistered — state::update echoes the full old and
new value per append and bypasses max_value_bytes, and the only trim ran
on a read path nothing calls.

Record now does a capped read-append-trim-replace through state::set
(history_max_entries / history_max_bytes, defaults 200 entries / 256KB,
0 disables, hot-reloaded). A stored value that cannot be read is
replaced wholesale on the next write, so a pre-existing oversized blob
self-heals without ever round-tripping it.
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview Aug 6, 2026 8:46pm
workers-tech-spec Ready Ready Preview Aug 6, 2026 8:46pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@andersonleal, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 81669a7a-989d-4c6f-99ea-da4e38122e68

📥 Commits

Reviewing files that changed from the base of the PR and between 5b94eb1 and 992e857.

📒 Files selected for processing (4)
  • database/tests/e2e/README.md
  • database/tests/e2e/workers/harness/src/cases-history.ts
  • database/tests/e2e/workers/harness/src/database-config.ts
  • database/tests/e2e/workers/harness/src/runner.ts
📝 Walkthrough

Walkthrough

Database query history now supports configurable per-database entry and serialized-byte limits. History is trimmed during writes, disabled when configured to zero, and reset after unreadable stored state.

Changes

Query history limits

Layer / File(s) Summary
History limit configuration
database/src/config.rs, database/config.yaml.example, database/tests/e2e/workers/harness/fixtures/database.schema.json, database/README.md
Adds configurable entry and byte limits with defaults, schema validation, live-application documentation, and zero-value disablement.
Bounded history recording
database/src/handlers/query.rs, database/src/handlers/saved.rs
Passes worker configuration to recording. Writes enforce both caps, retain newest entries, skip disabled recording, and recover from unreadable state.
History limit validation
database/src/handlers/saved.rs
Tests entry trimming, byte accounting, oversized entries, no-op trimming, and reset-flag lifecycle.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant QueryHandler
  participant WorkerConfig
  participant saved_record
  participant StateStorage
  QueryHandler->>WorkerConfig: clone current configuration
  QueryHandler->>saved_record: record query with configuration
  saved_record->>WorkerConfig: read history caps
  saved_record->>StateStorage: replace history with capped entries
Loading

Suggested reviewers: rohitg00, sergiofilhowz

Poem

I’m a rabbit with bounded logs,
Trimming old hops from storage bogs.
Bytes and entries keep their pace,
Freshest queries hold their place.
If zero comes, the trail sleeps tight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: bounding query history stored through state::update.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/database-state-cap

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ions

No state worker runs in the e2e stack, so the harness registers
state::get/set/update itself — observing every history write and
scripting the failure modes a real state worker cannot safely
reproduce. Covers entry/byte rotation (tiny caps seeded in the e2e
config), oversized-backlog trim, the unreadable-value blind-write
self-heal, and a guard that history never touches state::update.
Verified against the pre-fix worker: cases now fail in ~85ms with
'history wrote via state::update (uncapped append)' rather than waiting
out the 5s write timeout.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant