DBIO-PostgreSQL-EV
view release on metacpan or search on metacpan
lib/DBIO/PostgreSQL/EV/Storage.pm view on Meta::CPAN
}
else {
$conn->query($sql, $cb);
}
EV::run(EV::RUN_ONCE()) until $done;
croak "pool connect statement failed: $err" if defined $err;
return;
}
# --- Pipeline mode seams ------------------------------------------------------
# The inherited DBIO::Storage::Async::pipeline scaffold acquires a connection,
# brackets the batch with these three seams and releases the connection. EV::Pg
# expresses pipelining natively, so the base scaffold fits exactly -- no
# parallel pipeline() is needed (WP4 decision).
sub _pipeline_enter { $_[1]->enter_pipeline }
sub _pipeline_sync {
my ($self, $pg) = @_;
my $f = Future->new;
$pg->pipeline_sync(sub { $f->done });
return $f;
}
sub _pipeline_exit { $_[1]->exit_pipeline }
# --- LISTEN/NOTIFY ------------------------------------------------------------
sub listen {
my ($self, $channel, $cb) = @_;
$self->{_listeners}{$channel} = $cb;
# Use a dedicated connection for LISTEN (not from the pool).
# EV::Pg->new returns before the socket is actually connected; query()
# dispatched on a not-yet-connected handle throws "not connected".
# We buffer LISTEN/UNLISTEN until on_connect fires and then flush.
#
# karr #15: a second listen() on an already-connected dedicated conn
# used to dispatch its LISTEN SQL directly via _listen_pg->query().
# That races the still-in-flight CommandComplete from the first
# LISTEN â libpq refuses with "another command is already in
# progress", the LISTEN SQL is lost, and notifications on the new
# channel are never delivered. Fix: route every LISTEN/UNLISTEN SQL
# through a single _dispatch_listen_queue serialiser so we never have
# two in flight at once. on_connect sets _listen_connected and calls
# us; listen/unlisten push onto _listen_pending and call us; we drain
# one at a time and re-enter from the in-flight query's callback.
$self->{_listen_pg} ||= do {
require EV::Pg;
$self->{_listen_pending} = [];
$self->{_listen_dispatching} = 0;
$self->{_listen_connected} = 0;
my $pg = EV::Pg->new(
conninfo => $self->_conninfo_string,
keep_alive => 1,
on_connect => sub {
$self->{_listen_connected} = 1;
$self->_dispatch_listen_queue;
},
on_error => sub { warn "LISTEN connection error: $_[0]\n" },
on_notify => sub {
my ($ch, $payload, $pid) = @_;
if (my $handler = $self->{_listeners}{$ch}) {
$handler->($ch, $payload, $pid);
}
},
);
$pg;
};
my $quoted = $self->sql_maker->_quote($channel);
my $sql = "LISTEN $quoted";
push @{ $self->{_listen_pending} }, $sql;
$self->_dispatch_listen_queue if $self->{_listen_connected};
}
# Drain one SQL off _listen_pending and send it via the dedicated LISTEN
# connection, then chain the next dispatch off its callback. This is the
# single point through which ALL LISTEN/UNLISTEN SQLs flow, so we never
# have two in flight on the same libpq connection. Without this, a second
# listen() issued while the first LISTEN's CommandComplete is still in
# flight races libpq and dies with "another command is already in
# progress", losing the second subscription. (karr #15)
sub _dispatch_listen_queue {
my ($self) = @_;
# Not connected yet â on_connect will call us when the socket is up.
return unless $self->{_listen_connected};
# A previous dispatch is still in flight; its callback will re-enter
# us once libpq has consumed its CommandComplete + ReadyForQuery.
return if $self->{_listen_dispatching};
# Nothing queued.
return unless $self->{_listen_pending} && @{$self->{_listen_pending}};
my $sql = shift @{$self->{_listen_pending}};
$self->{_listen_dispatching} = 1;
$self->{_listen_pg}->query($sql, sub {
$self->{_listen_dispatching} = 0;
$self->_dispatch_listen_queue;
});
}
sub unlisten {
my ($self, $channel) = @_;
delete $self->{_listeners}{$channel};
if ($self->{_listen_pg}) {
my $quoted = $self->sql_maker->_quote($channel);
my $sql = "UNLISTEN $quoted";
push @{ $self->{_listen_pending} }, $sql;
$self->_dispatch_listen_queue if $self->{_listen_connected};
}
}
lib/DBIO/PostgreSQL/EV/Storage.pm view on Meta::CPAN
# put_copy_end could not even queue the EOF marker (hard libpq
# error). Treat as COPY failure and complete the Future here
# because the second firing will never arrive.
$self->pool->release($pg);
$f->fail("put_copy_end failed");
return;
}
# put_copy_end queued (or was about to queue) the EOF marker.
# Second callback firing will release the pool and complete $f.
});
return $f;
});
}
# --- Async Deploy -------------------------------------------------------------
#
# WP4 note (karr #22): deploy_async / _execute_ddl_async / _drop_statements_for
# are kept here rather than hoisted into core. They are mostly generic
# (statement-splitting + sequential pinned _query_async inside a txn) and could
# be hoisted, but hoisting is a separate small core ticket and is NOT done here
# -- keeping the driver behaviour identical is the point of this thin-transport
# refactor. If/when core grows a generic deploy_async, this can inherit it.
sub deploy_async {
my ($self, $schema, $opts) = @_;
$opts //= {};
# Generate the install DDL from the schema classes (synchronous, in-memory
# â no DB roundtrips here; the whole point of routing through the DBIO
# deploy pipeline is to keep DDL construction out of the storage layer).
my $ddl = $schema->pg_install_ddl;
# Optional DROP TABLE pre-pass for idempotent re-runs. Mirrors the
# {add_drop_table => 1} option that DBIO::Schema->deploy accepts.
if ($opts->{add_drop_table}) {
$ddl = join("\n\n", _drop_statements_for($schema), $ddl);
}
# Run all DDL on a single pinned connection inside an async transaction.
# PostgreSQL's transactional DDL means a failure on statement N of M
# rolls back the previous N-1 â same semantics as
# DBIO::Deploy::Base::_execute_ddl with _use_transactional_ddl(1),
# just on the Future side of the wire.
return $self->txn_do_async(sub {
my ($ctx) = @_;
return $self->_execute_ddl_async($ctx->txn_pg, $ddl);
});
}
# Split a DDL string and execute each statement on a pinned EV::Pg
# connection, one at a time. Returns a Future that resolves on the last
# successful statement or fails on the first libpq error â the surrounding
# txn_do_async then COMMITs or ROLLBACKs accordingly.
#
# The recursion pumps exactly one DDL at a time on the pinned connection:
# libpq never has two in flight on the same handle. Each step waits for
# CommandComplete + ReadyForQuery before dispatching the next. The
# surrounding txn_do_async retain()s the returned Then-Future, which keeps
# the whole chain alive until COMMIT/ROLLBACK fires.
sub _execute_ddl_async {
my ($self, $pg, $ddl) = @_;
my @stmts = grep { !/^\s*--/ } _split_statements($ddl);
my $recur;
$recur = sub {
my $stmt;
unless (defined($stmt = shift @stmts)) {
return Future->done;
}
my $f = Future->new;
$self->_debug_query($stmt, []) if $self->{debug};
$pg->query($stmt, sub {
my (undef, $err) = @_;
$err ? $f->fail($err) : $f->done;
});
return $f->then($recur);
};
return $recur->();
}
# Build DROP TABLE IF EXISTS ... CASCADE for every regular table in the
# schema. Skips views, virtual views, and scalar-ref names that aren't
# plain identifiers. Returns a single string the caller prepends to the
# install DDL.
sub _drop_statements_for {
my ($schema) = @_;
my @out;
for my $name ($schema->sources) {
my $source = $schema->source($name);
next if $source->isa('DBIO::ResultSource::View');
my $table = $source->name;
next if ref $table;
next if $table =~ /\s|\(/;
push @out, sprintf 'DROP TABLE IF EXISTS %s CASCADE;', _quote_ident($table);
}
return join("\n\n", @out);
}
sub deploy {
my $self = shift;
return $self->deploy_async(@_)->get;
}
# --- Schema Integration -------------------------------------------------------
# schema / debug / connected / DESTROY and the sync CRUD/txn ->get fallbacks are
# inherited from DBIO::Storage::Async. disconnect is overridden to also tear down
# the dedicated LISTEN connection.
sub disconnect {
my $self = shift;
if ($self->{pool}) {
$self->{pool}->shutdown;
$self->{pool} = undef;
}
if ($self->{_listen_pg}) {
$self->{_listen_pg}->finish;
$self->{_listen_pg} = undef;
}
( run in 4.455 seconds using v1.01-cache-2.11-cpan-14f38c9f855 )