Skip to content

feat(mesh): causally-stable tombstone GC driven by per-peer ack watermarks#1686

Open
CatherineSue wants to merge 6 commits into
chang/mesh-dead-node-sweepfrom
chang/mesh-stable-gc
Open

feat(mesh): causally-stable tombstone GC driven by per-peer ack watermarks#1686
CatherineSue wants to merge 6 commits into
chang/mesh-dead-node-sweepfrom
chang/mesh-stable-gc

Conversation

@CatherineSue

Copy link
Copy Markdown
Member

Description

Problem

Tombstone GC was deliberately undriven: time-based collection is unsound, because a peer absent past the grace replays its retained insert against vacant metadata and resurrects the deleted key. Without any driven GC, tombstone metadata and the Remove ops they pin accumulate forever — dead-node sweeps (#1674) made this acute by becoming the first mass-delete producer.

Solution

Collect a tombstone only at causal stability — when every member has provably acked past it:

  • CrdtWatermark::covers(key, version) turns the per-peer send watermarks (feat(mesh): per-key CRDT send watermark + ack + retry #1586) into stability proofs; engines gain gc_tombstones_where(grace, stable) (time-based gc_tombstones is the always-true special case).
  • Both sender paths register their per-peer ack watermark in a controller-owned table — client-side at dial, server-side when the peer identifies. Registrations are stream-scoped (Drop deregisters unless a replacement stream re-registered), reconnects start fresh (conservative), removal drops the entry (spec §5.2 step 4).
  • The 60-round housekeeping tick runs GC only when every member has a registered watermark, with stable = all peers cover it. A peer acked tombstone ⇒ it holds and has compacted against it ⇒ it can never relay the dominated insert.

Found by adversarial review, fixed in-PR (3 confirmed must-fixes):

  • Removal un-vetoed GC while the stack explicitly supports the removed node returning (refutation + holddown lift) with its full in-memory log — after grace + collection its replay resurrected deleted keys cluster-wide, and a survivor that removed every peer degenerated to vacuously-true stability. Removals now sit in a 600s quarantine (> grace) during which stable GC is paused entirely: a node returning inside the window finds every tombstone intact and merges them.
  • A graceful Leaving departure stalled GC permanently (name stranded in membership, watermark frozen in the table). Leaving now expires from membership like Down — spec §5.2 removes on either, and an exited process has no in-memory state to replay, so removal + sweep is unconditionally safe.
  • Legacy timestamp-only acks (ReplicaId::MAX sentinel) claimed coverage of same-timestamp tombstones they also suppress from ever being sent. Sentinel entries now prove coverage only strictly below their timestamp.

It also delivers the promise from the #1674 review threads: dead-node sweeps no longer retire replica-registry entries; quarantined names are re-swept each tick (late-relayed keys tombstoned with attribution intact), and retirement sweeps any remaining ghost keys in the same pass before deleting the entries.

Known limitations (disclosed)

  • Residual replay window: a live node partitioned longer than the 600s quarantine that then heals can still replay around collected tombstones. Fencing that requires incarnation numbers (SWIM hardening, recorded follow-up; Cassandra's gc_grace + rebootstrap rule is the operational precedent). Documented at the GC gate.
  • A dead stream's deregistration blocks GC conservatively until reconnect (better than the prior frozen-entry permanent stall); per-key watermark entries for collected keys persist for the connection's lifetime (bounded; reset on reconnect).
  • The GC-blocked regime (any member without a watermark, any quarantine active) is the exception in a healthy cluster but recurs ~10min after each removal.

Changes

  • crates/mesh/src/crdt_kv/watermark.rscovers (sentinel-aware)
  • crates/mesh/src/crdt_kv/engine/{mod,lww,rate_limit}.rsgc_tombstones_where
  • crates/mesh/src/crdt_kv/crdt.rs, crates/mesh/src/kv.rsgc_stable_tombstones; retirement sweeps ghosts
  • crates/mesh/src/gossip_controller.rsPeerWatermarks table + WatermarkRegistration, quarantine, Leaving expiry, GC driver
  • crates/mesh/src/gossip_service.rs — server-side registration
  • crates/mesh/src/service.rs — table shared controller → service

Test Plan

  • cargo test -p smg-mesh green (214); cargo clippy --workspace --all-targets --all-features -- -D warnings clean.
  • New: stable GC blocks uncovered tombstones in both engines; covers semantics + sentinel strictness; Leaving expires like Down; ghost-sweeping retirement; existing dead-node/holddown/refutation suites updated and green.
  • Adversarially reviewed (correctness + memory lenses, independent refuters): all 3 confirmed must-fixes fixed in-PR; minors addressed (stream-scoped registrations, ghost sweep) or disclosed above.

Stacked on #1674; retarget after it merges.

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

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: eee991a7-d7b1-42f4-a199-b3bf95a4a246

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chang/mesh-stable-gc

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

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements causally-stable tombstone garbage collection (GC) for CRDTs by tracking per-peer acknowledgment watermarks and only collecting tombstones that every live peer has acknowledged. Key changes include adding gc_stable_tombstones to the CRDT engines, introducing watermark coverage checks, and updating the gossip controller and service to share watermark tables. The review feedback highlights potential lock nesting and contention risks: specifically, holding DashMap shard write locks while calling the stable closure, and repeatedly acquiring read locks on peer watermarks inside the closure. The reviewer suggests pre-filtering candidates outside of the DashMap locks and snapshotting the watermarks once before executing the GC.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

.is_none_or(|winner| {
winner.is_tombstone
&& now.saturating_duration_since(winner.created_at) >= grace
&& stable(&key, winner.version_key())

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.

high

Holding the DashMap shard write lock (via remove_if) while calling the stable closure—which internally acquires read locks on multiple RwLock<CrdtWatermark>s—introduces a lock nesting risk. If other threads attempt to write to those watermarks while holding other locks, or if there is high contention, this can block the DashMap shard and degrade performance.

Consider refactoring gc_tombstones_where to pre-filter the candidates and evaluate stable outside of the DashMap locks.

References
  1. To avoid holding DashMap shard locks during slow operations, collect the keys and/or values into a temporary collection (e.g., Vec) before iterating and performing the operations.

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.

Addressed via the sibling thread's restructure (59b08ae): stable() now reads pre-cloned snapshots, so no watermark lock is ever taken inside remove_if. For the record, the nesting as-was could not deadlock — the ack paths take only the watermark lock over a pre-materialized op snapshot and never touch engine shard locks, so the shard->watermark order had no inversion partner — but eliminating the nesting class entirely is strictly better than relying on that argument.

entry
.tombstoned_at
.is_some_and(|at| now.saturating_duration_since(at) >= grace)
&& stable(&key, (version.timestamp, version.replica_id))

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.

high

Holding the DashMap shard write lock (via remove_if) while calling the stable closure—which internally acquires read locks on multiple RwLock<CrdtWatermark>s—introduces a lock nesting risk. If other threads attempt to write to those watermarks while holding other locks, or if there is high contention, this can block the DashMap shard and degrade performance.

Consider refactoring gc_tombstones_where to pre-filter the candidates and evaluate stable outside of the DashMap locks.

References
  1. To avoid holding DashMap shard locks during slow operations, collect the keys and/or values into a temporary collection (e.g., Vec) before iterating and performing the operations.

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.

Same fix as the LWW twin: 59b08ae snapshots the watermarks before the GC pass, so the RateLimitEngine predicate also runs lock-free under remove_if.

Comment on lines +422 to +426
let stable = |key: &str, version: (u64, ReplicaId)| {
handles
.iter()
.all(|acked| acked.read().covers(key, version))
};

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.

medium

Evaluating acked.read() inside the stable closure for every single tombstone candidate acquires and releases a read lock on each peer's RwLock<CrdtWatermark> per candidate. If there are many tombstones, this can cause significant lock contention with the stream tasks that frequently write to these watermarks.

We can optimize this by cloning/snapshotting the watermarks of all peers once before running the GC, completely eliminating per-candidate lock acquisition.

                        let watermarks: Vec<CrdtWatermark> = handles
                            .iter()
                            .map(|acked| acked.read().clone())
                            .collect();
                        let stable = move |key: &str, version: (u64, ReplicaId)| {
                            watermarks
                                .iter()
                                .all(|wm| wm.covers(key, version))
                        };

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.

Adopted in 59b08ae — the driver now clones each registered watermark once per GC run (60s cadence, ~O(keys) HashMap clone per peer, transient), so the stability predicate evaluates against plain snapshots with zero lock acquisition under the engine shard locks. Acks landing mid-run defer coverage to the next run, which is conservative by construction.

@CatherineSue
CatherineSue force-pushed the chang/mesh-stable-gc branch from 58650ce to dbdb96e Compare June 11, 2026 21:39
@github-actions github-actions Bot added tests Test changes mesh Mesh crate changes labels Jun 11, 2026
CatherineSue added a commit that referenced this pull request Jun 11, 2026
The stability predicate ran under engine shard locks while acquiring
every peer's watermark RwLock per tombstone candidate. Cloning each
registered watermark once per run (60s cadence) removes all lock
acquisition from the predicate; acks landing mid-run defer coverage
to the next run, which is conservative. Suggested by review on #1686.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
@CatherineSue
CatherineSue force-pushed the chang/mesh-dead-node-sweep branch from 8880ea6 to 0baf890 Compare June 13, 2026 09:36
CatherineSue added a commit that referenced this pull request Jun 13, 2026
The stability predicate ran under engine shard locks while acquiring
every peer's watermark RwLock per tombstone candidate. Cloning each
registered watermark once per run (60s cadence) removes all lock
acquisition from the predicate; acks landing mid-run defer coverage
to the next run, which is conservative. Suggested by review on #1686.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
@CatherineSue
CatherineSue force-pushed the chang/mesh-stable-gc branch from 59b08ae to e7c841b Compare June 13, 2026 09:36

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

ℹ️ 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 crates/mesh/src/kv.rs Outdated
Comment on lines +667 to +668
if *attribution == DeadKeyAttribution::AuthorReplica {
let swept = self.sweep_authored_by(prefix, &absent_replicas);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Sweep suffix-attributed ghosts before retirement

When a removed node uses DeadKeyAttribution::NodeNameSuffix, this retirement path deletes the last registry entry without running sweep_node_suffix. If a stale survivor relays a suffix-owned key after the final holddown re-sweep but before retire_absent_replica_entries runs, the key survives this pass; once the registry is deleted and the name is no longer held, the controller has no later opportunity to call handle_node_removed("dead-node"), so the ghost key can remain indefinitely. The retirement pass should apply the suffix sweep for the absent node names before deleting their registry entries, just as it does author-replica sweeps here.

Useful? React with 👍 / 👎.

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 76d2f1b. retire_absent_replica_entries now also runs the NodeNameSuffix sweep — for each absent node name it calls sweep_node_suffix across the suffix-attributed prefixes before deleting the registry entries, so a late-relayed rl: shard is tombstoned in the same (last) pass that can attribute it rather than stranded. Regression test absent_entry_retirement_sweeps_suffix_attributed_ghosts.

/// is paused, so a partitioned-but-alive node returning inside the window
/// finds every tombstone intact and merges them instead of replaying its
/// stale inserts around collected ones. Must exceed the tombstone grace.
const REMOVAL_QUARANTINE: Duration = Duration::from_secs(600);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Nit: The safety of the quarantine depends on REMOVAL_QUARANTINE > DEFAULT_TOMBSTONE_GRACE (defined in crdt.rs), but nothing enforces this at compile time. If someone bumps the grace past 600s without updating this constant, the returning-node replay protection silently breaks. A compile-time assertion would lock the invariant:

Suggested change
const REMOVAL_QUARANTINE: Duration = Duration::from_secs(600);
const REMOVAL_QUARANTINE: Duration = Duration::from_secs(600);
const _: () = assert!(
REMOVAL_QUARANTINE.as_secs() > crate::crdt_kv::DEFAULT_TOMBSTONE_GRACE.as_secs(),
"quarantine must exceed tombstone grace for returning-node replay safety"
);

CatherineSue added a commit that referenced this pull request Jun 13, 2026
The stability predicate ran under engine shard locks while acquiring
every peer's watermark RwLock per tombstone candidate. Cloning each
registered watermark once per run (60s cadence) removes all lock
acquisition from the predicate; acks landing mid-run defer coverage
to the next run, which is conservative. Suggested by review on #1686.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
CatherineSue added a commit that referenced this pull request Jun 13, 2026
retire_absent_replica_entries swept only AuthorReplica prefixes before
deleting a departed node's registry entries; a NodeNameSuffix key (e.g.
an rl: shard) relayed after the node left holddown was never swept and,
with the registry entry gone and the name no longer held, no later
handle_node_removed could attribute it — a permanent ghost. Retirement
now also sweeps NodeNameSuffix prefixes by the absent node names (which
need no registry entry) in the same pass. Reported by review on #1686.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
@CatherineSue
CatherineSue force-pushed the chang/mesh-stable-gc branch from e7c841b to d001a57 Compare June 13, 2026 09:57

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

ℹ️ 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 crates/mesh/src/kv.rs
Comment on lines +664 to +665
if absent_keys.is_empty() {
return 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Sweep suffix ghosts even without registry entries

When a departed node has no live replica: entry (for example an older/partially upgraded node, or one whose registry entry was already tombstoned) but a late DeadKeyAttribution::NodeNameSuffix key is relayed after the holddown re-sweeps, this early return skips suffix cleanup entirely. At that point the controller has already dropped the node name from removed_holddown, so there is no later handle_node_removed(node) call to tombstone rl:*:<node> ghosts; suffix-attributed cleanup needs to scan/sweep absent suffixes even when there are no registry entries to retire.

Useful? React with 👍 / 👎.

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.

Deferring with rationale. The suffix sweep here is driven by the replica registry, so a departed node that left NO trace (no replica: entry — pre-upgrade, or an entry already retired in a prior pass) gives retire_absent_replica_entries no node name to sweep by, and handle_node_removed no longer fires for it. Closing this fully needs a membership-diff sweep (rl:*: where is in no live member), which is a different mechanism with its own live-node-sweep risk and belongs with the SWIM membership work. It's also enforcement-inert in the meantime: get_aggregate only sums max-epoch shards, so a stale-epoch ghost from a dead node contributes 0 to the cluster total — the leftover is a bounded, inert shard, not a counted one. Tracking as a known limitation of suffix-attribution cleanup rather than expanding this PR.

@CatherineSue
CatherineSue force-pushed the chang/mesh-dead-node-sweep branch from ddc7ae8 to 023cf62 Compare June 17, 2026 19:36
CatherineSue added a commit that referenced this pull request Jun 17, 2026
The stability predicate ran under engine shard locks while acquiring
every peer's watermark RwLock per tombstone candidate. Cloning each
registered watermark once per run (60s cadence) removes all lock
acquisition from the predicate; acks landing mid-run defer coverage
to the next run, which is conservative. Suggested by review on #1686.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
CatherineSue added a commit that referenced this pull request Jun 17, 2026
retire_absent_replica_entries swept only AuthorReplica prefixes before
deleting a departed node's registry entries; a NodeNameSuffix key (e.g.
an rl: shard) relayed after the node left holddown was never swept and,
with the registry entry gone and the name no longer held, no later
handle_node_removed could attribute it — a permanent ghost. Retirement
now also sweeps NodeNameSuffix prefixes by the absent node names (which
need no registry entry) in the same pass. Reported by review on #1686.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
@CatherineSue
CatherineSue force-pushed the chang/mesh-stable-gc branch from d001a57 to d2a78eb Compare June 17, 2026 19:36
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had any activity within 14 days. It will be automatically closed if no further activity occurs within 16 days. Leave a comment if you feel this pull request should remain open. Thank you!

@github-actions github-actions Bot added the stale PR has been inactive for 14+ days label Jul 2, 2026
…marks

Time-based tombstone GC was deliberately undriven: a peer absent past
the grace replays its retained insert against empty metadata and
resurrects the deleted key. Collection is sound only at causal
stability — every live peer acked past the tombstone — which the
per-peer send watermarks prove.

CrdtWatermark::covers reports whether a peer acked a key at a version;
engines gain gc_tombstones_where(grace, stable), with time-based
gc_tombstones as the always-true special case. Both sender paths
(client- and server-side) register their per-peer watermark in a
controller-owned table, replaced on reconnect (fresh = conservative)
and dropped on node removal (spec 5.2 step 4). The 60-round
housekeeping tick collects tombstones every member covers, skipping
collection while any member lacks a registered watermark.

The same tick closes the late-relayed-key window from #1674 review:
dead-node sweeps no longer retire replica-registry entries; held
(recently removed) names are re-swept so late keys are tombstoned
with attribution intact, and entries retire only once their node is
neither in membership nor held and nothing live is attributed to
them. The GC primitive and its driver land together because the
primitive alone is dead code under -D warnings.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
…tamps

A legacy timestamp-only ack maps to (v, ReplicaId::MAX), which allows()
deliberately overstates to suppress resends — but covers() reused it as
a causal-stability proof, claiming coverage of a same-timestamp
tombstone from another replica that the sentinel also suppresses from
ever being sent. Collecting on that proof and a later replay of the
peer's retained insert would silently resurrect the deleted key.
Sentinel entries now prove coverage only below their timestamp.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
…Leaving

Adversarial review proved the removal path reopened the replay hole
stable GC exists to close: removing a node dropped its GC veto, yet the
stack explicitly supports that node returning (refutation + holddown
lift) with its full in-memory log — after grace + collection, its
replayed dominated inserts landed on vacant metadata and resurrected
deleted keys cluster-wide; a survivor that removed every peer even
degenerated to vacuously-true stability (pure time-based GC).

Removals now sit in a 600s quarantine (longer than the tombstone
grace) during which stable GC is paused entirely, so a node returning
inside the window finds every tombstone intact and merges them; the
residual — a live node partitioned past the quarantine that then
heals — needs incarnation numbers (SWIM hardening) and is documented
at the gate.

Leaving now expires from membership like Down (spec 5.2 removes on
either; an exited process has no in-memory state to replay), closing
the review's other confirmed leak: a graceful scale-down used to
strand its name in membership and its frozen watermark in the table,
stalling stable GC permanently. Watermark registrations are now
stream-scoped (Drop deregisters unless a replacement re-registered),
so a dead stream blocks GC only until reconnect instead of forever.
Registry retirement sweeps ghost keys (late-relayed after the
quarantine) in the same pass instead of retaining their attribution
indefinitely.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
The stability predicate ran under engine shard locks while acquiring
every peer's watermark RwLock per tombstone candidate. Cloning each
registered watermark once per run (60s cadence) removes all lock
acquisition from the predicate; acks landing mid-run defer coverage
to the next run, which is conservative. Suggested by review on #1686.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
retire_absent_replica_entries swept only AuthorReplica prefixes before
deleting a departed node's registry entries; a NodeNameSuffix key (e.g.
an rl: shard) relayed after the node left holddown was never swept and,
with the registry entry gone and the name no longer held, no later
handle_node_removed could attribute it — a permanent ghost. Retirement
now also sweeps NodeNameSuffix prefixes by the absent node names (which
need no registry entry) in the same pass. Reported by review on #1686.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
The under-lock re-check guarding against sweeping a revived node matched
only Down, but this branch expires Leaving nodes too; a Leaving node
that reached dead_timeout would fail the re-check and never be removed
or swept. Match is_departed, consistent with expire_down_nodes.

Signed-off-by: Chang Su <8605658+CatherineSue@users.noreply.github.com>
@CatherineSue
CatherineSue force-pushed the chang/mesh-dead-node-sweep branch from 023cf62 to 496ca0d Compare July 2, 2026 16:21
@CatherineSue
CatherineSue force-pushed the chang/mesh-stable-gc branch from d2a78eb to 8db7cce Compare July 2, 2026 16:21
@github-actions github-actions Bot removed the stale PR has been inactive for 14+ days label Jul 3, 2026
@github-actions

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had any activity within 14 days. It will be automatically closed if no further activity occurs within 16 days. Leave a comment if you feel this pull request should remain open. Thank you!

@github-actions github-actions Bot added the stale PR has been inactive for 14+ days label Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mesh Mesh crate changes stale PR has been inactive for 14+ days tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant