DEV Community

Cover image for PAGI 0.001016: Navigating the Future::IO Configuration Problem
John Napiorkowski
John Napiorkowski

Posted on

PAGI 0.001016: Navigating the Future::IO Configuration Problem

PAGI 0.001016 includes a decision we wrestled with for a while: where should Future::IO configuration live? This post walks through the tradeoffs we considered and why we landed where we did.

What is Future::IO?

Perl's async ecosystem has multiple event loops: IO::Async, Mojo::IOLoop, AnyEvent, UV. This diversity is a strength - different loops have different strengths - but it creates a problem for library authors. If you write an async Redis client, which event loop do you target?

Future::IO solves this by providing an abstraction layer. Libraries code against Future::IO's API (sleep, read, write), and Future::IO delegates to whatever event loop is actually running. This means libraries like Async::Redis work with any event loop - IO::Async, UV, or anything else that has a Future::IO backend.

The catch: Future::IO needs to be told which backend to use. It doesn't auto-detect the running loop. Someone has to explicitly configure it:

use Future::IO;
Future::IO->load_impl('IOAsync');  # Tell Future::IO to use IO::Async
Enter fullscreen mode Exit fullscreen mode

This configuration must happen before any Future::IO operations are attempted. Get it wrong - or forget it entirely - and things break in confusing ways.

The Problem

Given that Future::IO needs explicit configuration, who should do it? Someone has to call:

use Future::IO;
Future::IO->load_impl('IOAsync');  # or 'UV', etc.
Enter fullscreen mode Exit fullscreen mode

The question is: who?

What Doesn't Work

A quick note: PAGI is built on the async Perl ecosystem created largely by Paul Evans (LeoNerd) - IO::Async, Future, and Future::IO. His guidance shaped our thinking here, and the constraints he identified are real engineering concerns, not arbitrary rules.

Libraries configuring Future::IO - This was our first instinct. Async::Redis could call Future::IO->load_best_impl to auto-detect the best backend. But load_best_impl picks based on what's installed, not what's running. If you have UV installed but you're running under IO::Async (like PAGI::Server), you get a mismatch. Things explode.

Paul's guidance: libraries shouldn't configure Future::IO because they don't know the runtime context. This makes sense - a library is a guest in someone else's application.

PAGI::Server (the module) configuring it - We tried this too. It seemed logical - PAGI::Server runs IO::Async, so it knows the context. But there's a legitimate concern: PAGI::Server is still a module being used, and someone might embed it in a larger application.

Consider: you're running PAGI::Server alongside other event-driven code - maybe a metrics collector, a background job processor, or a connection to a message queue. Your main script creates the IO::Async loop and adds multiple notifiers to it:

my $loop = IO::Async::Loop->new;

# Your other async code
$loop->add($metrics_reporter);
$loop->add($job_processor);

# PAGI::Server is just one component
my $server = PAGI::Server->new(app => $app, port => 8080);
$loop->add($server);

$loop->run;
Enter fullscreen mode Exit fullscreen mode

In this scenario, the main script is the entry point, not PAGI::Server. If PAGI::Server configured Future::IO, it would be a module reaching out to modify global state - potentially conflicting with configuration the main script already set up. The principle that only entry points should configure global state exists precisely for cases like this.

The Tradeoffs We Considered

Option 1: Apps Configure Future::IO

The "pure" approach - make apps explicit about their dependencies:

# app.pl
use Future::IO;
Future::IO->load_impl('IOAsync');

use Async::Redis;
# ...
Enter fullscreen mode Exit fullscreen mode

Pros: Explicit, no magic, apps declare what they need.

Cons: This fundamentally breaks PAGI's value proposition. The whole point of PAGI is write-once portability - your app should run under any PAGI-compliant server. If your app.pl says load_impl('IOAsync'), what happens when you want to run under Conduit (Paul's Future::IO-native web server that currently supports PSGI and may support PAGI in the future)? You'd have to change your app code to switch servers. That's exactly what PAGI exists to prevent.

I kept coming back to this. Boilerplate is annoying but tolerable. Breaking server-agnosticism is a dealbreaker.

Option 2: Extend the PAGI Spec

We could add async capabilities directly to the PAGI spec - have servers provide event loop primitives through the scope:

# Hypothetical: server provides async capabilities
$scope->{'pagi.sleep'} = sub { ... };
$scope->{'pagi.timeout'} = sub { ... };
Enter fullscreen mode Exit fullscreen mode

Pros: Clean abstraction. Apps use spec-defined capabilities, servers implement them however they want. True portability.

Cons: Where does it stop? PAGI is modeled after Python's ASGI, which is deliberately minimal - it defines the request/response lifecycle and nothing more. Once you add sleep, what about DNS resolution? Database connections? HTTP clients?

The spec becomes a grab-bag of async primitives, and every PAGI server implementation has to support all of them. That's a maintenance burden that discourages new server implementations. ASGI's success comes partly from its simplicity - implementing a basic ASGI server is tractable.

Option 3: Entry Point Configures

Have pagi-server (the CLI script, not the module) configure Future::IO. The script is the application entry point.

Pros: Follows the "only entry points configure" principle. CLI users get zero-config experience.

Cons: Still feels like action at distance. Programmatic users of PAGI::Server must configure it themselves.

What We Chose

We went with Option 3. In pagi-server:

# In PAGI::Runner (called by pagi-server)
sub _configure_future_io {
    my ($self) = @_;

    my $configured = eval {
        require Future::IO::Impl::IOAsync;
        1;
    };

    if ($configured && $self->mode ne 'production' && !$self->{quiet}) {
        warn "Future::IO configured for IO::Async\n";
    }
}
Enter fullscreen mode Exit fullscreen mode

The result:

  • pagi-server myapp.pl - Future::IO auto-configured, Async::Redis and other Future::IO libraries just work
  • PAGI::Server->new(...) programmatically - You configure Future::IO yourself

For programmatic usage, here's what that looks like:

#!/usr/bin/env perl
use strict;
use warnings;
use IO::Async::Loop;

# Configure Future::IO BEFORE loading libraries that use it
use Future::IO;
Future::IO->load_impl('IOAsync');

# Now these work correctly
use Async::Redis;
use PAGI::Server;

my $redis = Async::Redis->new(host => 'localhost');

my $app = sub {
    my ($scope, $receive, $send) = @_;
    # ... your app using $redis
};

my $loop = IO::Async::Loop->new;
my $server = PAGI::Server->new(app => $app, port => 8080);
$loop->add($server);
$server->listen->get;
$loop->run;
Enter fullscreen mode Exit fullscreen mode

We're comfortable with this tradeoff. If you're writing your own server orchestration - creating loops, adding notifiers, managing the lifecycle - you're already in "I know what I'm doing" territory. You're the kind of developer who reads documentation about event loop integration. Adding two lines to configure Future::IO is not a burden for someone already writing fifteen lines of loop setup.

The CLI user who just wants pagi-server app.pl to work shouldn't need to know any of this. The power user embedding PAGI::Server in a custom harness can handle it. (See the LOOP INTEROPERABILITY section in PAGI::Server docs.)

Why This Felt Right (Eventually)

I had doubts. Isn't this still "magic"? Isn't the app implicitly depending on server configuration?

But here's the reframing that helped: configuring the async runtime is the server's job. Python's ASGI servers don't ask users to "configure asyncio" - the runtime just works. The server provides the execution environment.

The key insight is that this isn't about any single library. It's about the entire Future::IO ecosystem - database drivers, HTTP clients, Redis, message queues, and anything else built on Future::IO. These libraries represent the future of async Perl. If every PAGI user has to understand Future::IO configuration internals before using any of them, that's a barrier to adoption that hurts the whole ecosystem.

By having pagi-server configure Future::IO, we're saying: "When you run under pagi-server, Future::IO libraries work. That's part of what the server provides." It's similar to how Python's ASGI servers provide an asyncio environment - you don't configure the async runtime, you just use libraries that depend on it.

Also in This Release: SSE Query Parameters

PAGI::SSE now has query parameter parsing, matching PAGI::Request and PAGI::WebSocket:

my $sse = PAGI::SSE->new($scope, $receive, $send);

my $channel = $sse->query_param('channel');
my $params  = $sse->query_params;  # Hash::MultiValue
Enter fullscreen mode Exit fullscreen mode

This is straightforward consistency work - all three scope types now have the same query parameter API.

This Is Experimental - RFC Welcome

I want to be clear: the Future::IO integration in this release is experimental. We've made a pragmatic choice that works, but I'm not certain it's the right long-term answer.

The PAGI spec will probably need to address async capability configuration at some point. I'm just not sure what that should look like yet. Should servers declare what async backend they use? Should there be a standard way for apps to discover capabilities? Should Future::IO configuration be part of the lifespan protocol?

I'm treating this release as a request for comments. If you have thoughts on:

  • Whether pagi-server auto-configuring Future::IO is the right call
  • How the PAGI spec should handle async ecosystem integration
  • Alternative approaches we haven't considered
  • Pain points you hit when using Future::IO libraries with PAGI

...I want to hear them. Please leave your thoughts on GitHub issue #33, which I've opened specifically for this discussion.


PAGI is a PSGI-like specification for async Perl web applications. Feedback welcome at github.com/jjn1056/pagi.

Top comments (0)