Skip to content

fix(queuefs): serve list_unacked in id order so a large queue cannot fail startup - #6151

Open
Tong-bit-art wants to merge 1 commit into
volcengine:mainfrom
Tong-bit-art:fix/queuefs-list-unacked-sort
Open

Tong-bit-art wants to merge 1 commit into
volcengine:mainfrom
Tong-bit-art:fix/queuefs-list-unacked-sort

Conversation

@Tong-bit-art

Copy link
Copy Markdown
Contributor

Description

list_unacked filters status IN ('pending','processing') and orders by id, but
idx_queue_status is (queue_name, status, id). With two status values that index
cannot deliver rows in id order, so SQLite inserts a sorter:

SEARCH queue_messages USING INDEX idx_queue_status (queue_name=? AND status=?)
USE TEMP B-TREE FOR ORDER BY

Once the unacknowledged set is large enough the sorter spills to a temporary file, and
with no writable temp directory list_unacked fails with unable to open database file
even though the database is intact. Task recovery snapshots every queue before any
consumer starts, so that aborts startup — and since a queue only drains while the server
runs, the state never heals on its own.

Add idx_queue_unacked on (queue_name, id) so that query is served in id order
without a sorter.

Refs #6127

Human Involvement

  • A human participated in the implementation or review loop
  • This PR was generated entirely by AI agents without human participation in the loop

An AI agent located the defect, produced the analysis and authored the change. The root
cause, the fix shape and the choice to verify at the SQL level before submitting were
directed and reviewed by the human author. See Additional Notes for what could not be
verified locally.

Related Issue

Fixes #6127

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Changes Made

  • crates/ragfs/src/plugins/queuefs/backend.rs: add idx_queue_unacked on
    (queue_name, id) to the idempotent migration list, with a comment recording why the
    existing index cannot serve this query and why the new index needs its own name.
  • crates/ragfs/src/plugins/queuefs/backend.rs: extend the existing migration test to
    also assert the new index exists.

No query is changed. idx_queue_status is kept — it is still the best plan for the four
single-status queries (dequeue next, peek, count pending, last enqueue).

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this on the following platforms:
    • Linux
    • macOS
    • Windows

Why the fix needs a new index name

This is the part worth reviewing carefully. run_migrations uses idempotent statements
that tolerate already exists. Changing the column order of idx_queue_status would
therefore be a no-op on any database that already has it — which is precisely the
population hitting this failure. The new index must have its own name so
CREATE INDEX IF NOT EXISTS actually creates it on an existing install.

The same reasoning rules out dropping and recreating the old index on every startup:
that rebuilds an index over the whole queue table on each launch.

Validation actually run

I could not compile Rust here (see the limitation below), so I verified the change at the
SQL level instead, driving real SQLite with the migration statements parsed out of the
modified Rust file
so the strings under test are the ones in the diff:

FRESH database, migration applied:
  list_unacked (bug)     SEARCH queue_messages USING INDEX idx_queue_unacked (queue_name=?)
  dequeue next           SEARCH ... idx_queue_status (queue_name=? AND status=?)
  peek                   SEARCH ... idx_queue_status (queue_name=? AND status=?)
  count pending          SEARCH ... COVERING INDEX idx_queue_status (queue_name=? AND status=?)
  last enqueue           SEARCH ... idx_queue_status (queue_name=? AND status=?)
  # no sorter anywhere, and the four single-status plans are unchanged

EXISTING database built with the old schema, then migrated (the real upgrade path):
  BEFORE: list_unacked  ... idx_queue_status (...) | USE TEMP B-TREE FOR ORDER BY   <-- reproduced
  AFTER : list_unacked  ... idx_queue_unacked (queue_name=?)                        <-- fixed
  indexes present: idx_queue_message_id, idx_queue_status, idx_queue_unacked
  the four single-status plans are unchanged

CONTROL: reusing the existing index name instead
  BEFORE and AFTER are identical; USE TEMP B-TREE FOR ORDER BY remains
  # this is why the fix introduces a new name rather than reordering the old index

Reproduction of the sorter matches the EXPLAIN QUERY PLAN output in #6127.

Limitation, stated plainly

I could not compile or run the Rust test suite in my environment — no cargo, and no
C++/CMake toolchain for this workspace. So:

  • the SQL behaviour above is verified empirically against real SQLite;
  • the Rust edit itself is a string literal added to the existing migration array plus two
    string literals in the existing assertion, which I reviewed by hand but did not compile;
  • cargo test -p ragfs and cargo fmt --check have not been run by me.

check-deps treats crates/ as a dependency change, so the build job should compile
this. I would rather say this up front than imply a green run I did not perform.

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

Additional Notes

Owner module per the CONTRIBUTING routing table: Storage — crates/ragfs* → @baojun-zhang.

Scope is one defect. I did not touch the error text: unable to open database file is
SQLite's own message surfacing through sqlite list unacked row error, and renaming it
would change behaviour beyond this fix. It may be worth a follow-up so the message names
the sorter rather than the database, but that is a separate change.

I also deliberately did not drop idx_queue_status. Reordering it looked like the
smaller diff until the migration path was checked; /explain shows the four single-status
queries still prefer it.

Diff is +14/-2 in one file.

…fail startup

list_unacked filters status IN ('pending','processing') and orders by id, but
idx_queue_status is (queue_name, status, id). With two status values that index
cannot deliver rows in id order, so SQLite inserts a sorter:

  SEARCH queue_messages USING INDEX idx_queue_status (queue_name=? AND status=?)
  USE TEMP B-TREE FOR ORDER BY

Once the unacknowledged set is large enough the sorter spills to a temporary
file, and with no writable temp directory list_unacked fails with 'unable to
open database file' even though the database is intact. Task recovery snapshots
every queue before any consumer starts, so that aborts server startup, and the
queue only drains while the server runs, so it never heals on its own.

Add idx_queue_unacked on (queue_name, id) so that query is served in id order
without a sorter. The single-status queries keep using idx_queue_status, so no
existing plan changes.

The index needs its own name: CREATE INDEX IF NOT EXISTS cannot redefine an
existing index, so changing the column order of idx_queue_status would silently
not apply to databases that already have it, which is exactly the population
hitting this failure. Verified against SQLite via EXPLAIN QUERY PLAN for a fresh
database and for one migrated from the old schema.

Fixes volcengine#6127
@Tong-bit-art

Copy link
Copy Markdown
Contributor Author

Heads-up on the plugin-tests failure, so it doesn't cost review time.

It is the OpenCode state-retention flake, not this change:

? captured message parts are not retained in the session state file (460.003602ms)
  TypeError: Cannot read properties of undefined (reading 'parts')
      at examples/opencode-plugin/tests/memory-session.test.mjs:453:35
1303 passed, 1 failed, 8 skipped

That is a Node test under examples/opencode-plugin/; this PR only adds a SQLite index in
crates/ragfs/src/plugins/queuefs/backend.rs.

It is also already diagnosed. #5408 documents the identical TypeError:

The OpenCode state-retention test sleeps 350ms before reading the state file, but the
300ms debounce only starts an asynchronous write. Under load the test can read the
earlier snapshot, where msg-pending is absent, and fail with Cannot read properties of undefined (reading 'parts').

and has the fix open (await the persisted state instead of sleeping). So there is nothing
to investigate on my side — the two changes are independent, and #5408 is the one to land
for it.

On the Rust build

Worth recording that Build aarch64-apple-darwin passed (4m58s). I flagged in the PR
description that I could not compile Rust in my environment, so the build matrix running
here is what covers that gap; it confirms the migration array and the updated test
assertion compile.

@github-project-automation github-project-automation Bot moved this from Backlog to Done in OpenViking project Oct 16, 2026
@Tong-bit-art Tong-bit-art reopened this Oct 16, 2026
@Tong-bit-art

Copy link
Copy Markdown
Contributor Author

Correction to my earlier note: after re-triggering, plugin-tests is now green on this
PR. The failure I described was from the superseded run.

Effective check status now (newest run per check name):

pass     plugin-tests
pass     check-deps, plugin-versions
pass     Build aarch64-apple-darwin / aarch64-unknown-linux-musl /
         x86_64-apple-darwin / x86_64-pc-windows-msvc / x86_64-unknown-linux-musl
pass     build / Build distribution (linux x86_64 + aarch64, macos-14, macos-15-intel,
         windows-latest, sdist py3.12)
pass     cuvs-tests / Lite Test (ubuntu-24.04, 3.11)
fail     API & CLI Integration Tests (ubuntu-24.04)

The single remaining red is the fork-PR credential issue, same as on #5683: repository
secrets are not exposed to pull_request runs from forks, so the embedding call gets an
empty key and returns 401 The API key format is incorrect. It fails identically on every
fork branch (akushonkamen, ktz03, ehz0ah, ...) while in-repo branches pass, and
#5771 was merged by a maintainer with that same check failing, so it is not a merge gate.

Nothing else on this PR is red.

This branch has not been deployed

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug]: QueueFS list_unacked builds a sort it does not need; an unusable temp dir makes the server refuse to start

2 participants