Repository navigation
Use Feuer Disk-Cache - #6962
Use Feuer Disk-Cache#6962PSeitz wants to merge 42 commits into
Conversation
Port memory and disk cache integration from Pomsky while preserving upstream search behavior. Update configuration, metrics, documentation, and tests. Validation: 495 config/storage/search library tests passed on macOS; search, serve, and Lambda checks passed. Linux io_uring disk tests remain unverified. Generated with OpenAI gpt-6-astra.
Inline small test suites and consolidate overlapping cases and fixtures without changing production behavior. Validation: make fmt, git diff --check, and 484 config/storage/search library tests passed on macOS. Linux io_uring execution remains unverified. Generated with OpenAI gpt-6-astra.
Pin Feuer's portable zero-disk constructors and remove the direct feuer-memory dependency. Replace the separate fast-field cache with deprecated configuration aliases for memory-only split-range caching. Preserve shared-download cancellation across Feuer's initializer takeover and wait for metadata writes when testing disk recovery. Validation against the Git pin: 475 macOS and 500 Linux config/storage/search library tests passed. Search, serve, and Lambda compile checks, formatting, license generation, and diff checks passed. Generated with OpenAI gpt-6-astra.
|
Replaces #6958 to use the source branch |
Extract the download coalescer from quickwit-oss#6962 so split readers can batch nearby reads without Feuer. Copy proper subranges to avoid retaining larger merged allocations, and preserve cancellation and error propagation. AI-assisted extraction using openai/gpt-6-astra.
Extract the download coalescer from quickwit-oss#6962 so split readers can batch nearby reads without Feuer. Copy proper subranges to avoid retaining larger merged allocations, and preserve cancellation and error propagation. AI-assisted extraction using openai/gpt-6-astra.
Extract the download coalescer from quickwit-oss#6962 so split readers can batch nearby reads without Feuer. Copy proper subranges to avoid retaining larger merged allocations, and preserve cancellation and error propagation. AI-assisted extraction using openai/gpt-6-astra.
Extract the download coalescer from quickwit-oss#6962 so split readers can batch nearby reads without Feuer. Copy proper subranges to avoid retaining larger merged allocations, and preserve cancellation and error propagation. AI-assisted extraction using openai/gpt-6-astra.
trinity-1686a
left a comment
There was a problem hiding this comment.
i still have to go through quickwit-search
| # # Feuer fast-field RAM cache. Default 1G. Omitted while disk_cache | ||
| # # is set disables this cache; set a capacity explicitly to keep both. | ||
| # fast_field_cache_capacity: 1G |
There was a problem hiding this comment.
i wonder what we want to do about deprecated config options like that, i think we want to remove them from the sample documentation (but keep them in the configuration docs until actually removed)
There was a problem hiding this comment.
my plan is to use their values for feuer
fast_field_cache_capacity + split_footer_capacity = feuer_capacity
There was a problem hiding this comment.
I commented to as deprecated
| # disk_cache: | ||
| # disk_capacity: 1500GiB | ||
| # memory_capacity: 15GiB | ||
| # # Alternatively, replace the scalar result-cache settings above with: |
There was a problem hiding this comment.
a disk cache with a memory capacity is a rare sight
There was a problem hiding this comment.
renamed to storage_cache
storage_cache_directory is outside, because it is shared by the (holy) 3 caches
| /// Search split cache. `None` if no split cache is configured. | ||
| pub split_cache_opt: Option<Arc<SearchSplitCache>>, | ||
| /// Process-wide cache for split byte ranges, with optional disk storage. | ||
| pub disk_cache_opt: Option<Arc<FeuerSplitRangeCache>>, |
There was a problem hiding this comment.
disk cache isn't a great name if the disk part is even optional
There was a problem hiding this comment.
Called storage_cache now
| .predicate_cache | ||
| .get(split_id.clone(), term_absence_cache_key(term)) | ||
| .get_async(split_id.clone(), term_absence_cache_key(term)) | ||
| .await |
There was a problem hiding this comment.
should this get parallelized? (awaiting futures in a loop instead of concurrently)
There was a problem hiding this comment.
it should, p95 accesstime can peak to a few seconds ... although I don't have it per requested size in the metrics. I'll update that
There was a problem hiding this comment.
although we usually only have one hit here currently, so there's no real parallelism yet
| ctx.searcher_context | ||
| .predicate_cache | ||
| .put_async(split_id.clone(), key, segment_id, HitSet::empty()) | ||
| .await; |
There was a problem hiding this comment.
should this get parallelized? (awaiting futures in a loop instead of concurrently)
|
|
||
| /// A (half-open) range bounded inclusively below and exclusively above [start..end). | ||
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] | ||
| #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)] |
There was a problem hiding this comment.
oh, that's interesting. I wonder if feuer should take a key that's Hash, and have a normal looking Hasher with a finish_u128() function so that it can be compatible with what people are used too. that might have limitations when used with type for which the Hash impl can easily cause collisions independantly of hashers though (i recall reading something about PathBuf)
going totally the other way, feuer could accept any Serialize + Eq, and internally use some sort of HashingSerializer so there's no need for json encoding in cache access
(these are ideas, not change requests)
There was a problem hiding this comment.
hmm yes Serialize on the keys + hashing to u128 would make sense
There was a problem hiding this comment.
Hash is actually less of a hassle than Serialize
| assert!( | ||
| encoded_result.len() >= 32, | ||
| "invalid cached predicate header" | ||
| ); |
There was a problem hiding this comment.
i'm a bit scared by these assertion. In the realm of a possibly persistent cache, that can mean a bad entry keeps a pod from recovering until a PV gets manually deleted, i'd prefer if we ignored the error, or cleared the faulty entry from cache (same for LeafSearchCache::get_async actually))
There was a problem hiding this comment.
Yes, that's the biggest risk. In normal operation we should be safe by two mechanisms.
- Every disk payload is checksummed
- You can give the cache a payload version, so it will ignore the whole disk cache if a version changes. (so we don't need to version partial response cache etc.)
There was a problem hiding this comment.
I changed it so it will ignore entries that can't be deserialized
There was a problem hiding this comment.
(i'm not too worried about corruption, i haven't checked feuer disk yet, but i know about the checksumming, i'm more concerned about logic error inserting invalid values somehow; possibly through code that doesn't exist today but will exist tomorrow, which up to now was problematic but not that much, and with the assertion that was, would become very problematic. in general this would fit clearly in the "new failure mode" bucket, which is a bucket i'd rather keep small if not empty)
There was a problem hiding this comment.
Without disk cache that's the same issue we have today, no?
| } | ||
| } | ||
|
|
||
| pub fn disk(cache: FeuerCache) -> Self { |
There was a problem hiding this comment.
in memory cache gets metrics, but disk doesn't?
also i'm in favour of making async functions that require a runtime handle, even if they don't actually await
There was a problem hiding this comment.
in memory cache gets metrics, but disk doesn't?
I enabled it for both. Long-term we may drop them since they are covered by Feuer
also i'm in favour of making async functions that require a runtime handle, even if they don't actually await
replaced it with sync put (which I added to Feuer)
|
|
||
| /// Bridge Tantivy's synchronous predicate API. Disk-backed calls MUST run on a CPU or | ||
| /// blocking thread, never a Tokio worker. The saved runtime drives Feuer's I/O tasks. | ||
| pub fn block_on<F: Future>(&self, future: F) -> F::Output { |
There was a problem hiding this comment.
i don't like this function for reasons already stated. it's not clear to me why we need to call feueur from blocking context. if we do need that, having a block_on on ResultCache feels very strange. having something like a get_many_blocking could make sense so that at least we don't do multiple rtt to storage when we could do concurrent reads
Migrate to Feuer's Cache builder API at revision b5af890. Copy coalesced subranges into independent allocations so each entry retains and charges only its payload. Move exclusively owned whole buffers into the cache without copying. Update ownership tests and trace metadata for the pinned revision.
Combine legacy fast-field and footer budgets when disk_cache is omitted, preserving the default total of 1.5 GB. Let explicit disk_cache settings override those budgets while validating legacy options and rejecting overflow when summing capacities. Initialize memory-only range caching for standalone and Lambda searchers. Update configuration tests, cache coverage, and docs.
Parallelize required-term cache lookups and simplify cache documentation.
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 41d35f3dfc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| ) || error.is::<io::Error>()) => | ||
| { | ||
| let mut memory_config = config.clone(); | ||
| memory_config.disk_capacity = bytesize::ByteSize::b(0); |
There was a problem hiding this comment.
Fail startup instead of discarding the disk tier
When a production node cannot open the configured cache because of a permission, filesystem, direct-I/O, or io_uring error, this branch rewrites the disk capacity to zero and lets startup succeed. The node then runs until restart without the configured disk capacity, potentially sending a large volume of unexpected misses to object storage; a warning and gauge do not make the deployment valid. Return the initialization error instead of hiding it behind a memory-only cache, as required for production-path failures.
AGENTS.md reference: AGENTS.md:L19-L19
Useful? React with 👍 / 👎.
| return Ok(()); | ||
| } | ||
| let _in_flight_guards = object_storage_get_slice_in_flight_guards(buffer.len()); | ||
| default_get_slice_into(self, path, range, buffer).await?; |
There was a problem hiding this comment.
Retry Azure reads through the full response body
When an Azure download fails after its first chunk, this path calls default_get_slice_into, which consumes the reader after get_slice_stream's retry(...) closure has already returned (that closure only peeks the first chunk). Before this change, get_slice kept download_all inside the retry closure; with the default storage cache, every cache miss now uses this method, so a retryable later page/body error fails the split immediately. Keep the full body read under the Azure retry policy.
Useful? React with 👍 / 👎.
| // TODO remove the clone here. | ||
| .get(split.clone(), search_request.clone()) | ||
| .get_async(split.clone(), search_request.clone()) | ||
| .await |
There was a problem hiding this comment.
Run per-split disk cache lookups concurrently
When partial_request_cache has a disk tier and a leaf request contains many splits, each lookup here can perform a disk read, but the surrounding loop does not start the next lookup until this .await completes. This puts the sum of all disk-hit latencies on the request's critical path before any search tasks are scheduled, which is especially costly for requests assigned hundreds or thousands of splits. Drive these lookups concurrently with a sensible bound, then merge the results.
Useful? React with 👍 / 👎.
| range: Range<usize>, | ||
| buffer: &mut [u8], | ||
| ) -> StorageResult<()> { | ||
| crate::default_get_slice_into(self, path, range, buffer).await |
There was a problem hiding this comment.
Instrument GCS get_slice_into reads
When a storage-cache miss reads a GCS-backed split, Feuer calls this new get_slice_into path. It delegates directly to get_slice_stream, bypassing the in-flight guards, OBJECT_STORAGE_GET_TOTAL increment, and duration timer used by get_slice, so those production GCS search GETs disappear from the existing object-storage telemetry. Add equivalent instrumentation around the streamed read.
Useful? React with 👍 / 👎.
Description
Describe the proposed changes made in this PR.
How was this PR tested?
Describe how you tested this PR.