11//! W1b ahead-firing batch writer — the kanban board IS the write-ahead log (M24).
22//!
3- //! `cast()` records intent moves AHEAD of any storage ack; `ack()` confirms;
3+ //! `cast()` records intent moves AHEAD of any storage ack; `ack()` confirms
4+ //! at the `LanceVersion` the sink assigned (the CastId↔LanceVersion join
5+ //! wiring the WAL into the temporal classifier; see `crate::temporal`);
46//! `unacked()` is the crash-replay surface. Payload-generic: the writer never
57//! inspects `P` (DTO purity — ownership rides the cast pairing, never the DTO).
68//!
2224//! mint a parallel `KanbanMove`; see the D-MBX-A6 Outcome adapter context in
2325//! `crate::strategy::style_strategy`.
2426
25- use std:: collections:: HashMap ;
27+ use std:: collections:: { BTreeMap , HashMap } ;
2628
2729use lance_graph_contract:: collapse_gate:: MailboxId ;
2830use lance_graph_contract:: kanban:: KanbanMove ;
2931
32+ use crate :: temporal:: LanceVersion ;
33+
3034/// Identity of one `cast()` — a write-ahead intent record on the kanban board.
31- #[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash ) ]
35+ ///
36+ /// `next_id` is monotonically increasing, so `CastId`'s derived `Ord` gives
37+ /// insertion order — this is what lets `board`/`acked` use `BTreeMap` and
38+ /// have `unacked()` iterate in cast order for free.
39+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , PartialOrd , Ord , Hash ) ]
3240pub struct CastId ( pub u64 ) ;
3341
3442/// Ahead-firing batch writer: intent (`cast`) is visible on the board before
@@ -40,10 +48,15 @@ pub struct BatchWriter<P> {
4048 /// Board: intent moves recorded per cast, keyed by `CastId`, alongside the
4149 /// mailbox the cast was recorded on behalf of. Visible between `cast()`
4250 /// and `ack()` (and beyond, for crash-replay via `unacked()`).
43- board : HashMap < CastId , ( MailboxId , Vec < KanbanMove > ) > ,
44- /// Casts that have been confirmed (acked). A cast present in `board` but
45- /// absent from `acked` is the crash-replay surface (`unacked()`).
46- acked : std:: collections:: HashSet < CastId > ,
51+ /// `BTreeMap` (not `HashMap`) so iteration is in ascending `CastId` order
52+ /// — i.e. cast (insertion) order, matching `unacked()`'s ordering contract.
53+ board : BTreeMap < CastId , ( MailboxId , Vec < KanbanMove > ) > ,
54+ /// Casts that have been confirmed (acked), mapped to the `LanceVersion`
55+ /// the sink assigned when it flushed — the CastId↔LanceVersion join
56+ /// wiring the WAL into the temporal classifier (see `crate::temporal`).
57+ /// A cast present in `board` but absent from `acked` is the crash-replay
58+ /// surface (`unacked()`).
59+ acked : BTreeMap < CastId , LanceVersion > ,
4760 /// W1c delegation cache: `on_behalf` mailbox -> resolved owner mailbox.
4861 delegation_cache : HashMap < MailboxId , MailboxId > ,
4962 /// Payloads recorded per cast (payload-generic; the writer never inspects `P`).
@@ -61,8 +74,8 @@ impl<P> BatchWriter<P> {
6174 pub fn new ( ) -> Self {
6275 Self {
6376 next_id : 0 ,
64- board : HashMap :: new ( ) ,
65- acked : std :: collections :: HashSet :: new ( ) ,
77+ board : BTreeMap :: new ( ) ,
78+ acked : BTreeMap :: new ( ) ,
6679 delegation_cache : HashMap :: new ( ) ,
6780 pending_payloads : Vec :: new ( ) ,
6881 }
@@ -71,47 +84,58 @@ impl<P> BatchWriter<P> {
7184 /// AHEAD: records the intent (moves visible on the board) BEFORE any ack.
7285 /// Returns the cast id.
7386 ///
74- /// Probe-first skeleton (D-V3-W1e): body pending — see
75- /// `tests/w1_probes.rs::probe_ahead_update_ordering` /
76- /// `probe_kill_after_cast_replay`.
77- pub fn cast ( & mut self , _on_behalf : MailboxId , _moves : Vec < KanbanMove > , _payload : P ) -> CastId {
78- todo ! ( "W1b" )
87+ /// "Melden macht frei" (plan Addendum-7): casting is REPORTING, never
88+ /// refused because earlier casts on the same mailbox are still unacked —
89+ /// stacked casts are stacked WAL entries, each with its own `CastId`.
90+ pub fn cast ( & mut self , on_behalf : MailboxId , moves : Vec < KanbanMove > , payload : P ) -> CastId {
91+ let cast = CastId ( self . next_id ) ;
92+ self . next_id += 1 ;
93+ self . board . insert ( cast, ( on_behalf, moves) ) ;
94+ self . pending_payloads . push ( ( cast, payload) ) ;
95+ cast
7996 }
8097
81- /// Confirmation — marks the cast acked.
82- ///
83- /// Probe-first skeleton (D-V3-W1e): body pending — see
84- /// `tests/w1_probes.rs::probe_ahead_update_ordering`.
85- pub fn ack ( & mut self , _cast : CastId ) {
86- todo ! ( "W1b" )
98+ /// Confirmation — marks the cast acked at the Lance version the sink
99+ /// assigned (the CastId↔LanceVersion join wiring the WAL into the
100+ /// temporal classifier; see planner `temporal.rs`).
101+ pub fn ack ( & mut self , cast : CastId , version : LanceVersion ) {
102+ self . acked . insert ( cast, version) ;
87103 }
88104
89- /// Crash-replay surface (M24): casts recorded but not yet acked.
90- ///
91- /// Probe-first skeleton (D-V3-W1e): body pending — see
92- /// `tests/w1_probes.rs::probe_kill_after_cast_replay`.
105+ /// The `LanceVersion` a cast was acked at, if it has been acked.
106+ #[ must_use]
107+ pub fn acked_version ( & self , cast : CastId ) -> Option < LanceVersion > {
108+ self . acked . get ( & cast) . copied ( )
109+ }
110+
111+ /// Crash-replay surface (M24): casts recorded but not yet acked, in
112+ /// ascending `CastId` (cast) order.
113+ #[ must_use]
93114 pub fn unacked ( & self ) -> Vec < CastId > {
94- todo ! ( "W1b" )
115+ self . board
116+ . keys ( )
117+ . filter ( |cast| !self . acked . contains_key ( cast) )
118+ . copied ( )
119+ . collect ( )
95120 }
96121
97122 /// Board read: intent moves recorded for a cast (visible between cast and ack).
98- ///
99- /// Probe-first skeleton (D-V3-W1e): body pending — see
100- /// `tests/w1_probes.rs::probe_ahead_update_ordering` /
101- /// `probe_kill_after_cast_replay`.
102- pub fn intent_moves ( & self , _cast : CastId ) -> Option < & [ KanbanMove ] > {
103- todo ! ( "W1b" )
123+ #[ must_use]
124+ pub fn intent_moves ( & self , cast : CastId ) -> Option < & [ KanbanMove ] > {
125+ self . board . get ( & cast) . map ( |( _, moves) | moves. as_slice ( ) )
104126 }
105127
106128 /// W1c delegation cache: resolve an owner once, cache; returns `(owner, was_cache_hit)`.
107- ///
108- /// Probe-first skeleton (D-V3-W1e): body pending — see
109- /// `tests/w1_probes.rs::probe_delegation_miss_then_hit`.
110129 pub fn resolve_owner (
111130 & mut self ,
112- _on_behalf : MailboxId ,
113- _resolver : impl FnOnce ( MailboxId ) -> MailboxId ,
131+ on_behalf : MailboxId ,
132+ resolver : impl FnOnce ( MailboxId ) -> MailboxId ,
114133 ) -> ( MailboxId , bool ) {
115- todo ! ( "W1c" )
134+ if let Some ( & owner) = self . delegation_cache . get ( & on_behalf) {
135+ return ( owner, true ) ;
136+ }
137+ let owner = resolver ( on_behalf) ;
138+ self . delegation_cache . insert ( on_behalf, owner) ;
139+ ( owner, false )
116140 }
117141}
0 commit comments