Skip to content

fix: find recovery candidates through effects - #61

Open
cardmagic wants to merge 2 commits into
mainfrom
fix/sqlite-claim-join-order
Open

cardmagic wants to merge 2 commits into
mainfrom
fix/sqlite-claim-join-order

Conversation

@cardmagic

@cardmagic cardmagic commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Why

The recovery candidate query runs at the start of every claimEffect and every stale-process cleanup. On SQLite it chose a bad plan when most effects were complete.

sqlite_stat1 records only the average row count for each effect status. When most effects are complete, SQLite estimated that status = 'processing' matched most of the effects table. It then read every recovery row through the instance index:

SCAN recoveries USING INDEX solid_objects_effect_recoveries_instance
SEARCH effects USING INDEX sqlite_autoindex_solid_objects_effects_1 (id=?)
SEARCH owners USING INDEX sqlite_autoindex_solid_objects_processes_1 (id=?) LEFT-JOIN
USE TEMP B-TREE FOR LAST TERM OF ORDER BY

On generated data with 100,000 completed effects and 10,000 recovery rows, the query took 6.3 ms.

Fix

On SQLite the status test now carries likelihood(..., 0.000001). SQLite uses that value in place of the sqlite_stat1 estimate for the term:

SEARCH effects USING INDEX solid_objects_effects_poll (status=?)
SEARCH recoveries USING INDEX sqlite_autoindex_solid_objects_effect_recoveries_1 (effect_id=?)
SEARCH owners USING INDEX sqlite_autoindex_solid_objects_processes_1 (id=?) LEFT-JOIN
USE TEMP B-TREE FOR ORDER BY

The same query took 0.015 ms. The PostgreSQL and MySQL SQL does not change. unlikely() is too weak at 500,000 effects, and CROSS JOIN alone scans the whole effects table when every effect has one status. The Ruby PR has that comparison.

Ruby 0.16.1 has the same fix in cardmagic/solid-objects-ruby#82. That PR also fixes the join order of the Ruby claimed-message scan. JavaScript has no such scan, and SQLite already starts recoverExpiredClaims from the claimed messages, so that fix needs no change here.

Tests

The new SQLite plan test in test/polling-queries.test.ts failed on the old code:

AssertionError: expected 'SCAN recoveries USING INDEX solid_obj…' to match /^SEARCH effects USING INDEX solid_obj…/
+ Received: "SCAN recoveries USING INDEX solid_objects_effect_recoveries_instance"

It also asserts the cause: the poll index statistics give 3,000 rows for each status. The test records the real query that claimEffect sends, with its parameters, and runs EXPLAIN QUERY PLAN on it. The existing test/effect-recovery.test.ts cases cover which candidates come back on every adapter.

Validation

Command Result
pnpm run format:check pass
pnpm run check pass
pnpm test (SQLite) 60 files, 529 passed, 32 skipped
SOLID_OBJECTS_DATABASE_URL=postgresql://... pnpm run test:postgresql (17) 52 passed
SOLID_OBJECTS_DATABASE_URL=mysql://... pnpm run test:mysql (8.4) 39 passed, 7 skipped

I did not run test:coverage, test:cloudflare, or the browser suites. The Cloudflare runtime does not use this query.

Docs

  • CHANGELOG.md: an entry under Unreleased. The package version stays 0.16.0.
  • docs/parity.md: the effect recovery section records that both runtimes mark the status test with likelihood() on SQLite. The row status stays Native.

@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Adjusts database query for effect recovery candidate lookup.

The PR appears safe to merge; no outstanding finding or new blocking issue was identified.

Summary

The PR steers SQLite’s effect-recovery candidate query toward the effects poll index while leaving the PostgreSQL and MySQL predicates unchanged. The latest changes also inline the SQLite condition, replace test annotations with a database-derived parameter type, update a transitive Cloudflare test dependency, and lengthen the Cloudflare suite’s poll timeout. Both previous findings are addressed.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Recovery candidate poll] --> B{Database family}
  B -->|SQLite| C[Processing-status predicate with likelihood hint]
  B -->|PostgreSQL or MySQL| D[Unchanged processing-status predicate]
  C --> E[Recovery eligibility and owner checks]
  D --> E
Loading

Reviews (2) · Last reviewed commit: "refactor: keep the SQLite hint beside th..."

Comment thread test/polling-queries.test.ts Outdated
Comment thread src/effect-recovery-coordinator.ts Outdated
sqlite_stat1 records only the average row count for each effect
status. When most effects are complete, SQLite estimated that
status = 'processing' matched most of the effects table. It then read
every effect_recoveries row through the instance index on each effect
poll. On generated data with 100,000 completed effects and 10,000
recovery rows, the query took 6.3 ms.

On SQLite the status test now carries likelihood(..., 0.000001), which
overrides the statistics estimate for that term. The plan searches the
effects poll index first and takes 0.015 ms on the same data. The
PostgreSQL and MySQL SQL does not change. Ruby 0.16.1 has the same fix.

Ruby 0.16.1 also fixes the join order of its claimed-message scan.
JavaScript has no such scan, and SQLite already starts
recoverExpiredClaims from the claimed messages, so that fix needs no
change here.
recoverAvailable calls processingCondition() once, so the method only
moved the SQLite-specific SQL away from the query that uses it. The
condition is now a local value next to the query.

The recording databases in polling-queries.test.ts typed SQL
parameters as unknown. The repository rule disallows unknown in type
annotations, so they now use the parameter type that DatabaseConnection
declares.
@cardmagic
cardmagic force-pushed the fix/sqlite-claim-join-order branch from 783580a to d97b360 Compare October 2, 2026 17:18
@cardmagic

Copy link
Copy Markdown
Owner Author

@greptileai Please review the latest commit d97b360. It answers both findings: the SQLite condition sits beside the recovery query, and the test types no longer use unknown. The branch is also rebased onto the latest main.

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