Skip to content

feat(smg): add WorkerSyncAdapter for v2 worker: CRDT namespace - #1313

Merged
CatherineSue merged 4 commits into
mainfrom
feat/mesh-worker-sync-adapter
Apr 22, 2026
Merged

CatherineSue merged 4 commits into
mainfrom
feat/mesh-worker-sync-adapter

Conversation

@CatherineSue

@CatherineSue CatherineSue commented Apr 22, 2026 •

Copy link
Copy Markdown
Member

Description

Problem

Mesh v2 routes worker state through the typed worker: CRDT namespace on MeshKV, but the gateway has no adapter that writes to / reads from that namespace yet. Step 5's dual-mode deployment needs this bridge in place before the v1 MeshSyncManager worker-sync path can be torn down.

Solution

Add WorkerSyncAdapter under model_gateway/src/mesh/worker_sync.rs:

  • on_worker_changed(worker_id, &state) / on_worker_removed(worker_id) — bincode-serialise WorkerState and call CrdtNamespace::put / delete under worker:{worker_id}.
  • start(self: &Arc<Self>) — spawns a task that subscribes to the worker: prefix and routes non-tombstone events through the existing WorkerRegistry::on_remote_worker_state sink (the same entry point the current WorkerStateSubscriber trait feeds), so URL-dedupe, health promotion, and the Registered event fan-out are unchanged. Tombstones are logged at debug for now — the registry has no remote-remove hook yet, and wiring one belongs alongside the v1-mirror teardown in a later step.
  • new asserts the namespace prefix is worker: so a mis-wired caller fails loudly at startup.

No server.rs wiring in this PR. That comes with Step 5 dual-mode deployment together with shutting down the v1 path.

Third in the Step 4 adapter sequence; builds on the auto-registered config: prefix merged in the previous PR.

Changes

  • New model_gateway/src/mesh/ module with WorkerSyncAdapter.
  • Exposed as pub mod mesh from model_gateway/src/lib.rs.
  • Unit tests covering: outbound write round-trips, tombstone removal, inbound routing into a real WorkerRegistry, graceful handling of malformed payloads, and the prefix assertion.

Test Plan

  • cargo test -p smg --lib mesh::worker_sync — 5 tests passing.
  • cargo clippy -p smg --lib --tests -- -D warnings
  • cargo +nightly fmt --all
Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • (Optional) Documentation updated
  • (Optional) Please join us on Slack #sig-smg to discuss, review, and merge PRs

Summary by CodeRabbit

  • New Features

    • Mesh adapter added to synchronize worker state across gateway instances.
    • Automatic propagation of worker updates and removals, with deletion tombstone handling.
    • Inbound sync backfills existing entries and maintains continuous subscriptions for updates.
    • Robust syncing that tolerates malformed payloads and logs decode issues without stopping.
  • Tests

    • Unit tests covering sync, tombstones, backfill, error resilience, and startup behavior.

@CatherineSue
CatherineSue requested a review from slin1237 as a code owner April 22, 2026 14:53
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Apr 22, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new public mesh module and mesh::adapters::WorkerSyncAdapter which syncs WorkerState bidirectionally between a CrdtNamespace (namespace worker:) and the local WorkerRegistry.

Changes

Cohort / File(s) Summary
Public API surface
model_gateway/src/lib.rs, model_gateway/src/mesh/mod.rs, model_gateway/src/mesh/adapters/mod.rs
Adds mesh module, declares adapters submodule, and re-exports WorkerSyncAdapter at model_gateway::mesh::WorkerSyncAdapter.
Worker sync implementation & tests
model_gateway/src/mesh/adapters/worker_sync.rs
Introduces WorkerSyncAdapter with constructor validation for worker: prefix, start (subscription + backfill), on_worker_changed (bincode serialize + put), on_worker_removed (delete/tombstone), inbound routing into WorkerRegistry::on_remote_worker_state, logging for malformed data, and unit tests covering outbound/inbound/tombstone/error/backfill cases.

Sequence Diagram(s)

sequenceDiagram
    participant WR as WorkerRegistry
    participant WSA as WorkerSyncAdapter
    participant CN as CrdtNamespace
    participant Remote as RemoteMesh

    rect rgba(100, 150, 200, 0.5)
    Note over WR,Remote: Outbound: Local → Mesh
    WR->>WSA: on_worker_changed(worker_id, state)
    WSA->>WSA: bincode::serialize(state)
    WSA->>CN: put("worker:{id}", bytes)
    CN->>Remote: propagate update
    end

    rect rgba(150, 100, 200, 0.5)
    Note over WR,Remote: Inbound: Mesh → Local
    Remote->>CN: mesh update
    CN->>WSA: subscription yields (key, Some(value))
    WSA->>WSA: strip "worker:" → worker_id
    WSA->>WSA: bincode::deserialize(value)
    WSA->>WR: on_remote_worker_state(worker_id, state)
    end

    rect rgba(200, 100, 100, 0.5)
    Note over WR,Remote: Removal / Tombstone
    WR->>WSA: on_worker_removed(worker_id)
    WSA->>CN: delete("worker:{id}")
    CN->>Remote: propagate tombstone
    CN->>WSA: subscription yields (key, None)
    WSA->>WSA: treat as tombstone / ignore
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

tests

Suggested reviewers

  • slin1237
  • tonyluj
  • llfl

Poem

🐰 I hop on keys with tiny paws,
I bincode states and mind the laws,
Worker:prefix neat and bright,
I ferry updates through the night,
Mesh hums softly — all is right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding WorkerSyncAdapter for the v2 worker CRDT namespace, which aligns perfectly with the primary changes across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mesh-worker-sync-adapter

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

@github-actions github-actions Bot added the model-gateway Model gateway crate changes label Apr 22, 2026

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

Clean, well-structured adapter. Error handling is appropriate (warn on ser/de failures, debug on tombstones), the echo-back idempotence from local put→subscribe is documented and safe, and tests cover the core paths including malformed payloads and wrong-prefix rejection. No issues found.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/mesh/worker_sync.rs`:
- Around line 188-213: Extract a reusable async polling helper (e.g.,
poll_until) in the test module and replace the repeated for-loop in
start_routes_remote_state_into_registry with a call to that helper; implement
poll_until to accept a predicate (FnMut() -> bool) and a failure message,
perform the same retry/sleep loop (20 iterations, 10ms sleep) and panic with the
message if the predicate never becomes true, then call it as poll_until(||
registry.get_by_url("http://remote:8080").is_some(), "registry did not see the
remote worker").await; keep references to the same symbols
(start_routes_remote_state_into_registry, registry.get_by_url,
WorkerSyncAdapter::new) so the test logic is unchanged.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 41dc53e5-a929-4d78-a7d7-33dfc4aad57c

📥 Commits

Reviewing files that changed from the base of the PR and between 3f36c80 and 6fc23b1.

📒 Files selected for processing (3)
  • model_gateway/src/lib.rs
  • model_gateway/src/mesh/mod.rs
  • model_gateway/src/mesh/worker_sync.rs

Comment thread model_gateway/src/mesh/adapters/worker_sync.rs

@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: 6fc23b10da

ℹ️ 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".

Comment thread model_gateway/src/mesh/adapters/worker_sync.rs
@mergify

mergify Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Hi @CatherineSue, this PR has merge conflicts that must be resolved before it can be merged. Please rebase your branch:

git fetch origin main
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts that need to be resolved label Apr 22, 2026

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/mesh/adapters/worker_sync.rs`:
- Around line 73-76: The start() method currently only calls
self.workers.subscribe("") so it misses pre-existing CRDT entries; before
subscribing, read the current worker set from the CRDT and insert each into the
WorkerRegistry (or whatever registry field is used) to "backfill" existing
worker: entries, then proceed to call self.workers.subscribe("") to receive live
updates; locate start(), the workers subscribe call, and the registry insertion
logic (e.g., WorkerRegistry or self.registry, and functions that add workers)
and invoke the CRDT read API (e.g., get_all()/iter_entries()/snapshot) to
iterate existing entries and register them prior to subscribing.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ab2cde1b-5326-4dfb-bc98-6908e6366422

📥 Commits

Reviewing files that changed from the base of the PR and between 6fc23b1 and 2f9d41e.

📒 Files selected for processing (3)
  • model_gateway/src/mesh/adapters/mod.rs
  • model_gateway/src/mesh/adapters/worker_sync.rs
  • model_gateway/src/mesh/mod.rs

Comment thread model_gateway/src/mesh/adapters/worker_sync.rs
Bridges the `worker:` CRDT namespace (MeshKV) and the in-process
WorkerRegistry. Outbound APIs (`on_worker_changed`,
`on_worker_removed`) bincode-serialise WorkerState and write to the
namespace. An inbound task spawned by `start` routes remote updates
through WorkerRegistry::on_remote_worker_state — the same sink the
existing WorkerStateSubscriber implementation feeds — so URL
deduplication, health promotion, and the Registered event fan-out
stay unchanged.

No server.rs wiring yet. Step 5 (dual-mode deployment) will wire
the adapter alongside teardown of the v1 MeshSyncManager worker
sync path.

Third in the Step 4 adapter sequence; builds on the auto-registered
config: prefix merged in the previous PR.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
Makes room for non-adapter mesh glue (bootstrap, shutdown, config
helpers) to land alongside without mixing with the per-namespace
adapters. Pure rename + module re-wire; no behaviour change.

Signed-off-by: Chang Su <chang.s.su@oracle.com>
@CatherineSue
CatherineSue force-pushed the feat/mesh-worker-sync-adapter branch from 2f9d41e to 4e7e665 Compare April 22, 2026 15:10
@mergify mergify Bot removed the needs-rebase PR has merge conflicts that need to be resolved label Apr 22, 2026
Comment thread model_gateway/src/mesh/adapters/worker_sync.rs
`start` previously only processed live subscription events, so a
node that joined after workers had already gossiped would wait for
the next unrelated write before its registry was populated — the
v1 path explicitly replayed via `get_all_worker_states()` for
exactly this reason.

Subscribe first (so no replay/live race loses an event), then
iterate the namespace and feed existing entries through the same
`on_remote_worker_state` sink the live loop uses. Add a test that
pre-seeds the namespace before `start` and asserts the registry
sees the worker.

Signed-off-by: Chang Su <chang.s.su@oracle.com>

@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: 5d300b02f0

ℹ️ 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".

Comment on lines +86 to +87
this.backfill_existing();
while let Some((key, value)) = sub.receiver.recv().await {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Drain subscription queue while backfilling

start subscribes and then runs backfill_existing() before the first recv, so the adapter does not consume subscription events during the whole replay window. In this codebase, subscription delivery is bounded and drop-on-full (CrdtNamespace::subscribe + SubscriberRegistry::notify in crates/mesh/src/kv.rs use an mpsc channel with try_send), so a busy cluster can overflow the queue while backfill is running; dropped worker updates are never replayed, leaving WorkerRegistry stale until a later write touches the same keys. This is most visible on startup when many workers are present and updates continue concurrently.

Useful? React with 👍 / 👎.

@CatherineSue CatherineSue Apr 22, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in cbce753. backfill_existing now runs on the caller's thread, outside the spawned task, so the recv loop is free to drain the channel concurrently on a multi-threaded runtime. The sink is idempotent on URL, so a key seen by both backfill and a live event just refreshes health. In practice the worker: channel capacity (1000) and the low update rate for worker state make the overflow very unlikely, but the reorder is free and tidier.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/mesh/adapters/worker_sync.rs`:
- Around line 117-119: The backfill loop silently skips malformed keys when
doing let Some(worker_id) = key.strip_prefix(PREFIX).filter(|s| !s.is_empty())
else { continue; }; — change this to log a warning when a key does not start
with PREFIX or yields an empty suffix (i.e., when strip_prefix returns None or
the filtered empty string) so malformed keys are observable at startup; include
the offending key and mention PREFIX in the warning and still continue
processing subsequent keys.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1321ad4a-e32e-4e6c-b5ff-ceb6707002c4

📥 Commits

Reviewing files that changed from the base of the PR and between 4e7e665 and 5d300b0.

📒 Files selected for processing (1)
  • model_gateway/src/mesh/adapters/worker_sync.rs

Comment thread model_gateway/src/mesh/adapters/worker_sync.rs
Move `backfill_existing` out of the spawned recv task so the live
loop starts draining immediately. `notify` uses `try_send` into a
bounded mpsc (worker: capacity 1000), and a slow backfill inside
the same task blocks the recv — on a busy startup that window
could drop live updates the gossip path would not resend.

Also mirror the live-path warning for malformed keys in
`backfill_existing` so startup data-shape issues are observable.

Signed-off-by: Chang Su <chang.s.su@oracle.com>

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/mesh/adapters/worker_sync.rs`:
- Around line 130-143: Reject payloads whose embedded WorkerState.worker_id
disagrees with the authoritative key: in apply_incoming, after
bincode::deserialize::<WorkerState>(bytes) check that state.worker_id ==
worker_id and if not log a warning (include both ids) and return without calling
self.worker_registry.on_remote_worker_state(&state); likewise in
on_worker_changed verify state.worker_id == worker_id before bincode::serialize
and self.workers.put(&format!("{PREFIX}{worker_id}"), bytes), and if they differ
log a warning and skip publishing. Use the existing warn! call pattern and
reference apply_incoming, on_worker_changed, WorkerState, worker_registry,
workers.put and PREFIX to find the places to add these checks.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 149249af-f64c-4a0d-b829-d2e53de2f671

📥 Commits

Reviewing files that changed from the base of the PR and between 5d300b0 and cbce753.

📒 Files selected for processing (1)
  • model_gateway/src/mesh/adapters/worker_sync.rs

Comment thread model_gateway/src/mesh/adapters/worker_sync.rs

@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: cbce753eb7

ℹ️ 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".

}
debug!("WorkerSyncAdapter subscription closed");
});
self.backfill_existing();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Yield before synchronous backfill to drain subscription queue

Fresh evidence for the earlier queue-drop concern: even though start() now calls tokio::spawn first, it immediately runs backfill_existing() synchronously, so on a single-thread Tokio runtime (including current_thread and 1-worker deployments) the spawned recv loop cannot poll until backfill returns. I checked crates/mesh/src/kv.rs, where subscriber delivery is bounded and drop-on-full via try_send, so a busy cluster can still lose worker updates during this window and leave WorkerRegistry stale until another write arrives.

Useful? React with 👍 / 👎.

@CatherineSue CatherineSue Apr 22, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining this one. Production runs on the multi-thread tokio runtime (default for #[tokio::main]), where the reorder in cbce753 already lets the recv loop drain on another worker thread while backfill runs, so the overflow window is closed.

On current_thread (1-worker test/dev runtimes), an extra tokio::spawn wrapper for backfill wouldn't actually help — backfill_existing is fully synchronous, so once the scheduler polls it the sync loop runs to completion without yielding regardless of whether it lives in its own task. A real fix for single-thread would require making backfill_existing async and sprinkling tokio::task::yield_now().await between iterations. Given worker state isn't a hot path (register / unregister / health toggle) and the channel capacity is 1000, the combination "single-thread runtime + sustained >200k worker-state writes/sec" needed to overflow isn't a realistic production scenario.

Happy to revisit if we ever ship on current_thread under real load.

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.

1 participant