Skip to content

feat(mesh): wire TreeSyncAdapter so cache-aware state syncs across nodes - #2119

Merged
slin1237 merged 4 commits into
smg-project:mainfrom
purp1e-ace:fix/1578-wire-tree-sync
Aug 13, 2026
Merged

slin1237 merged 4 commits into
smg-project:mainfrom
purp1e-ace:fix/1578-wire-tree-sync

Conversation

@purp1e-ace

Copy link
Copy Markdown
Contributor

Description

Problem

Cache-aware policy tree state does not synchronize across mesh nodes even
when --enable-mesh is on. Router B starts with an empty routing tree after
Router A fails, losing all cache affinity. path_hash_index metrics return
-1; observed routing consistency after failover is -100% across all seven
warm-up strategies tested in #1578.

Root cause: the pieces are all present — TreeSyncAdapter implementation,
CacheAwarePolicy::apply_known_remote_insert / apply_repair_page /
populate_hash_index flag, hash-index generators — but the composition root
MeshAdapters::start (model_gateway/src/mesh/wiring.rs) only wires the
worker: and rl: CRDT namespaces. The td: / tree:req: / tree:page:
stream namespaces are never registered, no TreeSyncAdapter is ever
constructed, no producer-side hook fires, and populate_hash_index stays
false at its default.

Closes #1578.

Solution

Extend MeshAdapters::start to register the three tree-sync stream
namespaces, construct a TreeSyncAdapter, and attach it to the running
PolicyRegistry. The registry propagates the adapter (and flips
populate_hash_index on) to every existing and future CacheAwarePolicy,
matching the pattern already used for KvEventMonitor / LoadReceiver.

Producer side: CacheAwarePolicy::select_worker_with_tokens and
select_worker_with_text publish a TreeDelta right after every
hash_index write, using the same hash_token_path(tokens) /
hash_node_path(text) keys — so peers that request repair land on the same
tree node.

Consumer side: PolicyRegistryTreeHandle implements TreeHandle and
dispatches per-model_id to whichever CacheAwarePolicy the registry has
for that model. ClusterStatePeerList implements PeerList by reading
ClusterState and filtering to alive non-self peers.

Changes

  • model_gateway/src/mesh/wiring.rs — MeshAdapters::start grows two
    parameters (ClusterState, Arc<PolicyRegistry>); registers td:
    (broadcast), tree:req: (targeted), tree:page: (targeted) stream
    namespaces; constructs TreeSyncAdapter with PolicyRegistryTreeHandle +
    ClusterStatePeerList; starts it before flipping set_mesh_tree_sync on
    the registry (order matters — see comment).
  • model_gateway/src/policies/cache_aware.rs — adds mesh_tree_sync
    field and set_mesh_tree_sync setter that atomically attaches the
    adapter AND flips populate_hash_index (single call, one write lock,
    one atomic store) so the paired invariant cannot drift; adds
    sync_local_insert helper (clones the adapter Arc out of the read
    guard before invoking on_local_insert, so a future adapter path can
    never deadlock against the policy read lock); wires the producer hook
    into both select_worker_* sites right after the hash_index write.
  • model_gateway/src/policies/registry.rs — adds mesh_tree_sync field,
    set_mesh_tree_sync (propagates to default + PD + all model policies),
    maybe_inject_mesh_tree_sync helper, and injection in
    create_policy_from_type so lazily-created per-model policies inherit
    the adapter. The registry uses the single atomic setter throughout;
    the standalone set_populate_hash_index on CacheAwarePolicy is
    gated #[cfg(test)] because the pair now moves together in
    production.
  • model_gateway/src/mesh/adapters/tree_sync.rs — makes the three prefix
    constants pub(crate) so wiring can reference them; adds a
    #[cfg(test)] pub(crate) fn pending_delta_count_for_test(...) view
    onto the outbound buffer so wiring integration tests can assert the
    producer hook fired without waiting a gossip tick for drain.
  • model_gateway/src/server.rs — passes handler.state and
    app_context.policy_registry to MeshAdapters::start.

The wiring flips populate_hash_index and the outbound adapter together —
individually flipping only one would either OOM the gateway (index writes
with no readers, mesh off) or publish deltas the local node cannot resolve
on repair (adapter attached but index empty, mesh on).

Test Plan

Unit tests, added to model_gateway/src/mesh/wiring.rs:

  • start_wires_worker_inbound_end_to_end — existing test, extended with
    the new MeshAdapters::start signature; confirms worker: CRDT inbound
    loop still runs.
  • rl_namespace_uses_epoch_max_wins — existing, extended.
  • tree_adapter_registers_and_flips_populate_flag — new. Constructs
    MeshAdapters with a PolicyRegistry defaulted to cache_aware, then
    fetches a per-model policy via get_policy_or_default. Asserts that
    populate_hash_index reads true on the fetched policy — proves the
    adapter is attached and the flag propagates to lazily-created policies.
  • start_propagates_to_preexisting_model_policies — new. Creates a
    per-model cache-aware policy BEFORE wiring runs (so it lives in
    model_policies at attach time), then starts wiring and asserts the
    populate flag flipped on the same live policy Arc. Regression guard
    for the propagation path that walks model_policies in
    set_mesh_tree_sync.
  • detach_clears_populate_flag — new. After attach, calls
    set_mesh_tree_sync(None) and asserts the populate flag goes back
    off. Guards the paired invariant that the atomic setter undoes both
    flips together.
  • producer_hook_publishes_delta_on_select_worker — new. End-to-end
    regression guard for [Bug]: Mesh Cache-Aware Routing Tree Sync Mechanism Is Not Wired — Router B Never Receives Router A's Routing Tree #1578: builds a full MeshAdapters, drives one
    string request and one token request through
    CacheAwarePolicy::select_worker, and asserts the outbound
    pending_deltas buffer on the TreeSyncAdapter grew for each. Deleting
    either sync_local_insert call site fails this test.
  • peer_list_reports_alive_only — new. Populates a ClusterState with
    Alive + Alive + Down entries and asserts alive_peers() excludes self
    and the Down node.
  • start_panics_on_second_call — existing, extended.
  • start_panics_on_colon_node_name — existing, extended.

Manual reproduction of the #1578 scenario across two mesh nodes is NOT part
of this PR's test evidence — I don't have the multi-node cluster to run
that on. If the pre-merge reviewer wants that, I can coordinate a
follow-up.

Full run:

cargo test -p smg --lib
# test result: ok. 1460 passed; 0 failed; 5 ignored
cargo test -p smg --lib mesh::wiring
# test result: ok. 9 passed; 0 failed
cargo +nightly fmt --all -- --check
# clean
cargo clippy -p smg --lib --tests -- -D warnings
# clean on this diff (see note below on monitor.rs:744)

Note on monitor.rs:744 — a pre-existing clippy::unneeded_wildcard_pattern
lint appears on upstream main at c2cf59c7. I confirmed this by stashing
my changes and re-running clippy; the error persists. Out of scope for this
PR per the "one concern per PR" rule.

Checklist
  • cargo +nightly fmt passes
  • cargo clippy -p smg --lib --tests -- -D warnings passes on this
    diff (a pre-existing unneeded_wildcard_pattern on monitor.rs:744
    remains — see note in Test Plan; workspace --all-features skipped
    because system OpenCV is not installed locally, per CONTRIBUTING.md
    fallback)
  • (Optional) Documentation updated
  • (Optional) Please join us on Slack [#sig-smg](https://slack.lightseek.org) to discuss, review, and merge PRs

…te syncs across nodes

Extend MeshAdapters::start to register the td:/tree:req:/tree:page:
stream namespaces, construct a TreeSyncAdapter, and attach it to the
running PolicyRegistry. The registry propagates the adapter (and the
paired populate_hash_index flag) to every existing and future
CacheAwarePolicy, matching the pattern already used for
KvEventMonitor / LoadReceiver.

CacheAwarePolicy::select_worker_with_tokens and select_worker_with_text
publish a TreeDelta immediately after every hash_index write, keyed by
the same hash_token_path(tokens) / hash_node_path(text) — so peers that
repair against us land on the same tree node.

CacheAwarePolicy::set_mesh_tree_sync is a single atomic setter that
attaches the adapter AND flips populate_hash_index; the two fields
cannot drift apart. PolicyRegistryTreeHandle dispatches per-model_id
to whichever CacheAwarePolicy the registry has for that model, logging
at debug when this node is not authoritative for a model so operators
can distinguish legitimate empty results from repair storms.
ClusterStatePeerList reads ClusterState and filters to alive non-self
peers for TreeSyncAdapter's repair fan-out.

Closes smg-project#1578.

Signed-off-by: purp1e-ace <45795655+purp1e-ace@users.noreply.github.com>
@github-actions github-actions Bot added the model-gateway Model gateway crate changes label Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3e056242-4ff2-48a1-a46d-ec4e04ae3683

📥 Commits

Reviewing files that changed from the base of the PR and between caf4fb2 and 69ce5c5.

📒 Files selected for processing (5)
  • model_gateway/src/mesh/adapters/tree_sync.rs
  • model_gateway/src/mesh/wiring.rs
  • model_gateway/src/policies/cache_aware.rs
  • model_gateway/src/policies/registry.rs
  • model_gateway/src/server.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added mesh-based tree synchronization for cache-aware policies.
    • Automatically synchronizes policy state across available cluster peers.
    • Publishes routing updates to keep distributed policy caches current.
    • Supports attaching or detaching synchronization at runtime.
    • Added targeted repair requests and paging for recovering missed updates.
  • Bug Fixes
    • Improved handling of policy registration, removal, and peer availability.
    • Improved synchronization of hash-index population settings across connected policies.
    • Added bounded buffering to improve synchronization and repair reliability during startup.

Walkthrough

The change wires TreeSyncAdapter into mesh startup and policy management. CacheAwarePolicy publishes token and string routing deltas. PolicyRegistry propagates the adapter to cache-aware policies.

Changes

Mesh tree synchronization

Layer / File(s) Summary
Policy delta publication
model_gateway/src/policies/cache_aware.rs
CacheAwarePolicy attaches an optional TreeSyncAdapter, controls hash-index population, and publishes token and string routing deltas.
Policy registry propagation
model_gateway/src/policies/registry.rs
PolicyRegistry stores the adapter, propagates it to existing policies, injects it into new cache-aware policies, and exposes deduplicated policies for a model.
Mesh startup and tree handles
model_gateway/src/mesh/adapters/tree_sync.rs, model_gateway/src/mesh/wiring.rs, model_gateway/src/server.rs
Mesh startup configures tenant-delta and repair streams, starts the adapter, connects policy and peer handles, updates the startup API, and adds synchronization tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Server
  participant MeshAdapters
  participant TreeSyncAdapter
  participant PolicyRegistry
  participant CacheAwarePolicy
  Server->>MeshAdapters: start with ClusterState and PolicyRegistry
  MeshAdapters->>TreeSyncAdapter: configure streams and start
  MeshAdapters->>PolicyRegistry: attach TreeSyncAdapter
  PolicyRegistry->>CacheAwarePolicy: set mesh tree synchronization
  CacheAwarePolicy->>TreeSyncAdapter: publish TreeDelta
Loading

Possibly related PRs

Suggested labels: mesh, tests

Suggested reviewers: slin1237, catherinesue

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: wiring TreeSyncAdapter to synchronize cache-aware state across mesh nodes.
Description check ✅ Passed The description directly explains the synchronization bug, implementation, affected components, tests, and validation results.
Linked Issues check ✅ Passed The PR addresses issue [#1578] by wiring tree streams, publishing deltas, propagating synchronization to policies, and filtering alive peers.
Out of Scope Changes check ✅ Passed The changes remain focused on TreeSyncAdapter wiring, cache-aware policy integration, registry propagation, startup updates, and related tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
model_gateway/src/mesh/wiring.rs (3)

246-275: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🟡 Nit: the TreeHandle bridge has no direct test.

apply_known_remote_insert, open_repair_stream, and apply_repair_page are new production code. The added tests cover registration, propagation, detach, peer filtering, and the outbound producer hook, but no test drives an inbound path through PolicyRegistryTreeHandle.

A test that registers a cache-aware policy, seeds the hash index, and calls apply_known_remote_insert through the handle would pin the fallback contract (false / None / 0) and would catch the resolution gap described in the comment on Lines 212-236.

As per coding guidelines: "Run the pr-test-analyzer agent to verify that tests adequately cover new or changed functionality."

🤖 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 `@model_gateway/src/mesh/wiring.rs` around lines 246 - 275, Add a direct test
for the PolicyRegistryTreeHandle bridge that registers a cache-aware policy,
seeds the hash index, and invokes apply_known_remote_insert through the
TreeHandle interface. Assert the delegated result and verify the fallback values
false, None, and 0 for unresolved models across apply_known_remote_insert,
open_repair_stream, and apply_repair_page, covering the resolution behavior in
with_cache_aware.

Source: Coding guidelines


365-374: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🟡 Nit: this policy config duplicates the cache_aware_policy_config helper.

The helper at Lines 455-466 defines the identical PolicyConfig::CacheAware literal. Reuse it here so a threshold change updates one place.

♻️ Proposed fix
-        let policy_registry = Arc::new(PolicyRegistry::new(PolicyConfig::CacheAware {
-            cache_threshold: 0.5,
-            balance_abs_threshold: 32,
-            balance_rel_threshold: 1.5,
-            eviction_interval_secs: 60,
-            max_tree_size: 128,
-            block_size: 16,
-            balance_token_usage_threshold: 1.0,
-            overload_token_usage_threshold: 1.0,
-        }));
+        let policy_registry = Arc::new(PolicyRegistry::new(cache_aware_policy_config()));
🤖 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 `@model_gateway/src/mesh/wiring.rs` around lines 365 - 374, Replace the
duplicated PolicyConfig::CacheAware literal in the policy_registry
initialization with the existing cache_aware_policy_config helper, preserving
the current PolicyRegistry construction while centralizing these threshold
values.

29-44: 🚀 Performance & Scalability | 🔵 Trivial

🟡 Nit: the three stream buffers are a fixed, unconfigurable memory commitment.

TD_BUFFER_BYTES (4 MiB), REPAIR_REQ_BUFFER_BYTES (256 KiB), and REPAIR_PAGE_BUFFER_BYTES (32 MiB) total roughly 36 MiB per node, reserved whenever mesh is enabled. The doc comments explain the sizing intent well, but an operator running many small gateway replicas cannot lower it, and an operator with large trees cannot raise it.

Consider exposing these through the mesh server config, and emit a gauge for buffer occupancy so FIFO eviction on the td: namespace (which silently degrades to the repair path) is observable.

🤖 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 `@model_gateway/src/mesh/wiring.rs` around lines 29 - 44, Make TD_BUFFER_BYTES,
REPAIR_REQ_BUFFER_BYTES, and REPAIR_PAGE_BUFFER_BYTES configurable through the
mesh server configuration instead of fixed constants, preserving their current
values as defaults. Use the configured values when initializing the
corresponding td:, tree:req:, and tree:page: buffers, and add an occupancy gauge
for each stream so FIFO eviction and buffer usage are observable.
model_gateway/src/policies/cache_aware.rs (1)

279-286: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🟡 Nit: the adapter store and the populate flag are not updated atomically.

The doc comment states both fields flip "in one atomic step". The write lock is released at Line 284 before the store at Line 285. Two concurrent callers (attach and detach) can interleave and leave mesh_tree_sync == None with populate_hash_index == true. The hash index would then grow with no reader, which is the exact failure the flag prevents.

Today only mesh startup calls this, so the window is not reachable. Either hold the write guard across the store, or soften the doc comment to state the caller must serialize.

♻️ Proposed fix
     pub fn set_mesh_tree_sync(&self, adapter: Option<Arc<TreeSyncAdapter>>) {
         let populate = adapter.is_some();
-        {
-            let mut guard = self.mesh_tree_sync.write();
-            *guard = adapter;
-        }
-        self.populate_hash_index.store(populate, Ordering::Relaxed);
+        let mut guard = self.mesh_tree_sync.write();
+        *guard = adapter;
+        // Store under the guard so no observer sees the pair split.
+        self.populate_hash_index.store(populate, Ordering::Relaxed);
     }
🤖 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 `@model_gateway/src/policies/cache_aware.rs` around lines 279 - 286, Update
set_mesh_tree_sync so the mesh_tree_sync write guard remains held while
populate_hash_index is updated, making both fields change within the same
synchronization window. Preserve the existing adapter presence logic and atomic
flag ordering.
🤖 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.

Inline comments:
In `@model_gateway/src/mesh/wiring.rs`:
- Around line 212-236: Update with_cache_aware and inbound tree-delta handling
to resolve each delta through the policy owning its dispatch leg, including
PD/EPD prefill_policy, decode_policy, encode_policy, and the default policy when
no model entry exists, rather than consulting only model_policies. Normalize
model IDs before on_worker_added stores them so empty IDs become
UNKNOWN_MODEL_ID and match producer-published deltas.

In `@model_gateway/src/policies/cache_aware.rs`:
- Around line 1116-1124: Replace the hardcoded epoch: 0 values in both
sync_local_insert producer sites with a monotonic per-policy epoch counter,
ensuring each emitted TreeDelta receives the correct intra-batch ordering value
for both token and string paths. Update the surrounding policy state and call
sites as needed so the counter is shared and incremented consistently.

---

Nitpick comments:
In `@model_gateway/src/mesh/wiring.rs`:
- Around line 246-275: Add a direct test for the PolicyRegistryTreeHandle bridge
that registers a cache-aware policy, seeds the hash index, and invokes
apply_known_remote_insert through the TreeHandle interface. Assert the delegated
result and verify the fallback values false, None, and 0 for unresolved models
across apply_known_remote_insert, open_repair_stream, and apply_repair_page,
covering the resolution behavior in with_cache_aware.
- Around line 365-374: Replace the duplicated PolicyConfig::CacheAware literal
in the policy_registry initialization with the existing
cache_aware_policy_config helper, preserving the current PolicyRegistry
construction while centralizing these threshold values.
- Around line 29-44: Make TD_BUFFER_BYTES, REPAIR_REQ_BUFFER_BYTES, and
REPAIR_PAGE_BUFFER_BYTES configurable through the mesh server configuration
instead of fixed constants, preserving their current values as defaults. Use the
configured values when initializing the corresponding td:, tree:req:, and
tree:page: buffers, and add an occupancy gauge for each stream so FIFO eviction
and buffer usage are observable.

In `@model_gateway/src/policies/cache_aware.rs`:
- Around line 279-286: Update set_mesh_tree_sync so the mesh_tree_sync write
guard remains held while populate_hash_index is updated, making both fields
change within the same synchronization window. Preserve the existing adapter
presence logic and atomic flag ordering.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5816f8d2-6c92-40ad-b401-d7e04876fe20

📥 Commits

Reviewing files that changed from the base of the PR and between caf4fb2 and dfa35aa.

📒 Files selected for processing (5)
  • model_gateway/src/mesh/adapters/tree_sync.rs
  • model_gateway/src/mesh/wiring.rs
  • model_gateway/src/policies/cache_aware.rs
  • model_gateway/src/policies/registry.rs
  • model_gateway/src/server.rs

Comment thread model_gateway/src/mesh/wiring.rs Outdated
Comment thread model_gateway/src/policies/cache_aware.rs
Broaden inbound resolution scope. `PolicyRegistryTreeHandle` now walks
every distinct cache-aware policy the registry could dispatch requests
through for a model (per-model → default → PD/EPD legs), deduplicated
by Arc identity. Previously the bridge consulted only `model_policies`,
so PD/EPD deployments and non-PD deployments that resolve `model_id`
through the default policy never saw inbound deltas.

Tighten the atomic pairing on `CacheAwarePolicy::set_mesh_tree_sync`.
The `populate_hash_index` store now happens under the same
`mesh_tree_sync` write guard as the adapter swap, so no observer can
see the pair split.

Clarify `TreeDelta.epoch` as a reserved slot. The receiver does not
consult it — only `trace!`/`debug!` log lines read it — so hardcoding
`0` at both producer sites is documented rather than a latent
ordering bug. If a consumer ever wants intra-batch ordering, updating
the doc + adding a counter is one change.

Test additions in `mesh::wiring::tests`:

- `bridge_returns_fallbacks_when_no_cache_aware_policy` — asserts
  `false` / `None` / `0` fallbacks when the chain has no cache-aware
  policy (non-cache-aware default, no per-model entries, no PD legs).
- `bridge_dispatches_through_default_policy` — asserts the bridge
  reaches the default policy for models without a per-model entry,
  which is the single-model non-PD deployment shape.

Also folded a duplicated `PolicyConfig::CacheAware` literal into the
existing `cache_aware_policy_config` helper.

Signed-off-by: purp1e-ace <45795655+purp1e-ace@users.noreply.github.com>
@slin1237
slin1237 merged commit 473ad56 into smg-project:main Aug 13, 2026
47 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

model-gateway Model gateway crate changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Mesh Cache-Aware Routing Tree Sync Mechanism Is Not Wired — Router B Never Receives Router A's Routing Tree

2 participants