Files
Tyler Cloutier d5c75d944d Ungate procedures from the unstable feature (#5164)
# Description of Changes

Graduate **procedures** (and the outgoing HTTP client used from
procedures) out of the `unstable` feature gate /
`SPACETIMEDB_UNSTABLE_FEATURES` across all module libraries. The
`procedure` macro, `ProcedureContext`, `with_tx`/`try_with_tx`,
scheduled procedures, and `ctx.http` are now available without opting
into `unstable`.

**Still gated** (unchanged): HTTP handlers/webhooks, views, RLS /
`client_visibility_filter`, and immediate-scheduling
(`volatile_nonatomic_schedule_immediate`).

Per library:
- **`bindings-sys`**: ungate the `procedure` host-call module + raw ABI
imports (`procedure_start/commit/abort_mut_tx`,
`procedure_http_request`) and `call_no_ret`. Scheduling ABI stays gated.
- **`bindings` (Rust)**: ungate the `procedure` macro,
`ProcedureContext`, `TxContext`/`with_tx`/`try_with_tx`,
`db_read_only`/`get_read_only`, the procedure traits,
`register_procedure`, `__call_procedure__`, and procedure RNG. The
`http` module (previously gated as a unit) now has fine-grained gating
so the outgoing `HttpClient` is exposed while
`HandlerContext`/`Router`/handler macros stay gated.
- **`bindings-csharp`**: drop `[Experimental("STDB_UNSTABLE")]` from
`ProcedureContext.WithTx/TryWithTx` (runtime + codegen) and the
generated `ProcedureTxContext`; FFI snapshots regenerated. Handler
context members + RLS attribute stay gated.
- **`bindings-cpp`**: ungate the procedure ABI (`abi.h`/`FFI.h`),
`tx_execution.h`, the outgoing HTTP client (`http.h`/`http_convert.h`),
and `procedure_context.h`.
`handler_context.h`/`router.h`/`http_handler_macros.h` still `#error`
without `SPACETIMEDB_UNSTABLE_FEATURES`.
- **docs**: remove the procedures beta notices (HTTP handlers stay
marked beta for a later release); regenerate `static/llms.md`.
- **`sdk-test-procedure`**: no longer needs `features = ["unstable"]`.

# API and ABI breaking changes

Not breaking. This is purely additive to the stable surface: procedures
become available **without** the `unstable` feature, while modules that
already opt into `unstable` are unaffected. The wasm host-ABI imports
were already implemented host-side and merely gated module-side, so
there is no ABI version change. (No breaking-change label needed.)

# Expected complexity level and risk

**3/5.** The diff itself is mostly removing
`#[cfg]`/`#ifdef`/`[Experimental]` guards, but the gate bundled
procedures + outgoing HTTP + handlers + the ABI crate together, so the
work was in *separating* procedures from the features that stay gated.
Areas reviewers may want to scrutinize:
- The Rust `http` module went from "gated as a whole" to fine-grained
per-item gating; the outgoing `HttpClient` is exposed while handler
types/macros stay behind `unstable`.
- The `unstable` cut reaches the ABI crate (`bindings-sys`), which is
the only way procedures can compile without the feature.
- C++ separation of the outgoing HTTP client from HTTP handlers across
several headers.

# Testing

- [x] Rust: `cargo check -p spacetimedb` passes in default, `--features
unstable`, and `--no-default-features --features unstable`.
- [x] Rust end-to-end: `sdk-test-procedure` (uses procedures,
`with_tx`/`try_with_tx`, `ctx.http.get`, `new_uuid_v7`, scheduled
procedures) builds **without** `unstable`.
- [x] C#: Codegen + Runtime build; `Codegen.Tests` pass (6/6) after FFI
snapshot regen.
- [x] C++: host syntax-check confirms procedures + `ctx.http.send()`
compile **without** the flag, full headers compile **with** it, and
`handler_context.h` still `#error`s without it. (Not built for the real
wasm/emscripten target locally — relying on CI.)
- [x] `cargo ci lint` components reproduced locally: rustfmt, clippy
(`-D warnings`, default + `unstable`), csharpier, and `cargo doc --deny
warnings`.
- [ ] Reviewer: confirm the wasm bindings + C#/C++ test suites pass in
CI (especially the C++ wasm build, which couldn't run locally).

---------

Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com>
2026-06-05 16:09:36 +00:00

147 lines
4.4 KiB
C++

#ifndef SPACETIMEDB_INTERNAL_TX_EXECUTION_H
#define SPACETIMEDB_INTERNAL_TX_EXECUTION_H
#include <spacetimedb/abi/FFI.h>
#include <spacetimedb/outcome.h>
#include <spacetimedb/tx_context.h>
#include <type_traits>
#include <utility>
namespace SpacetimeDB::Internal {
template<typename T>
struct is_outcome : std::false_type {};
template<typename T>
struct is_outcome<Outcome<T>> : std::true_type {};
template<typename T>
inline constexpr bool is_outcome_v = is_outcome<std::remove_cv_t<std::remove_reference_t<T>>>::value;
template<typename T>
bool tx_result_should_commit(const T& result) {
using ResultType = std::remove_cv_t<std::remove_reference_t<T>>;
// TODO(http-handlers-cpp): Consider tightening try_with_tx in a future breaking release
// so rollback-aware callbacks use Outcome<T> (and possibly bool for compatibility)
// instead of silently treating arbitrary return types as commit-on-success.
if constexpr (std::is_same_v<ResultType, bool>) {
return result;
} else if constexpr (is_outcome_v<ResultType>) {
return result.is_ok();
} else {
return true;
}
}
class TxAbortGuard {
public:
TxAbortGuard() = default;
TxAbortGuard(const TxAbortGuard&) = delete;
TxAbortGuard& operator=(const TxAbortGuard&) = delete;
~TxAbortGuard() {
if (!armed_) {
return;
}
Status status = FFI::procedure_abort_mut_tx();
if (is_error(status)) {
LOG_PANIC("Failed to abort transaction");
}
}
void disarm() {
armed_ = false;
}
private:
bool armed_ = true;
};
inline void commit_tx_or_panic() {
Status status = FFI::procedure_commit_mut_tx();
if (is_error(status)) {
LOG_PANIC("Failed to commit transaction");
}
}
inline bool try_commit_tx() {
return is_ok(FFI::procedure_commit_mut_tx());
}
inline void abort_tx_or_panic() {
Status status = FFI::procedure_abort_mut_tx();
if (is_error(status)) {
LOG_PANIC("Failed to abort transaction");
}
}
template<typename MakeReducerContext, typename Func>
auto run_tx_once(MakeReducerContext&& make_reducer_ctx, Func& body) -> decltype(body(std::declval<TxContext&>())) {
using ResultType = decltype(body(std::declval<TxContext&>()));
int64_t tx_timestamp = 0;
Status status = FFI::procedure_start_mut_tx(&tx_timestamp);
if (is_error(status)) {
LOG_PANIC("Failed to start transaction");
}
TxAbortGuard abort_guard;
ReducerContext reducer_ctx = make_reducer_ctx(Timestamp::from_micros_since_epoch(tx_timestamp));
TxContext tx{reducer_ctx};
if constexpr (std::is_void_v<ResultType>) {
body(tx);
abort_guard.disarm();
} else {
ResultType result = body(tx);
abort_guard.disarm();
return result;
}
}
template<typename MakeReducerContext, typename Func>
auto with_tx(MakeReducerContext&& make_reducer_ctx, Func& body) -> decltype(body(std::declval<TxContext&>())) {
using ResultType = decltype(body(std::declval<TxContext&>()));
if constexpr (std::is_void_v<ResultType>) {
run_tx_once(std::forward<MakeReducerContext>(make_reducer_ctx), body);
if (!try_commit_tx()) {
run_tx_once(std::forward<MakeReducerContext>(make_reducer_ctx), body);
commit_tx_or_panic();
}
} else {
ResultType result = run_tx_once(std::forward<MakeReducerContext>(make_reducer_ctx), body);
if (!try_commit_tx()) {
result = run_tx_once(std::forward<MakeReducerContext>(make_reducer_ctx), body);
commit_tx_or_panic();
}
return result;
}
}
template<typename MakeReducerContext, typename Func>
auto try_with_tx(MakeReducerContext&& make_reducer_ctx, Func& body) -> decltype(body(std::declval<TxContext&>())) {
using ResultType = decltype(body(std::declval<TxContext&>()));
ResultType result = run_tx_once(std::forward<MakeReducerContext>(make_reducer_ctx), body);
if (!tx_result_should_commit(result)) {
abort_tx_or_panic();
return result;
}
if (!try_commit_tx()) {
result = run_tx_once(std::forward<MakeReducerContext>(make_reducer_ctx), body);
if (tx_result_should_commit(result)) {
commit_tx_or_panic();
} else {
abort_tx_or_panic();
}
}
return result;
}
} // namespace SpacetimeDB::Internal
#endif // SPACETIMEDB_INTERNAL_TX_EXECUTION_H