NAME
PAGI::Spec::Lifespan - PAGI Specification Documentation
NOTICE
This documentation is auto-generated from the PAGI specification markdown files. For the authoritative source, see:
https://github.com/jjn1056/PAGI/tree/main/docs/specs
Lifespan Protocol
Version: 0.1 (Draft)
The Lifespan PAGI sub-specification outlines how to communicate lifespan events such as startup and shutdown within PAGI.
The lifespan messages allow for an application to initialise and shutdown in the context of a running event loop. An example of this would be creating a connection pool and subsequently closing the connection pool to release the connections.
Execution Model
Lifespan events must be sent by every PAGI server instance or worker that manages an application event loop. This ensures that application-level resources are initialized and cleaned up in the context in which they run.
- -
-
In multi-process deployments, each worker process must independently send
lifespan.startupon init andlifespan.shutdownon exit. - -
-
In in-process async mode, the main event loop will send the events.
This matches the ASGI model and ensures that applications have the opportunity to manage state correctly in async-safe scopes.
Lifespans and requests must occur on the same event loop. This guarantees that async-local state (such as DB pools or background tasks) is not shared unsafely across threads or processes.
Event Loop Access
Unlike ASGI (where Python's asyncio.get_running_loop() provides ambient access to the current event loop), PAGI applications obtain the event loop via IO::Async::Loop->new.
In IO::Async, calling IO::Async::Loop->new returns the existing cached loop instance if one has already been created (via the $ONE_TRUE_LOOP package variable). This means:
# First call creates the loop (done by the server at startup)
my $loop = IO::Async::Loop->new;
# Subsequent calls anywhere in the codebase return the SAME instance
my $same_loop = IO::Async::Loop->new; # Same object as $loop
Applications requiring direct event loop access (for timers, spawning tasks, or
integrating with other IO::Async-based libraries) can simply call
C<<< IO::Async::Loop->new >>> from anywhere in their code:
use IO::Async::Loop;
async sub app ($scope, $receive, $send) {
my $loop = IO::Async::Loop->new; # Gets the server's cached loop
# Use for timers, async operations, etc.
await $loop->delay_future(after => 1);
# ... rest of application
}
B<Key difference from Python's asyncio>: IO::Async's C<<< ->new >>> will create a loop
if none exists, whereas C<asyncio.get_running_loop()> raises C<RuntimeError>
outside async context. For PAGI applications where a loop is guaranteed to exist
during request handling, this distinction is not relevant.
A possible implementation of this protocol in modern Perl using Future::AsyncAwait could look like this:
use strict;
use warnings;
use Future::AsyncAwait;
async sub app ($scope, $receive, $send) {
# Only handle 'lifespan' protocol
if ($scope->{type} eq 'lifespan') {
while (1) {
my $message = await $receive->();
if ($message->{type} eq 'lifespan.startup') {
# ... Do some startup actions here ...
await $send->({
type => 'lifespan.startup.complete',
});
}
elsif ($message->{type} eq 'lifespan.shutdown') {
# ... Do some shutdown/cleanup actions here ...
await $send->({
type => 'lifespan.shutdown.complete',
});
return; # End the loop
}
else {
# Unknown or additional message type; you can handle it or ignore it
}
}
}
else {
die "Unsupported protocol type: $scope->{type}";
}
}
Scope
The lifespan scope exists for the duration of the event loop.
The scope information passed in scope contains basic metadata:
- -
-
type(String) â"lifespan". - -
-
pagi["version"](String) â The version of the PAGI core spec that governs the worker. - -
-
pagi["spec_version"](String) â The version of this lifespan sub-specification. Optional; if missing it defaults to"0.1". - -
-
pagi["is_worker"](Int, optional) â1if running as a worker in a multi-process deployment,0or absent for single-process mode. Applications can use this to avoid duplicate initialization (e.g., only print startup messages from worker 1). - -
-
pagi["worker_num"](Int, optional) â Worker identifier (1, 2, 3, ...) in multi-process mode. Absent orundeffor single-process mode. Combined withis_worker, applications can identify which worker they are running in. - -
-
state(HashRef, optional) â A namespace where the application can persist values to be copied into subsequent request scopes. Servers omit this key if the feature is unsupported.
If an exception is raised when calling the application coderef with a lifespan.startup message (or when a lifespan scope is created), the server must cease sending further lifespan events for that worker. The server should log the error and may either exit or continue starting without lifespan support, depending on operator configuration. Applications that wish to prevent startup entirely should explicitly send lifespan.startup.failed with a message.
Lifespan State
Applications often want to persist data from the lifespan cycle to request/response handling. For example, a database connection can be established in the lifespan cycle and persisted to the request/response cycle.
The scope["state"] namespace provides a place to store these sorts of things. The server will ensure that a shallow copy of the namespace is passed into each subsequent request/response call into the application. Since the server manages the application lifespan and often the event loop as well this ensures that the application is always accessing the database connection (or other stored object) that corresponds to the right event loop and lifecycle, without using context variables, global mutable state or having to worry about references to stale/closed connections.
PAGI servers that implement this feature will provide state as part of the lifespan scope:
"scope": {
...
"state": {},
}
The namespace is controlled completely by the PAGI application; the server will not
interact with it other than to copy it.
Nonetheless, applications should be cooperative by properly naming their keys such that they
will not collide with other frameworks or middleware.
Startup - receive event
Sent to the application when the server is ready to startup and receive connections, but before it has started to do so.
Keys:
Startup Complete - send event
Sent by the application when it has completed its startup. A server must wait for this message before it starts processing connections.
Keys:
Startup Failed - send event
Sent by the application when it has failed to complete its startup. If a server sees this it should log/print the message provided and then exit.
Keys:
- -
-
type(String) â"lifespan.startup.failed". - -
-
message(String) â Optional; if missing defaults to"".
Shutdown - receive event
Sent to the application when the server has stopped accepting connections and closed all active connections.
Keys:
Shutdown Complete - send event
Sent by the application when it has completed its cleanup. A server must wait for this message before terminating.
Keys:
Shutdown Failed - send event
Sent by the application when it has failed to complete its cleanup. If a server sees this it should log/print the message provided and then terminate.
Keys:
- -
-
type(String) â"lifespan.shutdown.failed". - -
-
message(String) â Optional; if missing defaults to"".
Version History
Copyright
This document has been placed in the public domain.