view release on metacpan or search on metacpan
include/dbil_pool.h view on Meta::CPAN
static AV *dbil_clos_cap(pTHX_ CV *cv) {
MAGIC *mg = mg_findext((SV *)cv, PERL_MAGIC_ext, &dbil_clos_vtbl);
return mg ? ((dbil_clos *)mg->mg_ptr)->cap : NULL;
}
/* ---- pool + workers -------------------------------------------------------- */
typedef struct dbil_worker {
pid_t pid;
int fd; /* parent's end of the duplex socketpair */
int busy;
int reserved; /* checked out to a transaction */
int gen; /* bumped on respawn: a txn pinned to an */
/* earlier gen is lost, not silently moved */
SV *future; /* in-flight future (+1), or NULL */
SV *rbuf; /* accumulated response bytes (+1) */
AV *txq; /* reserved-slot queue [bytes,future,...] (+1) */
struct dbil_pool *p; /* back-pointers: the C read-ready */
int wi; /* callback's ud is the worker */
} dbil_worker;
include/dbil_pool.h view on Meta::CPAN
_exit(0);
}
close(sp[1]);
/* The parent's end must not survive an exec either. */
{
int fl = fcntl(sp[0], F_GETFD);
if (fl >= 0) (void)fcntl(sp[0], F_SETFD, fl | FD_CLOEXEC);
}
w->pid = pid;
w->fd = sp[0];
w->busy = 0;
w->reserved = 0;
w->gen++; /* txns pinned to the old gen fail */
w->future = NULL;
if (!w->rbuf) w->rbuf = newSVpvs(""); /* reused across respawns */
if (!w->txq) w->txq = newAV();
w->p = p;
w->wi = wi;
if (p->vt) {
/* C-vtable adapter: readiness dispatches C-to-C, no Perl frame */
p->vt->add_reader(aTHX_ p->vt->ctx, w->fd, dbil_pool_reader_c, w);
include/dbil_pool.h view on Meta::CPAN
: newSVsv(err ? err : sv_2mortal(newSVpvs("failed"))));
resp = sv_2mortal(newRV_noinc((SV *)out));
}
frozen = dbil_freeze(aTHX_ resp);
if (frozen) (void)dbil_write_frame(aTHX_ fd, frozen);
}
close(fd);
_exit(0);
}
/* send request bytes to worker wi and mark it busy with `future` (+1 taken) */
/* Can anything still take general (non-transaction) work? */
static int dbil_pool_has_live_worker(const dbil_pool *p) {
int wi;
for (wi = 0; wi < p->nw; wi++)
if (p->w[wi].fd >= 0 && !p->w[wi].reserved) return 1;
return 0;
}
static void dbil_pool_send(pTHX_ dbil_pool *p, int wi, SV *bytes, SV *future) {
dbil_worker *w = &p->w[wi];
int reserved;
w->busy = 1;
w->future = SvREFCNT_inc(future);
if (dbil_write_frame(aTHX_ w->fd, bytes) >= 0) return;
/* The write failed, so the frame did not arrive whole and the statement
* provably never ran: dbil_write_frame only returns < 0 having written
* less than all of it.
*
* What matters here is the SLOT, not this one request. A worker killed
* while idle is invisible until something is written to it - death is
* otherwise only ever noticed as EOF on the read side, and an idle
include/dbil_pool.h view on Meta::CPAN
* failed every future from then on while the healthy workers sat idle.
* Treat a failed write as the death notice it is. */
reserved = w->reserved;
if (!reserved) {
/* Nothing executed, so this is a retry and not a loss. Put it back at
* the head of the queue so it keeps its place, and let the respawn
* inside worker_died pick it up. */
SvREFCNT_dec(w->future);
w->future = NULL;
w->busy = 0;
av_unshift(p->queue, 2);
av_store(p->queue, 0, newSVsv(bytes));
av_store(p->queue, 1, SvREFCNT_inc(future));
}
/* A reserved slot is a transaction pinned to that one connection. It
* cannot be moved, so leave the future in place for worker_died to fail
* alongside the rest of the transaction. */
dbil_pool_worker_died(aTHX_ p, wi);
include/dbil_pool.h view on Meta::CPAN
av_push(req, newSVsv(sql));
av_push(req, dbil_bind_rv(aTHX_ bind));
bytes = dbil_freeze(aTHX_ sv_2mortal(newRV_noinc((SV *)req)));
}
if (!bytes) {
dbil_future_settle_fail(aTHX_ future,
sv_2mortal(newSVpvs("could not serialise request")));
return future;
}
for (wi = 0; wi < p->nw; wi++)
if (!p->w[wi].busy && !p->w[wi].reserved && p->w[wi].fd >= 0)
{ dbil_pool_send(aTHX_ p, wi, bytes, future); return future; }
/* all busy: queue (bytes, future) - unless the backpressure cap is hit */
if (p->max_queue > 0 && (av_len(p->queue) + 1) / 2 >= p->max_queue) {
dbil_future_settle_fail(aTHX_ future,
sv_2mortal(newSVpvs("queue full (max_queue exceeded)")));
return future;
}
av_push(p->queue, newSVsv(bytes));
av_push(p->queue, SvREFCNT_inc(future));
return future;
}
/* pull the next queued request onto the now-free worker wi. A reserved
* worker only drains its own transaction queue; a free worker first serves a
* waiting acquire() (reserving itself), then the general queue. */
static void dbil_pool_dispatch(pTHX_ dbil_pool *p, int wi) {
dbil_worker *w = &p->w[wi];
if (w->reserved) {
if (!w->busy && av_len(w->txq) >= 1) {
SV *bytes = av_shift(w->txq);
SV *future = av_shift(w->txq);
dbil_pool_send(aTHX_ p, wi, sv_2mortal(bytes), future);
SvREFCNT_dec(future); /* send took its own +1 */
}
return;
}
if (av_len(p->acq) >= 0) { /* a txn is waiting for a slot */
SV *fut = av_shift(p->acq);
w->reserved = 1;
include/dbil_pool.h view on Meta::CPAN
/* ---- slot checkout (transactions) ------------------------------------------ */
/* acquire a slot: future resolves with the worker index (reserved for the
* caller). Resolves immediately when a slot is free. (+1) */
static SV *dbil_pool_acquire(pTHX_ dbil_pool *p) {
SV *future = dbil_future_new(aTHX_ "DBIx::Loop::Future");
int wi;
for (wi = 0; wi < p->nw; wi++) {
dbil_worker *w = &p->w[wi];
if (!w->busy && !w->reserved && w->fd >= 0) {
w->reserved = 1;
dbil_future_settle_done1(aTHX_ future, sv_2mortal(newSViv(wi)));
return future;
}
}
av_push(p->acq, SvREFCNT_inc(future));
return future;
}
/* release a reserved slot back to the pool and let it pick up work */
static void dbil_pool_release(pTHX_ dbil_pool *p, int wi) {
if (wi < 0 || wi >= p->nw) return;
p->w[wi].reserved = 0;
if (!p->w[wi].busy) dbil_pool_dispatch(aTHX_ p, wi);
}
/* send a statement on a reserved slot (FIFO behind any in-flight one).
* gen guards a txn whose worker died and respawned: that txn is lost, not
* silently continued on a fresh connection. */
static void dbil_pool_send_tx(pTHX_ dbil_pool *p, int wi, int gen,
SV *bytes, SV *future) {
dbil_worker *w;
if (wi < 0 || wi >= p->nw) {
dbil_future_settle_fail(aTHX_ future,
sv_2mortal(newSVpvs("transaction has no slot")));
return;
}
w = &p->w[wi];
if (w->fd < 0 || w->gen != gen) {
dbil_future_settle_fail(aTHX_ future,
sv_2mortal(newSVpvs("transaction lost (worker died)")));
return;
}
if (w->busy) {
av_push(w->txq, newSVsv(bytes));
av_push(w->txq, SvREFCNT_inc(future));
return;
}
dbil_pool_send(aTHX_ p, wi, bytes, future);
}
/* unregister fd from the loop via whichever seam the adapter has */
static void dbil_pool_loop_remove(pTHX_ dbil_pool *p, int fd) {
if (p->vt) {
include/dbil_pool.h view on Meta::CPAN
static void dbil_pool_worker_died(pTHX_ dbil_pool *p, int wi) {
dbil_worker *w = &p->w[wi];
if (w->future) {
dbil_future_settle_fail(aTHX_ w->future,
sv_2mortal(newSVpvs("worker exited unexpectedly")));
SvREFCNT_dec(w->future); w->future = NULL;
}
dbil_pool_loop_remove(aTHX_ p, w->fd);
close(w->fd);
w->fd = -1;
w->busy = 0;
if (w->pid > 0) { waitpid(w->pid, NULL, 0); w->pid = 0; }
if (w->rbuf) SvCUR_set(w->rbuf, 0); /* drop any partial frame */
/* a transaction pinned here is lost: fail everything queued on the slot
* (the gen bump in spawn stops later tx statements too) */
while (av_len(w->txq) >= 1) {
SV *bytes = av_shift(w->txq);
SV *fut = av_shift(w->txq);
SvREFCNT_dec(bytes);
dbil_future_settle_fail(aTHX_ fut,
sv_2mortal(newSVpvs("transaction lost (worker died)")));
include/dbil_pool.h view on Meta::CPAN
const unsigned char *b = (const unsigned char *)SvPVX(w->rbuf);
STRLEN len;
if (have < 4) break;
len = ((STRLEN)b[0] << 24) | ((STRLEN)b[1] << 16)
| ((STRLEN)b[2] << 8) | (STRLEN)b[3];
if (have < 4 + len) break;
{
SV *resp = dbil_thaw(aTHX_ (const char *)b + 4, len);
SV *fut = w->future;
w->future = NULL;
w->busy = 0;
if (fut) {
if (resp && SvROK(resp) && SvTYPE(SvRV(resp)) == SVt_PVAV) {
AV *ra = (AV *)SvRV(resp);
int ok = (int)SvIV(*av_fetch(ra, 0, 0));
SV *data = *av_fetch(ra, 1, 0);
if (ok) dbil_future_settle_done1(aTHX_ fut, data);
else dbil_future_settle_fail(aTHX_ fut, data);
} else {
dbil_future_settle_fail(aTHX_ fut,
sv_2mortal(newSVpvs("bad response frame from worker")));
lib/DBIx/Loop.pm view on Meta::CPAN
Either way the result is the same shape: C<query> resolves to
C<< { rows => [ [...], ... ], columns => [ ... ] } >> (arrayref rows - they
benchmarked ~4x faster to build than hashrefs) and C<do> to
C<< { rows_affected => $n, insert_id => $id } >> (insert_id best-effort via
C<last_insert_id>).
A note on B<SQLite and more than one worker>: each worker is another process
with its own connection, and SQLite takes one writer at a time for the whole
file. Reads run concurrently; writes serialise, and a writer that keeps losing
the race gets C<database is locked> back once DBD::SQLite's busy timeout (30
seconds by default) is spent - which on a loaded machine a deep burst of
concurrent writes can genuinely do. For write-heavy SQLite either use
C<workers =E<gt> 1>, which costs no responsiveness (the queue still keeps the
loop free) and removes the contention outright, or raise
C<sqlite_busy_timeout>. Client/server engines have no such limit.
=head1 CONSTRUCTORS
=head2 connect
my $db = DBIx::Loop->connect($dsn, $user, $pass, \%attr,
loop => $adapter, # required; or 'auto'
workers => 4, # pool backend: worker count
max_queue => 0, # pool backend: pending cap (0 = unbounded)
);
lib/DBIx/Loop.pm view on Meta::CPAN
Failures carry the DBI error string.
=head2 txn
my $future = $db->txn(sub {
my ($tx) = @_;
# $tx->query / $tx->do are pinned to ONE connection
return $tx->do(...)->then(sub { $tx->do(...) });
});
Acquires a pool slot (waiting when all are busy), runs C<BEGIN>, calls the
block with a L<DBIx::Loop::Txn> handle pinned to that connection, then
C<COMMIT>s - or C<ROLLBACK>s when the block dies or its returned future fails,
failing the outer future with the original error. The block may return a plain
value or a future; the outer future resolves to it after commit.
Plain C<$db> statements during a transaction run on B<other> slots - they
never join the transaction and never steal its connection. A C<txn> inside a
block is an independent transaction on another slot (beware awaiting one while
its parents hold every slot). If a worker dies mid-transaction the transaction
B<fails> - it is never silently resumed on the respawned connection. Pool
t/02-pool.t view on Meta::CPAN
is($res->{rows}[0][0], 5, 'pool still serves after an error');
}
# ---- worker crash: in-flight future fails, slot respawns, pool recovers ---------
{
my @pids = $db->_worker_pids;
is(scalar @pids, 3, 'pool reports 3 worker pids');
# Kill a worker while a query is genuinely in flight on it.
#
# `busy` is the parent's bookkeeping, not the child's state: writing the
# request frame says nothing about whether the child has already read,
# run and answered it. Against SELECT COUNT(*) over five rows the child
# routinely finished before the signal landed, and the whole block then
# exercised the idle-death path instead (which is worth testing, and is
# tested separately below). A statement slow enough to still be running
# is what makes this deterministic.
my $SLOW = 'WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL '
. 'SELECT x+1 FROM c WHERE x < 3000000) SELECT COUNT(*) FROM c';
my @f = map { $db->query($SLOW) } 1 .. 3;
kill 'KILL', $pids[0];
$ad->await($_) for @f;
my $failed = grep { $_->is_failed } @f;
ok($failed >= 1, 'killing a busy worker fails its in-flight future')
or diag map { $_->is_failed ? "failed: " . $_->failure : "done" } @f;
# the slot respawned: pool still has 3 live pids, the dead one replaced
my @pids2 = $db->_worker_pids;
is(scalar(grep { $_ > 0 } @pids2), 3, 'dead slot respawned (3 live pids)');
isnt($pids2[0], $pids[0], 'slot 0 has a new pid');
# and keeps serving at full width
my @g = map { $db->query("SELECT COUNT(*) FROM t") } 1 .. 6;
$ad->await($_) for @g;
t/03-native-mock.t view on Meta::CPAN
package Mock::Attr; # anything read via ->FETCH($key)
sub new { my ($c, %h) = @_; bless {%h}, $c }
sub FETCH { $_[0]{ $_[1] } }
package Mock::PgSth;
sub new { my ($c, %h) = @_; bless {%h}, $c }
sub execute {
my $self = shift;
my $dbh = $self->{dbh};
die "mock execute: connection busy\n" if $dbh->{inflight};
$dbh->{inflight} = $self;
# completion arrives asynchronously: a forked child writes one byte to the
# far end of the socketpair after `delay` seconds
my $pid = fork; defined $pid or die "fork: $!";
if (!$pid) {
select undef, undef, undef, ($self->{delay} || 0.02);
syswrite $dbh->{notify}, "x", 1;
POSIX::_exit(0) if eval { require POSIX; 1 };
exit 0;
}
t/lib/BackendParity.pm view on Meta::CPAN
my $wide = $await->($db->query(
"SELECT id, name, n, id+1, id+2, id+3, id+4, id+5, id+6, id+7 "
. "FROM p WHERE id = 1"));
is(scalar @{ $wide->{rows}[0] }, 10, 'ten columns cross intact');
# -- a larger result set --------------------------------------------------
#
# The seed writes go one at a time, on purpose. Two pool workers are
# two processes writing one SQLite file and SQLite serialises writers,
# so fired off as a 500-deep burst they fight for the write lock; on a
# loaded machine a starved writer sits out its whole busy timeout and
# comes back "database is locked". Nothing here ever looked at the
# write futures, so that arrived as a silent short row count (a CPAN
# smoker reported 497). Nothing about crossing 500 rows needs
# concurrent writers - the concurrent-work path is what the read burst
# below, AdapterConformance and t/02-pool.t are for - and a failed
# write is now a named failure rather than a missing row.
{
my ($written, $err) = (0, undef);
for my $i (100 .. 599) {
my $f = $db->do("INSERT INTO p (id, name, n) VALUES (?,?,?)",