Skip to content

Commit 647c579

Browse files
committed
procedures: add wasm abi for anon mut txes
1 parent 4143c15 commit 647c579

10 files changed

Lines changed: 414 additions & 119 deletions

File tree

crates/bindings-sys/src/lib.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ pub mod raw {
645645
pub fn get_jwt(connection_id_ptr: *const u8, bytes_source_id: *mut BytesSource) -> u16;
646646
}
647647

648+
#[cfg(feature = "unstable")]
648649
#[link(wasm_import_module = "spacetime_10.3")]
649650
extern "C" {
650651
/// Suspends execution of this WASM instance until approximately `wake_at_micros_since_unix_epoch`.
@@ -661,8 +662,76 @@ pub mod raw {
661662
/// - The calling WASM instance is holding open a transaction.
662663
/// - The calling WASM instance is not executing a procedure.
663664
// TODO(procedure-sleep-until): remove this
664-
#[cfg(feature = "unstable")]
665665
pub fn procedure_sleep_until(wake_at_micros_since_unix_epoch: i64) -> i64;
666+
667+
/// Starts a mutable transaction,
668+
/// suspending execution of this WASM instance until
669+
/// a mutable transaction lock is aquired.
670+
///
671+
/// Upon resuming, returns `0` on success,
672+
/// enabling further calls that require a pending transaction,
673+
/// or an error code otherwise.
674+
///
675+
/// # Traps
676+
///
677+
/// This function does not trap.
678+
///
679+
/// # Errors
680+
///
681+
/// Returns an error:
682+
///
683+
/// - `WOULD_BLOCK_TRANSACTION`, if there's already an ongoing transaction.
684+
pub fn procedure_start_mut_transaction() -> u16;
685+
686+
/// Commits a mutable transaction,
687+
/// suspending execution of this WASM instance until
688+
/// the transaction has been committed
689+
/// and subscription queries have been run and broadcast.
690+
///
691+
/// Upon resuming, returns `0` on success, or an error code otherwise.
692+
///
693+
/// # Traps
694+
///
695+
/// This function does not trap.
696+
///
697+
/// # Errors
698+
///
699+
/// Returns an error:
700+
///
701+
/// - `TRANSACTION_NOT_ANONYMOUS`,
702+
/// if the transaction was not started in [`procedure_start_mut_transaction`].
703+
/// This can happen if this syscall is erroneously called by a reducer.
704+
/// The code `NOT_IN_TRANSACTION` does not happen,
705+
/// as it is subsumed by `TRANSACTION_NOT_ANONYMOUS`.
706+
/// - `TRANSACTION_IS_READ_ONLY`, if the pending transaction is read-only.
707+
/// This currently does not happen as anonymous read transactions
708+
/// are not exposed to modules.
709+
pub fn procedure_commit_mut_transaction() -> u16;
710+
711+
/// Aborts a mutable transaction,
712+
/// suspending execution of this WASM instance until
713+
/// the transaction has been rolled back.
714+
///
715+
/// Upon resuming, returns `0` on success, or an error code otherwise.
716+
///
717+
/// # Traps
718+
///
719+
/// This function does not trap.
720+
///
721+
/// # Errors
722+
///
723+
/// Returns an error:
724+
///
725+
/// - `TRANSACTION_NOT_ANONYMOUS`,
726+
/// if the transaction was not started in [`procedure_start_mut_transaction`].
727+
/// This can happen if this syscall is erroneously called by a reducer.
728+
/// The code `NOT_IN_TRANSACTION` does not happen,
729+
/// as it is subsumed by `TRANSACTION_NOT_ANONYMOUS`.
730+
/// - `TRANSACTION_IS_READ_ONLY`, if the pending transaction is read-only.
731+
/// This currently does not happen as anonymous read transactions
732+
/// are not exposed to modules.
733+
pub fn procedure_abort_mut_transaction() -> u16;
734+
666735
}
667736

668737
/// What strategy does the database index use?

crates/core/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ pub enum NodesError {
266266
BadColumn,
267267
#[error("can't perform operation; not inside transaction")]
268268
NotInTransaction,
269+
#[error("can't perform operation; a transaction already exists")]
270+
WouldBlockTransaction,
269271
#[error("table with name {0:?} already exists")]
270272
AlreadyExists(String),
271273
#[error("table with name `{0}` start with 'st_' and that is reserved for internal system tables.")]

crates/core/src/host/instance_env.rs

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ use crate::db::relational_db::{MutTx, RelationalDB};
44
use crate::error::{DBError, DatastoreError, IndexError, NodesError};
55
use crate::host::wasm_common::TimingSpan;
66
use crate::replica_context::ReplicaContext;
7+
use crate::util::asyncify;
78
use chrono::{DateTime, Utc};
89
use core::mem;
910
use parking_lot::{Mutex, MutexGuard};
1011
use smallvec::SmallVec;
12+
use spacetimedb_datastore::execution_context::Workload;
1113
use spacetimedb_datastore::locking_tx_datastore::state_view::StateView;
1214
use spacetimedb_datastore::locking_tx_datastore::{FuncCallType, MutTxId};
15+
use spacetimedb_datastore::traits::IsolationLevel;
1316
use spacetimedb_lib::{ConnectionId, Identity, Timestamp};
1417
use spacetimedb_primitives::{ColId, ColList, IndexId, TableId};
1518
use spacetimedb_sats::{
@@ -204,6 +207,14 @@ impl InstanceEnv {
204207
self.tx.get()
205208
}
206209

210+
pub(crate) fn take_tx(&self) -> Result<MutTxId, GetTxError> {
211+
self.tx.take()
212+
}
213+
214+
pub(crate) fn relational_db(&self) -> &Arc<RelationalDB> {
215+
&self.replica_ctx.relational_db
216+
}
217+
207218
pub(crate) fn get_jwt_payload(&self, connection_id: ConnectionId) -> Result<Option<String>, NodesError> {
208219
let tx = &mut *self.get_tx()?;
209220
Ok(tx.get_jwt_payload(connection_id).map_err(DBError::from)?)
@@ -274,7 +285,7 @@ impl InstanceEnv {
274285
}
275286

276287
pub fn insert(&self, table_id: TableId, buffer: &mut [u8]) -> Result<usize, NodesError> {
277-
let stdb = &*self.replica_ctx.relational_db;
288+
let stdb = self.relational_db();
278289
let tx = &mut *self.get_tx()?;
279290

280291
let (row_len, row_ptr, insert_flags) = stdb
@@ -343,7 +354,7 @@ impl InstanceEnv {
343354
}
344355

345356
pub fn update(&self, table_id: TableId, index_id: IndexId, buffer: &mut [u8]) -> Result<usize, NodesError> {
346-
let stdb = &*self.replica_ctx.relational_db;
357+
let stdb = self.relational_db();
347358
let tx = &mut *self.get_tx()?;
348359

349360
let (row_len, row_ptr, update_flags) = stdb
@@ -386,8 +397,8 @@ impl InstanceEnv {
386397
rstart: &[u8],
387398
rend: &[u8],
388399
) -> Result<u32, NodesError> {
389-
let stdb = &*self.replica_ctx.relational_db;
390-
let tx = &mut *self.tx.get()?;
400+
let stdb = self.relational_db();
401+
let tx = &mut *self.get_tx()?;
391402

392403
// Find all rows in the table to delete.
393404
let (table_id, _, _, iter) = stdb.index_scan_range(tx, index_id, prefix, prefix_elems, rstart, rend)?;
@@ -416,7 +427,7 @@ impl InstanceEnv {
416427
/// - a row couldn't be decoded to the table schema type.
417428
#[tracing::instrument(level = "trace", skip(self, relation))]
418429
pub fn datastore_delete_all_by_eq_bsatn(&self, table_id: TableId, relation: &[u8]) -> Result<u32, NodesError> {
419-
let stdb = &*self.replica_ctx.relational_db;
430+
let stdb = self.relational_db();
420431
let tx = &mut *self.get_tx()?;
421432

422433
// Track the number of bytes coming from the caller
@@ -443,7 +454,7 @@ impl InstanceEnv {
443454
/// and `TableNotFound` if the table does not exist.
444455
#[tracing::instrument(level = "trace", skip_all)]
445456
pub fn table_id_from_name(&self, table_name: &str) -> Result<TableId, NodesError> {
446-
let stdb = &*self.replica_ctx.relational_db;
457+
let stdb = self.relational_db();
447458
let tx = &mut *self.get_tx()?;
448459

449460
// Query the table id from the name.
@@ -457,7 +468,7 @@ impl InstanceEnv {
457468
/// and `IndexNotFound` if the index does not exist.
458469
#[tracing::instrument(level = "trace", skip_all)]
459470
pub fn index_id_from_name(&self, index_name: &str) -> Result<IndexId, NodesError> {
460-
let stdb = &*self.replica_ctx.relational_db;
471+
let stdb = self.relational_db();
461472
let tx = &mut *self.get_tx()?;
462473

463474
// Query the index id from the name.
@@ -471,7 +482,7 @@ impl InstanceEnv {
471482
/// and `TableNotFound` if the table does not exist.
472483
#[tracing::instrument(level = "trace", skip_all)]
473484
pub fn datastore_table_row_count(&self, table_id: TableId) -> Result<u64, NodesError> {
474-
let stdb = &*self.replica_ctx.relational_db;
485+
let stdb = self.relational_db();
475486
let tx = &mut *self.get_tx()?;
476487

477488
// Query the row count for id.
@@ -488,8 +499,8 @@ impl InstanceEnv {
488499
pool: &mut ChunkPool,
489500
table_id: TableId,
490501
) -> Result<Vec<Vec<u8>>, NodesError> {
491-
let stdb = &*self.replica_ctx.relational_db;
492-
let tx = &mut *self.tx.get()?;
502+
let stdb = self.relational_db();
503+
let tx = &mut *self.get_tx()?;
493504

494505
// Track the number of rows and the number of bytes scanned by the iterator
495506
let mut rows_scanned = 0;
@@ -521,8 +532,8 @@ impl InstanceEnv {
521532
rstart: &[u8],
522533
rend: &[u8],
523534
) -> Result<Vec<Vec<u8>>, NodesError> {
524-
let stdb = &*self.replica_ctx.relational_db;
525-
let tx = &mut *self.tx.get()?;
535+
let stdb = self.relational_db();
536+
let tx = &mut *self.get_tx()?;
526537

527538
// Track rows and bytes scanned by the iterator
528539
let mut rows_scanned = 0;
@@ -569,26 +580,52 @@ impl InstanceEnv {
569580

570581
written
571582
}
583+
584+
pub async fn start_mutable_tx(&mut self) -> Result<(), NodesError> {
585+
if self.get_tx().is_ok() {
586+
return Err(NodesError::WouldBlockTransaction);
587+
}
588+
589+
let stdb = self.replica_ctx.relational_db.clone();
590+
// TODO(procedure-tx): should we add a new workload, e.g., `AnonTx`?
591+
let tx = asyncify(move || stdb.begin_mut_tx(IsolationLevel::Serializable, Workload::Internal)).await;
592+
self.tx.set_raw(tx);
593+
594+
Ok(())
595+
}
572596
}
573597

574598
impl TxSlot {
575-
pub fn set<T>(&mut self, tx: MutTxId, f: impl FnOnce() -> T) -> (MutTxId, T) {
599+
/// Sets the slot to `tx`, ensuring that there was no tx before.
600+
pub fn set_raw(&mut self, tx: MutTxId) {
576601
let prev = self.inner.lock().replace(tx);
577602
assert!(prev.is_none(), "reentrant TxSlot::set");
578-
let remove_tx = || self.inner.lock().take();
603+
}
604+
605+
/// Sets the slot to `tx` runs `work`, and returns back `tx`.
606+
pub fn set<T>(&mut self, tx: MutTxId, work: impl FnOnce() -> T) -> (MutTxId, T) {
607+
self.set_raw(tx);
608+
609+
let remove_tx = || self.take().expect("tx was removed during transaction");
579610

580611
let res = {
581612
scopeguard::defer_on_unwind! { remove_tx(); }
582-
f()
613+
work()
583614
};
584615

585-
let tx = remove_tx().expect("tx was removed during transaction");
616+
let tx = remove_tx();
586617
(tx, res)
587618
}
588619

620+
/// Returns the tx in the slot.
589621
pub fn get(&self) -> Result<impl DerefMut<Target = MutTxId> + '_, GetTxError> {
590622
MutexGuard::try_map(self.inner.lock(), |map| map.as_mut()).map_err(|_| GetTxError)
591623
}
624+
625+
/// Steals th tx from the slot.
626+
pub fn take(&self) -> Result<MutTxId, GetTxError> {
627+
self.inner.lock().take().ok_or(GetTxError)
628+
}
592629
}
593630

594631
#[derive(Debug)]

crates/core/src/host/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,7 @@ pub enum AbiCall {
178178
VolatileNonatomicScheduleImmediate,
179179

180180
ProcedureSleepUntil,
181+
ProcedureStartMutTransaction,
182+
ProcedureCommitMutTransaction,
183+
ProcedureAbortMutTransaction,
181184
}

crates/core/src/host/wasm_common.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ pub(super) type TimingSpanSet = ResourceSlab<TimingSpanIdx>;
347347
pub fn err_to_errno(err: &NodesError) -> Option<NonZeroU16> {
348348
match err {
349349
NodesError::NotInTransaction => Some(errno::NOT_IN_TRANSACTION),
350+
NodesError::WouldBlockTransaction => Some(errno::WOULD_BLOCK_TRANSACTION),
350351
NodesError::DecodeRow(_) => Some(errno::BSATN_DECODE_ERROR),
351352
NodesError::TableNotFound => Some(errno::NO_SUCH_TABLE),
352353
NodesError::IndexNotFound => Some(errno::NO_SUCH_INDEX),
@@ -422,6 +423,9 @@ macro_rules! abi_funcs {
422423

423424
$link_async! {
424425
"spacetime_10.3"::procedure_sleep_until,
426+
"spacetime_10.3"::procedure_start_mut_transaction,
427+
"spacetime_10.3"::procedure_commit_mut_transaction,
428+
"spacetime_10.3"::procedure_abort_mut_transaction,
425429
}
426430
};
427431
}

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

Lines changed: 17 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,12 @@
1-
use bytes::Bytes;
2-
use prometheus::{Histogram, IntCounter, IntGauge};
3-
use spacetimedb_datastore::locking_tx_datastore::FuncCallType;
4-
use spacetimedb_datastore::locking_tx_datastore::ViewCallInfo;
5-
use spacetimedb_lib::db::raw_def::v9::Lifecycle;
6-
use spacetimedb_lib::de::DeserializeSeed as _;
7-
use spacetimedb_primitives::ProcedureId;
8-
use spacetimedb_primitives::TableId;
9-
use spacetimedb_primitives::ViewFnPtr;
10-
use spacetimedb_primitives::ViewId;
11-
use spacetimedb_schema::auto_migrate::{MigratePlan, MigrationPolicy, MigrationPolicyError};
12-
use spacetimedb_schema::def::ModuleDef;
13-
use std::future::Future;
14-
use std::sync::Arc;
15-
use std::time::Duration;
16-
use tracing::span::EnteredSpan;
17-
181
use super::instrumentation::CallTimes;
19-
use crate::client::ClientConnectionSender;
2+
use super::*;
203
use crate::database_logger;
214
use crate::energy::{EnergyMonitor, FunctionBudget, FunctionFingerprint};
22-
use crate::host::instance_env::InstanceEnv;
23-
use crate::host::instance_env::TxSlot;
5+
use crate::host::instance_env::{InstanceEnv, TxSlot};
246
use crate::host::module_common::{build_common_module_from_raw, ModuleCommon};
25-
use crate::host::module_host::ViewCallResult;
26-
use crate::host::module_host::ViewOutcome;
277
use crate::host::module_host::{
288
CallProcedureParams, CallReducerParams, CallViewParams, DatabaseUpdate, EventStatus, ModuleEvent,
29-
ModuleFunctionCall, ModuleInfo,
9+
ModuleFunctionCall, ModuleInfo, ViewCallResult, ViewOutcome,
3010
};
3111
use crate::host::{
3212
ArgsTuple, ProcedureCallError, ProcedureCallResult, ReducerCallResult, ReducerId, ReducerOutcome, Scheduler,
@@ -36,18 +16,27 @@ use crate::identity::Identity;
3616
use crate::messages::control_db::HostType;
3717
use crate::module_host_context::ModuleCreationContextLimited;
3818
use crate::replica_context::ReplicaContext;
39-
use crate::subscription::module_subscription_actor::WriteConflict;
19+
use crate::subscription::module_subscription_actor::commit_and_broadcast_event;
4020
use crate::util::prometheus_handle::{HistogramExt, TimerGuard};
4121
use crate::worker_metrics::WORKER_METRICS;
22+
use bytes::Bytes;
23+
use core::future::Future;
24+
use core::time::Duration;
25+
use prometheus::{Histogram, IntCounter, IntGauge};
4226
use spacetimedb_datastore::db_metrics::DB_METRICS;
4327
use spacetimedb_datastore::execution_context::{self, ReducerContext, Workload};
44-
use spacetimedb_datastore::locking_tx_datastore::MutTxId;
28+
use spacetimedb_datastore::locking_tx_datastore::{FuncCallType, MutTxId, ViewCallInfo};
4529
use spacetimedb_datastore::traits::{IsolationLevel, Program};
4630
use spacetimedb_lib::buffer::DecodeError;
31+
use spacetimedb_lib::db::raw_def::v9::Lifecycle;
32+
use spacetimedb_lib::de::DeserializeSeed;
4733
use spacetimedb_lib::identity::AuthCtx;
4834
use spacetimedb_lib::{bsatn, ConnectionId, RawModuleDef, Timestamp};
49-
50-
use super::*;
35+
use spacetimedb_primitives::{ProcedureId, TableId, ViewFnPtr, ViewId};
36+
use spacetimedb_schema::auto_migrate::{MigratePlan, MigrationPolicy, MigrationPolicyError};
37+
use spacetimedb_schema::def::ModuleDef;
38+
use std::sync::Arc;
39+
use tracing::span::EnteredSpan;
5140

5241
pub trait WasmModule: Send + 'static {
5342
type Instance: WasmInstance;
@@ -668,7 +657,7 @@ impl InstanceCommon {
668657
request_id,
669658
timer,
670659
};
671-
let event = commit_and_broadcast_event(&self.info, client, event, out.tx);
660+
let event = commit_and_broadcast_event(&info.subscriptions, client, event, out.tx).event;
672661

673662
let res = ReducerCallResult {
674663
outcome: ReducerOutcome::from(&event.status),
@@ -1051,24 +1040,6 @@ fn lifecyle_modifications_to_tx(
10511040
}
10521041
*/
10531042

1054-
/// Commits the transaction
1055-
/// and evaluates and broadcasts subscriptions updates.
1056-
fn commit_and_broadcast_event(
1057-
info: &ModuleInfo,
1058-
client: Option<Arc<ClientConnectionSender>>,
1059-
event: ModuleEvent,
1060-
tx: MutTxId,
1061-
) -> Arc<ModuleEvent> {
1062-
match info
1063-
.subscriptions
1064-
.commit_and_broadcast_event(client, event, tx)
1065-
.unwrap()
1066-
{
1067-
Ok(res) => res.event,
1068-
Err(WriteConflict) => todo!("Write skew, you need to implement retries my man, T-dawg."),
1069-
}
1070-
}
1071-
10721043
pub trait InstanceOp {
10731044
fn name(&self) -> &str;
10741045
fn timestamp(&self) -> Timestamp;

0 commit comments

Comments
 (0)