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/rollbackon that handle, and every model on the database is inside it for the length of the block whether it came through$tx->modelor$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->modelinside the block is outside the transaction - its writes commit on their own - and$tx->modelis 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)