Rust trait objects for queue delivery policies — Box and the dispatcher that swaps backends
Put the receive mode behind the trait, not behind a match you will keep extending.
<!-- hal:authoritative:yaml -->
*A Vec of concrete structs stays closed. A Vec<Box<dyn Trait>> stays open. Put the receive mode behind the trait, not behind a match you will keep extending.*
§I. Frame
This morning's Duha session taught the Object Ledger and the Dyn Pointer from TRPL chapter 18: Rust keeps data-plus-methods and encapsulation, refuses classical inheritance, and uses Box<dyn Trait> when the set of types is open. The book's GUI example stores Box<dyn Draw> in a Screen and calls draw on each.
Today's Ops lesson lists Azure Service Bus queues. The ARM surface reports delivery ceilings and dead-letter depth. The receive mode (peek-lock vs receive-and-delete) and session binding live on the client. That split is a natural trait-object drill: one trait, three concrete policies, one dispatcher that does not know the concrete types.
The crate queue-delivery sits under pkg/. It has no dependencies.
§II. The trait and three backends
pub trait DeliveryPolicy {
fn name(&self) -> &str;
fn receive(&self, msg: &Message) -> Outcome;
}
pub struct PeekLock { pub max_delivery: u32 }
pub struct ReceiveAndDelete;
pub struct SessionAware { pub session: String, pub max_delivery: u32 }
Each impl DeliveryPolicy returns a different Outcome:
- PeekLock returns
Lockedwhiledelivery_count < max_delivery, elseDeadLetteredwith reasonMaxDeliveryCountExceeded. - ReceiveAndDelete always returns
Deleted. There is no settle step and no DLQ path in this offline model. - SessionAware returns
SessionHeld { session }under the ceiling, else the same dead-letter outcome as peek-lock.
Object safety holds: no Self returned, no generic methods. The trait is a thin Dyn Pointer target, same rules as Draw in ch18-02.
§III. The Open Collection
TRPL's Screen holds components: Vec<Box<dyn Draw>> and run calls draw on each. The dispatcher here is the same shape with a queue name:
pub struct Dispatcher {
policies: Vec<Box<dyn DeliveryPolicy>>,
}
impl Dispatcher {
pub fn run(&self, messages: &[Message]) -> Vec<(String, Outcome)> {
let mut out = Vec::new();
for policy in &self.policies {
for msg in messages {
out.push((policy.name().to_string(), policy.receive(msg)));
}
}
out
}
}
Construction stays open at the call site:
let d = Dispatcher::new(vec![
Box::new(PeekLock { max_delivery: 10 }),
Box::new(ReceiveAndDelete),
Box::new(SessionAware { session: "order-7".into(), max_delivery: 10 }),
]);
Generics would force one concrete type per Dispatcher. Trait objects let one Vec hold all three. That is the Open Collection move from Duha, applied to receive policies instead of GUI widgets.
Named technique: Policy Swap. Add a fourth backend later by writing one struct and one impl. Do not open the dispatcher and add another match arm.
§IV. What the tests pin
Five tests in pkg/queue-delivery:
| Test | Pins |
|---|---|
| peek-lock under ceiling | Locked |
| peek-lock at ceiling | DeadLettered / MaxDeliveryCountExceeded |
| receive-and-delete | Deleted even at high delivery count |
| session | SessionHeld carries the session string |
| dispatcher | three Box<dyn DeliveryPolicy> rows for one poison message |
cargo test on the box: 5 passed. cargo run --bin demo prints six lines (three policies times two messages), including a DLQ row for the poison message under peek-lock and session policies.
§V. What not to do
- Encoding receive mode as an enum you match forever inside
Dispatcher::run. That closes the set the trait object was meant to keep open. - Putting
max_deliveryonly on the dispatcher. Peek-lock and session each need their own ceiling. - Claiming receive-and-delete participates in
maxDeliveryCountthe same way peek-lock does. Settlement never happens; the broker already deleted the message. - Pre-empting TRPL Patterns (#22). This lesson stops at trait objects.
§VI. Close instruction
Clone pkg/queue-delivery. Add a fourth policy struct that always dead-letters with reason FilterFailure, push Box::new(...) into the dispatcher vec, and extend the dispatcher test. If you had to edit run's match arms, you left the Open Collection path.
Related
- Ops: Service Bus queue DLQ census (same trio)
- Cert: AZ-900 three messaging shapes (same trio)
- Duha: OOP / trait objects (ch18)
- — Draw,
Box<dyn Draw>, Screen::run