Skip to content

Feature: Composable transactions and OrElse combinator #16

@engineering87

Description

@engineering87

Summary

Currently, STMEngine.Atomic<T> executes a single, flat transaction defined as a lambda. There is no support for composing smaller transactional operations into larger atomic units, nor for expressing alternative retry paths. This limits reusability: if two transactional operations already exist as separate functions, combining them into a single atomic commit requires inlining their logic into one lambda.

Motivation

Today, atomically incrementing one STMVariable<int> and decrementing another requires writing all the logic in a single closure:

await STMEngine.Atomic<int>(tx =>
{
    var a = tx.Read(varA);
    var b = tx.Read(varB);
    tx.Write(varA, a + 1);
    tx.Write(varB, b - 1);
});

If Increment(varA) and Decrement(varB) are already defined as reusable transactional building blocks, there is no way to combine them into a single atomic transaction without duplicating their internals.

With composable transactions, the goal would be to express this as sequential composition of existing operations, both running inside the same Transaction<T> and committed together.

Similarly, an OrElse combinator would enable fallback strategies: attempt a transactional read-modify-write on varA; if that path hits a conflict and would retry, automatically fall back to the same operation on varB, all within the same transaction boundary, without the caller needing manual retry orchestration.

Proposed design

Introduce an StmAction<T> abstraction representing a composable transactional operation, with two combinators:

  • AndThen: sequential composition: both actions run in the same Transaction<T> context. If either causes a conflict at commit, the entire composed transaction retries via STMEngine.
  • OrElse: alternative composition: if the primary action triggers a retry (conflict), the transaction state is rolled back to a checkpoint taken before the primary action, and the fallback is attempted. If both paths fail, the whole transaction retries.

A new STMEngine.Atomic<T>(StmAction<T>, ...) overload would interpret the composed action tree.

Design considerations

Aspect Notes
AndThen semantics Both actions share the same Transaction<T> instance. same _reads, _writes, _snapshotVersions. Conflict in either causes a full retry.
OrElse semantics Requires checkpoint/rollback support in Transaction<T>. snapshotting _reads, _writes, and _snapshotVersions at OrElse boundaries and restoring them on fallback. This is the most significant internal change.
Checkpoint/rollback Transaction<T>.Clear() already exists; a new Checkpoint()/Restore() pair would save and restore the three dictionaries. Dictionary copy cost is proportional to the read/write set size, which is typically small.
Nested transactions True nesting (child commits independently) is not proposed. Composition is always flattened into a single commit, consistent with the Haskell STM model.
Return values Consider a variant with a result type for actions that produce a value, enabling map/bind-style composition. This would complement the existing Atomic<T, TResult> overloads.
Backward compatibility Fully additive, existing lambda-based Action<ITransaction<T>> and Func<ITransaction<T>, Task> overloads remain unchanged.

Implementation outline

  1. Add the StmAction<T> base abstraction with AndThen and OrElse combinators, plus concrete implementations (SequenceAction<T>, OrElseAction<T>, LambdaAction<T>).
  2. Add Checkpoint() and Restore() methods to Transaction<T> for saving and rolling back _reads, _writes, and _snapshotVersions.
  3. Add STMEngine.Atomic<T>(StmAction<T>, ...) overload that walks the action tree and executes it against a Transaction<T>.
  4. Unit tests, compose read/write operations on multiple STMVariable<int> instances, verify atomicity of AndThen, verify fallback semantics of OrElse under contention.
  5. Update README with composition examples using the STMSharp API.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions