Skip to content

Commit c1d7c9a

Browse files
committed
renaming
1 parent 37853a2 commit c1d7c9a

4 files changed

Lines changed: 34 additions & 36 deletions

File tree

crates/client-api/src/routes/database.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ pub struct CallFromDatabaseQuery {
236236
msg_id: u64,
237237
}
238238

239+
239240
/// Call a reducer on behalf of another database, with deduplication.
240241
///
241242
/// Endpoint: `POST /database/:name_or_identity/call-from-database/:reducer`
@@ -244,11 +245,12 @@ pub struct CallFromDatabaseQuery {
244245
/// - `sender_identity` — hex-encoded identity of the sending database.
245246
/// - `msg_id` — the inter-database message ID from the sender's st_outbound_msg.
246247
///
247-
/// Before invoking the reducer, the receiver checks `st_inbound_msg`.
248-
/// If the incoming `msg_id` is the last delivered msg_id for `sender_identity`,
249-
/// the call is a duplicate and 200 OK is returned immediately without running the reducer.
250-
/// Otherwise the reducer is invoked, the dedup index is updated atomically in the same
251-
/// transaction, and an acknowledgment is returned on success.
248+
/// Semantics:
249+
/// - The client **must send strictly increasing `msg_id` values per `sender_identity`.**
250+
/// - If a `msg_id` is **less than the last seen msg_id** for that sender, the request
251+
/// is treated as **invalid and rejected with a bad request error**.
252+
/// - If the incoming `msg_id` is equal to the last delivered msg_id, the call is treated
253+
/// as a duplicate and **200 OK is returned without invoking the reducer**.
252254
pub async fn call_from_database<S: ControlStateDelegate + NodeDelegate>(
253255
State(worker_ctx): State<S>,
254256
Extension(auth): Extension<SpacetimeAuth>,
@@ -295,6 +297,7 @@ pub async fn call_from_database<S: ControlStateDelegate + NodeDelegate>(
295297
)
296298
.await;
297299

300+
//Wait for durability before sending response
298301
if let Ok(rcr) = result.as_mut()
299302
&& let Some(tx_offset) = rcr.tx_offset.as_mut()
300303
&& let Some(mut durable_offset) = module.durable_tx_offset()
@@ -316,13 +319,14 @@ pub async fn call_from_database<S: ControlStateDelegate + NodeDelegate>(
316319
axum::body::Body::from(rcr.reducer_return_value.unwrap_or_default()),
317320
),
318321
// 422 = reducer ran but returned Err; the IDC actor uses this to distinguish
319-
// reducer failures from transport errors (which it retries).
322+
// reducer failures from other errors (which it retries).
320323
ReducerOutcome::Failed(errmsg) => {
321324
(
322325
StatusCode::UNPROCESSABLE_ENTITY,
323326
axum::body::Body::from(errmsg.to_string()),
324327
)
325328
}
329+
// This will be retried by IDC acttor
326330
ReducerOutcome::BudgetExceeded => {
327331
log::warn!(
328332
"Node's energy budget exceeded for identity: {owner_identity} while executing {reducer}"

crates/core/src/host/idc_actor.rs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,15 @@ struct PendingMessage {
9898
}
9999

100100
/// Per-target-database delivery state.
101-
struct TargetState {
101+
struct DatabaseQueue {
102102
queue: VecDeque<PendingMessage>,
103103
/// When `Some`, this target is in backoff and should not be retried until this instant.
104104
blocked_until: Option<Instant>,
105105
/// Current backoff duration for this target (doubles on each transport error).
106106
backoff: Duration,
107107
}
108108

109-
impl TargetState {
109+
impl DatabaseQueue {
110110
fn new() -> Self {
111111
Self {
112112
queue: VecDeque::new(),
@@ -141,7 +141,7 @@ enum DeliveryOutcome {
141141
ReducerError(String),
142142
/// Budget exceeded (HTTP 402).
143143
BudgetExceeded,
144-
/// Transport error: network failure, unexpected HTTP status, etc. Caller should retry.
144+
/// Transport error: database/reducer not found, network failure, unexpected HTTP status, etc. Caller should retry.
145145
TransportError(String),
146146
}
147147

@@ -155,21 +155,21 @@ async fn run_idc_loop(
155155
let client = reqwest::Client::new();
156156

157157
// Per-target-database delivery state.
158-
let mut targets: HashMap<Identity, TargetState> = HashMap::new();
158+
let mut db_queues: HashMap<Identity, DatabaseQueue> = HashMap::new();
159159

160160
// On startup, load any pending messages that survived a restart.
161-
load_pending_into_targets(&db, &mut targets);
161+
load_pending_into_targets(&db, &mut db_queues);
162162

163163
loop {
164164
// Deliver one message per ready target, then re-check.
165165
let mut any_delivered = true;
166166
while any_delivered {
167167
any_delivered = false;
168-
for state in targets.values_mut() {
169-
if !state.is_ready() {
168+
for queue in db_queues.values_mut() {
169+
if !queue.is_ready() {
170170
continue;
171171
}
172-
let Some(msg) = state.queue.front().cloned() else {
172+
let Some(msg) = queue.queue.front().cloned() else {
173173
continue;
174174
};
175175
let outcome = attempt_delivery(&client, &config, &msg).await;
@@ -180,12 +180,12 @@ async fn run_idc_loop(
180180
msg.msg_id,
181181
msg.target_db_identity.to_hex(),
182182
);
183-
state.record_transport_error();
183+
queue.record_transport_error();
184184
// Do NOT pop the front — keep retrying this message for this target.
185185
}
186186
outcome => {
187-
state.queue.pop_front();
188-
state.record_success();
187+
queue.queue.pop_front();
188+
queue.record_success();
189189
any_delivered = true;
190190
let (result_status, result_payload) = outcome_to_result(&outcome);
191191
finalize_message(&db, &module_host, &msg, result_status, result_payload).await;
@@ -195,7 +195,7 @@ async fn run_idc_loop(
195195
}
196196

197197
// Compute how long to sleep: min over all blocked targets' unblock times.
198-
let next_unblock = targets
198+
let next_unblock = db_queues
199199
.values()
200200
.filter_map(|s| s.blocked_until)
201201
.min()
@@ -204,15 +204,17 @@ async fn run_idc_loop(
204204

205205
// Wait for a notification or the next retry time.
206206
tokio::select! {
207+
//TODO:(shub) optimise this to send new entry directly instead of calling
208+
//`load_pending_into_targets`
207209
_ = notify_rx.recv() => {
208210
// Drain all pending notifications (coalesce bursts).
209211
while notify_rx.try_recv().is_ok() {}
210212
}
211213
_ = tokio::time::sleep(sleep_duration) => {}
212214
}
213215

214-
// Reload pending messages from DB (catches anything missed and handles restart recovery).
215-
load_pending_into_targets(&db, &mut targets);
216+
// Reload pending messages from DB (catches new entries).
217+
load_pending_into_targets(&db, &mut db_queues);
216218
}
217219
}
218220

@@ -247,7 +249,7 @@ fn reducer_workload(module: &ModuleInfo, params: &CallReducerParams) -> Workload
247249
})
248250
}
249251

250-
fn duplicate_result_from_row(row: spacetimedb_datastore::system_tables::StInboundMsgRow) -> ReducerCallResult {
252+
fn duplicate_result_from_st_inbound_row(row: spacetimedb_datastore::system_tables::StInboundMsgRow) -> ReducerCallResult {
251253
let outcome = match row.result_status {
252254
StInboundMsgResultStatus::Success => ReducerOutcome::Committed,
253255
StInboundMsgResultStatus::ReducerError => ReducerOutcome::Failed(Box::new(
@@ -294,7 +296,7 @@ where
294296
if let Some(row) = tx.get_inbound_msg_row(sender_identity) {
295297
if sender_msg_id == row.last_outbound_msg {
296298
let _ = db.rollback_mut_tx(tx);
297-
return Ok((duplicate_result_from_row(row), false));
299+
return Ok((duplicate_result_from_st_inbound_row(row), false));
298300
}
299301
if sender_msg_id < row.last_outbound_msg {
300302
let expected = row.last_outbound_msg + 1;
@@ -426,6 +428,8 @@ async fn finalize_message(
426428
return;
427429
}
428430
Err(e) => {
431+
432+
delete_message(db, msg.msg_id);
429433
log::error!(
430434
"idc_actor: on_result reducer '{}' failed for msg_id={}: {e:?}",
431435
on_result_reducer,
@@ -435,16 +439,14 @@ async fn finalize_message(
435439
}
436440
}
437441

438-
// Delete the row regardless of whether on_result succeeded or failed.
439-
delete_message(db, msg.msg_id);
440442
}
441443

442444
/// Load all messages from ST_OUTBOUND_MSG into the per-target queues, resolving delivery data
443445
/// from the corresponding outbox table rows.
444446
///
445447
/// A row's presence in ST_OUTBOUND_MSG means it has not yet been processed.
446448
/// Messages already in a target's queue (by msg_id) are not re-added.
447-
fn load_pending_into_targets(db: &RelationalDB, targets: &mut HashMap<Identity, TargetState>) {
449+
fn load_pending_into_targets(db: &RelationalDB, db_queues: &mut HashMap<Identity, DatabaseQueue>) {
448450
let tx = db.begin_tx(Workload::Internal);
449451

450452
let st_outbound_msg_rows: Vec<StOutboundMsgRow> = db
@@ -557,7 +559,7 @@ fn load_pending_into_targets(db: &RelationalDB, targets: &mut HashMap<Identity,
557559
pending.sort_by_key(|m| m.msg_id);
558560

559561
for msg in pending {
560-
let state = targets.entry(msg.target_db_identity).or_insert_with(TargetState::new);
562+
let state = db_queues.entry(msg.target_db_identity).or_insert_with(DatabaseQueue::new);
561563
// Only add if not already in the queue (avoid duplicates after reload).
562564
let already_queued = state.queue.iter().any(|m| m.msg_id == msg.msg_id);
563565
if !already_queued {

crates/core/src/host/module_host.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,16 +1749,6 @@ impl ModuleHost {
17491749
res
17501750
}
17511751

1752-
/// Variant of [`Self::call_reducer`] for database-to-database calls.
1753-
///
1754-
/// Behaves identically to `call_reducer`, except that it enforces at-most-once
1755-
/// delivery using the `st_inbound_msg` dedup index.
1756-
/// Before invoking the reducer, the receiver checks whether
1757-
/// `sender_msg_id` ≤ the last delivered msg_id for `sender_database_identity`.
1758-
/// If so, the call is a duplicate and the stored committed or failed result is returned
1759-
/// without running the reducer.
1760-
/// Otherwise the reducer runs, and the dedup index is updated atomically
1761-
/// within the same transaction.
17621752
pub async fn call_reducer_from_database(
17631753
&self,
17641754
caller_identity: Identity,

crates/core/src/host/wasm_common/module_host_actor.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::instrumentation::CallTimes;
2+
23
use super::*;
34
use crate::client::ClientActorId;
45
use crate::database_logger;
@@ -953,6 +954,7 @@ impl InstanceCommon {
953954
};
954955
let res = lifecycle_res
955956
.map_err(anyhow::Error::from)
957+
// Call `on_success`, when about to commit
956958
.and_then(|_| on_success(&mut tx, &return_value));
957959
match res {
958960
Ok(()) => (EventStatus::Committed(DatabaseUpdate::default()), return_value),

0 commit comments

Comments
 (0)