Transactions
The transaction action wraps several other actions in a single database
transaction, exercising commit/rollback interplay with the metadata catalog
and (on MySQL) the fact that DDL is not transactional.
Overview
transaction is a built-in action, registered by default at weight 100
alongside the other DDL/DML actions. Each run:
- picks a random isolation level (see Isolation weights) and opens
the transaction with the dialect's
BEGIN/SET TRANSACTIONstatements, - picks a random count of sub-actions (
min_sub_actions..max_sub_actions), drawing each one from the same pool used outside of transactions (minustransactionitself, to avoid nesting), - runs them one by one, honoring the configured error mode,
- finishes with
COMMIT(probabilitycommit_prob) orROLLBACK.
Like any other action, it can be tuned or removed through the registry:
registry = sw.default_action_registry()
# change how often it's picked relative to other actions
registry.get("transaction").weight = 300
# or drop it entirely
registry.remove("transaction")
There is no separate "frequency" config knob - the registry weight is the frequency knob, exactly like for every other action.
Metadata semantics
Sub-actions run against a buffered metadata context (TxnBuffer), not the
worker's shared catalog directly - the catalog must only ever reflect
durable state, never work that might still roll back. Deltas (table/index/
partition creation, drops, renames, ...) accumulate in the buffer as the
sub-actions execute.
- On
COMMITsuccess, the whole buffer is published into the shared catalog in one shot. - On
ROLLBACK(chosen randomly, or forced by a sub-action failure inabortmode), the buffer is discarded - the catalog never saw it.
This is why the drift oracle (validate_metadata / checksum comparison)
needs no special-casing for transactions: whatever it can see in the
catalog is exactly what's durable in the database.
Error modes
Controlled by error_mode:
savepoint(default) - before each sub-action, the transaction issuesSAVEPOINT spN. If the sub-action fails (a SQL error or anActionException), the action rolls back to that savepoint and the buffered deltas for that sub-action are discarded - the transaction itself survives and the loop continues with the next sub-action.
After a successful sub-action, there's an additional rollback_to_savepoint_prob
percent chance of rewinding further: a random earlier savepoint is picked
and the transaction rolls back to it (discarding every buffered delta
since, and dropping the now-dead later savepoints), pstress-style. This
exercises multi-step rollback within one still-open transaction.
abort- the first sub-action failure rolls back the entire transaction (no savepoints are taken) and the action stops; nothing from that transaction is committed. This is the harsher, less realistic mode - useful for stressing rollback-of-everything paths without the savepoint machinery in between.
Isolation weights
The isolation level for each transaction is a weighted random pick between
server_default, read_committed, repeatable_read, and serializable
(isolation_weights.*, all weights 1 by default: an even split with a
bias toward whatever the server defaults to). The dialect renders the
matching BEGIN/SET TRANSACTION ISOLATION LEVEL statements for the
connected server - weights are relative, not percentages, same as action
weights in the registry.
MySQL divergence
PostgreSQL DDL is transactional: it rolls back like any other statement.
MySQL DDL is not - it implicitly commits whatever was open before it runs.
mysql_ddl_mode decides how the transaction action handles this on
MySQL-family servers (it's a no-op on PostgreSQL, where DDL is always
transactional):
-
mirror(default) - DDL sub-actions stay in the pool, and the implicit commit is modeled truthfully: when a DDL sub-action actually sends a statement to the server (tracked via the connection's query counter, so an action that no-ops - e.g. it skipped because a limit was already hit - does not falsely end the transaction), the buffer publishes immediately, in-flight savepoints are dropped (they die with the implicit commit anyway), and every remaining sub-action for that transaction runs in autocommit instead. A DDL failure still counts as a failure, but the implicit commit already happened, so it does not roll back anything. -
exclude- DDL-typed actions are filtered out of the sub-action pool entirely, so atransactiononly ever contains DML. This gives clean all-or-nothing transactional semantics at the cost of never exercising the DDL-inside-transaction interaction on MySQL.
Python action rules
Because the transaction action owns BEGIN/COMMIT/ROLLBACK/SAVEPOINT
framing:
- Python actions (custom, registered via
@sw.action/register_python) must never issue their ownBEGIN,COMMIT, orROLLBACK- doing so from inside atransactionsub-action would desynchronize the buffered metadata from the real connection state. - If a Python action runs DDL, register it with
action_type="ddl"somysql_ddl_mode="mirror"can detect the implicit commit, andmysql_ddl_mode="exclude"can filter it out. See Python actions for the registration signature.
Config reference
See the transaction.* rows in Config parameters
for the full knob table.