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, or create, 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 workers parameter 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 CHI for memory-efficient caching. For read-heavy workloads, ensure cache_ttl is set. Setting it to 0 will disable caching, which is useful for data that changes every second.

  • Batch Operations (Concurrent Execution)

    Instead of sequential await calls, which force workers to sit idle, use Future-wait_all> or Future-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_connections setting to ensure it can handle Total 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_retry is 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:

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.