Skip to content

perf(cache)!: use fixed-size cache keys#7878

Open
Ali2Arslan wants to merge 5 commits into
lance-format:mainfrom
Ali2Arslan:perf/fixed-cache-keys
Open

perf(cache)!: use fixed-size cache keys#7878
Ali2Arslan wants to merge 5 commits into
lance-format:mainfrom
Ali2Arslan:perf/fixed-cache-keys

Conversation

@Ali2Arslan

@Ali2Arslan Ali2Arslan commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace heap-allocated, repeatedly hashed logical string keys with one opaque, canonical 16-byte InternalCacheKey.
  • Derive keys from stable type/schema identity plus typed fields using domain-separated BLAKE3, and migrate every in-tree cache-key producer to allocation-free typed encoding.
  • Keep CacheBackend object-safe while simplifying it around one physical key type; preserve codecs, accounting, clear, and Moka single-flight behavior.
  • Add deterministic cache/concurrency contracts, allocation guards, persistent-backend restart coverage, and paired Criterion benchmarks.

Closes #7832.

Stable key format

CACHE_KEY_FORMAT is blake3-128-v1:

  1. The root 32-byte namespace is generated with BLAKE3 derive_key using a fixed Lance context.
  2. Each with_key_prefix segment derives a new keyed namespace with explicit domain and length framing.
  3. Each entry hashes a stable type ID, author-defined schema ID/version, and tagged logical fields under that namespace.
  4. Field tags are defined by a #[repr(u8)] enum. Variable-width values are length-framed; integers are fixed-width little-endian; options, variants, sequences, fixed bytes, and variable bytes have distinct one-byte tags.
  5. The first 128 bits become the canonical backend key. InternalCacheKey::{as_bytes,into_bytes,from_bytes} are the persistence boundary.

Changing a key schema version intentionally produces a cold miss. Persistent backends should include CACHE_KEY_FORMAT in their physical namespace and allow entries from older formats to age out; there is no runtime legacy-key fallback.

The digest is a cache identity, not an authentication or authorization mechanism. Deterministic namespace keys are not secret. A 128-bit digest has approximately 64 bits of generic birthday-collision resistance and 128 bits of targeted preimage resistance. This change does not add a FIPS mode: #7832 selects BLAKE3 and Lance has no existing FIPS configuration surface.

Intentional backend API break

This removes APIs that require retaining logical strings or a secondary inventory:

  • CacheBackend::invalidate_prefix
  • backend key inventory / LanceCache::keys
  • readable key and prefix accessors
  • session cache-key inventory methods

Use clear for explicit invalidation, or derive/version a new namespace when a logical scope changes.

Custom backend migration:

  • Store/copy InternalCacheKey directly, or persist key.into_bytes() as exactly 16 bytes.
  • Reconstruct keys with InternalCacheKey::from_bytes when needed.
  • Route serialized values with CacheCodec::type_id() instead of inferring value type from a logical key string.
  • Replace with_backend_and_prefix with with_backend(...).with_key_prefix(...).
  • Remove prefix scans and key-string parsing. The CacheBackend trait remains non-generic and object-safe.

Existing out-of-tree CacheKey / UnsizedCacheKey implementations retain a source-compatible default bridge through key(). Performance-sensitive implementations should define a stable CacheKeySchema and override write_key; all current in-tree producers do so.

Correctness and persistence coverage

The proof suite covers:

  • official and golden BLAKE3 vectors, exact builder output, framing boundaries, endianness, type/schema/namespace separation, and options/variants/sequences;
  • default sized and unsized string bridges plus schema-version cold misses;
  • shared strong/weak cache state, expired weak handles, no-cache behavior, custom backends, Moka weights scaled safely above 4 GiB, and contextual type-collision misses/errors;
  • deterministic single-flight success, error, and owner-cancellation behavior with contenders explicitly parked before release/abort;
  • zero allocations for complete production-shaped typed page and optional-UUID keys after warm-up;
  • deletion-file cache identity across distinct storage bases;
  • a shared serializing backend that retains only bytes and opaque keys across restart and always decodes with the lookup codec;
  • BTree and IVF restart queries that prove serialized state/partitions are reused, assert vector recall, and perform zero index I/O once non-serializable readers are reconstructed, plus existing FTS/metadata/scalar integration coverage.

The removed unstable IVF cache_key_prefix protobuf payload is reserved by field number and name. Its codec version is unchanged because protobuf removal is wire-compatible and the new physical-key format already guarantees a cold miss.

Benchmarks

Three independent Criterion passes used release-with-debug, 100 samples, Rust 1.97.0, and an AMD Ryzen 9 3900X under x86_64 WSL2. Both paths include the backend's outer hash. Reported ranges compare median time for the fixed path against the benchmark-local implementation of the previous string-key path:

  • Long production-shaped key preparation: 2.8%–10.3% faster.
  • Short isolated key preparation: 139%–156% slower; this exposes BLAKE3's fixed setup cost instead of hiding it. The motivating long-prefix workload improves.
  • Strong warmed hits: 7.7%–16.6% faster.
  • Weak warmed hits: 1.4%–6.0% faster.
  • Bounded rotating inserts with prebuilt values: between 8.9% faster and 3.0% slower (effectively neutral; median pass was 1.3% faster).
  • Typed key preparation: 0 allocations after warm-up.

Namespace derivation is benchmarked separately so one-time scope setup is not folded into per-entry preparation.

Validation

Base: a3c6fce816befb7072505fbe05cc55cd205a171e

Passed after rebasing onto that base and again after review follow-ups:

  • cargo fmt --all -- --check
  • CARGO_INCREMENTAL=0 cargo check --workspace --tests --benches --locked
  • CARGO_INCREMENTAL=0 cargo clippy --all --tests --benches -- -D warnings
  • CARGO_INCREMENTAL=0 cargo test --workspace --locked
  • CARGO_INCREMENTAL=0 cargo +1.91.0 check --workspace --tests --benches --locked
  • CARGO_INCREMENTAL=0 cargo check --manifest-path python/Cargo.toml --locked
  • CARGO_INCREMENTAL=0 cargo check --manifest-path java/lance-jni/Cargo.toml --locked
  • targeted lance-core cache/allocation and Lance serializing-restart tests on Rust 1.91
  • error-path single-flight stress test repeated 1,000 times
  • three complete, symmetrically hashed paired benchmark passes

All three lockfiles contain only the intentional BLAKE3 dependency change (plus the upstream release-version updates already present in the base).

Interaction with open cache work

Made with Cursor

Ali2Arslan and others added 2 commits July 21, 2026 00:06
Replace logical string keys with domain-separated BLAKE3-128 identities to remove long-prefix hashing and simplify persistent backends.

Co-authored-by: Cursor <cursoragent@cursor.com>
Preserve the existing examples and section markers while adapting the cache implementation.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

Important

This PR touches the Lance format specification.

Substantive changes to the format specification — the .proto definitions
and the spec docs under docs/src/format/ — require a PMC vote before merge.
Minor edits such as typo fixes, wording, or formatting are excluded; use your
judgment.

If this is a meaningful format change:

  • Start a vote following the Lance community voting process.
    Format specification modifications need 3 binding +1 votes (excluding the
    proposer), held on GitHub Discussions, with a minimum voting period of 1 week.
  • Once the vote passes, link the completed vote in this PR. It should not be
    merged until the vote is linked.

@github-actions github-actions Bot added A-python Python bindings A-index Vector index, linalg, tokenizer A-java Java bindings + JNI A-deps Dependency updates A-encoding Encoding, IO, file reader/writer performance labels Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change replaces string-based cache keys with versioned typed BLAKE3-derived keys, updates cache backend and lifecycle APIs, migrates cache-key implementations across Lance components, removes IVF prefix persistence, and adds serializing-backend, allocation, and benchmark coverage.

Changes

Typed cache key migration

Layer / File(s) Summary
Key and cache foundation
Cargo.toml, rust/lance-core/src/cache/*
Adds canonical BLAKE3 key construction, typed schemas, namespaces, shared cache state, revised backend APIs, and updated Moka sizing.
Typed key implementations
rust/lance-encoding/..., rust/lance-file/..., rust/lance-index/..., rust/lance/src/...
Adds explicit schemas and structured serialization for dataset, metadata, scalar-index, inverted-index, vector-index, and session cache keys.
Persistence and cache API migration
rust/lance-index/protos-cache/*, rust/lance/src/index/vector/ivf/*, rust/lance/src/session*, rust/lance/src/utils/test/*, rust/lance/src/dataset/tests/*
Removes IVF cache prefixes and cache-key inventory APIs, adds SerializingCacheBackend, and updates restart and prewarm coverage.
Validation and benchmarks
rust/lance-core/benches/cache_keys.rs, rust/lance-core/tests/cache_key_allocations.rs, rust/lance-core/Cargo.toml
Adds Criterion benchmarks and verifies zero post-warmup allocations for typed key preparation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: xuanwo, westonpace, wjones127

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title succinctly matches the main change: switching cache keys to fixed-size keys.
Description check ✅ Passed The description is directly about the same cache-key redesign and related validation work.
Linked Issues check ✅ Passed The changes implement the requested 128-bit BLAKE3 cache keys, typed serialization, versioning, and removal of prefix/inventory APIs.
Out of Scope Changes check ✅ Passed The benchmarks, tests, and utility refactors all support the cache-key migration and no unrelated code changes stand out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

@wjones127
wjones127 self-requested a review July 21, 2026 15:14
.unwrap_or(u32::MAX)
entry_weight(key, entry.size_bytes)
})
.support_invalidation_closures()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no longer needed, and saves us some (accounting) space inside moka, but I can add it back if we need it for some reason

/// - **type_name**: distinguishes different value types stored under the same
/// user key (e.g. `"Vec<IndexMetadata>"`)
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct InternalCacheKey {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I opted for reworking InternalCacheKey directly instead of having 2 different formats due to the the amount of breaking API changes this PR was already introducing, and the tests assert the stability.
This does mean that all existing persistent backend will see new keys on deployment, but the contract is explicit that going forward format changes are expected and should lead to cache misses (with persistent backends responsible for cleaning up the stale keys - likely through TTLs).
If we really want to support both layouts and have the caller pick, I can add that back in (still requires breaking API changes) - at the cost of some wrapper structs that we'll need to maintain. IMHO only really worth it if we envision a future where we'll want to allow different cache key layouts.

Account for large entries without u32 truncation and include storage base in deletion keys. Strengthen migration, benchmark, and persistence proofs discovered during review.

Co-authored-by: Cursor <cursoragent@cursor.com>
//! protocol produces cold misses instead of aliases. Persistent or tiered
//! backends can route serializable values with [`CacheCodec::type_id`].
//!
//! Prefix invalidation and key inventory are intentionally not part of this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

can remove this paragraph, if we don't need to carry historical context

.try_into()
.unwrap_or(u32::MAX)
.max_capacity(capacity_weight)
.weigher(move |key: &InternalCacheKey, entry: &MokaCacheEntry| {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

bit of a drive-by here to not cap an entry weight to u32::MAX (which I know we've said before that entries should not get this large) but since we've seen this in the past with our indexes lookup files, I think this is probably worth it, just as defensive posturing for the future?

Represent stable field discriminants as a repr(u8) enum so their relationship and one-byte encoding are explicit without changing key bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Ali2Arslan
Ali2Arslan marked this pull request as ready for review July 21, 2026 17:29
@@ -0,0 +1,365 @@
// SPDX-License-Identifier: Apache-2.0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file can be removed before merging, if we want.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
rust/lance-index/src/scalar/bitmap.rs (1)

150-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document why write_key uses row_offset instead of value.

write_key intentionally diverges from key() here: key() hashes value, but write_key() only encodes row_offset. This is correct — row_offset is a bijective stand-in for value within a single loaded index (index_map: BTreeMap<Value, usize>) — and it's covered by test_bitmap_cache_key_uses_row_offset_identity. However, every other CacheKey/UnsizedCacheKey impl touched in this PR mirrors the exact fields used in key(), so this is the one outlier. A short comment would prevent a future refactor from "fixing" this back to hashing value (which would be more expensive and unnecessary) or breaking the row_offset-uniqueness invariant it relies on.

📝 Suggested comment
     fn schema() -> CacheKeySchema {
         CacheKeySchema::new("lance.scalar.bitmap-row-offset-key", 1)
     }

+    // `row_offset` is a bijective stand-in for `value` within a loaded index
+    // (each entry in `index_map` has a unique row_offset), so it alone is
+    // sufficient physical identity — cheaper to encode than the arbitrary
+    // `ScalarValue`. See `test_bitmap_cache_key_uses_row_offset_identity`.
     fn write_key(&self, builder: &mut KeyBuilder) {
         builder.write_u64(self.row_offset);
     }

As per coding guidelines, "Add doc comments to magic constants, thresholds, and non-obvious transformation functions, explaining what the value represents and why it was chosen."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-index/src/scalar/bitmap.rs` around lines 150 - 168, Add a concise
comment to BitmapKey::write_key explaining that row_offset is a bijective,
cheaper identity for value within a loaded index, so it preserves uniqueness
without encoding value directly. Keep the existing write_u64(self.row_offset)
behavior unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@rust/lance-index/src/scalar/bitmap.rs`:
- Around line 150-168: Add a concise comment to BitmapKey::write_key explaining
that row_offset is a bijective, cheaper identity for value within a loaded
index, so it preserves uniqueness without encoding value directly. Keep the
existing write_u64(self.row_offset) behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: f539033c-e139-4816-b83c-9a84c9ee9313

📥 Commits

Reviewing files that changed from the base of the PR and between a3c6fce and 2c66b2b.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • java/lance-jni/Cargo.lock is excluded by !**/*.lock
  • python/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • Cargo.toml
  • rust/lance-core/Cargo.toml
  • rust/lance-core/benches/cache_keys.rs
  • rust/lance-core/src/cache/backend.rs
  • rust/lance-core/src/cache/codec.rs
  • rust/lance-core/src/cache/key.rs
  • rust/lance-core/src/cache/mod.rs
  • rust/lance-core/src/cache/moka.rs
  • rust/lance-core/tests/cache_key_allocations.rs
  • rust/lance-encoding/src/encodings/logical/primitive.rs
  • rust/lance-file/src/previous/reader.rs
  • rust/lance-file/src/reader.rs
  • rust/lance-index/protos-cache/cache.proto
  • rust/lance-index/src/scalar/bitmap.rs
  • rust/lance-index/src/scalar/btree.rs
  • rust/lance-index/src/scalar/inverted/index.rs
  • rust/lance-index/src/scalar/label_list.rs
  • rust/lance-index/src/scalar/lance_format.rs
  • rust/lance-index/src/scalar/ngram.rs
  • rust/lance-index/src/scalar/registry.rs
  • rust/lance-index/src/scalar/rtree.rs
  • rust/lance/src/dataset/fragment.rs
  • rust/lance/src/dataset/tests/dataset_index.rs
  • rust/lance/src/index.rs
  • rust/lance/src/index/vector/ivf.rs
  • rust/lance/src/index/vector/ivf/partition_serde.rs
  • rust/lance/src/index/vector/ivf/v2.rs
  • rust/lance/src/session.rs
  • rust/lance/src/session/caches.rs
  • rust/lance/src/session/index_caches.rs
  • rust/lance/src/utils/test.rs
  • rust/lance/src/utils/test/serializing_cache.rs
💤 Files with no reviewable changes (1)
  • rust/lance/src/index/vector/ivf/partition_serde.rs

// type is generic over the quantizer; the proto envelope still provides
// additive evolution for the surrounding fields.
string quantizer_metadata_json = 7;
string cache_key_prefix = 8;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can revert this, and just have cache_key_prefix be used, instead of removing/deprecating at the proto level.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
rust/lance-encoding/src/encodings/logical/primitive.rs (2)

1744-1787: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Untrusted-metadata subtraction can underflow.

items_in_page - counted in analyze_value_counts and flat_value_counts_iter assumes the per-chunk log values decoded from the on-disk metadata word buffer always sum to at most items_in_page. If a corrupted or mismatched file violates that (only debug_assert!-checked, not enforced in release), this underflows: a panic in debug builds, or a silently wrapped huge u64 in release that then feeds PrefixSums/row-mapping construction and downstream chunk lookups.

Consider using checked_sub and surfacing a descriptive Error::invalid_input_source (propagated through build_chunk_index's callers, which already return Result) instead of relying on a debug-only assertion.

Based on learnings, watch for corrupted/misaligned decoded metadata driving unchecked arithmetic into incorrect or huge values; prefer a fallible path that maps to a proper input-validation error instead of proceeding with unchecked arithmetic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-encoding/src/encodings/logical/primitive.rs` around lines 1744 -
1787, Make analyze_value_counts and flat_value_counts_iter fallible when
subtracting counted values from items_in_page. Replace unchecked subtraction
with checked subtraction and return a descriptive Error::invalid_input_source
when decoded metadata exceeds the page count, rather than relying on
debug_assert! or wrapping. Propagate the error through build_chunk_index and its
existing Result-returning callers.

Source: Learnings


1793-1855: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

assert! on file-derived data can panic on malformed input.

assert!(rep_index_data.len() % 8 == 0) at line 1815 asserts on a length read from the file's repetition-index buffer. Per coding guidelines, library code must not use assert!/panic! for fallible conditions over external/untrusted input — a malformed or truncated file would crash the reader instead of surfacing a structured error, unlike the sibling checks elsewhere in this same file (e.g. chunk-size/log-value validation) that were converted from panics to Error::invalid_input_source.

🛡️ Suggested direction
-        assert!(rep_index_data.len() % 8 == 0);
+        if rep_index_data.len() % 8 != 0 {
+            return Err(Error::invalid_input_source(
+                format!(
+                    "Repetition index buffer length {} is not a multiple of 8 bytes",
+                    rep_index_data.len()
+                )
+                .into(),
+            ));
+        }

Note: this requires build_chunk_index (and its callers) to return Result, which is a larger but consistent-with-the-file change.

As per coding guidelines, "Never use .unwrap(), .expect(), panic!(), or assert!() in library code for fallible operations; use ? with Result and proper error types."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-encoding/src/encodings/logical/primitive.rs` around lines 1793 -
1855, Replace the file-derived assert in build_chunk_index with structured
invalid-input error handling: make build_chunk_index return
Result<MiniBlockChunkIndex, Error>, propagate the result through its callers
with ?, and return Error::invalid_input_source when rep_index_data is not 8-byte
aligned. Preserve successful chunk-index construction for valid repetition-index
data and update the final MiniBlockChunkIndex::new path accordingly.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance-encoding/src/encodings/logical/primitive.rs`:
- Around line 1744-1787: Make analyze_value_counts and flat_value_counts_iter
fallible when subtracting counted values from items_in_page. Replace unchecked
subtraction with checked subtraction and return a descriptive
Error::invalid_input_source when decoded metadata exceeds the page count, rather
than relying on debug_assert! or wrapping. Propagate the error through
build_chunk_index and its existing Result-returning callers.
- Around line 1793-1855: Replace the file-derived assert in build_chunk_index
with structured invalid-input error handling: make build_chunk_index return
Result<MiniBlockChunkIndex, Error>, propagate the result through its callers
with ?, and return Error::invalid_input_source when rep_index_data is not 8-byte
aligned. Preserve successful chunk-index construction for valid repetition-index
data and update the final MiniBlockChunkIndex::new path accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 502d411f-b9a4-49ae-bba1-cc2258f30e29

📥 Commits

Reviewing files that changed from the base of the PR and between 2c66b2b and 35559ac.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • java/lance-jni/Cargo.lock is excluded by !**/*.lock
  • python/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • Cargo.toml
  • rust/lance-encoding/src/encodings/logical/primitive.rs
  • rust/lance-file/src/reader.rs
  • rust/lance-index/src/scalar/inverted/index.rs
  • rust/lance-index/src/scalar/registry.rs
  • rust/lance/src/index.rs
  • rust/lance/src/index/vector/ivf/v2.rs

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

Labels

A-deps Dependency updates A-encoding Encoding, IO, file reader/writer A-index Vector index, linalg, tokenizer A-java Java bindings + JNI A-python Python bindings breaking-change performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fixed-size (u128) cache keys

1 participant