NAME
DBIx::Class::Async - Non-blocking, multi-worker asynchronous wrapper for DBIx::Class
VERSION
Version 0.55
DISCLAIMER
This is pure experimental currently.
You are encouraged to try and share your suggestions.
QUICK START
This example demonstrates how to set up a small SQLite database and perform an asynchronous "Create and Count" operation.
use strict;
use warnings;
use IO::Async::Loop;
use DBIx::Class::Async::Schema;
my $loop = IO::Async::Loop->new;
# 1. Connect to your database
my $schema = DBIx::Class::Async::Schema->connect(
"dbi:SQLite:dbname=myapp.db", undef, undef, {},
{
workers => 2, # Keep 2 database connections ready
schema_class => 'MyApp::Schema' # Your DBIC Schema name
}
);
# 2. Deploy (Create tables if they don't exist)
$schema->await($schema->deploy);
# 3. Non-blocking workflow
# We don't "wait" for the DB; we tell the loop what to do when it's done.
$schema->resultset('User')
->create({ name => 'Starlight', email => 'star@perl.org' })
->then(
sub {
return $schema->resultset('User')->count;
})
->on_done(
sub {
my $count = shift;
print "User saved! Total users now: $count\n";
})
->on_fail(
sub {
warn "Something went wrong: @_\n";
});
# 4. Start the engine
$loop->run;
SYNOPSIS
use IO::Async::Loop;
use DBIx::Class::Async::Schema;
my $loop = IO::Async::Loop->new;
# Connect returns a "Virtual Schema" that behaves like DBIC
# but returns Futures instead of data.
my $schema = DBIx::Class::Async::Schema->connect(
"dbi:SQLite:dbname=app.db", undef, undef, {},
{
workers => 4, # Parallel database connections
schema_class => 'My::Schema', # Your standard DBIC Schema
async_loop => $loop,
}
);
# 1. Simple CRUD (Returns a Future)
my $future = $schema->resultset('User')->find(1);
$future->on_done(sub {
my $user = shift;
print "Found: " . $user->name . "\n" if $user;
});
# 2. Modern Async/Await style
async sub get_user_count {
my $count = await $schema->resultset('User')->count;
return $count;
}
# 3. Batch Transactions
my $tx = await $schema->txn_do([
{ action => 'create', resultset => 'User', data => { name => 'Alice' }, name => 'new_user' },
{ action => 'create', resultset => 'Log', data => { msg => "Created user \$new_user.id" } }
]);
THE ASYNC DESIGN APPROACH
The "Real Truth" about DBIx::Class is that it is inherently synchronous. Under the hood, DBI and most database drivers (like DBD::SQLite or DBD::mysql) block the entire Perl process while waiting for the database to respond.
How it used to be (The Old Design)
In traditional async Perl, developers often tried to wrap DBIC calls in a simple Future. However, because the underlying DBI call was still blocking, one slow query would "freeze" the entire event loop. Your UI would hang, and other network requests would stop.
How we do it now (The "Bridge & Worker" Design)
DBIx::Class::Async uses a Worker-Pool Architecture.
The Bridge (Main Process)
When you call
find,search, orcreate, the main process doesn't talk to the database. It packages your request and sends it over a pipe to an available worker. It then immediately returns a Future and goes back to work handling other events.The Workers (Background Processes)
We maintain a pool of background processes. Each worker has its own dedicated connection to the database. The worker performs the blocking DBIC call, serialises the result, and sends it back to the Bridge.
The Result
The Bridge receives the data, resolves the
Future, and your code continues.
Why this is better:
Zero Loop Freezing
Even if a query takes 10 seconds, your main application loop remains 100% responsive.
True Parallelism
With 4 workers, you can execute 4 heavy database queries simultaneously. Standard DBIC can only do 1 at a time.
Automatic Serialisation
We handle the complex task of turning "live" DBIC objects into data structures that can safely travel between processes.
METHODS
create_async_db
Initialises the async environment and spawns workers.
my $db = DBIx::Class::Async->create_async_db(
schema_class => 'MyApp::Schema',
connect_info => [ 'dbi:SQLite:db.sqlite' ],
workers => 4,
cache_ttl => 300, # 5 minutes
enable_retry => 1,
);
disconnect
Gracefully shuts down all background workers and clears timers. Always call this before your application exits.
DBIx::Class::Async->disconnect($db);
EVENT LOOP INTEGRATION
LOOP AGNOSTICISM
DBIx::Class::Async is built atop IO::Async, but it is designed to be loop-agnostic. It does not force you to use a specific event loop engine. This is critical for applications already running within an established ecosystem like Mojolicious, AnyEvent, etc.
The bridge follows a "Smart Default" pattern:
Implicit
If no loop is provided, it automatically detects your OS and instantiates the best available IO::Async::Loop (e.g., Epoll, KQueue, or Poll).
Explicit
If you provide a loop object via the loop attribute, the bridge "slaves" itself to that engine.
UNDER THE HOOD
When an external loop is provided, DBIx::Class::Async performs the following steps:
Process Delegation
The library initialises a pool of persistent background workers. These are separate processes that hold their own database handles to prevent blocking the main event loop.
Pipe Multiplexing
Communication between your application and the workers happens via asynchronous pipes. Your event loop (Mojo, Poll, etc.) watches these pipes for "Read Ready" signals.
Heartbeat Sharing
All internal timers (Health Checks, TTL Caching) are registered as Notifiers within the parent loop’s reactor, ensuring they only fire when the loop is "ticking".
Non-Blocking Flow
Because the database I/O happens in a separate memory space, a 10-second query will not increase the "latency" or "lag" of your web server's main loop.
EXAMPLE: MOJOLICIOUS INTEGRATION
To use this library inside a Mojolicious application, use the IO::Async::Loop::Mojo bridge. This allows your database workers to share the same reactor as your web server, preventing I/O starvation.
use Mojolicious::Lite;
use DBIx::Class::Async::Schema;
use IO::Async::Loop::Mojo;
# 1. Create a bridge between IO::Async and Mojo
my $loop = IO::Async::Loop::Mojo->new;
# 2. Connect your schema using the Mojo-backed loop
helper db => sub {
state $schema = DBIx::Class::Async::Schema->connect(
$dsn, $user, $pass, \%dbic_attrs,
{
schema_class => 'My::App::Schema',
workers => 4,
loop => $loop, # The Magic Connection
}
);
};
# 3. Use non-blocking DBIC in your routes
get '/stats' => sub {
my $c = shift;
$c->db->resultset('User')
->search({ active => 1 })
->all
->on_done(sub {
my @users = @_;
$c->render(json => { count => scalar @users });
});
};
app->start;
PERFORMANCE TIPS
Worker Count & CPU Affinity
Adjust the
workersparameter based on your database connection limits and expected concurrency. Since each worker is a separate process, 2-4 workers per CPU core is the sweet spot. Too many workers can cause context-switching overhead on your OS.Caching Strategy
The new design uses
CHIfor memory-efficient caching. For read-heavy workloads, ensurecache_ttlis set. Setting it to0will disable caching, which is useful for data that changes every second.Batch Operations (Concurrent Execution)
Instead of sequential
awaitcalls, which force workers to sit idle, useFuture-wait_all> orFuture-needs_all> to fire multiple queries across your worker pool simultaneously.Connection Persistence
Each worker maintains a persistent connection to the database. This eliminates the "connection tax" (handshake time) for every query. Monitor your database's
max_connectionssetting to ensure it can handleTotal Apps * Workers Per App.Timeout Guardrails
The
query_timeout(default 30s) is your safety net. In the new design, a hung query only blocks one worker; the others stay active. Without a timeout, a single "zombie" query could permanently reduce your pool size.
ERROR HANDLING
The bridge uses Future->fail to propagate errors from the workers back to your main process. You should handle these using await within a try/catch block or the ->catch method.
Worker Connectivity
If a worker cannot connect to the DB, it will throw an exception during the first query or a health check.
Automatic Retries
If
enable_retryis true, the bridge will automatically retry queries that fail due to transient issues (like database deadlocks) using an exponential backoff (starting at 1s, doubling each time).Serialisation Failures
Because data must travel between processes, any custom objects in your ResultSets must be serialiisable. The new design handles most DBIC inflation automatically, but be wary of passing "live" filehandles or sockets.
METRICS
If enable_metrics => 1 is passed to the constructor and Metrics::Any is available, the following gauges and counters are tracked:
+------------------------------------+-----------+-----------------------------------------------------+
| Metric | Type | Description |
+------------------------------------+-----------+-----------------------------------------------------+
| C<db_async_queries_total> | Counter | Total number of operations sent to workers. |
| C<db_async_cache_hits_total> | Counter | Queries resolved via the internal C<CHI> cache. |
| C<db_async_query_duration_seconds> | Histogram | Latency distribution of database operations. |
| C<db_async_workers_active> | Gauge | Number of workers passing the periodic health check.|
+------------------------------------+-----------+-----------------------------------------------------+
DEDICATION
This module is dedicated to the memory of Matt S. Trout (mst), a brilliant contributor to the Perl community, DBIx::Class core developer, and friend who is deeply missed.
AUTHOR
Mohammad Sajid Anwar, <mohammad.anwar at yahoo.com>
REPOSITORY
https://github.com/manwar/DBIx-Class-Async
BUGS
Please report any bugs or feature requests through the web interface at https://github.com/manwar/DBIx-Class-Async/issues. I will be notified and then you'll automatically be notified of progress on your bug as I make changes.
SUPPORT
You can find documentation for this module with the perldoc command.
perldoc DBIx::Class::Async
You can also look for information at:
BUG Report
CPAN Ratings
Search MetaCPAN
LICENSE AND COPYRIGHT
Copyright (C) 2026 Mohammad Sajid Anwar.
This program is free software; you can redistribute it and / or modify it under the terms of the the Artistic License (2.0). You may obtain a copy of the full license at: http://www.perlfoundation.org/artistic_license_2_0 Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License.By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license. If your Modified Version has been derived from a Modified Version made by someone other than you,you are nevertheless required to ensure that your Modified Version complies with the requirements of this license. This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder. This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement,then this Artistic License to you shall terminate on the date that such litigation is filed. Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.