|
11 | 11 | /// `on_result_reducer` (read from the outbox table's schema) and deletes the st_outbound_msg row. |
12 | 12 | /// 6. Enforces sequential delivery per target database: msg N+1 is only delivered after N is done. |
13 | 13 | use crate::db::relational_db::RelationalDB; |
14 | | -use crate::host::module_host::WeakModuleHost; |
15 | | -use crate::host::FunctionArgs; |
| 14 | +use crate::energy::EnergyQuanta; |
| 15 | +use crate::host::module_host::{CallReducerParams, ModuleInfo, WeakModuleHost}; |
| 16 | +use crate::host::{FunctionArgs, ReducerCallResult, ReducerOutcome}; |
| 17 | +use anyhow::anyhow; |
16 | 18 | use bytes::Bytes; |
17 | | -use spacetimedb_datastore::execution_context::Workload; |
| 19 | +use spacetimedb_datastore::execution_context::{ReducerContext, Workload}; |
| 20 | +use spacetimedb_datastore::locking_tx_datastore::MutTxId; |
18 | 21 | use spacetimedb_datastore::system_tables::{st_inbound_msg_result_status, StOutboundMsgRow, ST_OUTBOUND_MSG_ID}; |
19 | 22 | use spacetimedb_datastore::traits::IsolationLevel; |
20 | 23 | use spacetimedb_lib::{AlgebraicValue, Identity, ProductValue}; |
@@ -224,6 +227,119 @@ fn outcome_to_result(outcome: &DeliveryOutcome) -> (u8, Bytes) { |
224 | 227 | } |
225 | 228 | } |
226 | 229 |
|
| 230 | +pub(crate) type ReducerSuccessAction = Box<dyn FnOnce(&mut MutTxId, &Option<Bytes>) -> anyhow::Result<()> + Send>; |
| 231 | + |
| 232 | +fn reducer_workload(module: &ModuleInfo, params: &CallReducerParams) -> Workload { |
| 233 | + let reducer_def = module.module_def.reducer_by_id(params.reducer_id); |
| 234 | + Workload::Reducer(ReducerContext { |
| 235 | + name: reducer_def.name.clone(), |
| 236 | + caller_identity: params.caller_identity, |
| 237 | + caller_connection_id: params.caller_connection_id, |
| 238 | + timestamp: params.timestamp, |
| 239 | + arg_bsatn: params.args.get_bsatn().clone(), |
| 240 | + }) |
| 241 | +} |
| 242 | + |
| 243 | +fn duplicate_result_from_row(row: spacetimedb_datastore::system_tables::StInboundMsgRow) -> ReducerCallResult { |
| 244 | + let outcome = match row.result_status { |
| 245 | + st_inbound_msg_result_status::SUCCESS => ReducerOutcome::Committed, |
| 246 | + st_inbound_msg_result_status::REDUCER_ERROR => ReducerOutcome::Failed(Box::new( |
| 247 | + String::from_utf8_lossy(&row.result_payload).into_owned().into_boxed_str(), |
| 248 | + )), |
| 249 | + status => { |
| 250 | + log::warn!( |
| 251 | + "IDC: unexpected inbound dedup result_status={} for sender {}", |
| 252 | + status, |
| 253 | + Identity::from(row.database_identity) |
| 254 | + ); |
| 255 | + ReducerOutcome::Failed(Box::new("unexpected inbound dedup result status".into())) |
| 256 | + } |
| 257 | + }; |
| 258 | + |
| 259 | + ReducerCallResult { |
| 260 | + outcome, |
| 261 | + reducer_return_value: (row.result_status == st_inbound_msg_result_status::SUCCESS).then_some(row.result_payload), |
| 262 | + energy_used: EnergyQuanta::ZERO, |
| 263 | + execution_duration: Duration::ZERO, |
| 264 | + tx_offset: None, |
| 265 | + } |
| 266 | +} |
| 267 | + |
| 268 | +fn record_failed_inbound_result(db: &RelationalDB, sender_identity: Identity, sender_msg_id: u64, error: &str) { |
| 269 | + let mut dedup_tx = db.begin_mut_tx(IsolationLevel::Serializable, Workload::Internal); |
| 270 | + if let Err(e) = dedup_tx.upsert_inbound_last_msg( |
| 271 | + sender_identity, |
| 272 | + sender_msg_id, |
| 273 | + st_inbound_msg_result_status::REDUCER_ERROR, |
| 274 | + error.to_string().into(), |
| 275 | + ) { |
| 276 | + log::error!("IDC: failed to record reducer error in dedup table for sender {sender_identity}: {e}"); |
| 277 | + let _ = db.rollback_mut_tx(dedup_tx); |
| 278 | + } else if let Err(e) = db.commit_tx(dedup_tx) { |
| 279 | + log::error!("IDC: failed to commit dedup error record for sender {sender_identity}: {e}"); |
| 280 | + } |
| 281 | +} |
| 282 | + |
| 283 | +pub(crate) fn call_reducer_from_database<F>( |
| 284 | + module: &ModuleInfo, |
| 285 | + db: &RelationalDB, |
| 286 | + params: CallReducerParams, |
| 287 | + sender_identity: Identity, |
| 288 | + sender_msg_id: u64, |
| 289 | + call_reducer: F, |
| 290 | +) -> (ReducerCallResult, bool) |
| 291 | +where |
| 292 | + F: FnOnce(Option<MutTxId>, CallReducerParams, ReducerSuccessAction) -> (ReducerCallResult, bool), |
| 293 | +{ |
| 294 | + let tx = db.begin_mut_tx(IsolationLevel::Serializable, reducer_workload(module, ¶ms)); |
| 295 | + if let Some(row) = tx.get_inbound_msg_row(sender_identity) |
| 296 | + && sender_msg_id <= row.last_outbound_msg |
| 297 | + { |
| 298 | + let _ = db.rollback_mut_tx(tx); |
| 299 | + return (duplicate_result_from_row(row), false); |
| 300 | + } |
| 301 | + |
| 302 | + let (result, trapped) = call_reducer( |
| 303 | + Some(tx), |
| 304 | + params, |
| 305 | + Box::new(move |tx, reducer_return_value| { |
| 306 | + tx.upsert_inbound_last_msg( |
| 307 | + sender_identity, |
| 308 | + sender_msg_id, |
| 309 | + st_inbound_msg_result_status::SUCCESS, |
| 310 | + reducer_return_value.clone().unwrap_or_default(), |
| 311 | + ) |
| 312 | + .map_err(anyhow::Error::from) |
| 313 | + }), |
| 314 | + ); |
| 315 | + |
| 316 | + if let ReducerOutcome::Failed(err) = &result.outcome { |
| 317 | + record_failed_inbound_result(db, sender_identity, sender_msg_id, err); |
| 318 | + } |
| 319 | + |
| 320 | + (result, trapped) |
| 321 | +} |
| 322 | + |
| 323 | +pub(crate) fn call_reducer_delete_outbound_on_success<F>( |
| 324 | + module: &ModuleInfo, |
| 325 | + db: &RelationalDB, |
| 326 | + params: CallReducerParams, |
| 327 | + msg_id: u64, |
| 328 | + call_reducer: F, |
| 329 | +) -> (ReducerCallResult, bool) |
| 330 | +where |
| 331 | + F: FnOnce(Option<MutTxId>, CallReducerParams, ReducerSuccessAction) -> (ReducerCallResult, bool), |
| 332 | +{ |
| 333 | + let tx = db.begin_mut_tx(IsolationLevel::Serializable, reducer_workload(module, ¶ms)); |
| 334 | + call_reducer( |
| 335 | + Some(tx), |
| 336 | + params, |
| 337 | + Box::new(move |tx, _reducer_return_value| { |
| 338 | + tx.delete_outbound_msg(msg_id).map_err(|e| anyhow!(e)) |
| 339 | + }), |
| 340 | + ) |
| 341 | +} |
| 342 | + |
227 | 343 | /// Finalize a delivered message: call the on_result reducer (if any), then delete from ST_OUTBOUND_MSG. |
228 | 344 | /// |
229 | 345 | /// On the happy path, `on_result_reducer` success and deletion of `st_outbound_msg` |
|
0 commit comments