Skip to content

fix: Reliably garbage collect reject signals - #11263

Open
alin-at-dfinity wants to merge 3 commits into
masterfrom
alin/reliably-gc-reject-signals
Open

fix: Reliably garbage collect reject signals#11263
alin-at-dfinity wants to merge 3 commits into
masterfrom
alin/reliably-gc-reject-signals

Conversation

@alin-at-dfinity

Copy link
Copy Markdown
Contributor

XNetPayloadBuilder currently pools and selects slices that have either messages or new signals. This ensures that we can always make progress (new messages are reliably inducted; messages already inducted by the remote subnet are reliably garbage collected); but does not ensure any time bound on garbage collecting reject signals.

Streams don't have explicit accept signals, only a signals_end index (pointing beyond the last inducted message). But we do have explicit reject signals (for missing or migrating canisters, full queues, etc.). These reject signals must be (and are) garbage collected when the remote subnet indicates that it has acted on them (by garbage collecting messages and advancing its stream begin index). But currently this only happens when a stream slice is inducted for other reasons (new messages or signals).

In order to ensure that reject signals don't remain alive indefinitely in an otherwise idle stream, we must also actively induct stream slices with no messages and no new signals iff the stream begin they advertise points past at least one of our reject signals.

We do this by adding a gced_message_index field to ExpectedIndices (next to the existing message_index and stream_index). This field points to the next reject signal, which may not be the first if the validation context includes some payloads on top of the certified state (i.e. if we already included slices that garbage collected some reject signals). Based on this field, we can decide (while selecting, validating or garbage collecting slices) whether a slice with no messages and no signals should be considered "non-empty" because its induction would result in garbage collecting at least one reject signal.

This change also drops the signals_begin field (only used for metrics and extra validation). And ensuring that it always converges to signals_end on an idle stream would be the equivalent of having explicit reject signals (requiring otherwise unnecessary extra roundtrips). Dropping the field altogether means that rolling back to an older replica version would simply result in that replica starting with signals_begin = 0, which is entirely benign as it respects all invariants.

`XNetPayloadBuilder` currently pools and selects slices that have either messages or new signals. This ensures that we can always make progress (new messages are reliably inducted; messages already inducted by the remote subnet are reliably garbage collected); but does not ensure any time bound on garbage collecting reject signals.

Streams don't have explicit accept signals, only a `signals_end` index (pointing beyond the last inducted message). But we do have explicit reject signals (for missing or migrating canisters, full queues, etc.). These reject signals must be (and are) garbage collected when the remote subnet indicates that it has acted on them (by garbage collecting messages and advancing its stream begin index). But currently this only happens when a stream slice is inducted for other reasons (new messages or signals).

In order to ensure that reject signals don't remain alive indefinitely in an otherwise idle stream, we must also actively induct stream slices with no messages and no new signals iff the stream begin they advertise points past at least one of our reject signals.

We do this by adding a `gced_message_index` field to `ExpectedIndices` (next to the existing `message_index` and `stream_index`). This field points to the next reject signal, which may not be the first reject if the validation context includes some payloads on top of the certified state (i.e. if we already included slices that GC-ed some reject signals). Based on this field, we can decide (when selecting, validating or garbage collecting slices) whether a slice with no messages and no signals should still be retained and/or inducted because its induction will result in garbage collecting at least one reject signal.

This change also drops the `signals_begin` field (only used for metrics and extra validation). And ensuring that it always converges to `signals_end` on an idle stream would be the equivalent of having explicit reject signals (requiring otherwise unnecessary extra roundtrips). Dropping the field altogether means that rolling back to an older replica version would simply result in that replica starting with `signals_begin = 0`, which is entirely benign as it respects all invariants.
@alin-at-dfinity
alin-at-dfinity requested a review from a team as a code owner August 21, 2026 14:10
@github-actions github-actions Bot added the fix label Aug 21, 2026
@alin-at-dfinity
alin-at-dfinity requested a balanced review from Copilot August 21, 2026 14:10
@zeropath-ai

zeropath-ai Bot commented Aug 21, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 80cf5a3.

Security Overview
Detected Code Changes
Change Type Relevant files
Refactor ► rs/messaging/src/routing/stream_builder.rs
    Rename signals_begin to reject_signals usage and adjust metrics label
► rs/messaging/src/routing/stream_handler.rs
    Remove assertion on signals_begin/signals_end and related logic
► rs/protobuf/def/state/queues/v1/queues.proto
    Remove signals_begin field and adjust reserved fields
► rs/protobuf/src/gen/state/state.queues.v1.rs
    Drop signals_begin field from generated struct
► rs/protobuf/src/gen/types/state.queues.v1.rs
    Drop signals_begin field from generated struct
► rs/replicated_state/src/metadata_state.rs
    Remove signals_begin field and adjust related invariants and defaults
► rs/replicated_state/src/metadata_state/proto.rs
    Remove signals_begin handling in From/TryFrom conversions
► rs/replicated_state/src/metadata_state/tests.rs
    Update tests referencing signals_begin and related behavior
► rs/xnet/payload_builder/src/certified_slice_pool.rs
    Enhance garbage_collect doc and return semantics with gced_message_index, adjust logic to drop slice when appropriate
► rs/xnet/payload_builder/src/impl_tests.rs
    Update test fixtures to accommodate new gced_message_index semantics
► rs/xnet/payload_builder/src/lib.rs
    Extend ExpectedIndices with gced_message_index and related documentation; adjust expected indices logic to use new field
► rs/xnet/payload_builder/src/test_fixtures.rs
    Update expected indices for streams to include gced_message_index and remove signals_begin usage
► rs/xnet/payload_builder/src/impl_tests.rs (state manager fixtures and tests)
    Add tests and fixtures for reject signal GC behavior and expected indices
► rs/xnet/payload_builder/src/test_fixtures.rs
    Adjust for new gced_message_index semantics in expected indices
Bug Fix / Behavioral Change ► rs/messaging/src/routing/stream_builder.rs
    Update stream_signals metric description to reflect rejection signals
► rs/messaging/src/routing/stream_builder/tests.rs
    Adjust test expectations for stream_signals metric based on reject signals
► rs/replicated_state/src/metadata_state/proto.rs
    Commentary / handling adjusted for removal of signals_begin
► rs/replicated_state/src/metadata_state/tests.rs
    Tests updated to reflect removal of signals_begin and new next_reject_signal_index behavior
Enhancement ► rs/messaging/src/routing/stream_builder.rs
    Replace signals_begin usage with reject_signals reference in metrics and counting logic
► rs/replicated_state/src/metadata_state.rs
    Introduce next_reject_signal_index helper and related invariants
► rs/xnet/payload_builder/src/lib.rs
    Introduce gced_message_index in ExpectedIndices and document its purpose
► rs/xnet/payload_builder/src/test_fixtures.rs
    Update fixtures to include gced_message_index in expected indices

Copilot AI 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.

Pull request overview

Updates XNet payload handling to reliably garbage collect reject signals on idle streams.

Changes:

  • Tracks reject-signal GC progress during slice selection and validation.
  • Removes persisted signals_begin state.
  • Updates stream metrics and adds regression tests.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
rs/xnet/payload_builder/tests/xnet_payload_builder.rs Tests header-only GC slices.
rs/xnet/payload_builder/tests/certified_slice_pool.rs Tests reject-signal-aware pool GC.
rs/xnet/payload_builder/src/test_fixtures.rs Updates expected stream indices.
rs/xnet/payload_builder/src/lib.rs Implements reject-signal GC tracking.
rs/xnet/payload_builder/src/impl_tests.rs Adds index and validation tests.
rs/xnet/payload_builder/src/certified_slice_pool.rs Retains useful header-only slices.
rs/test_utilities/state/src/lib.rs Removes generated signals_begin.
rs/replicated_state/src/metadata_state/tests.rs Updates stream-state tests.
rs/replicated_state/src/metadata_state/proto.rs Removes field serialization.
rs/replicated_state/src/metadata_state.rs Removes field and adds reject lookup.
rs/protobuf/src/gen/types/state.queues.v1.rs Updates generated protobuf type.
rs/protobuf/src/gen/state/state.queues.v1.rs Updates generated state protobuf type.
rs/protobuf/def/state/queues/v1/queues.proto Removes and reserves protobuf tag.
rs/messaging/src/routing/stream_handler/tests.rs Updates stream-handler fixtures.
rs/messaging/src/routing/stream_handler.rs Removes obsolete signal-begin validation.
rs/messaging/src/routing/stream_builder/tests.rs Updates signal metric expectations.
rs/messaging/src/routing/stream_builder.rs Reports explicit reject-signal counts.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread rs/xnet/payload_builder/src/lib.rs Outdated
Comment on lines +486 to +488
gced_message_index: stream.map_or(StreamIndex::from(0), |s| {
s.next_reject_signal_index(max_header_begin)
}),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

While this could happen (block maker validates based on an older certified state; the intervening payloads have resulted in us inducting more messages in subsequent states; remote subnet has seen more recent certified states and its header.begin() has advanced past our outdated signals_end), the worst outcome is inducting multiple empty slices with monotonically increasing header.begin() values, none of which result in any reject signals getting garbage collected. A couple hundred bytes of wasted block space each.

Still, we can easily avoid this waste if we make gced_message_index an Option<StreamIndex> and past the last reject signal set it to None. As in "no header.begin() will result in GC-ing any reject signals, so don't bother". This condition may change in the future (e.g. when we induct the messages in the intervening payloads, we may generate reject signals for some of them); but the condition will be re-evaluated once those future certified states become available to the payload builder; and we can decide to induct the previously rejected otherwise empty header then.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. gced_message_index is now an Option and we only return Some when it refers to an actual reject signal to be garbage collected. Past the last reject signal, we return None.

Comment thread rs/xnet/payload_builder/src/lib.rs Outdated
Comment on lines +847 to +852
gced_message_index: state
.streams()
.get(&subnet_id)
.map_or(StreamIndex::from(0), |stream| {
stream.next_reject_signal_index(slice.header().begin())
}),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See above.

Comment thread rs/messaging/src/routing/stream_builder.rs
Comment thread rs/xnet/payload_builder/tests/certified_slice_pool.rs Outdated
Comment thread rs/protobuf/def/state/queues/v1/queues.proto
…n() beyond it will definitely result in GC-ing a reject signal. Past the last reject signal, return None.

Copilot AI 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.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

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