Skip to content

feat(storage): add split range disk cache - #6781

Open
congx4 wants to merge 5 commits into
quickwit-oss:mainfrom
congx4:congxie/finalDiskCache
Open

feat(storage): add split range disk cache#6781
congx4 wants to merge 5 commits into
quickwit-oss:mainfrom
congx4:congxie/finalDiskCache

Conversation

@congx4

@congx4 congx4 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a instance-level Foyer cache for split footer and body byte ranges including all kinds of data types(Fast fields, term dict, posting list, etc)
  • configure cache capacity, admission, recovery, compression, reclaiming, and write throttling
  • integrate the cache into searcher startup, shutdown, footer reads, and body reads
  • export cache request, admission, and Foyer device metrics
  • disable the long-lived fast-field RAM cache by default when Foyer is configured

Test plan

  • make fmt
  • cargo test -p quickwit-config --lib node_config::tests::test_split_range_disk_cache -- --nocapture
  • cargo test -p quickwit-storage --all-features --lib split_range_cache -- --nocapture
  • cargo test -p quickwit-search --all-features --lib split_range_cache_layer -- --nocapture
  • cargo clippy -p quickwit-config -p quickwit-storage -p quickwit-search --all-features --tests -- -D warnings

Made with Cursor

congx4 and others added 5 commits September 8, 2026 10:27
Based on quickwit-oss#6700. Pin Foyer and add the disabled-by-default cache configuration, including clean-block and write-throughput defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>
Based on quickwit-oss#6701. Encode cache keys portably while rejecting only I/O failures and non-UTF-8 URIs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Based on quickwit-oss#6702. Build and recover the process-wide cache, flush write-on-eviction entries on close, and apply configured reclaim and write throttling.

Co-authored-by: Cursor <cursoragent@cursor.com>
Based on quickwit-oss#6707. Add the storage decorator, manage cache lifecycle in searchers, and route footer and body range reads through Foyer without changing whole-split cache behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Based on quickwit-oss#6709. Export cache metrics, document production sizing, and disable the long-lived fast-field RAM cache by default when Foyer is configured.

Co-authored-by: Cursor <cursoragent@cursor.com>
@congx4
congx4 requested a review from a team as a code owner September 8, 2026 14:45
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-08T14:57:09.306309Z 053cb91 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 053cb917b3

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +61 to +62
let uri_len = read_u64(reader)? as usize;
let mut uri_bytes = vec![0; uri_len];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject oversized URI lengths before allocating

When recovering a truncated or corrupted cache image, the first eight key bytes can decode to an arbitrarily large u64; casting that value and immediately allocating vec![0; uri_len] can exhaust memory or abort the process instead of returning a Foyer decode error so recovery can reject the entry. Validate the conversion and bound the URI length against the encoded entry size before allocating.

AGENTS.md reference: AGENTS.md:L21-L22

Useful? React with 👍 / 👎.

Comment on lines +171 to +173
_buckets: Vec<f64>,
) -> BoxedHistogramVec {
self.register_histogram_vec(name, desc, label_names)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Foyer's explicit histogram buckets

When Foyer registers a histogram through register_histogram_vec_with_buckets, this implementation discards the supplied boundaries and creates an ordinary recorder histogram instead. The Prometheus exporter will consequently aggregate these observations using its default buckets rather than Foyer's requested buckets, making the resulting latency and operation distributions inaccurate; propagate the boundaries into the recorder configuration rather than ignoring them.

Useful? React with 👍 / 👎.

Comment thread config/quickwit.yaml
# compression: lz4
# recover_mode: quiet
# block_size: 64M
# max_entry_size: 60M

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a full float field will use 8byte per element

We target 10M docs, but we overshoot sometimes, so we may have 12M docs.
So the max size is closer to 100MB

what happens if the size is larger than max_entry_size?

| `partial_request_cache_capacity` | Partial request in memory cache capacity on a Searcher. Cache intermediate state for a request, possibly making subsequent requests faster. It can be disabled by setting the size to `0`. | `64M` |
| `max_num_concurrent_split_searches` | Maximum number of concurrent split search requests running on a Searcher. | `100` |
| `split_cache` | Searcher split cache configuration options defined in the section below. Cache disabled if unspecified. | |
| `split_range_disk_cache` | Process-wide on-disk cache for exact split footer and body byte ranges. Configuration options are defined in the section below. Cache disabled if unspecified. | |

@PSeitz PSeitz Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `split_range_disk_cache` | Process-wide on-disk cache for exact split footer and body byte ranges. Configuration options are defined in the section below. Cache disabled if unspecified. | |
| `split_range_disk_cache` | Process-wide on-disk cache for split byte ranges. Configuration options are defined in the section below. Cache disabled if unspecified. | |


### Searcher split range disk cache configuration

This section contains the configuration options for the process-wide on-disk cache of exact split footer and body ranges. The cache is disabled when this section is omitted or set to `null`. If it is set and `fast_field_cache_capacity` is omitted, the long-lived fast field RAM cache is disabled; set a capacity explicitly to keep both.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This section contains the configuration options for the process-wide on-disk cache of exact split footer and body ranges. The cache is disabled when this section is omitted or set to `null`. If it is set and `fast_field_cache_capacity` is omitted, the long-lived fast field RAM cache is disabled; set a capacity explicitly to keep both.
This section contains the configuration options for the process-wide on-disk cache of split byte ranges. The cache is disabled when this section is omitted or set to `null`. If it is set and `fast_field_cache_capacity` is omitted, the long-lived fast field RAM cache is disabled; set a capacity explicitly to keep both.

/// Disabled-by-default searcher disk cache for exact split byte ranges.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SplitRangeDiskCacheConfig {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid putting to many foyer specific parameters in the config. We can have defaults and override them via env parameters instead.

fn default() -> Self {
SearcherConfig {
fast_field_cache: CacheConfig::default_with_capacity(ByteSize::gb(1)),
fast_field_cache: None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you change the default here?

NonZeroU64::new(30).unwrap()
}

/// Long-lived `.fast` RAM cache after applying defaults.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why long-lived? do we have a short-lived RAM cache?

split_and_footer_offsets: &SplitIdAndFooterOffsets,
) -> anyhow::Result<(OwnedBytes, BundleStorage)> {
let split_file = PathBuf::from(format!("{}.split", split_and_footer_offsets.split_id));
let foyer_storage: Arc<dyn Storage> = match &searcher_context.split_range_disk_cache_opt {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong variable name, the None case is not foyer

) -> anyhow::Result<(OwnedBytes, BundleStorage)> {
let split_file = PathBuf::from(format!("{}.split", split_and_footer_offsets.split_id));
let foyer_storage: Arc<dyn Storage> = match &searcher_context.split_range_disk_cache_opt {
Some(cache) => wrap_storage_with_split_range_cache(cache.clone(), index_storage.clone()),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Some(cache) => wrap_storage_with_split_range_cache(cache.clone(), index_storage.clone()),
Some(cache) => wrap_storage_with_disk_cache(cache.clone(), index_storage.clone()),

pub predicate_cache: Arc<PredicateCacheImpl>,
/// Search split cache. `None` if no split cache is configured.
pub split_cache_opt: Option<Arc<SearchSplitCache>>,
/// Process-wide split range disk cache. `None` if not configured.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Process-wide split range disk cache. `None` if not configured.
/// Split range disk cache. `None` if not configured.

Process-wide implies it's different, but it's not

let storage_long_term_cache =
Arc::new(QuickwitCache::new(&searcher_config.fast_field_cache));
let fast_field_cache = searcher_config.resolved_fast_field_cache();
let storage_long_term_cache = if fast_field_cache.capacity().as_u64() == 0 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check should be in QuickwitCache::new

Some(config) => Some(Arc::new(
FoyerSplitRangeCache::open(config)
.await
.context("failed to open searcher split range disk cache")?,

@PSeitz PSeitz Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when will this happen? data corruption?

we probably want a way to safeguard against corruption or format changes, and clear the cache instead of rendering it unusable

/// Manual `Code` impl: keep Foyer's serde feature off so bincode is not pulled
/// in for keys or values. Foyer 0.22.3 already implements `Code` for `Bytes`,
/// so only this key needs a codec. Enabling serde would encode each value byte
/// as an integer; for a 15 MiB payload that is millions of serializer visits

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and that's a real performance problem?

};
pub use storage::{FoyerSplitRangeStorage, wrap_storage_with_split_range_cache};

/// Process-wide Foyer hybrid cache for exact split byte-range payloads.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Process-wide Foyer hybrid cache for exact split byte-range payloads.
/// Foyer hybrid cache for exact split byte-range payloads.

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.

2 participants