Conversation
A source whose checkpoint is stored in the shard table holds no shard until its first one is opened. The file-backed metastore dropped such an empty entry when serializing an index, and restored it on load only for `SourceType::IngestV2`. An SQS file source stores its checkpoint in the shard table but reports `SourceType::File`, so it fell through that restore. Once its shards had been pruned and the index was persisted, every shard API call on the reloaded index failed with `NotFound(Source)`, while the source itself stayed visible in the index metadata. Restarting did not help, since the persisted index was already missing the entry. Key both sides off `use_shard_api`, which covers every source whose checkpoint lives in the shard table. Restoring the entry on deserialization also repairs indexes already persisted without it. See quickwit-oss#5782
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes the root cause behind #5782, where a file-backed (S3) metastore with SQS ingestion starts returning
NotFound(Source)after a few hours.FileBackedIndex.per_source_shardsis meant to hold one entry per source. Bothadd_sourceandFrom<IndexMetadata>set it up that way. The serialization round trip breaks it:source_type() == SourceType::IngestV2An SQS source is
SourceParams::File(FileSourceParams::Notifications(_)). It keeps its checkpoint in the shard table, souse_shard_apireturns true, but it reportsSourceType::File. It falls through that restore.Once its shards are pruned to zero and the index is persisted, every shard API call on a reloaded index goes through
get_shards_for_source[_mut]and returnsNotFound(EntityKind::Source).flowchart LR A["in memory<br>sqs-source, no shards"] B["index.json<br>entry dropped"] C["after reload<br>entry still missing"] D["prune_shards<br>NotFound(Source)"] A -->|serialize| B -->|deserialize| C --> DThis lines up with the report. The source stays in
metadata.sources, so the CLI still lists it. Restarting does not help, because the persisted index is already missing the entry. It surfaces asfailed to prune shards error=NotFound(Source ...).The reporters also found that Kafka and the ingest API kept working on the same index. Neither touches the shard API, and ingest v2 was already covered by the
IngestV2branch, which leaves SQS as the only source type in the gap.The fix
Both sides now key off
use_shard_apirather thanSourceType::IngestV2, as @rdettai suggested in the issue.Serialization keeps the entry for shard API sources and still skips every other source, so the format stays compact. Deserialization restores missing entries, so a fixed node repairs an already-broken index the moment it loads it, without anyone hand-editing the file in S3.
flowchart LR A["in memory<br>sqs-source, no shards"] B["index.json<br>sqs-source: []"] E["index.json from an<br>affected version<br>(no entry)"] C["after reload<br>entry present"] D["prune_shards works"] A -->|"serialize keeps it"| B -->|deserialize| C --> D E -->|"deserialize restores it"| CKeeping the serialize side lets a node still running an affected version read an index written by a fixed one. That covers the red/black overlap described in the issue.
How was this PR tested?
Three new tests, each checked to fail when, and only when, the half it covers is reverted:
test_serialize_keeps_shardless_shard_api_sourcetest_deserialize_restores_dropped_shardless_shard_api_sourcetest_file_backed_metastore_shard_api_sources_survive_reloadThe end to end test reproduces the reported failure. It creates an index with an SQS source and an ingest v2 source, then has a second
FileBackedMetastorereload the index from the same storage and callprune_shards. Without the fix it panics withNotFound(Source { index_id: "test-index", source_id: "sqs-source" }).The shared shard suite previously used
SourceConfig::ingest_v2()in all 8 of its tests and never exercised a reload, so neither source type had reload coverage.The tests use
RamStorage, which exercises the same code path as S3.metastore_resolver.rsmapsProtocol::S3,Azure,Google,FileandRamall toMetastoreBackend::File, andload_indexreads bytes throughdyn Storagebefore handing identical JSON to the same deserialization path.Commands run locally against this branch:
cargo test -p quickwit-metastore: 138 passed, 0 failedtest_file_backed_index_backward_compatibilitypasses with no change to thetest-data/file-backed-index/*.expected.jsongoldenscargo check --workspace --all-targetsandcargo clippy -p quickwit-metastore --all-targetsare cleanI could not run
make test-allormake fmtlocally, since nightly and the Docker services were unavailable on this machine. A maintainer may want to kick off/ci-run-all-tests.Notes for reviewers
SourceType::IngestV2check. The released v0.8.x line predatesuse_shard_apiand SQS file sources, so it cannot hit this. The0.8.0build in the report (3a070c8) is a main build from 2025-04-23, not the release.index.json, so a write race can still discard splits, shards or checkpoints. That hazard is untouched here, which is why the commit saysSee #5782rather thanCloses.use_shard_apipanics onSourceParams::Stdin. Deserialization now calls it for every source on every index load, which is wider exposure than the Postgres metastore's per-publish call sites. It looks unreachable, sinceSourceConfigForSerialization::validate_and_buildrejectsStdinand it cannot reach persisted metadata, but a second opinion would be welcome. Returningfalseinstead would silently accept a state the function exists to reject.delete_sourcedrops a source from the metadata but leaves itsper_source_shardsentry, so a deleted source with non-empty shards round trips its stale shards indefinitely.