Skip to content

feat(kv-router): warn on duplicate store replays - #8473

Merged
PeaBrane merged 2 commits into
mainfrom
rupei/router-duplicate-store-warning
Apr 22, 2026
Merged

PeaBrane merged 2 commits into
mainfrom
rupei/router-duplicate-store-warning

Conversation

@PeaBrane

@PeaBrane PeaBrane commented Apr 21, 2026 •

Copy link
Copy Markdown
Contributor

Add a warning-only router metrics path for duplicate store replays without changing the hard-error event counters. Extend the router and scheduler test surfaces so clean streams assert zero warnings while duplicate no-op stores increment the new warning counter.


Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Added duplicate store event detection with new warning metrics for improved cache operation diagnostics and observability.
  • Tests

    • Enhanced test coverage to validate event warning metrics and ensure no unexpected duplicate store warnings occur during cache operations.

Signed-off-by: PeaBrane <yanrpei@gmail.com>
@PeaBrane
PeaBrane requested a review from a team April 21, 2026 21:57
@github-actions github-actions Bot added the feat label Apr 21, 2026
@PeaBrane
PeaBrane enabled auto-merge (squash) April 21, 2026 22:03
@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

The changes implement duplicate store detection and warning metrics throughout the KV router indexing system. A new EventWarningKind::DuplicateStore warning type with corresponding metrics infrastructure is introduced, and counters are threaded through event application across multiple indexer implementations to track duplicate insertions.

Changes

Cohort / File(s) Summary
Duplicate Store Detection in Radix Trees
lib/kv-router/src/indexer/concurrent_radix_tree.rs, lib/kv-router/src/indexer/concurrent_radix_tree_compressed.rs, lib/kv-router/src/indexer/radix_tree.rs, lib/kv-router/src/indexer/kv_indexer.rs
Threads optional PreBoundEventCounters through apply_event call chains and detects duplicate stores by tracking worker insertion outcomes. Introduces StoreInsertOutcome struct in compressed variant to return both block count and duplicate flag. Emits EventWarningKind::DuplicateStore warning when duplicates are detected and counters are provided.
Metrics and Warning Infrastructure
lib/kv-router/src/indexer/metrics.rs
Adds EventWarningKind enum with DuplicateStore variant and Display implementation. Extends KvIndexerMetrics with kv_cache_event_warnings counter and increment_event_warning() method. Updates PreBoundEventCounters with duplicate_store_warning counter and inc_warning() method for conditional warning emission.
Positional Indexer Duplicate Detection
lib/kv-router/src/indexer/positional.rs
Extends apply_event and store_blocks_impl signatures to accept optional counters. Implements duplicate detection during block storage using dashmap::mapref::entry::Entry branching, tracks num_blocks_added separately from original block count, and conditionally emits warnings through counters.inc_warning().
Test Infrastructure and Assertions
lib/kv-router/src/indexer/tests.rs, lib/mocker/src/scheduler/test_utils.rs, lib/mocker/src/scheduler/sglang/tests.rs, lib/mocker/src/scheduler/vllm/tests.rs
Refactors test utilities to expose warning metrics via new helper methods (warning_count(), warning_counts(), total_warning_count(), assert_no_event_warnings()) in RouterIndexerHarness. Adds two new duplicate-store-specific tests and integrates assert_no_event_warnings() assertions across existing test suites.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description explains the purpose (warning-only metrics for duplicate store replays) and scope (router and scheduler tests), but lacks detailed sections for overview, specific file locations, and related issues as specified in the template. Expand the description to include an 'Overview' section explaining the feature rationale, identify specific files for review in a 'Where should the reviewer start' section, and add a 'Related Issues' section with issue references if applicable.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(kv-router): warn on duplicate store replays' accurately and concisely describes the main change: adding warning metrics for duplicate store replays in the KV router.
Docstring Coverage ✅ Passed Docstring coverage is 96.36% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@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.

Actionable comments posted: 1

🧹 Nitpick comments (4)
lib/kv-router/src/indexer/tests.rs (1)

381-405: Redundant metrics re-binding; minor tidy.

You construct metrics on Line 382 and then immediately receive another metrics back from make_indexer_with_metrics on Line 383. Since the helper just returns the same Arc you passed in, the rebind is cosmetic. Either drop the second binding:

♻️ Tidy
-        let metrics = Arc::new(KvIndexerMetrics::new_unregistered());
-        let (index, metrics) = make_indexer_with_metrics(variant, metrics);
+        let metrics = Arc::new(KvIndexerMetrics::new_unregistered());
+        let (index, _) = make_indexer_with_metrics(variant, metrics.clone());

or, better, make the helper only take &str and return (Box<dyn KvIndexerInterface>, Arc<KvIndexerMetrics>) itself (constructing metrics internally). Same comment applies to Line 411-412.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/kv-router/src/indexer/tests.rs` around lines 381 - 405, The test re-binds
the metrics Arc unnecessarily: you create metrics with
KvIndexerMetrics::new_unregistered() then pass it to make_indexer_with_metrics
and reassign metrics from the tuple; remove the redundant re-binding by either
(a) keep your initially created `metrics` and destructure only the index from
`make_indexer_with_metrics` (use its returned index and ignore the returned
metrics), or (b) change `make_indexer_with_metrics` to not require an external
metrics Arc and instead construct and return it; apply the same tidy to the
other occurrences (e.g., the second test around lines 411–412) and update
references to `metrics` in tests like
test_duplicate_store_replay_warns_without_error accordingly.
lib/kv-router/src/indexer/metrics.rs (2)

216-225: Consider taking EventWarningKind instead of &'static str for type safety.

PreBoundEventCounters::inc_warning (Line 355) already uses the typed EventWarningKind enum, which gives compile-time exhaustiveness. KvIndexerMetrics::increment_event_warning diverges by taking a &'static str, which lets any ad-hoc label leak in and create unintended time series.

♻️ Optional: stronger typing
-    pub fn increment_event_warning(&self, warning_kind: &'static str) {
+    pub fn increment_event_warning(&self, warning_kind: EventWarningKind) {
         #[cfg(feature = "metrics")]
         {
+            let label: &'static str = match warning_kind {
+                EventWarningKind::DuplicateStore => METRIC_WARNING_DUPLICATE_STORE,
+            };
             self.kv_cache_event_warnings
-                .with_label_values(&[warning_kind])
+                .with_label_values(&[label])
                 .inc_by(1);
         }
         #[cfg(not(feature = "metrics"))]
         let _ = (self, warning_kind);
     }

(Test on Line 2258 of tests.rs and any other callers would need to switch to EventWarningKind::DuplicateStore.)

This also mirrors the existing EventKind/inc pattern. I recognize that keeping parity with increment_event_applied(&'static str, …) was likely the motivation, but EventKind is also used throughout the inc/prebind fast path, so the typed variant would be more consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/kv-router/src/indexer/metrics.rs` around lines 216 - 225, Change
KvIndexerMetrics::increment_event_warning to accept the typed EventWarningKind
instead of &'static str: update the function signature and use the enum to
produce the label value (e.g., via a to_string()/as_str() or impl Display on
EventWarningKind) when calling
self.kv_cache_event_warnings.with_label_values(&[...]). Also update all callers
(including tests referenced) to pass EventWarningKind variants (e.g.,
EventWarningKind::DuplicateStore) and ensure parity with
PreBoundEventCounters::inc_warning and the EventKind/inc pattern so no ad-hoc
strings can create unintended time series.

124-146: Partial-failure logging hides the second error.

In from_component, when both create_intcountervec calls fail, the (Err(e), _) | (_, Err(e)) pattern only binds the first error, so you only log one of two registration failures. Minor, but operators may want both contexts.

♻️ Log both on double-failure
-                    ) {
-                        (Ok(kv_cache_events_applied), Ok(kv_cache_event_warnings)) => Arc::new(
-                            Self::new(kv_cache_events_applied, kv_cache_event_warnings),
-                        ),
-                        (Err(e), _) | (_, Err(e)) => {
-                            tracing::warn!("Failed to create kv indexer metrics from component: {}. Using unregistered metrics as fallback.", e);
-                            Arc::new(Self::new_unregistered())
-                        }
-                    }
+                    ) {
+                        (Ok(applied), Ok(warnings)) => {
+                            Arc::new(Self::new(applied, warnings))
+                        }
+                        (applied_res, warnings_res) => {
+                            if let Err(e) = &applied_res {
+                                tracing::warn!("Failed to register kv_cache_events_applied: {e}");
+                            }
+                            if let Err(e) = &warnings_res {
+                                tracing::warn!("Failed to register kv_cache_event_warnings: {e}");
+                            }
+                            tracing::warn!("Using unregistered kv indexer metrics as fallback.");
+                            Arc::new(Self::new_unregistered())
+                        }
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/kv-router/src/indexer/metrics.rs` around lines 124 - 146, The match in
from_component that calls component.metrics().create_intcountervec for
KV_CACHE_EVENTS_APPLIED_SUFFIX and KV_CACHE_EVENT_WARNINGS_SUFFIX currently uses
a combined arm (Err(e), _) | (_, Err(e)) which only binds one error and hides
the other; change the matching to capture both Results (e.g., match (r1, r2) or
pattern arms (Err(e1), Err(e2)), (Err(e1), Ok(_)), (Ok(_), Err(e2))) so you can
construct and log both error contexts when both fail (referencing
create_intcountervec, KV_CACHE_EVENTS_APPLIED_SUFFIX,
KV_CACHE_EVENT_WARNINGS_SUFFIX) and fall back to
Arc::new(Self::new_unregistered()) as before.
lib/kv-router/src/indexer/positional.rs (1)

272-315: Duplicate-store detection logic looks correct.

Tracing through the flow:

  • duplicate_store starts true only when !blocks.is_empty(), and is flipped to false the moment any block path indicates "new insertion" (vacant index entry, new worker in SeqEntry::insert, or a fresh worker_blocks_entry mapping).
  • num_blocks_added is only incremented on genuinely new entries in worker_blocks_entry (the None arm), so tree-size accounting and the warning flag stay mutually consistent: if num_blocks_added > 0 then duplicate_store == false.
  • The SeqEntry::insert bool return threaded from Line 60 correctly distinguishes "worker newly associated with (seq_hash)" vs "worker already present", which is exactly what duplicate detection needs.

One subtle case worth calling out: when Some(existing) with existing != (position, local_hash) is hit (Line 295), this implies the same seq_hash was previously mapped to a different (position, local_hash) for this worker — that normally shouldn't happen in a well-formed event stream, and silently clearing duplicate_store without any warn log may hide a real inconsistency. Consider at least a tracing::warn! for that branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/kv-router/src/indexer/positional.rs` around lines 272 - 315, The branch
handling match worker_blocks_entry.insert(seq_hash, (position, local_hash))
where Some(existing) != (position, local_hash) should emit a warning instead of
silently clearing duplicate_store; update that arm to call tracing::warn!
(include worker, seq_hash, existing, and the new (position, local_hash) values
for context) and then leave the existing duplicate_store=false behavior intact
so SeqEntry::insert, worker_blocks_entry, and duplicate_store logic remain
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/kv-router/src/indexer/kv_indexer.rs`:
- Line 52: The routing-decision path currently calls trie.apply_event(event) in
process_routing_decision_with_hashes which bypasses the warning counters; change
that call to use the same counters plumbing as the normal path by calling
trie.apply_event_with_counters(event, Some(counters)) so the duplicate-store
warning metrics are incremented consistently, ensuring both
trie.apply_event_with_counters (used at the earlier router event path) and the
process_routing_decision_with_hashes path use the same counters instance without
altering the existing counters.inc(...) behavior.

---

Nitpick comments:
In `@lib/kv-router/src/indexer/metrics.rs`:
- Around line 216-225: Change KvIndexerMetrics::increment_event_warning to
accept the typed EventWarningKind instead of &'static str: update the function
signature and use the enum to produce the label value (e.g., via a
to_string()/as_str() or impl Display on EventWarningKind) when calling
self.kv_cache_event_warnings.with_label_values(&[...]). Also update all callers
(including tests referenced) to pass EventWarningKind variants (e.g.,
EventWarningKind::DuplicateStore) and ensure parity with
PreBoundEventCounters::inc_warning and the EventKind/inc pattern so no ad-hoc
strings can create unintended time series.
- Around line 124-146: The match in from_component that calls
component.metrics().create_intcountervec for KV_CACHE_EVENTS_APPLIED_SUFFIX and
KV_CACHE_EVENT_WARNINGS_SUFFIX currently uses a combined arm (Err(e), _) | (_,
Err(e)) which only binds one error and hides the other; change the matching to
capture both Results (e.g., match (r1, r2) or pattern arms (Err(e1), Err(e2)),
(Err(e1), Ok(_)), (Ok(_), Err(e2))) so you can construct and log both error
contexts when both fail (referencing create_intcountervec,
KV_CACHE_EVENTS_APPLIED_SUFFIX, KV_CACHE_EVENT_WARNINGS_SUFFIX) and fall back to
Arc::new(Self::new_unregistered()) as before.

In `@lib/kv-router/src/indexer/positional.rs`:
- Around line 272-315: The branch handling match
worker_blocks_entry.insert(seq_hash, (position, local_hash)) where
Some(existing) != (position, local_hash) should emit a warning instead of
silently clearing duplicate_store; update that arm to call tracing::warn!
(include worker, seq_hash, existing, and the new (position, local_hash) values
for context) and then leave the existing duplicate_store=false behavior intact
so SeqEntry::insert, worker_blocks_entry, and duplicate_store logic remain
consistent.

In `@lib/kv-router/src/indexer/tests.rs`:
- Around line 381-405: The test re-binds the metrics Arc unnecessarily: you
create metrics with KvIndexerMetrics::new_unregistered() then pass it to
make_indexer_with_metrics and reassign metrics from the tuple; remove the
redundant re-binding by either (a) keep your initially created `metrics` and
destructure only the index from `make_indexer_with_metrics` (use its returned
index and ignore the returned metrics), or (b) change
`make_indexer_with_metrics` to not require an external metrics Arc and instead
construct and return it; apply the same tidy to the other occurrences (e.g., the
second test around lines 411–412) and update references to `metrics` in tests
like test_duplicate_store_replay_warns_without_error accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4310a3ff-7f86-477a-86a6-c73d84bef5cc

📥 Commits

Reviewing files that changed from the base of the PR and between 55a949c and b4440cf.

📒 Files selected for processing (10)
  • lib/kv-router/src/indexer/concurrent_radix_tree.rs
  • lib/kv-router/src/indexer/concurrent_radix_tree_compressed.rs
  • lib/kv-router/src/indexer/kv_indexer.rs
  • lib/kv-router/src/indexer/metrics.rs
  • lib/kv-router/src/indexer/positional.rs
  • lib/kv-router/src/indexer/radix_tree.rs
  • lib/kv-router/src/indexer/tests.rs
  • lib/mocker/src/scheduler/sglang/tests.rs
  • lib/mocker/src/scheduler/test_utils.rs
  • lib/mocker/src/scheduler/vllm/tests.rs

Comment thread lib/kv-router/src/indexer/kv_indexer.rs Resolved
@PeaBrane
PeaBrane merged commit 4833e29 into main Apr 22, 2026
140 of 142 checks passed
@PeaBrane
PeaBrane deleted the rupei/router-duplicate-store-warning branch April 22, 2026 01:40

This branch was previously deployed

1 inactive deployment
GITLAB — 4302eaca Deployed Apr 22, 2026 by copy-pr-bot[bot] via Trigger CI Pipeline #24646
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants