# 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.startup` on init and `lifespan.shutdown` on 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:

```perl
# 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
`IO::Async::Loop->new` from anywhere in their code:

```perl
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
}
```

**Key difference from Python's asyncio**: IO::Async's `->new` will create a loop
if none exists, whereas `asyncio.get_running_loop()` raises `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:

```perl
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) – `1` if running as a worker in a multi-process deployment, `0` or 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 or `undef` for single-process mode. Combined with `is_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:

```json
"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**:

- `type` (String) – `"lifespan.startup"`.

## 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**:

- `type` (String) – `"lifespan.startup.complete"`.

## 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**:

- `type` (String) –  `"lifespan.shutdown"`.

## Shutdown Complete - `send` event

Sent by the application when it has completed its cleanup. A server
must wait for this message before terminating.

**Keys**:

- `type` (String) – `"lifespan.shutdown.complete"`.

## 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

- 0.1 (Draft)

## Copyright

This document has been placed in the public domain.

