Skip to content

feat(codec): zero-copy SBE codec#52

Merged
zannis merged 4 commits into
mainfrom
feat/sbe-codec
Jul 8, 2026
Merged

feat(codec): zero-copy SBE codec#52
zannis merged 4 commits into
mainfrom
feat/sbe-codec

Conversation

@zannis

@zannis zannis commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Adds Simple Binary Encoding (SBE) codec support behind a new sbe cargo feature (zero additional dependencies), preserving SBE's flyweight model end to end: publishing passes the user-encoded frame through without re-encoding, and handlers wrap sbe-tool-generated decoders directly over the received buffer.

Design

  • SbeFrame<T> is the topic message type: an owned, header-validated wire frame backed by reference-counted Bytes. T is a zero-sized type tag implementing SbeMessage (schema id, template id, byte order — the constants sbe-tool generates).
  • SbeCodec validates schemaId/templateId on every decode (typed SbeCodecError with expected/actual ids), and requires the buffer to cover the header plus the acting blockLength. Wire version/blockLength pass through per SBE extension semantics via frame.header(), so cross-version decoding works.
  • Codec trait extension (backward compatible): new defaulted encode_bytes/decode_owned methods let backends move owned buffers through the codec. Existing custom codecs compile and behave unchanged.
  • String transports refuse SBE outright: encode_to_string fails unconditionally (StringTransportUnsupported) — even for frames that happen to be valid UTF-8 — so SNS/SQS and Redis Streams failures are deterministic rather than data-dependent.

Per-backend copy behavior

Backend Publish Consume
NATS zero-copy zero-copy
In-memory zero-copy zero-copy
RabbitMQ zero-copy zero-copy (ReceivedDelivery shares one buffer across decode and retry/DLQ republish)
Kafka zero-copy into the client one copy out of librdkafka's borrowed buffer
SNS/SQS, Redis Streams rejected at publish time

Testing

  • Unit: 17 SBE codec tests (round trip, both endiannesses, id-mismatch/truncation/too-short rejection, version passthrough, pointer-equality zero-copy assertions, unconditional string-transport rejection).
  • In-memory integration: end-to-end pub/sub with flyweight decode, plus a pointer-equality test proving the handler observes the exact published allocation.
  • Real-broker integration (Docker): SBE round trips on NATS, Kafka, and RabbitMQ; publish-rejection on Redis Streams and SNS/SQS (LocalStack) using a UTF-8-valid frame.
  • Full suites green locally: NATS+RabbitMQ (485), Kafka (313), RabbitMQ incl. transactional (400), all-features clippy, no-default-features clippy, fmt.
  • CI matrix now runs every backend suite with sbe enabled.

Review

Independent codex review loop: 3 rounds, 4 findings fixed (UTF-8-passthrough on string transports, missing root-block length validation, missing real-broker coverage, RabbitMQ consume copy), round 3 returned zero findings.

🤖 Generated with Claude Code

zannis added 3 commits July 7, 2026 16:56
… Codec paths

Add an `sbe` feature (no new dependencies) with SbeMessage, SbeHeader,
SbeFrame<T>, and SbeCodec: topics carry owned, header-validated SBE wire
frames, so handlers wrap sbe-tool-generated flyweight decoders directly
over the received buffer with no intermediate struct.

Extend the Codec trait with defaulted encode_bytes/decode_owned so
backends can move owned buffers through the codec instead of copying:

- NATS: publish and consume are fully zero-copy (Bytes end to end)
- InMemory: envelope payload is now Bytes; publish/consume zero-copy
- Kafka/RabbitMQ: publish is zero-copy into the client; consume keeps
  the borrowed-slice path (client-owned buffers), costing one memcpy
- SNS/SQS and Redis Streams reject binary SBE frames at publish time
  via the existing encode_to_string UTF-8 check

schemaId/templateId are validated on every frame; wire version and
blockLength pass through per SBE extension semantics via frame.header().

bytes is promoted from an optional (kafka/nats) to a required dependency;
it was already unconditionally in the tree via tokio-util.
…ck length, real-broker tests

Address independent review findings:

- SbeCodec::encode_to_string now fails unconditionally with
  SbeCodecError::StringTransportUnsupported instead of relying on the
  default UTF-8 check, which would let UTF-8-valid frames ride SNS/SQS
  and Redis Streams and make failures data-dependent
- SbeFrame::new rejects frames shorter than header + acting blockLength
  (RootBlockTruncated), so flyweight root-block reads cannot run past
  the buffer
- SBE round-trip integration tests against real NATS, Kafka, and
  RabbitMQ brokers; publish-rejection tests on Redis Streams and
  SNS/SQS (LocalStack) using a UTF-8-valid frame
- CI matrix runs each backend suite with the sbe feature enabled
- README and docs: codec reference links and updated rejection semantics
…decode and republish

Address review finding: lapin hands over an owned delivery buffer, but
the consumer still decoded through the borrowed-slice path, costing SBE
frames a copy per delivery.

Introduce ReceivedDelivery, created at receipt: the payload moves out of
delivery.data into a reference-counted Bytes once. Codec::decode_owned
gets a refcount clone, and the hold-queue retry/defer republish paths
borrow the same allocation. The concurrent, concurrent-sequenced, and
DLQ consume loops all route through it; route_retry/route_defer take the
payload explicitly instead of reading delivery.data.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
shove-docs 1992530 Commit Preview URL

Branch Preview URL
Jul 07 2026, 03:26 PM

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.53975% with 50 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/backends/rabbitmq/consumer.rs 74.31% 28 Missing ⚠️
src/codecs/sbe.rs 93.91% 18 Missing ⚠️
src/backends/inmemory/consumer.rs 60.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@zannis
zannis merged commit dd397d8 into main Jul 8, 2026
16 checks passed
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.

1 participant