NAME

Punk::Txn - the transaction a $c->txn block receives

SYNOPSIS

my $order = $c->txn(sub {
    my ($tx) = @_;
    my $o = $tx->model('Order')->create(\%data);
    $tx->model('Stock')->update({ id => $sku, held => $held + 1 });
    return $o;
});

$c->txn(analytics => sub { my ($tx) = @_; ... });

# on Punk::Model::DBIx::Loop the block's result is a future, and so
# is txn's - return it from the handler
return $c->txn(sub {
    my ($tx) = @_;
    return $tx->model('Order')->create(\%data)
      ->then(sub { $tx->model('Stock')->update({ ... }) });
})->then(sub { $c->json($_[0]) });

DESCRIPTION

A transaction belongs to a database, not to a model, so it is started from the context - "txn" in Punk::Context - on the default database or one the database keyword named, and the block is handed this object. What comes back is what the block returns: the value itself on Punk::Model::DBI, a Punk::Future of it on Punk::Model::DBIx::Loop, in both cases after COMMIT. A die inside the block is a ROLLBACK and a rethrow; on the async backend a failed future from the block is the same.

What $tx->model means, per backend

$tx->model($name) is the registered model bound to this transaction: a copy of the per-worker instance whose statements run on the transaction's connection. The shared instance is untouched, so nothing leaks out of the block. The two backends differ in what that binding buys, and the difference is the reason the method exists:

  • Punk::Model::DBI holds one connection per database per worker. A transaction is begin_work / commit / rollback on that handle, and every model on the database is inside it for the length of the block whether it came through $tx->model or $c->model. Here the binding is a check - a model on another database croaks - and a reminder of which database the block is on.

  • Punk::Model::DBIx::Loop runs statements on a pool, and DBIx::Loop pins a transaction to one slot: its own rule is that plain statements during a transaction run on other slots and never join. So a model reached through $c->model inside the block is outside the transaction - its writes commit on their own - and $tx->model is the only way in. A worker-wide "current transaction" would be wrong under the event loop, where the requests served between two of this block's queries belong to somebody else; binding the copy is what keeps one request's transaction its own.

METHODS

model($name)

The registered model, bound to this transaction. Croaks for a model that lives on another database, and after the block has ended.

handle

The connection the transaction is on, for a raw statement that has no model: the DBI $dbh on Punk::Model::DBI, the DBIx::Loop::Txn on Punk::Model::DBIx::Loop (whose query and do return futures).

name

The database's name as the database keyword gave it; default for the unnamed one.

backend

The backend class the transaction runs on.

is_active

True while the block is running (on the async backend, until its future settles); false after. model croaks once it is false.

NESTING, AND WHAT JOINS

On Punk::Model::DBI a txn inside a txn croaks. There is one connection, and a silent join would stop a rollback covering what the outer block thought it covered; savepoints are a later addition, not an implicit one. On Punk::Model::DBIx::Loop an inner txn is an independent transaction on another slot - DBIx::Loop's behaviour - and awaiting it while the outer one holds the last slot waits forever.

Anything on the DBI backend's connection joins: every model on the database, a raw statement on $tx->handle, and anything else an application pointed at the same $dbh. Punk::Queue holds its own connection and does not.

SEE ALSO

Punk::Context, Punk::Model, Punk::Model::DBI, Punk::Model::DBIx::Loop, "txn" in DBIx::Loop.

AUTHOR

LNATION <email@lnation.org>

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)