Skip to content

docs(054): revise after the pull-model prototype - #272

Merged
lxsaah merged 1 commit into
mainfrom
docs/054-revise-after-prototype
Oct 3, 2026
Merged

lxsaah merged 1 commit into
mainfrom
docs/054-revise-after-prototype

Conversation

@lxsaah

@lxsaah lxsaah commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Description

Revises design 054 (zero-allocation connector boundary) with what a throwaway prototype of the pull model showed. The prototype built §4 against main at dfe6adc: InboundDispatch, OutboundRoutes and TopicWriter in core, both MQTT backends, the WebSocket server's outbound path, and the embedded write ring. It was measured but not merged. This PR changes only the design doc.

Every allocation target in §5 held. The doc changes where the code disagreed with it:

  • §4.2 outbound pull. The doc defined next() as poll_fn(|cx| self.poll_next(cx)), and that does not compile: poll_next returns a borrow of its state, and a poll_fn closure cannot return one ("captured variable cannot escape FnMut closure body"). The same applies to the select arm in §4.7. The pull now takes two steps, poll_stage then take_staged. New rules:

    • re-poll a route after a skipped value or a lag report, so its waker stays registered;
    • Ready(None) is final, and a select arm must be disabled after it;
    • Pending does not mean every route is empty, because Tokio's cooperative budget can return it with values still ready.
  • §4.2 API details. RouteId is a plain usize. RouteInfo carries each route's topic and payload capacities. RouteStats counts skips. The message type is OutboundMessage, because aimdb_core::Outbound already exists. OutboundRoutes is built in build().

  • §4.3 topic writer. The doc's closure example fails type inference (E0282) under a generic TopicWriter bound, so the closure form gets its own method, with_topic_fn, beside with_topic_writer. impl From<fmt::Error> for TopicOverflow is added. Overflow is detected by TopicBuf itself, so a writer that ignores the error still has its value skipped.

  • §4.7 embedded write ring.

    • The ring only hands out contiguous space. Once its pointers have moved, an empty ring only guarantees about half its capacity: a 2,048-byte ring drained at offset 1,024 cannot grant 1,100 bytes. So the largest frame plus the reserve must fit in capacity / 2, not "the ring minus the reserve".
    • has_room is a probe grant, re-checked on every poll of the arm.
    • The control reserve covers one packet, so PUBACKs wait for room. They are encoded before state.receive, because the PUBACK borrows the client state.
    • The "polling notifier only" claim is wrong: two wakers replace it.
    • Ready(None) is latched. Without that, a session with only inbound links never yields; the idle-session test saw 0 pings.
  • §5 / §5.1 measurements.

    Path Before (main) Prototype
    Embedded MQTT, one TCP round trip 9 allocs, 302 B, 58.6 µs 0 allocs, 0 B, ~49 µs
    Native MQTT (rumqttc), one round trip 16 allocs, 2,406 B 11 allocs, 557 B
    Core bench rows (§5) 2–3 allocs on the outbound rows all on target

    A wake-up costs about 80 ns per idle SPMC route on the host. §5 gains a new outbound_next_parked row.

  • §4.6, §6, §7, §8.

    • Outage semantics per buffer type are confirmed on the Tokio buffers.
    • The no-compatibility decision is supported: while an ignored with_topic_provider still existed, ten WebSocket tests timed out instead of failing to compile.
    • Two more rejected alternatives are recorded.
    • Open questions are updated, including TCP_NODELAY, which is out of scope for this design.

The prototype's code and its full review page (verdicts, test runs, patch) are not part of this PR. Review page: https://claude.ai/artifact/27gTstCTf72gB7gVmuy8wZ (private until its owner shares it).

Related Issue

Checklist

  • I have read the CONTRIBUTING.md document.
  • My code follows the project's coding standards. (Docs only.)
  • I have added tests to cover my changes. (Not applicable: docs only.)
  • All new and existing tests passed (make check). (Not run for this docs-only change. The prototype's suites passed on its own branch; see §5.1.)
  • I have updated the documentation accordingly.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HXDnQEx2THUSTASYkThxCw


Generated by Claude Code

A throwaway prototype built §4 against main (core API, both MQTT backends,
the WebSocket server's outbound path, the embedded write ring) and measured
it. Every allocation target held; the doc changes where the code disagreed.

- §4.2: the lending poll_next cannot be wrapped in poll_fn (captured
  variable cannot escape FnMut), so neither next() nor a select arm
  compiled. Pull in two steps: poll_stage, then take_staged. Re-poll a
  route after a skip or lag so its waker stays registered; Ready(None) is
  final and must disarm a select arm; Tokio's coop budget makes Pending
  not mean empty. RouteId is a usize, RouteInfo carries the route's topic
  and payload capacities, RouteStats counts skips, the message type is
  OutboundMessage (Outbound is taken), and OutboundRoutes is built in
  build().
- §4.3: with_topic_fn beside with_topic_writer, since an unannotated
  closure fails inference (E0282) under a generic TopicWriter bound;
  From<fmt::Error> for TopicOverflow; overflow detected by TopicBuf even
  if the writer ignores the error.
- §4.7: an empty bipbuffer whose pointers have moved only guarantees about
  half its capacity, so max_frame + reserve must fit capacity / 2. The
  has_room gate is a probe grant re-checked on every poll; PUBACKs wait
  for room (the reserve covers one packet) and are encoded before
  state.receive (Puback borrows the state); two AtomicWakers replace
  "polling notifier only"; Ready(None) is latched, or an inbound-only
  session never yields.
- §5/§5.1: prototype measurements. Embedded MQTT round trip 9 -> 0
  allocations (302 B -> 0), 58.6 -> ~49 us on loopback; native 16 -> 11;
  wake-up cost about 80 ns per idle SPMC route on the host; new
  outbound_next_parked row.
- §4.6, §6, §7, §8: outage semantics confirmed on Tokio buffers; the
  no-compatibility decision supported (an ignored with_topic_provider made
  ten WebSocket tests time out instead of failing to compile); two more
  rejected alternatives; open questions updated, TCP_NODELAY noted as out
  of scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXDnQEx2THUSTASYkThxCw
@lxsaah
lxsaah merged commit ccad4a3 into main Oct 3, 2026
7 checks passed
@lxsaah
lxsaah deleted the docs/054-revise-after-prototype branch October 3, 2026 07:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants