33use std:: collections:: { BTreeMap , BTreeSet } ;
44use std:: sync:: { Arc , Mutex } ;
55
6- use tokio:: sync:: oneshot;
6+ use tokio:: sync:: { mpsc , oneshot} ;
77
88/// Calvin transaction identity in the sequencer-assigned coordinate space.
99///
@@ -55,6 +55,14 @@ struct PendingCompletion {
5555 /// tally to change flush/drop behavior yet (that is a follow-up); the
5656 /// leader's local decision still drives.
5757 votes : BTreeMap < u32 , bool > ,
58+ /// The authoritative commit/abort verdict once applied from a replicated
59+ /// `SequencerEntry::Verdict`. `None` until the leader's verdict is applied.
60+ /// Currently observed-only: nothing reads this to change flush/drop yet.
61+ verdict : Option < bool > ,
62+ /// Dedup guard: set the first time the vote tally becomes complete so the
63+ /// verdict signal is emitted exactly once, even if a retry re-proposes a
64+ /// vote and re-tallies afterwards.
65+ verdict_proposed : bool ,
5866}
5967
6068impl PendingCompletion {
@@ -66,6 +74,8 @@ impl PendingCompletion {
6674 mismatched : false ,
6775 routing_failed : None ,
6876 votes : BTreeMap :: new ( ) ,
77+ verdict : None ,
78+ verdict_proposed : false ,
6979 }
7080 }
7181
@@ -86,14 +96,33 @@ struct Inner {
8696 completions : BTreeMap < TxnId , PendingCompletion > ,
8797}
8898
89- #[ derive( Default ) ]
9099pub struct CalvinCompletionRegistry {
91100 inner : Mutex < Inner > ,
101+ /// Emits `(txn, commit)` exactly once when a staged cross-shard txn's vote
102+ /// tally becomes complete (all expected participants voted). The paired
103+ /// receiver lives in the `SequencerService`, whose leader-guarded arm turns
104+ /// the signal into a `SequencerEntry::Verdict` proposal.
105+ verdict_tx : mpsc:: Sender < ( TxnId , bool ) > ,
92106}
93107
94108impl CalvinCompletionRegistry {
95- pub fn new ( ) -> Arc < Self > {
96- Arc :: new ( Self :: default ( ) )
109+ /// Construct a registry wired to a verdict signal channel. The paired
110+ /// receiver must be handed to the `SequencerService` on this node so the
111+ /// leader can propose the aggregated verdict.
112+ pub fn new ( verdict_tx : mpsc:: Sender < ( TxnId , bool ) > ) -> Arc < Self > {
113+ Arc :: new ( Self {
114+ inner : Mutex :: new ( Inner :: default ( ) ) ,
115+ verdict_tx,
116+ } )
117+ }
118+
119+ /// Construct a registry with no verdict consumer: the signal channel is
120+ /// created internally and its receiver dropped, so vote-complete transitions
121+ /// are still computed and stored but never delivered to a sequencer service.
122+ /// For callers (and tests) that do not drive verdict proposal.
123+ pub fn new_detached ( ) -> Arc < Self > {
124+ let ( verdict_tx, _verdict_rx) = mpsc:: channel ( 1 ) ;
125+ Self :: new ( verdict_tx)
97126 }
98127
99128 pub fn register_submission ( & self , inbox_seq : u64 ) -> oneshot:: Receiver < ( u64 , u32 , usize ) > {
@@ -280,6 +309,70 @@ impl CalvinCompletionRegistry {
280309 . entry ( txn_id)
281310 . or_insert_with ( || PendingCompletion :: new ( 0 ) ) ;
282311 entry. votes . insert ( vshard, commit) ;
312+
313+ // On the transition to a complete tally, emit the aggregated verdict
314+ // exactly once. `expected_participants` is seeded on the sequencer
315+ // leader (the participant count arrives with the assignment); a replica
316+ // whose registry never received that seed keeps `expected == 0` and
317+ // never emits here — correct, because only the leader proposes the
318+ // verdict. Deterministic all-replica seeding for failover is a
319+ // follow-up. The `verdict_proposed` guard dedups a re-tally caused by a
320+ // re-proposed vote on retry.
321+ if entry. expected_participants > 0
322+ && entry. votes . len ( ) == entry. expected_participants
323+ && !entry. verdict_proposed
324+ {
325+ let commit_all = entry. votes . values ( ) . all ( |& v| v) ;
326+ entry. verdict_proposed = true ;
327+ // Non-blocking: a full channel drops the signal, mirroring how the
328+ // apply fan-out drops on backpressure. A dropped signal is a missed
329+ // proposal (the leader re-drives on the next tally that isn't
330+ // deduped), never lost state — the verdict is stored separately via
331+ // `note_verdict` when the leader's proposal is applied.
332+ let _ = self . verdict_tx . try_send ( ( txn_id, commit_all) ) ;
333+ }
334+ }
335+
336+ /// Store the authoritative commit/abort verdict for `txn`, applied from a
337+ /// replicated `SequencerEntry::Verdict` on every replica.
338+ ///
339+ /// Idempotent: re-applying the same verdict is a no-op. A verdict that
340+ /// differs from a previously stored one is a determinism bug (the tally is
341+ /// computed deterministically from replicated votes) — it is logged at
342+ /// `warn` and the latest value is stored. Currently observed-only: nothing
343+ /// reads the stored verdict to change flush/drop behavior yet.
344+ pub fn note_verdict ( & self , txn : TxnId , commit : bool ) {
345+ let mut inner = self . inner . lock ( ) . unwrap_or_else ( |p| p. into_inner ( ) ) ;
346+ let entry = inner
347+ . completions
348+ . entry ( txn)
349+ . or_insert_with ( || PendingCompletion :: new ( 0 ) ) ;
350+ match entry. verdict {
351+ Some ( existing) if existing != commit => {
352+ tracing:: warn!(
353+ epoch = txn. epoch,
354+ position = txn. position,
355+ existing,
356+ proposed = commit,
357+ "calvin verdict differs from a previously applied one; \
358+ determinism bug — overwriting with the latest"
359+ ) ;
360+ entry. verdict = Some ( commit) ;
361+ }
362+ Some ( _) => { }
363+ None => entry. verdict = Some ( commit) ,
364+ }
365+ }
366+
367+ /// Test/inspection accessor: returns the stored commit/abort verdict for
368+ /// `txn_id`, or `None` if no verdict has been applied yet.
369+ pub fn verdict ( & self , txn_id : TxnId ) -> Option < bool > {
370+ self . inner
371+ . lock ( )
372+ . unwrap_or_else ( |p| p. into_inner ( ) )
373+ . completions
374+ . get ( & txn_id)
375+ . and_then ( |entry| entry. verdict )
283376 }
284377
285378 /// Test/inspection accessor: returns the current per-vshard vote tally for
@@ -311,7 +404,7 @@ mod tests {
311404
312405 #[ tokio:: test]
313406 async fn completion_entry_removed_after_all_acks ( ) {
314- let reg = CalvinCompletionRegistry :: new ( ) ;
407+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
315408 let txn = TxnId :: new ( 7 , 0 ) ;
316409 reg. note_assigned ( 1 , txn, 2 ) ;
317410 let rx = reg. register_completion ( txn, 2 ) ;
@@ -330,7 +423,7 @@ mod tests {
330423
331424 #[ tokio:: test]
332425 async fn completion_entry_removed_when_register_arrives_after_acks ( ) {
333- let reg = CalvinCompletionRegistry :: new ( ) ;
426+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
334427 let txn = TxnId :: new ( 9 , 3 ) ;
335428 reg. note_assigned ( 1 , txn, 2 ) ;
336429 reg. note_completion_ack ( txn, 10 ) ;
@@ -350,7 +443,7 @@ mod tests {
350443
351444 #[ tokio:: test]
352445 async fn mismatch_arriving_before_register_fires_mismatch ( ) {
353- let reg = CalvinCompletionRegistry :: new ( ) ;
446+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
354447 let txn = TxnId :: new ( 11 , 1 ) ;
355448 reg. note_assigned ( 1 , txn, 2 ) ;
356449 // Mismatch observed before the coordinator registers its waiter: the
@@ -369,7 +462,7 @@ mod tests {
369462
370463 #[ tokio:: test]
371464 async fn register_before_mismatch_fires_mismatch ( ) {
372- let reg = CalvinCompletionRegistry :: new ( ) ;
465+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
373466 let txn = TxnId :: new ( 12 , 5 ) ;
374467 reg. note_assigned ( 1 , txn, 2 ) ;
375468 let rx = reg. register_completion ( txn, 2 ) ;
@@ -391,7 +484,7 @@ mod tests {
391484 // register_completion must seed expected_participants from the assignment.
392485 // Without the seed (or with the is_complete>0 guard absent) this would
393486 // spuriously fire Completed with zero acks.
394- let reg = CalvinCompletionRegistry :: new ( ) ;
487+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
395488 let txn = TxnId :: new ( 21 , 0 ) ;
396489 let rx = reg. register_completion ( txn, 1 ) ;
397490 assert_eq ! (
@@ -411,7 +504,7 @@ mod tests {
411504 // coordinator calls register_completion. With expected_participants still
412505 // unknown (0), the ack must persist without firing/evicting; the later
413506 // register_completion seeds the count and then completes.
414- let reg = CalvinCompletionRegistry :: new ( ) ;
507+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
415508 let txn = TxnId :: new ( 22 , 0 ) ;
416509 reg. note_completion_ack ( txn, 7 ) ;
417510 assert_eq ! (
@@ -427,7 +520,7 @@ mod tests {
427520
428521 #[ tokio:: test]
429522 async fn routing_failed_arriving_before_register_fires_failed ( ) {
430- let reg = CalvinCompletionRegistry :: new ( ) ;
523+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
431524 let txn = TxnId :: new ( 14 , 1 ) ;
432525 reg. note_assigned ( 1 , txn, 2 ) ;
433526 // Routing failure observed before the coordinator registers its
@@ -452,7 +545,7 @@ mod tests {
452545
453546 #[ tokio:: test]
454547 async fn register_before_routing_failed_fires_failed ( ) {
455- let reg = CalvinCompletionRegistry :: new ( ) ;
548+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
456549 let txn = TxnId :: new ( 15 , 4 ) ;
457550 reg. note_assigned ( 1 , txn, 2 ) ;
458551 let rx = reg. register_completion ( txn, 2 ) ;
@@ -475,7 +568,7 @@ mod tests {
475568
476569 #[ tokio:: test]
477570 async fn routing_failed_takes_precedence_over_pending_acks_and_mismatch ( ) {
478- let reg = CalvinCompletionRegistry :: new ( ) ;
571+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
479572 let txn = TxnId :: new ( 16 , 0 ) ;
480573 reg. note_assigned ( 1 , txn, 2 ) ;
481574 // No waiter registered yet: an ack and a mismatch both persist onto
@@ -505,7 +598,7 @@ mod tests {
505598
506599 #[ tokio:: test]
507600 async fn note_vote_tallies_one_vote_per_vshard ( ) {
508- let reg = CalvinCompletionRegistry :: new ( ) ;
601+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
509602 let txn = TxnId :: new ( 30 , 0 ) ;
510603 reg. note_vote ( txn, 1 , true ) ;
511604 reg. note_vote ( txn, 2 , false ) ;
@@ -515,9 +608,47 @@ mod tests {
515608 assert_eq ! ( tally. get( & 2 ) , Some ( & false ) ) ;
516609 }
517610
611+ #[ tokio:: test]
612+ async fn complete_all_true_tally_emits_commit_verdict_once ( ) {
613+ let ( tx, mut rx) = mpsc:: channel ( 8 ) ;
614+ let reg = CalvinCompletionRegistry :: new ( tx) ;
615+ let txn = TxnId :: new ( 30 , 1 ) ;
616+ // Seed the expected participant count the way the leader does.
617+ reg. note_assigned ( 1 , txn, 2 ) ;
618+
619+ reg. note_vote ( txn, 1 , true ) ;
620+ // First vote: tally incomplete (1 of 2), nothing emitted yet.
621+ assert ! ( rx. try_recv( ) . is_err( ) ) ;
622+
623+ reg. note_vote ( txn, 2 , true ) ;
624+ // Second vote completes the tally → commit verdict emitted exactly once.
625+ assert_eq ! ( rx. try_recv( ) . expect( "verdict emitted" ) , ( txn, true ) ) ;
626+
627+ // A re-proposed vote re-tallies but must NOT emit again (dedup).
628+ reg. note_vote ( txn, 2 , true ) ;
629+ assert ! (
630+ rx. try_recv( ) . is_err( ) ,
631+ "dedup: verdict must emit only on the first complete tally"
632+ ) ;
633+ }
634+
635+ #[ tokio:: test]
636+ async fn complete_tally_with_one_abort_emits_abort_verdict ( ) {
637+ let ( tx, mut rx) = mpsc:: channel ( 8 ) ;
638+ let reg = CalvinCompletionRegistry :: new ( tx) ;
639+ let txn = TxnId :: new ( 31 , 0 ) ;
640+ reg. note_assigned ( 1 , txn, 2 ) ;
641+
642+ reg. note_vote ( txn, 1 , true ) ;
643+ reg. note_vote ( txn, 2 , false ) ;
644+ // Any abort vote makes the aggregated verdict false.
645+ assert_eq ! ( rx. try_recv( ) . expect( "verdict emitted" ) , ( txn, false ) ) ;
646+ assert ! ( rx. try_recv( ) . is_err( ) ) ;
647+ }
648+
518649 #[ tokio:: test]
519650 async fn mismatch_takes_precedence_over_pending_acks ( ) {
520- let reg = CalvinCompletionRegistry :: new ( ) ;
651+ let reg = CalvinCompletionRegistry :: new_detached ( ) ;
521652 let txn = TxnId :: new ( 13 , 2 ) ;
522653 reg. note_assigned ( 1 , txn, 2 ) ;
523654 let rx = reg. register_completion ( txn, 2 ) ;
0 commit comments