@@ -20,6 +20,7 @@ use axum::response::{ErrorResponse, IntoResponse};
2020use axum:: routing:: MethodRouter ;
2121use axum:: Extension ;
2222use axum_extra:: TypedHeader ;
23+ use http:: HeaderMap ;
2324use futures:: TryStreamExt ;
2425use http:: StatusCode ;
2526use log:: { info, warn} ;
@@ -41,7 +42,7 @@ use spacetimedb_lib::bsatn;
4142use spacetimedb_lib:: db:: raw_def:: v10:: RawModuleDefV10 ;
4243use spacetimedb_lib:: db:: raw_def:: v9:: RawModuleDefV9 ;
4344use spacetimedb_lib:: de:: DeserializeSeed ;
44- use spacetimedb_lib:: { sats, AlgebraicValue , Hash , ProductValue , Timestamp } ;
45+ use spacetimedb_lib:: { sats, AlgebraicValue , GlobalTxId , Hash , ProductValue , Timestamp , TX_ID_HEADER } ;
4546use spacetimedb_schema:: auto_migrate:: {
4647 MigrationPolicy as SchemaMigrationPolicy , MigrationToken , PrettyPrintStyle as AutoMigratePrettyPrintStyle ,
4748} ;
@@ -133,6 +134,7 @@ fn map_procedure_error(e: ProcedureCallError, procedure: &str) -> (StatusCode, S
133134pub async fn call < S : ControlStateDelegate + NodeDelegate > (
134135 State ( worker_ctx) : State < S > ,
135136 Extension ( auth) : Extension < SpacetimeAuth > ,
137+ headers : HeaderMap ,
136138 Path ( CallParams {
137139 name_or_identity,
138140 reducer,
@@ -141,6 +143,10 @@ pub async fn call<S: ControlStateDelegate + NodeDelegate>(
141143 body : Bytes ,
142144) -> axum:: response:: Result < impl IntoResponse > {
143145 let caller_identity = auth. claims . identity ;
146+ let tx_id = headers
147+ . get ( TX_ID_HEADER )
148+ . and_then ( |v| v. to_str ( ) . ok ( ) )
149+ . and_then ( |s| s. parse :: < GlobalTxId > ( ) . ok ( ) ) ;
144150
145151 let args = parse_call_args ( content_type, body) ?;
146152
@@ -162,6 +168,7 @@ pub async fn call<S: ControlStateDelegate + NodeDelegate>(
162168 . call_reducer_with_return (
163169 caller_identity,
164170 Some ( connection_id) ,
171+ tx_id,
165172 None ,
166173 None ,
167174 None ,
@@ -251,6 +258,7 @@ fn parse_call_args(content_type: headers::ContentType, body: Bytes) -> axum::res
251258pub async fn prepare < S : ControlStateDelegate + NodeDelegate > (
252259 State ( worker_ctx) : State < S > ,
253260 Extension ( auth) : Extension < SpacetimeAuth > ,
261+ headers : HeaderMap ,
254262 Path ( CallParams {
255263 name_or_identity,
256264 reducer,
@@ -260,14 +268,18 @@ pub async fn prepare<S: ControlStateDelegate + NodeDelegate>(
260268) -> axum:: response:: Result < impl IntoResponse > {
261269 let args = parse_call_args ( content_type, body) ?;
262270 let caller_identity = auth. claims . identity ;
271+ let tx_id = headers
272+ . get ( TX_ID_HEADER )
273+ . and_then ( |v| v. to_str ( ) . ok ( ) )
274+ . and_then ( |s| s. parse :: < GlobalTxId > ( ) . ok ( ) ) ;
263275
264276 let ( module, Database { owner_identity, .. } ) = find_module_and_database ( & worker_ctx, name_or_identity) . await ?;
265277
266278 // 2PC prepare is a server-to-server call; no client lifecycle management needed.
267279 // call_identity_connected/disconnected submit jobs to the module's executor, which
268280 // will be blocked holding the 2PC write lock after prepare_reducer returns — deadlock.
269281 let result = module
270- . prepare_reducer ( caller_identity, None , & reducer, args)
282+ . prepare_reducer ( caller_identity, None , tx_id , & reducer, args)
271283 . await ;
272284
273285 match result {
@@ -298,6 +310,12 @@ pub struct TwoPcParams {
298310 prepare_id : String ,
299311}
300312
313+ #[ derive( Deserialize ) ]
314+ pub struct GlobalTxParams {
315+ name_or_identity : NameOrIdentity ,
316+ global_tx_id : String ,
317+ }
318+
301319/// 2PC commit endpoint: finalize a prepared transaction.
302320///
303321/// `POST /v1/database/:name_or_identity/2pc/commit/:prepare_id`
@@ -389,6 +407,30 @@ pub async fn ack_commit_2pc<S: ControlStateDelegate + NodeDelegate>(
389407 Ok ( StatusCode :: OK )
390408}
391409
410+ /// 2PC wound endpoint.
411+ ///
412+ /// `POST /v1/database/:name_or_identity/2pc/wound/:global_tx_id`
413+ pub async fn wound_2pc < S : ControlStateDelegate + NodeDelegate > (
414+ State ( worker_ctx) : State < S > ,
415+ Extension ( _auth) : Extension < SpacetimeAuth > ,
416+ Path ( GlobalTxParams {
417+ name_or_identity,
418+ global_tx_id,
419+ } ) : Path < GlobalTxParams > ,
420+ ) -> axum:: response:: Result < impl IntoResponse > {
421+ let tx_id = global_tx_id
422+ . parse :: < GlobalTxId > ( )
423+ . map_err ( |e| ( StatusCode :: BAD_REQUEST , e) . into_response ( ) ) ?;
424+ let ( module, _database) = find_module_and_database ( & worker_ctx, name_or_identity) . await ?;
425+
426+ module. wound_global_tx ( tx_id) . await . map_err ( |e| {
427+ log:: warn!( "2PC wound failed for {tx_id}: {e}" ) ;
428+ ( StatusCode :: NOT_FOUND , e) . into_response ( )
429+ } ) ?;
430+
431+ Ok ( StatusCode :: OK )
432+ }
433+
392434fn reducer_outcome_response (
393435 module : & ModuleHost ,
394436 owner_identity : & Identity ,
@@ -426,6 +468,7 @@ fn reducer_outcome_response(
426468 // TODO: different status code? this is what cloudflare uses, sorta
427469 Ok ( ( StatusCode :: from_u16 ( 530 ) . unwrap ( ) , ( * errmsg) . into_response ( ) ) )
428470 }
471+ ReducerOutcome :: Wounded ( errmsg) => Ok ( ( StatusCode :: CONFLICT , ( * errmsg) . into_response ( ) ) ) ,
429472 ReducerOutcome :: BudgetExceeded => {
430473 log:: warn!( "Node's energy budget exceeded for identity: {owner_identity} while executing {reducer}" ) ;
431474 Ok ( (
@@ -1401,6 +1444,8 @@ pub struct DatabaseRoutes<S> {
14011444 pub commit_2pc_post : MethodRouter < S > ,
14021445 /// POST: /database/:name_or_identity/2pc/abort/:prepare_id
14031446 pub abort_2pc_post : MethodRouter < S > ,
1447+ /// POST: /database/:name_or_identity/2pc/wound/:global_tx_id
1448+ pub wound_2pc_post : MethodRouter < S > ,
14041449 /// GET: /database/:name_or_identity/2pc/status/:prepare_id
14051450 pub status_2pc_get : MethodRouter < S > ,
14061451 /// POST: /database/:name_or_identity/2pc/ack-commit/:prepare_id
@@ -1433,6 +1478,7 @@ where
14331478 prepare_post : post ( prepare :: < S > ) ,
14341479 commit_2pc_post : post ( commit_2pc :: < S > ) ,
14351480 abort_2pc_post : post ( abort_2pc :: < S > ) ,
1481+ wound_2pc_post : post ( wound_2pc :: < S > ) ,
14361482 status_2pc_get : get ( status_2pc :: < S > ) ,
14371483 ack_commit_2pc_post : post ( ack_commit_2pc :: < S > ) ,
14381484 }
@@ -1463,6 +1509,7 @@ where
14631509 . route ( "/prepare/:reducer" , self . prepare_post )
14641510 . route ( "/2pc/commit/:prepare_id" , self . commit_2pc_post )
14651511 . route ( "/2pc/abort/:prepare_id" , self . abort_2pc_post )
1512+ . route ( "/2pc/wound/:global_tx_id" , self . wound_2pc_post )
14661513 . route ( "/2pc/status/:prepare_id" , self . status_2pc_get )
14671514 . route ( "/2pc/ack-commit/:prepare_id" , self . ack_commit_2pc_post ) ;
14681515
0 commit comments