Skip to content

3/6: Async IDC Receiver side. - #6150

Open
Shubham8287 wants to merge 8 commits into
masterfrom
shub/idc-receiver
Open

Shubham8287 wants to merge 8 commits into
masterfrom
shub/idc-receiver

Conversation

@Shubham8287

@Shubham8287 Shubham8287 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Description of Changes

This PR builds the receiver side of async IDC, it contains 3 commits and can be reviewed accordingly:

  1. Reducer signature hashing

    Adds a canonical IDC reducer signature hash, receiver checks this hash before decoding positional BSATN arguments. This prevents an old sender from accidentally calling a reducer after the receiver has changed the reducer signature.

  2. Host-signed IDC authentication

    Adds IDC-specific database credentials signed by the host key.
    The IDC route authenticates the sending database separately from normal client auth. The module sees
    ctx.sender as the sending database identity, but it does not receive a client JWT. This keeps IDC transport
    credentials out of module-visible auth state.

  3. Durable receiver outcomes

    Adds the authenticated call-from-database receiver path.

    The receiver tracks ordered IDC streams with stream_id and seq, records terminal outcomes durably, replays
    already-decided outcomes on retry, and rejects out-of-order messages with a retryable response. Successful
    reducer effects and the receiver-side IDC outcome are committed atomically; terminal failures are recorded as
    durable outcomes so the stream can continue.

Proposal Drift

  1. Proposal does not provide much details about authentication, which has been discussed separately and tracked here: SpacetimeDB auth future plan #6151. PR includes interim solution.
  2. Proposal mention about signature hashing, but does provide detailed algorithm, hence implementaiton came up with one.
  3. We do not pipelining in first pass to simplify complexity and testing.

API and ABI breaking changes

NA, additive API.

Rollback safety impact

n/a

Expected complexity level and risk

3

Testing

unit and integration tests.

@Shubham8287
Shubham8287 force-pushed the shub/idc-receiver branch 3 times, most recently from 2d47c10 to f2f6ac8 Compare October 12, 2026 18:13
@Shubham8287 Shubham8287 changed the title Shub/idc receiver 3/6: Async IDC Receiver side. Oct 12, 2026
@Shubham8287
Shubham8287 force-pushed the shub/idc-receiver branch 8 times, most recently from dd953a3 to 0bdb12c Compare October 13, 2026 08:43

// A duplicate delivery replays the stored outcome without rerunning the reducer.
call_idc(Identity::ONE, 1, false, signature_hash, StatusCode::OK, "").await;
assert_counts(2, 1, 2).await;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we skip lifecycle reducers when we’re not running the actual reducer again? But then, couldn’t updated module logic reject an identity that was previously allowed? So, I may keep it like this

@Shubham8287
Shubham8287 force-pushed the shub/idc-receiver branch 4 times, most recently from 57c54ee to 3d147a6 Compare October 14, 2026 13:49
@Shubham8287
Shubham8287 marked this pull request as draft October 14, 2026 14:58
@Shubham8287
Shubham8287 marked this pull request as ready for review October 14, 2026 16:06
Comment thread crates/core/src/host/module_host.rs Outdated
let (tx, decision) = checked.map_err(DBError::from)?;
// Commit the acknowledgment cleanup even if this delivery cannot execute.
let (tx_offset, tx_data, tx_metrics, reducer) = stdb
.commit_tx(tx)?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check and the execution need to be atomic in order to maintain exactly once semantics on the receiver side, no? That is, we should run them in the same transaction.

@Shubham8287 Shubham8287 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It relies on the UniqueConstraintVoilation and the Existed check in record_inbound_idc_outcome for exactly-once semantics. During a race, one of the transactions will fail in record_inbound_idc_outcome.

To make it more explicit, I will lift the checks out of record_inbound_idc_outcome and do it in same tx before executing reducer to fail early. Does it makes sense?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed a commit, so that we do checking against st_* tables within a reducer or fallback tx.

Comment on lines +2686 to +2690
let tx = stdb.begin_mut_tx(IsolationLevel::Serializable, Workload::Internal);
let (tx, ()) = stdb
.with_auto_rollback(tx, |tx| {
tx.record_inbound_idc_outcome(sender_identity, stream_id, seq, status, payload.clone())
})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here, because recording a failure needs a separate transaction, don't we need to check if an outcome for this attempt was already recorded before trying to record the failure?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants