NAME

Punk::Model::DBIx::Loop - a non-blocking backend for Punk models

SYNOPSIS

# in the app class
database dsn     => 'dbi:SQLite:dbname=myapp.db',
         backend => 'Punk::Model::DBIx::Loop';
model 'Book';

DESCRIPTION

A Punk::Model backend. Same six-method contract as the default Punk::Model::DBI, same SQL, same result shapes - but every method returns a Punk::Future instead of a value, and the statement runs on DBIx::Loop over the worker's own event loop.

That is the whole point. Under the DBI backend a worker blocks for the whole database round trip and serves nobody; here it goes back to the loop and picks up other requests, which is what the rest of Punk already does for every other kind of I/O. The cost is that handlers have to be written for it - see "WRITING HANDLERS" - which is why it is not the default.

Everything below the call goes through DBIx::Loop's C ABI: the statement and its reshape are one call, and the continuation that settles the future is a C function. No closure is compiled per query, and no Perl frame runs when the rows land.

WRITING HANDLERS

A handler hands the future straight back - the dispatcher awaits any future a handler returns:

sub show {
    my ($c) = @_;
    return $c->model('Book')->get( id => $c->param('id') );
}

Post-processing is a then:

sub show {
    my ($c) = @_;
    return $c->model('Book')->get( id => $c->param('id') )->then(sub {
        my ($book) = @_;
        return $c->not_found unless $book;
        $c->render('book/view', { book => $book });
    });
}

$c->await works too, and is occasionally clearer, but it stops the worker for the duration - which is the thing this backend exists to avoid. Prefer the chain.

Concurrent queries are Punk::Future->needs_all:

my $f = Punk::Future->needs_all(
    $c->model('Book')->all,
    $c->model('Author')->all,
)->then(sub {
    my ($books, $authors) = @_;
    $c->render('index', { books => $books->{rows},
                          authors => $authors->{rows} });
});

CONFIGURATION

From the database keyword, as the DBI backend plus the pool sizes:

database
    backend   => 'Punk::Model::DBIx::Loop',   # required: not the default
    dsn       => 'dbi:SQLite:dbname=myapp.db',
    user      => $user,       # optional
    password  => $pass,       # optional
    attr      => { ... },     # optional, merged into the connect attrs
    workers   => 4,           # optional, DBIx::Loop pool size
    max_queue => 0;           # optional, 0 = unbounded

One DBIx::Loop per distinct connection (dsn, credentials and pool sizes) is shared by every model on it, built lazily on the first statement so punk console and tests that only instantiate never fork a pool. Two database blocks naming one dsn with different pool sizes get different pools rather than silently sharing one.

The handle carries the pid that built it and is rebuilt after a fork, so a preforking worker never inherits another process's pool.

THE CONTRACT

Identical to the default Punk::Model::DBI, wrapped in a future. get resolves to the row hashref or undef; search to { rows => [...], has_more_data => 0|1, next => $token|undef }; all to search({}, {}); create and update to the stored row; delete to the affected row count.

Pagination tokens are the same opaque encoding both backends use, so a next token minted by one decodes on the other and switching backends does not break paginated URLs already in flight.

Two things are deliberately synchronous, because both are programming errors rather than query failures, and a failed future would surface them as a 500 where a croak surfaces them at the call site:

  • Field validation (create/update) runs before the statement is built, exactly as it does on the DBI backend.

  • A malformed pagination token croaks from search rather than failing its future.

A query that fails in the database fails the future, and the error reaches your else or the dispatcher's error handler.

METHODS BEYOND THE CONTRACT

db

The DBIx::Loop connection for this backend's dsn. Custom model methods run their own SQL through it:

sub recent {
    my ($self, $room, $limit) = @_;
    return $self->backend->future(
        $self->backend->db->selectall_rowhash(
            'SELECT * FROM messages WHERE room = ?
              ORDER BY id DESC LIMIT ?', $room, $limit)
    );
}

future($dbil_future)

Bridges a raw DBIx::Loop::Future into a Punk::Future, so a custom method returns the same type as the contract ones.

await($future)

Resolves one of this backend's futures outside a worker, by pumping the adapter's own loop; returns the settled values and croaks on failure, like $future->get. Inside a worker $c->await already does this against the worker's loop. Scripts and tests can also just call $future->get.

dbh

DBIx::Loop's own parent DBI handle. Statements do not run on it - the pool workers hold their own connections - but it is the right place for boot work such as creating a schema, and it is what quotes identifiers.

adapter

The DBIx::Loop loop adapter this connection runs on. Inside a worker it is built on the worker's own loop, named explicitly; an adapter left to pick a loop for itself would construct one nothing ever runs, and every future would hang.

SEE ALSO

Punk::Model, Punk::Model::DBI, Punk::Future, DBIx::Loop, Punk.

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)