@@ -568,88 +568,94 @@ impl<T: WasmInstance> WasmModuleInstance<T> {
568568
569569 /// Run the reducer as a 2PC participant PREPARE.
570570 ///
571- /// Holds the write lock (MutTxId) open until a COMMIT or ABORT decision arrives.
572- /// The flow:
573- /// 1. Run reducer (no commit).
574- /// 2. If reducer failed: send failure via `prepared_tx`; rollback; return.
575- /// 3. If reducer succeeded: insert `st_2pc_state` row; send PREPARED result via `prepared_tx`.
576- /// 4. Block on `decision_rx`:
577- /// - `true` (COMMIT): commit via `commit_and_broadcast_event`, then delete `st_2pc_state`.
578- /// - `false` (ABORT) or channel closed: roll back.
579571 /// Run the reducer as a 2PC participant PREPARE.
580572 ///
581573 /// Holds the write lock (MutTxId) open until a COMMIT or ABORT decision arrives.
582574 /// The flow:
583- /// 1. Run reducer (no commit).
575+ /// 1. Run reducer (no commit); hold open MutTxId (write lock) .
584576 /// 2. If reducer failed: send failure via `prepared_tx`; rollback; return.
585- /// 3. If reducer succeeded: insert `st_2pc_state` row; send PREPARED result via `prepared_tx`.
586- /// 4. Block on `decision_rx`:
587- /// - `true` (COMMIT): commit via `commit_and_broadcast_event`, then delete `st_2pc_state`.
588- /// - `false` (ABORT) or channel closed: roll back.
577+ /// 3. If reducer succeeded: call `flush_2pc_prepare_marker` — inserts `st_2pc_state`
578+ /// directly into committed state (bumps tx_offset), returns `TxData` for the marker.
579+ /// Forward the `TxData` to the durability worker so the PREPARE is in the commitlog.
580+ /// The write lock remains held throughout.
581+ /// 4. Signal PREPARED via `prepared_tx`.
582+ /// 5. Block on `decision_rx`:
583+ /// - `true` (COMMIT): commit main tx (reducer changes get the next tx_offset), then
584+ /// delete `st_2pc_state` in a new tx.
585+ /// - `false` (ABORT) or channel closed: roll back main tx; delete `st_2pc_state` in
586+ /// a new tx (the marker row is already in committed state from step 3).
589587 pub fn call_reducer_prepare_and_hold (
590588 & mut self ,
591589 params : CallReducerParams ,
592590 prepare_id : String ,
593591 prepared_tx : tokio:: sync:: oneshot:: Sender < ( ReducerCallResult , Option < Bytes > ) > ,
594592 decision_rx : std:: sync:: mpsc:: Receiver < bool > ,
595593 ) {
596- let ( mut tx, event, client, trapped) =
597- crate :: callgrind_flag:: invoke_allowing_callgrind ( || {
598- self . common . run_reducer_no_commit ( None , params, & mut self . instance )
599- } ) ;
594+ let stdb = self . instance . replica_ctx ( ) . relational_db ( ) . clone ( ) ;
595+
596+ // Step 1: run the reducer and hold the write lock open.
597+ let ( mut tx, event, client, trapped) = crate :: callgrind_flag:: invoke_allowing_callgrind ( || {
598+ self . common . run_reducer_no_commit ( None , params, & mut self . instance )
599+ } ) ;
600600 self . trapped = trapped;
601601
602602 let energy_quanta_used = event. energy_quanta_used ;
603603 let total_duration = event. host_execution_duration ;
604604
605605 if !matches ! ( event. status, EventStatus :: Committed ( _) ) {
606- // Reducer failed — roll back and signal failure to the waiter .
606+ // Reducer failed — roll back and signal failure; no marker was written .
607607 let res = ReducerCallResult {
608608 outcome : ReducerOutcome :: from ( & event. status ) ,
609609 energy_used : energy_quanta_used,
610610 execution_duration : total_duration,
611611 } ;
612612 let return_value = event. reducer_return_value . clone ( ) ;
613613 let _ = prepared_tx. send ( ( res, return_value) ) ;
614- // commit_and_broadcast_event handles rollback for non-Committed status.
615- commit_and_broadcast_event ( & self . common . info . subscriptions , client, event, tx) ;
614+ let _ = stdb. rollback_mut_tx ( tx) ;
616615 return ;
617616 }
618617
619- // Insert the st_2pc_state marker into the held tx atomically with the reducer's changes.
620- if let Err ( e) = tx. insert_st_2pc_state ( & prepare_id) {
621- log:: error!( "call_reducer_prepare_and_hold: failed to insert st_2pc_state for {prepare_id}: {e}" ) ;
622- }
618+ // Step 3: flush the st_2pc_state marker directly into committed state, assign
619+ // a tx_offset, and forward to durability — all while holding the write lock.
620+ let marker_tx_data = match tx. flush_2pc_prepare_marker ( & prepare_id) {
621+ Ok ( td) => std:: sync:: Arc :: new ( td) ,
622+ Err ( e) => {
623+ log:: error!( "call_reducer_prepare_and_hold: flush_2pc_prepare_marker failed for {prepare_id}: {e}" ) ;
624+ let _ = stdb. rollback_mut_tx ( tx) ;
625+ return ;
626+ }
627+ } ;
628+ stdb. request_durability_for_tx_data ( None , & marker_tx_data) ;
623629
630+ // Step 4: signal PREPARED.
624631 let res = ReducerCallResult {
625632 outcome : ReducerOutcome :: from ( & event. status ) ,
626633 energy_used : energy_quanta_used,
627634 execution_duration : total_duration,
628635 } ;
629636 let return_value = event. reducer_return_value . clone ( ) ;
630- // Signal PREPARED — the coordinator can now send COMMIT or ABORT.
631637 let _ = prepared_tx. send ( ( res, return_value) ) ;
632638
633- // Block the executor thread until we receive a decision.
639+ // Step 5: block the executor thread until we receive a decision.
634640 let commit = decision_rx. recv ( ) . unwrap_or ( false ) ;
635641
636642 if commit {
643+ // Delete the marker in the same tx as the reducer changes so they are
644+ // committed atomically. The row is in committed state (inserted by
645+ // flush_2pc_prepare_marker), so delete_st_2pc_state finds it via iter.
646+ if let Err ( e) = tx. delete_st_2pc_state ( & prepare_id) {
647+ log:: error!( "call_reducer_prepare_and_hold: failed to delete st_2pc_state for {prepare_id}: {e}" ) ;
648+ }
637649 commit_and_broadcast_event ( & self . common . info . subscriptions , client, event, tx) ;
638-
639- // Delete the st_2pc_state row in a new tx so recovery knows COMMIT is done.
640- let stdb = self . instance . replica_ctx ( ) . relational_db ( ) ;
650+ } else {
651+ // ABORT: roll back reducer changes (tx_state discarded).
652+ // The marker row is already in committed state; clean it up in a new tx.
653+ let _ = stdb. rollback_mut_tx ( tx) ;
641654 if let Err ( e) = stdb. with_auto_commit :: < _ , _ , anyhow:: Error > ( Workload :: Internal , |del_tx| {
642655 Ok ( del_tx. delete_st_2pc_state ( & prepare_id) ?)
643656 } ) {
644- log:: error!( "call_reducer_prepare_and_hold: failed to delete st_2pc_state for {prepare_id}: {e}" ) ;
657+ log:: error!( "call_reducer_prepare_and_hold: abort: failed to delete st_2pc_state for {prepare_id}: {e}" ) ;
645658 }
646- } else {
647- // ABORT: roll back by passing a failure event.
648- let abort_event = ModuleEvent {
649- status : EventStatus :: FailedInternal ( "2PC abort" . into ( ) ) ,
650- ..event
651- } ;
652- commit_and_broadcast_event ( & self . common . info . subscriptions , None , abort_event, tx) ;
653659 }
654660 }
655661
@@ -975,10 +981,7 @@ impl InstanceCommon {
975981 ) ;
976982 let mut req = client. post ( & url) ;
977983 if let Some ( ref token) = auth_token {
978- req = req. header (
979- http:: header:: AUTHORIZATION ,
980- format ! ( "Bearer {token}" ) ,
981- ) ;
984+ req = req. header ( http:: header:: AUTHORIZATION , format ! ( "Bearer {token}" ) ) ;
982985 }
983986 match req. send ( ) . await {
984987 Ok ( resp) if resp. status ( ) . is_success ( ) => {
@@ -991,9 +994,7 @@ impl InstanceCommon {
991994 ) ;
992995 }
993996 Err ( e) => {
994- log:: error!(
995- "2PC {action}: transport error for {prepare_id} on {db_identity}: {e}"
996- ) ;
997+ log:: error!( "2PC {action}: transport error for {prepare_id} on {db_identity}: {e}" ) ;
997998 }
998999 }
9991000 }
@@ -1026,7 +1027,12 @@ impl InstanceCommon {
10261027 tx : Option < MutTxId > ,
10271028 params : CallReducerParams ,
10281029 inst : & mut I ,
1029- ) -> ( MutTxId , ModuleEvent , Option < Arc < crate :: client:: ClientConnectionSender > > , bool ) {
1030+ ) -> (
1031+ MutTxId ,
1032+ ModuleEvent ,
1033+ Option < Arc < crate :: client:: ClientConnectionSender > > ,
1034+ bool ,
1035+ ) {
10301036 let CallReducerParams {
10311037 timestamp,
10321038 caller_identity,
0 commit comments