# Architecture

Mayfly implements the [Lambda Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html)
(2018-06-01) so an Elixir release runs on the `provided.al2023` runtime.

## Lifecycle

1. Lambda executes `bootstrap` (generated by `Mayfly.Release.bootstrap/1`).
   For layer builds it first verifies that `/opt/erlang/bin/erl` exists and
   that the layer's ERTS version equals `releases/start_erl.data`, then runs
   `bin/<release> eval "Mayfly.Boot.main()"`.
2. `Mayfly.Boot` configures the Logger from `AWS_LAMBDA_LOG_FORMAT` /
   `AWS_LAMBDA_LOG_LEVEL`, starts every application in the release, then
   calls `Mayfly.start_link/0`.
3. `Mayfly.Supervisor` resolves `_HANDLER` **once** and runs the handler's
   `init/1`. On failure it posts to `/runtime/init/error` and the VM exits
   with status 1 – Lambda marks the function as failed.
4. It starts `AWS_LAMBDA_MAX_CONCURRENCY` (default 1) `Mayfly.Poller`
   processes. Each poller long-polls `/runtime/invocation/next`, decodes the
   event, builds a `Mayfly.Context`, calls `handle/3` and posts the result
   (`/response`, buffered or streamed) or the error (`/error`), then polls
   again.

Nothing in this chain runs unless `bootstrap` (or your code) starts it:
`mix test` and `iex -S mix` in a project depending on Mayfly are unaffected.

## Modules

| Module | Role |
|---|---|
| `Mayfly` | `start_link/1` – the public entry point |
| `Mayfly.Boot` | `main/0` used by `bootstrap`; logger setup, app start, exit codes |
| `Mayfly.Supervisor` | resolves + inits the handler, supervises pollers |
| `Mayfly.Poller` | one concurrency slot: poll → invoke → respond, with backoff |
| `Mayfly.Handler` | behaviour, `_HANDLER` resolution (module or legacy MFA), invocation with rescue/catch |
| `Mayfly.Context` | per-invocation metadata from headers |
| `Mayfly.Response` | buffered/streamed responses, Function URL prelude |
| `Mayfly.ErrorPayload` | error documents, header type normalisation, X-Ray cause |
| `Mayfly.RuntimeAPI` | the four API calls; a behaviour so tests can fake it |
| `Mayfly.HTTP` | HTTP/1.1 over `:gen_tcp`: content-length, chunked (both directions), trailers |
| `Mayfly.Telemetry` | optional `:telemetry` events |
| `Mayfly.LogFormatter` | JSON lines for advanced logging controls |
| `Mayfly.Release` | release steps: prepare, bootstrap, zip |
| `Mayfly.LocalRuntime` | Runtime API emulator for `mix lambda.invoke` and tests |
| `Mix.Tasks.Lambda.Build` / `.Invoke` | Docker build orchestration; local invoke |

## Why no `:httpc`

`:httpc` needs `:inets` and `:ssl` at boot (slower cold start), returns
charlists (8–16× memory for large payloads), honours proxy settings the
link-local Runtime API must never see, and cannot send chunked request bodies
with trailers – which response streaming requires. A 250-line `:gen_tcp`
client covers exactly what the Runtime API needs.

## Concurrency model

One BEAM process per concurrency slot; no shared mutable state in the runtime.
The only shared value is the handler `state` from `init/1`, passed immutably
to every `handle/3`. On standard Lambda there is one slot; on Managed
Instances there are `AWS_LAMBDA_MAX_CONCURRENCY`. A poller dies only on a
Runtime API container error (HTTP 500), which terminates the supervisor and,
via `Mayfly.Boot`, the VM – as the Runtime API contract demands.

## Error contract

| Where | `errorType` | Header |
|---|---|---|
| Handler exception | exception module | `Function.<Module>` |
| `{:error, term}` | `HandlerError` or the map's `errorType` | `Function.<Type>` |
| `exit` / `throw` | `Exit` / `Throw` | `Function.Exit` / `Function.Throw` |
| Bad return / unencodable | `Runtime.InvalidResponse` | same |
| Bad JSON event | `Runtime.InvalidEvent` | same |
| Bad `_HANDLER` | `Runtime.NoSuchHandler` (init) | same |
| `init/1` failure | `Runtime.InitError` (init) | same |
| Error after streaming started | trailers `Lambda-Runtime-Function-Error-Type/-Body` | – |

`Lambda-Runtime-Function-Error-Type` must look like `<Category.Reason>` or
Lambda normalises it to `Runtime.Unknown`/`Function.Unknown`; `ErrorPayload.header_type/1`
guarantees the shape. `Lambda-Runtime-Invocation-Id` is echoed on every
`/response` and `/error` call.

## Environment variables

| Variable | Set by | Use |
|---|---|---|
| `AWS_LAMBDA_RUNTIME_API` | Lambda | Runtime API host:port |
| `_HANDLER` | Lambda Handler setting, default from `bootstrap` | `Module` or `Module.function` |
| `AWS_LAMBDA_MAX_CONCURRENCY` | Lambda (Managed Instances) | number of pollers; also switches `bootstrap` from `+S 1:1` to all vCPUs |
| `AWS_LAMBDA_LOG_FORMAT`, `AWS_LAMBDA_LOG_LEVEL` | Lambda logging config | JSON formatter, level |
| `LOGLEVEL` | you | overrides the level |
| `_X_AMZN_TRACE_ID` | Mayfly, per invocation | X-Ray |
| `RELEASE_TMP`, `RELEASE_DISTRIBUTION` | `bootstrap` | `/tmp`, `none` |
| `MAYFLY_ERTS` | you (layer builds) | ERTS location, default `/opt/erlang` |
