Aeron try_claim Integration
ergo-sbe’s exact sizing is built for encoding straight into a claimed
region: size the frame, claim exactly that many bytes, encode into the claim,
commit. No scratch buffer, no copy. The generated send and send_fixed do
the last three steps and check the result, so a frame that does not fill its
claim is a typed error and the claim is aborted, never a corrupt publication.
The Claim trait
Every generated module’s sbe_rt declares Claim, a region a transport
reserved for one frame. ergo-sbe has no Aeron dependency, so you implement it
once for your transport. In the crate that generates the codec the trait is
local, so rusteron’s claim type can implement it directly (from another crate,
where both are foreign, wrap the claim in a newtype):
use rusteron_client::{AeronCError, AeronClaim};
impl messages::sbe_rt::Claim for AeronClaim {
type Error = AeronCError;
fn frame_mut(&mut self) -> &mut [u8] {
self.data()
}
fn commit(self) -> Result<(), AeronCError> {
AeronClaim::commit(self).map(drop)
}
fn abort(self) {
let _ = AeronClaim::abort(self);
}
}
send calls abort whenever it publishes nothing — an encoder error, a
length or header mismatch, and while unwinding from a panic in your closure —
so the term-buffer slot is released at once rather than after Aeron’s unblock
timeout.
No encoder writes a block’s padding (the bytes past its last field, up to the
schema’s blockLength), so they go out as the claimed region held them. An
Aeron try_claim region starts zeroed by the transport; the cost of that
zeroing is not measured here. A Claim you implement over a region the
transport reuses must hand it out zeroed, as OfferClaim (below) does.
Fixed-only messages: send_fixed
let claim = publication.try_claim_owned(HeartbeatEncoder::ENCODED_LENGTH)?;
HeartbeatEncoder::send_fixed(claim, &HeartbeatFixedFields { sequence: 7, timestamp: 0 })?;
A claim of any other size is SendError::LengthMismatch, and nothing is
written into it.
Messages with groups and var-data: send
Size the frame with the encoder’s compute_length* helpers (or the staged
EncodedLength builder for nested groups), claim that length, and hand
send a closure that writes the message. The closure receives the encoder
already wrapped on the claim, header written, and must return the message’s
terminal stage — so a frame with tails still to write does not compile:
let len = NoteEncoder::compute_length_with_header(body.len());
NoteEncoder::send(publication.try_claim_owned(len)?, |enc| {
enc.fixed(&NoteFixedFields { note_id: 1 }).body(body)
})?;
send checks that the frame is exactly the claim’s length (a claim sized
for a different frame would otherwise publish stale bytes or a truncated
message) and that it starts with the message’s own header. Each failure has
its own SendError variant: Encode for the encoder’s error,
LengthMismatch, HeaderMismatch, and Commit for the transport refusing
the frame — the only case where the claim was not aborted, because the
transport already had it.
Offer-style transports: OfferClaim
A transport that copies a finished frame, like Aeron’s offer, uses
sbe_rt::OfferClaim over a scratch buffer: the frame is encoded into the
buffer, and committing hands it to the closure. The same send and
send_fixed work unchanged, with the same checks. OfferClaim::new zeroes
the buffer, so a scratch buffer reused for every frame never puts the
previous frame’s bytes in the next one’s padding:
#![allow(unused)]
fn main() {
// Offer style: encode into `scratch`; commit hands the frame to the transport.
let mut scratch = [0u8; HeartbeatEncoder::ENCODED_LENGTH];
let mut published: Vec<Vec<u8>> = Vec::new();
let claim = sbe_rt::OfferClaim::new(&mut scratch, |frame: &[u8]| -> Result<(), &'static str> {
published.push(frame.to_vec()); // publication.offer(frame).map(drop)
Ok(())
});
HeartbeatEncoder::send_fixed(claim, &HeartbeatFixedFields { sequence: 7, timestamp: 0 })?;
assert_eq!(published.len(), 1);
}
#![allow(unused)]
fn main() {
let body = b"rebalance at close";
let len = NoteEncoder::compute_length_with_header(body.len());
let mut scratch = vec![0u8; len];
let claim = sbe_rt::OfferClaim::new(&mut scratch, |frame: &[u8]| -> Result<(), &'static str> {
assert_eq!(frame.len(), len);
Ok(())
});
// The closure gets the encoder wrapped on the claim and must return the
// finished frame: the type-state makes a half-written one a compile error.
NoteEncoder::send(claim, |enc| enc.fixed(&NoteFixedFields { note_id: 1 }).body(body))?;
}
#![allow(unused)]
fn main() {
// A claim one byte too long: nothing is offered, and the error says why.
let body = b"rebalance at close";
let len = NoteEncoder::compute_length_with_header(body.len());
let mut too_long = vec![0u8; len + 1];
let claim = sbe_rt::OfferClaim::new(&mut too_long, |_: &[u8]| -> Result<(), &'static str> {
unreachable!("an aborted claim is never offered")
});
let sent = NoteEncoder::send(claim, |enc| enc.fixed(&NoteFixedFields { note_id: 1 }).body(body));
assert_eq!(
sent,
Err(sbe_rt::SendError::LengthMismatch { claimed: len + 1, frame: len })
);
}
(From book/examples/claim-send.rs — compiled against the feature-tour codec.)
Encoding an application type
With with_source_traits, the
encoder can write any type that implements the message’s generated …Source
trait, sizing it first with encoded_length_with_header_from. encode_from
returns the terminal stage, so it is the whole send closure:
#![allow(unused)]
fn main() {
/// The application's own type: no generated struct is involved.
struct Reminder {
id: u32,
text: String,
}
impl NoteSource for Reminder {
fn note_id(&self) -> u32 {
self.id
}
fn body(&self) -> &[u8] {
self.text.as_bytes()
}
}
let reminder = Reminder { id: 9, text: "settle T+1".into() };
let len = NoteEncoder::encoded_length_with_header_from(&reminder)?;
let mut scratch = vec![0u8; len];
let claim = sbe_rt::OfferClaim::new(&mut scratch, |_: &[u8]| -> Result<(), &'static str> { Ok(()) });
NoteEncoder::send(claim, |enc| enc.encode_from(&reminder))?;
}
encode_from refuses what would not read back: Some of an optional
field’s null value (NaN for a float) is EncodeError::SomeNullValue, and a
value a domain conversion refuses is EncodeError::Conversion, each with the
field’s path and entry indices (fills[1].allocs[0].px). In a hand-written
closure, the setter of a field under with_enum_mapping or
with_composite_mapping returns sbe_rt::MappingError, which converts into
EncodeError, so enc.side(side)? works there too.
A frame written some other way: send_frame
sbe_rt::send_frame(claim, header, encode) is what send is built on: it
runs a closure that writes raw bytes into the claim and returns the frame’s
length, then applies the same length and header checks before committing.
Pass the message’s HEADER_TEMPLATE as header. Use it when the frame is not
written through one generated encoder, for example a bus that encodes
whichever message a caller hands it, and for a message whose own field, or
first group or var-data, is named send (or send_fixed): that accessor is
a method on the encoder too, so it keeps its name and the message has no
helper of that name.
Cluster clients
Cluster clients do not claim from the publication directly: use
AeronCluster::try_claim, which claims
SessionMessageHeader + payload and hands back the payload region via
ClusterClaim::payload_mut().
The cluster-ha-orderbook sample demonstrates the claim pattern in the context of a real Aeron Cluster publication loop.