From e8a209d3d469de42e417523877e7a5900f8ba397 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Tue, 17 Feb 2026 17:33:17 +0100 Subject: [PATCH 01/21] feature: v2 config amqp Signed-off-by: Valery Piashchynski --- queues/amqp.md | 359 +++++++++++++++++++++++-------------------------- 1 file changed, 167 insertions(+), 192 deletions(-) diff --git a/queues/amqp.md b/queues/amqp.md index a70138e..1bfd113 100644 --- a/queues/amqp.md +++ b/queues/amqp.md @@ -64,243 +64,218 @@ amqp: Upon establishing a connection to the server, you can create a new queue that utilizes this connection and encompasses the queue settings, including those specific to AMQP. -## Configuration +## Pipeline Configuration Formats -{% code title=".rr.yaml" %} - -```yaml -version: "3" - -amqp: - addr: amqp://guest:guest@127.0.0.1:5672 - - # AMQPS TLS configuration - # - # This section is optional - tls: - # Path to the key file - # - # This option is required - key: "" - - # Path to the certificate - # - # This option is required - cert: "" - - # Path to Root CAs used by the AMQP client to trust and verify the broker/server certificate during TLS dial. - # - # This option is optional - root_ca: "" - - # Client auth type (mTLS, peer verification). - # - # This option is optional. Default value: no_client_cert. Possible values: no_client_cert, request_client_cert, require_any_client_cert, verify_client_cert_if_given, require_and_verify_client_cert - client_auth_type: no_client_cert - -jobs: - pipelines: - # User defined name of the queue. - example: - # Driver name - # - # This option is required. - driver: amqp - - # Driver's configuration - # - # Should not be empty - config: - - # QoS - prefetch. - # - # Default: 10 - prefetch: 10 - - # Pipeline priority - # - # If the job has priority set to 0, it will inherit the pipeline's priority. Default: 10. - priority: 1 - - # Redial timeout (in seconds). How long to try to reconnect to the AMQP server. - # - # Default: 60 - redial_timeout: 60 - - # Durable queue - # - # Default: false - durable: false - - # Durable exchange (rabbitmq option: https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges) - # - # Default: false - exchange_durable: false - - # Auto-delete (exchange is deleted when last queue is unbound from it): https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges - # - # Default: false - exchange_auto_delete: false - - # Auto-delete (queue that has had at least one consumer is deleted when last consumer unsubscribes) (rabbitmq option: https://www.rabbitmq.com/queues.html#properties) - # - # Default: false - queue_auto_delete: false - - # Delete queue when stopping the pipeline - # - # Default: false - delete_queue_on_stop: false - - # Queue name - # - # Optional for producer-only pipelines. Required for run/resume/pause. - # Can be omitted for push-only mode. - queue: test-1-queue - - # Exchange name - # - # Optional. Default: amqp.default - exchange: amqp.default - - # Exchange type - # - # Default: direct. Possible values: direct, fanout, topic, headers. - exchange_type: direct - - # Routing key for the queue - # - # Default: empty. Required for push when exchange_type != fanout. - routing_key: test - - # Declare a queue exclusive at the exchange - # - # Default: false - exclusive: false - - # When multiple is true, this delivery and all prior unacknowledged deliveries - # on the same channel will be acknowledged. This is useful for batch processing - # of deliveries - # - # Default: false - multiple_ack: false - - # The consumer_id is identified by a string that is unique and scoped for all consumers on this channel. - # - # Default: "roadrunner" + uuid. - consumer_id: "roadrunner-uuid" - - # Use rabbitmq mechanism to requeue the job on fail - # - # Default: false - requeue_on_fail: false +AMQP static pipeline configuration is selected by `jobs.pipelines..config.version`: - # Queue headers (new in 2.12.2) - # - # Default: null - queue_headers: - x-queue-mode: lazy -``` - -{% endcode %} - -## Configuration options - -**Here is a detailed description of each of the amqp-specific options:** +- `0`, `1`, or omitted: legacy flat parser. +- `2`: nested parser with `exchange` and `queue` objects (recommended). -### Priority +## Version 2 Configuration (Recommended) -`priority`- job priority. A lower value corresponds to a higher priority. For instance, consider two pipelines: `pipe1` -with a priority of `1` and `pipe10` with a priority of `10`. Workers will only take jobs from `pipe10` if all the jobs -from `pipe1` have been processed. +Use `jobs.pipelines..config.version: 2` to enable nested static configuration. -### Prefetch +### Common keys (`jobs.pipelines..config`) -`prefetch` - rabbitMQ QoS prefetch. See also ["prefetch-size"](https://www.rabbitmq.com/amqp-0-9-1-reference.html#basic.qos.prefetch-size). Note that if you use a large number of workers and a small `prefetch` number, some of the workers may not be loaded with messages (jobs) due to the blocking nature of the prefetch. This would result in poor RoadRunner performance and waste of resources. +- `priority`: pipeline priority. If a job has priority `0`, it inherits the pipeline priority. Default: `10`. +- `prefetch`: RabbitMQ QoS prefetch. Default: `10`. +- `redial_timeout`: reconnect timeout in seconds. Default: `60`. -### Queue +### `exchange` object (`jobs.pipelines..config.exchange`) -`queue` - AMQP internal (inside the driver) queue name. Optional for producer-only pipelines, required for consumer lifecycle operations (`run`, `resume`, `pause`). +- `name`: exchange name. Default: `amqp.default`. +- `type`: exchange type. Supported: `direct`, `fanout`, `topic`, `headers`. Default: `direct`. +- `durable`: durable exchange flag. Default: `false`. +- `auto_delete`: auto-delete exchange when last queue is unbound. Default: `false`. +- `declare`: declare exchange on startup. Default: `true`. -### Exchange +### `queue` object (`jobs.pipelines..config.queue`) -`exchange` - rabbitMQ exchange name. Optional, default: `amqp.default`. +- `name`: queue name. Optional for producer-only pipelines; required for `run`, `resume`, and `pause`. +- `routing_key`: routing key. Required when `exchange.type != fanout`. +- `durable`: durable queue flag. Default: `false`. +- `auto_delete`: auto-delete queue after the last consumer unsubscribes. Default: `false`. +- `exclusive`: exclusive queue flag. Default: `false`. +- `consumer_id`: consumer identifier. Default: `roadrunner-`. +- `delete_on_stop`: delete queue when pipeline stops. Default: `false`. +- `multiple_ack`: ACK this and prior unacked deliveries on the same channel. Default: `false`. +- `requeue_on_fail`: use RabbitMQ requeue on failure (Nack). Default: `false`. +- `headers`: queue declaration arguments (for example, `x-queue-mode: lazy`). +- `declare`: declare and bind queue on startup. Default: `true`. {% hint style="info" %} See also [AMQP model](https://www.rabbitmq.com/tutorials/amqp-concepts.html#amqp-model) documentation section. {% endhint %} {% hint style="info" %} -Producer-only pipeline: `queue` can be empty, `push` works, but `run`, `resume`, and `pause` will fail without a queue. +Producer-only pipeline: `queue.name` can be empty, `push` works, but `run`, `resume`, and `pause` will fail without a queue name. {% endhint %} -### Exchange type - -`exchange_type` - rabbitMQ exchange type. May be one of `direct`, `fanout`, `topic`, `headers`. +{% hint style="info" %} +If `exchange.type` is not `fanout`, `queue.routing_key` must be set. +{% endhint %} -### Routing key +{% hint style="info" %} +Read more about Nack in RabbitMQ official docs: https://www.rabbitmq.com/confirms.html#consumer-nacks-requeue +{% endhint %} -`routing_key` - queue's routing key. Required for `push` when `exchange_type != fanout`. +{% code title=".rr.yaml" %} -### Exclusive +```yaml +version: "3" -`exclusive` - applied to the queue, exclusive queues cannot be redeclared. If set to true, and you attempt to declare -the same pipeline twice, it will result in an error. +amqp: + addr: amqp://guest:guest@127.0.0.1:5672/ + tls: + key: "" + cert: "" + root_ca: "" + client_auth_type: no_client_cert -### Multiple ack +jobs: + pipelines: + p1: + driver: amqp + config: + version: 2 + priority: 10 + prefetch: 10 + redial_timeout: 60 + exchange: + name: amqp.default + type: direct + durable: false + auto_delete: false + declare: true + queue: + name: p1-queue + routing_key: p1 + durable: false + auto_delete: false + exclusive: false + consumer_id: "" + delete_on_stop: false + multiple_ack: false + requeue_on_fail: false + headers: + x-queue-mode: lazy + declare: true +``` -`multiple_ack` - this delivery, along with all prior unacknowledged deliveries on the same channel, will be -acknowledged. This feature is beneficial for batch processing of deliveries and is applicable only for `Ack`, not for -`Nack`. +{% endcode %} -### Requeue on fail +## Read-only RabbitMQ Permissions -`requeue_on_fail` - requeue on Nack (by RabbitMQ). +If the RabbitMQ user does not have `configure` permission, disable declarations in static config: -{% hint style="info" %} -Read more about Nack in RabbitMQ official docs: https://www.rabbitmq.com/confirms.html#consumer-nacks-requeue -{% endhint %} +- `jobs.pipelines..config.exchange.declare: false` +- `jobs.pipelines..config.queue.declare: false` -### Queue headers +This avoids startup declaration failures and passive queue-check failures in restricted environments. -`queue_headers` - used to pass arguments to the `Queue` create method, such as `x-queue-mode: lazy` +{% code title=".rr.yaml" %} -### Durable +```yaml +version: "3" -`durable` - create a durable queue. +amqp: + addr: amqp://readonly:readonly@127.0.0.1:5675/TEST -Default: `false` +jobs: + pipelines: + readonly: + driver: amqp + config: + version: 2 + exchange: + name: CARGO_OUT + type: fanout + durable: true + auto_delete: false + declare: false + queue: + name: CARGO_ECN_EVENTS + routing_key: CARGO_ECN_EVENTS + durable: true + auto_delete: false + exclusive: false + declare: false +``` -### Delete queue on stop +{% endcode %} -`delete_queue_on_stop` - delete the queue when the pipeline is stopped. +## Runtime / RPC (`jobs.Declare`) -Default: `false` +Dynamic pipeline declaration over RPC remains flat. It does not use nested `config.exchange` / `config.queue` objects. -### Redial timeout +Declaration control keys in `jobs.Declare` payload: -`redial_timeout` - Redial timeout (in seconds). How long to try to reconnect to the AMQP server. +- `exchange_declare` +- `queue_declare` -### Exchange durable +## Legacy Flat Configuration (`version: 0|1`) -`exchange_durable` - Durable -exchange ([rabbitmq option](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges)). +Legacy flat keys remain supported under `jobs.pipelines..config` when `config.version` is `0`, `1`, or omitted: -Default: `false` +- `priority` +- `prefetch` +- `queue` +- `exchange` +- `exchange_type` +- `routing_key` +- `exclusive` +- `multiple_ack` +- `requeue_on_fail` +- `queue_headers` +- `durable` +- `delete_queue_on_stop` +- `redial_timeout` +- `exchange_durable` +- `exchange_auto_delete` +- `queue_auto_delete` +- `consumer_id` -### Exchange auto delete +{% code title=".rr.yaml" %} -`exchange_auto_delete` - Auto-delete (exchange is deleted when last queue is unbound from it): [link](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges). +```yaml +version: "3" -Default: `false` +amqp: + addr: amqp://guest:guest@127.0.0.1:5672/ -### Queue auto delete +jobs: + pipelines: + p1: + driver: amqp + config: + version: 1 + priority: 10 + prefetch: 10 + queue: p1-queue + exchange: amqp.default + exchange_type: direct + routing_key: p1 + exclusive: false + multiple_ack: false + requeue_on_fail: false + queue_headers: {} + durable: false + delete_queue_on_stop: false + redial_timeout: 60 + exchange_durable: false + exchange_auto_delete: false + queue_auto_delete: false + consumer_id: "" +``` -`queue_auto_delete` - Auto-delete (queue that has had at least one consumer is deleted when last consumer -unsubscribes): [link](https://www.rabbitmq.com/queues.html#properties). +{% endcode %} -Default: `false` +{% hint style="info" %} +In legacy format, `queue` can be omitted for push-only pipelines, but `run`, `resume`, and `pause` require a queue name. Also, `routing_key` is required when `exchange_type != fanout`. +{% endhint %} -### Consumer id +## Migration -`consumer_id` - string that is unique and scoped for all consumers on this channel. +- Prefer `config.version: 2` with nested `exchange` and `queue` objects for new configurations. +- Existing flat configurations remain compatible through `config.version: 0`, `config.version: 1`, or omitted `config.version`. +- For restricted RabbitMQ permissions, set `exchange.declare: false` and `queue.declare: false`. From a80bed9894933b7b136b10c7dc90f11750a84076 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Tue, 17 Feb 2026 17:45:28 +0100 Subject: [PATCH 02/21] chore: update Signed-off-by: Valery Piashchynski --- queues/amqp.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/queues/amqp.md b/queues/amqp.md index 1bfd113..dcc8842 100644 --- a/queues/amqp.md +++ b/queues/amqp.md @@ -69,19 +69,19 @@ the queue settings, including those specific to AMQP. AMQP static pipeline configuration is selected by `jobs.pipelines..config.version`: - `0`, `1`, or omitted: legacy flat parser. -- `2`: nested parser with `exchange` and `queue` objects (recommended). +- `2`: nested parser with `exchange` and `queue` sections (recommended). ## Version 2 Configuration (Recommended) -Use `jobs.pipelines..config.version: 2` to enable nested static configuration. +Set `jobs.pipelines..config.version: 2` and define `exchange` and `queue` sections. -### Common keys (`jobs.pipelines..config`) +### Options in `config` - `priority`: pipeline priority. If a job has priority `0`, it inherits the pipeline priority. Default: `10`. - `prefetch`: RabbitMQ QoS prefetch. Default: `10`. - `redial_timeout`: reconnect timeout in seconds. Default: `60`. -### `exchange` object (`jobs.pipelines..config.exchange`) +### Exchange settings - `name`: exchange name. Default: `amqp.default`. - `type`: exchange type. Supported: `direct`, `fanout`, `topic`, `headers`. Default: `direct`. @@ -89,7 +89,7 @@ Use `jobs.pipelines..config.version: 2` to enable nested static configurat - `auto_delete`: auto-delete exchange when last queue is unbound. Default: `false`. - `declare`: declare exchange on startup. Default: `true`. -### `queue` object (`jobs.pipelines..config.queue`) +### Queue settings - `name`: queue name. Optional for producer-only pipelines; required for `run`, `resume`, and `pause`. - `routing_key`: routing key. Required when `exchange.type != fanout`. @@ -188,14 +188,14 @@ jobs: config: version: 2 exchange: - name: CARGO_OUT + name: test-1-exchange type: fanout durable: true auto_delete: false declare: false queue: - name: CARGO_ECN_EVENTS - routing_key: CARGO_ECN_EVENTS + name: test-1-queue + routing_key: test-1 durable: true auto_delete: false exclusive: false @@ -206,7 +206,7 @@ jobs: ## Runtime / RPC (`jobs.Declare`) -Dynamic pipeline declaration over RPC remains flat. It does not use nested `config.exchange` / `config.queue` objects. +Dynamic pipeline declaration over RPC remains flat. It does not use nested `config.exchange` / `config.queue` sections. Declaration control keys in `jobs.Declare` payload: @@ -276,6 +276,6 @@ In legacy format, `queue` can be omitted for push-only pipelines, but `run`, `re ## Migration -- Prefer `config.version: 2` with nested `exchange` and `queue` objects for new configurations. +- Prefer `config.version: 2` with nested `exchange` and `queue` sections for new configurations. - Existing flat configurations remain compatible through `config.version: 0`, `config.version: 1`, or omitted `config.version`. - For restricted RabbitMQ permissions, set `exchange.declare: false` and `queue.declare: false`. From 9dfe2b5ac29a78c5d25658bade7bc2c090b1006f Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Tue, 17 Feb 2026 17:46:24 +0100 Subject: [PATCH 03/21] Update queues/amqp.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- queues/amqp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queues/amqp.md b/queues/amqp.md index dcc8842..45a0d52 100644 --- a/queues/amqp.md +++ b/queues/amqp.md @@ -258,7 +258,7 @@ jobs: exclusive: false multiple_ack: false requeue_on_fail: false - queue_headers: {} + queue_headers: {} # optional, e.g. { x-queue-mode: lazy } durable: false delete_queue_on_stop: false redial_timeout: 60 From 2b7597a4e01f87c1e7099b7603a3a8ff8d2f2d56 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Tue, 17 Feb 2026 17:51:30 +0100 Subject: [PATCH 04/21] chore: update doc Signed-off-by: Valery Piashchynski --- queues/amqp.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/queues/amqp.md b/queues/amqp.md index 45a0d52..54b3b6a 100644 --- a/queues/amqp.md +++ b/queues/amqp.md @@ -66,7 +66,7 @@ the queue settings, including those specific to AMQP. ## Pipeline Configuration Formats -AMQP static pipeline configuration is selected by `jobs.pipelines..config.version`: +AMQP pipeline configuration is selected by `jobs.pipelines..config.version`: - `0`, `1`, or omitted: legacy flat parser. - `2`: nested parser with `exchange` and `queue` sections (recommended). @@ -166,7 +166,7 @@ jobs: ## Read-only RabbitMQ Permissions -If the RabbitMQ user does not have `configure` permission, disable declarations in static config: +If the RabbitMQ user does not have `configure` permission, disable declarations in pipeline configuration: - `jobs.pipelines..config.exchange.declare: false` - `jobs.pipelines..config.queue.declare: false` From 2869d9191dc1bf2bc1f4d13ae90aaaf71b199765 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Tue, 17 Feb 2026 18:32:03 +0100 Subject: [PATCH 05/21] chore: add what's next Signed-off-by: Valery Piashchynski --- queues/amqp.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/queues/amqp.md b/queues/amqp.md index 54b3b6a..bb53691 100644 --- a/queues/amqp.md +++ b/queues/amqp.md @@ -279,3 +279,11 @@ In legacy format, `queue` can be omitted for push-only pipelines, but `run`, `re - Prefer `config.version: 2` with nested `exchange` and `queue` sections for new configurations. - Existing flat configurations remain compatible through `config.version: 0`, `config.version: 1`, or omitted `config.version`. - For restricted RabbitMQ permissions, set `exchange.declare: false` and `queue.declare: false`. + +## What's Next? + +1. [Queues and Jobs overview](overview-queues.md) - Review the full jobs pipeline model before configuring AMQP in production. +2. [Read-only RabbitMQ permissions](amqp.md#read-only-rabbitmq-permissions) - See declaration flags for restricted users and review the Runtime / RPC (`jobs.Declare`) section on this page for flat declaration keys. +3. [Pipeline configuration formats](amqp.md#pipeline-configuration-formats) - Confirm when to use `config.version: 2` versus legacy `0|1`, and review general RoadRunner config structure in [Configuration](../intro/config.md). +4. [Exchange settings](amqp.md#exchange-settings) and [Queue settings](amqp.md#queue-settings) - Compare exchange and queue responsibilities when tuning routing and consumption behavior. +5. [Allocate Timeout](../known-issues/allocate-timeout.md) and [CRC validation failed](../known-issues/stdout-crc.md) - Use these troubleshooting references when workers fail to process queue jobs as expected. From addd5cbb1c2f48ab4341f50f879c29a6a960e8e3 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 18 Feb 2026 15:14:55 +0100 Subject: [PATCH 06/21] chore: grpc interceptors docs Signed-off-by: Valery Piashchynski --- SUMMARY.md | 1 + grpc/grpc.md | 2 + grpc/interceptors.md | 191 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 grpc/interceptors.md diff --git a/SUMMARY.md b/SUMMARY.md index f707784..50d0c21 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -91,6 +91,7 @@ ## ⚑️ gRPC - [Intro into gRPC](grpc/grpc.md) +- [Interceptors](grpc/interceptors.md) - [Protoreg](grpc/protoreg.md) ## πŸ“ˆ Logging and Observability diff --git a/grpc/grpc.md b/grpc/grpc.md index 690842c..2d3e605 100644 --- a/grpc/grpc.md +++ b/grpc/grpc.md @@ -7,6 +7,8 @@ It consists of two main parts: 1. **protoc-plugin `protoc-gen-php-grpc`:** This is a plugin for the protoc compiler that generates PHP code from a gRPC service definition file (`.proto`). It generates PHP classes that correspond to the service definition and message types. These classes provide an interface for handling incoming gRPC requests and sending responses back to the client. 2. **gRPC server:** This is a server that starts PHP workers and listens for incoming gRPC requests. It receives requests from gRPC clients, proxies them to the PHP workers, and sends the responses back to the client. The server is responsible for managing the lifecycle of the PHP workers and ensuring that they are available to handle requests. +For custom gRPC interceptor plugins, see [Interceptors](./interceptors.md). + ## Protoc-plugin The first step is to define a `.proto` file that describes the gRPC service and messages that your PHP application will handle. diff --git a/grpc/interceptors.md b/grpc/interceptors.md new file mode 100644 index 0000000..677b4c2 --- /dev/null +++ b/grpc/interceptors.md @@ -0,0 +1,191 @@ +# gRPC Interceptors + +RoadRunner gRPC plugin supports custom unary interceptors. + +Use interceptors when you want to add cross-cutting behavior around RPC calls, such as: + +- request/response logging, +- auth checks, +- rate limiting, +- custom metrics. + +{% hint style="info" %} +At the moment, RoadRunner supports unary gRPC interceptors (`grpc.UnaryServerInterceptor`). +{% endhint %} + +## Interceptor contract + +Your plugin must implement this interface: + +{% code title="grpc/api/interfaces.go" %} + +```go +type Interceptor interface { + UnaryServerInterceptor() grpc.UnaryServerInterceptor + Name() string +} +``` + +{% endcode %} + +The `Name()` return value is used in the `grpc.interceptors` configuration list. + +## Configuration + +Add interceptor names under the `grpc.interceptors` section: + +{% code title=".rr.yaml" %} + +```yaml +version: "3" + +server: + command: "php grpc-worker.php" + +grpc: + listen: "tcp://127.0.0.1:9001" + + proto: + - "proto/helloworld.proto" + + interceptors: + - "sample-grpc-interceptor" +``` + +{% endcode %} + +{% hint style="warning" %} +Make sure every name in `grpc.interceptors` matches a registered interceptor plugin name. +{% endhint %} + +## Execution order + +RoadRunner applies configured interceptors in reverse order from the config list. + +Example: + +{% code title=".rr.yaml" %} + +```yaml +grpc: + interceptors: ["first", "second", "third"] +``` + +{% endcode %} + +Execution order will be: + +`third -> second -> first -> handler` + +## Example plugin + +Ready-to-use example code is available in the samples repository: + +- [gRPC interceptor sample](https://github.com/roadrunner-server/samples/tree/master/grpc_interceptor) + +## Build a custom RR binary with interceptor (step by step) + +1. Clone the RoadRunner repository. +2. Add your interceptor plugin package (you can start from the sample above). +3. Register interceptor plugin in `container/plugins.go`. +4. Build RoadRunner. +5. Configure `grpc.interceptors` in `.rr.yaml`. +6. Start RR and verify requests pass through the interceptor. + +### 1) Clone RoadRunner + +{% code %} + +```bash +git clone https://github.com/roadrunner-server/roadrunner.git +cd roadrunner +``` + +{% endcode %} + +### 2) Add interceptor package + +You can either: + +- copy sample code into your project, or +- import the sample package directly. + +To import the sample package directly: + +{% code %} + +```bash +go get github.com/roadrunner-server/samples/grpc_interceptor@latest +``` + +{% endcode %} + +### 3) Register plugin in container + +Edit `container/plugins.go` and add your interceptor plugin to the plugin list: + +{% code title="container/plugins.go" %} + +```go +import ( + grpcPlugin "github.com/roadrunner-server/grpc/v5" + grpcInterceptor "github.com/roadrunner-server/samples/grpc_interceptor" +) + +func Plugins() []any { + return []any{ + // ... + &grpcInterceptor.Plugin{}, + &grpcPlugin.Plugin{}, + // ... + } +} +``` + +{% endcode %} + +### 4) Build RR + +{% code %} + +```bash +make build +``` + +{% endcode %} + +The binary is created as `./rr`. + +### 5) Configure interceptor in `.rr.yaml` + +Use the plugin name returned by `Name()`: + +{% code title=".rr.yaml" %} + +```yaml +grpc: + interceptors: + - "sample-grpc-interceptor" +``` + +{% endcode %} + +### 6) Run and verify + +Start RoadRunner: + +{% code %} + +```bash +./rr serve -c .rr.yaml +``` + +{% endcode %} + +Send a request using your preferred gRPC client (for example, `grpc-client-cli`) and verify interceptor logs in RR output. + +## What's next? + +1. [Intro into gRPC](./grpc.md) +2. [Writing a Middleware](../customization/middleware.md) +3. [Building RR with a custom plugin](../customization/build.md) From 12ae6b4a4bbdcbeb94260fae644d858ff6762fec Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 18 Feb 2026 16:11:34 +0100 Subject: [PATCH 07/21] chore: update docs Signed-off-by: Valery Piashchynski --- grpc/interceptors.md | 41 +++++++++++++++++++++++++++++++++++++++++ queues/amqp.md | 6 +++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/grpc/interceptors.md b/grpc/interceptors.md index 677b4c2..83e41f8 100644 --- a/grpc/interceptors.md +++ b/grpc/interceptors.md @@ -156,6 +156,47 @@ make build The binary is created as `./rr`. +### 4a) Build RR with Velox (alternative) + +You can also use [Velox](../customization/build.md) to build a custom RoadRunner binary and include the interceptor plugin without editing `container/plugins.go` for every build. + +{% code title="velox.toml" %} + +```toml +[roadrunner] +ref = "v2025.1.5" + +[github] +[github.token] +token = "${RT_TOKEN}" + +[github.plugins] +appLogger = { ref = "v5.0.2", owner = "roadrunner-server", repository = "app-logger" } +logger = { ref = "v5.0.2", owner = "roadrunner-server", repository = "logger" } +lock = { ref = "v5.0.2", owner = "roadrunner-server", repository = "lock" } +rpc = { ref = "v5.0.2", owner = "roadrunner-server", repository = "rpc" } +server = { ref = "v5.0.2", owner = "roadrunner-server", repository = "server" } +grpc = { ref = "v5.0.2", owner = "roadrunner-server", repository = "grpc" } +grpcInterceptor = { ref = "master", owner = "roadrunner-server", repository = "samples", folder = "grpc_interceptor" } + +[log] +level = "info" +mode = "production" +``` + +{% endcode %} + +{% code %} + +```bash +go install github.com/roadrunner-server/velox/v2025/cmd/vx@latest +vx build -c velox.toml -o . +``` + +{% endcode %} + +The binary is created as `./rr`. + ### 5) Configure interceptor in `.rr.yaml` Use the plugin name returned by `Name()`: diff --git a/queues/amqp.md b/queues/amqp.md index bb53691..bf632ad 100644 --- a/queues/amqp.md +++ b/queues/amqp.md @@ -283,7 +283,7 @@ In legacy format, `queue` can be omitted for push-only pipelines, but `run`, `re ## What's Next? 1. [Queues and Jobs overview](overview-queues.md) - Review the full jobs pipeline model before configuring AMQP in production. -2. [Read-only RabbitMQ permissions](amqp.md#read-only-rabbitmq-permissions) - See declaration flags for restricted users and review the Runtime / RPC (`jobs.Declare`) section on this page for flat declaration keys. -3. [Pipeline configuration formats](amqp.md#pipeline-configuration-formats) - Confirm when to use `config.version: 2` versus legacy `0|1`, and review general RoadRunner config structure in [Configuration](../intro/config.md). -4. [Exchange settings](amqp.md#exchange-settings) and [Queue settings](amqp.md#queue-settings) - Compare exchange and queue responsibilities when tuning routing and consumption behavior. +2. [Read-only RabbitMQ permissions](https://www.rabbitmq.com/docs/access-control) - See declaration flags for restricted users and review the Runtime / RPC (`jobs.Declare`) section on this page for flat declaration keys. +3. [Pipeline configuration formats](../intro/config.md) - Confirm when to use `config.version: 2` versus legacy `0|1`, and review the general RoadRunner configuration structure. +4. [Exchange settings](kafka.md) and [Queue settings](sqs.md) - Compare exchange and queue responsibilities when tuning routing and consumption behavior. 5. [Allocate Timeout](../known-issues/allocate-timeout.md) and [CRC validation failed](../known-issues/stdout-crc.md) - Use these troubleshooting references when workers fail to process queue jobs as expected. From 0ab1defa6c9427d4acf3058742d608237e8ea361 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 18 Feb 2026 16:22:38 +0100 Subject: [PATCH 08/21] chore: update interceptors Signed-off-by: Valery Piashchynski --- grpc/interceptors.md | 46 ++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/grpc/interceptors.md b/grpc/interceptors.md index 83e41f8..9e25c8c 100644 --- a/grpc/interceptors.md +++ b/grpc/interceptors.md @@ -83,16 +83,16 @@ Ready-to-use example code is available in the samples repository: - [gRPC interceptor sample](https://github.com/roadrunner-server/samples/tree/master/grpc_interceptor) -## Build a custom RR binary with interceptor (step by step) +## Build a custom RoadRunner binary with interceptor 1. Clone the RoadRunner repository. 2. Add your interceptor plugin package (you can start from the sample above). 3. Register interceptor plugin in `container/plugins.go`. -4. Build RoadRunner. +4. Build the RoadRunner binary using one of the build options below. 5. Configure `grpc.interceptors` in `.rr.yaml`. -6. Start RR and verify requests pass through the interceptor. +6. Start RoadRunner and verify requests pass through the interceptor. -### 1) Clone RoadRunner +### Clone RoadRunner {% code %} @@ -103,7 +103,7 @@ cd roadrunner {% endcode %} -### 2) Add interceptor package +### Add interceptor package You can either: @@ -120,7 +120,7 @@ go get github.com/roadrunner-server/samples/grpc_interceptor@latest {% endcode %} -### 3) Register plugin in container +### Register plugin in container Edit `container/plugins.go` and add your interceptor plugin to the plugin list: @@ -144,21 +144,11 @@ func Plugins() []any { {% endcode %} -### 4) Build RR +### Build options -{% code %} - -```bash -make build -``` +#### Build with Velox -{% endcode %} - -The binary is created as `./rr`. - -### 4a) Build RR with Velox (alternative) - -You can also use [Velox](../customization/build.md) to build a custom RoadRunner binary and include the interceptor plugin without editing `container/plugins.go` for every build. +Use [Velox](../customization/build.md) when you want to build RoadRunner from a `velox.toml` configuration. {% code title="velox.toml" %} @@ -197,7 +187,21 @@ vx build -c velox.toml -o . The binary is created as `./rr`. -### 5) Configure interceptor in `.rr.yaml` +#### Build with RoadRunner + +Use this option when you build directly from the RoadRunner repository after updating `container/plugins.go`. + +{% code %} + +```bash +make build +``` + +{% endcode %} + +The binary is created as `./rr`. + +### Configure interceptor in `.rr.yaml` Use the plugin name returned by `Name()`: @@ -211,7 +215,7 @@ grpc: {% endcode %} -### 6) Run and verify +### Run and verify Start RoadRunner: From 4a3649d90dbca726cd8bb21fecf2df3b2a637de1 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 18 Feb 2026 17:14:17 +0100 Subject: [PATCH 09/21] chore: update Signed-off-by: Valery Piashchynski --- grpc/interceptors.md | 105 +++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 55 deletions(-) diff --git a/grpc/interceptors.md b/grpc/interceptors.md index 9e25c8c..cf620ca 100644 --- a/grpc/interceptors.md +++ b/grpc/interceptors.md @@ -85,14 +85,54 @@ Ready-to-use example code is available in the samples repository: ## Build a custom RoadRunner binary with interceptor -1. Clone the RoadRunner repository. -2. Add your interceptor plugin package (you can start from the sample above). -3. Register interceptor plugin in `container/plugins.go`. -4. Build the RoadRunner binary using one of the build options below. -5. Configure `grpc.interceptors` in `.rr.yaml`. -6. Start RoadRunner and verify requests pass through the interceptor. +### Build options + +#### Build with Velox + +Use [Velox](../customization/build.md) when you want to build RoadRunner from a `velox.toml` configuration. + +{% code title="velox.toml" %} + +```toml +[roadrunner] +ref = "v2025.1.5" + +[github] +[github.token] +token = "${RT_TOKEN}" + +[github.plugins] +appLogger = { ref = "v5.0.2", owner = "roadrunner-server", repository = "app-logger" } +logger = { ref = "v5.0.2", owner = "roadrunner-server", repository = "logger" } +lock = { ref = "v5.0.2", owner = "roadrunner-server", repository = "lock" } +rpc = { ref = "v5.0.2", owner = "roadrunner-server", repository = "rpc" } +server = { ref = "v5.0.2", owner = "roadrunner-server", repository = "server" } +grpc = { ref = "v5.0.2", owner = "roadrunner-server", repository = "grpc" } +grpcInterceptor = { ref = "master", owner = "roadrunner-server", repository = "samples", folder = "grpc_interceptor" } + +[log] +level = "info" +mode = "production" +``` + +{% endcode %} + +{% code %} + +```bash +go install github.com/roadrunner-server/velox/v2025/cmd/vx@latest +vx build -c velox.toml -o . +``` + +{% endcode %} + +The binary is created as `./rr`. + +#### Build with RoadRunner + +Use this option when you build directly from the RoadRunner repository. -### Clone RoadRunner +##### Clone RoadRunner {% code %} @@ -103,7 +143,7 @@ cd roadrunner {% endcode %} -### Add interceptor package +##### Add interceptor package You can either: @@ -120,7 +160,7 @@ go get github.com/roadrunner-server/samples/grpc_interceptor@latest {% endcode %} -### Register plugin in container +##### Register plugin in container Edit `container/plugins.go` and add your interceptor plugin to the plugin list: @@ -144,52 +184,7 @@ func Plugins() []any { {% endcode %} -### Build options - -#### Build with Velox - -Use [Velox](../customization/build.md) when you want to build RoadRunner from a `velox.toml` configuration. - -{% code title="velox.toml" %} - -```toml -[roadrunner] -ref = "v2025.1.5" - -[github] -[github.token] -token = "${RT_TOKEN}" - -[github.plugins] -appLogger = { ref = "v5.0.2", owner = "roadrunner-server", repository = "app-logger" } -logger = { ref = "v5.0.2", owner = "roadrunner-server", repository = "logger" } -lock = { ref = "v5.0.2", owner = "roadrunner-server", repository = "lock" } -rpc = { ref = "v5.0.2", owner = "roadrunner-server", repository = "rpc" } -server = { ref = "v5.0.2", owner = "roadrunner-server", repository = "server" } -grpc = { ref = "v5.0.2", owner = "roadrunner-server", repository = "grpc" } -grpcInterceptor = { ref = "master", owner = "roadrunner-server", repository = "samples", folder = "grpc_interceptor" } - -[log] -level = "info" -mode = "production" -``` - -{% endcode %} - -{% code %} - -```bash -go install github.com/roadrunner-server/velox/v2025/cmd/vx@latest -vx build -c velox.toml -o . -``` - -{% endcode %} - -The binary is created as `./rr`. - -#### Build with RoadRunner - -Use this option when you build directly from the RoadRunner repository after updating `container/plugins.go`. +##### Build binary {% code %} From 8a72dd3487d54881c386345ca04b0dee308792d9 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 18 Feb 2026 18:42:42 +0100 Subject: [PATCH 10/21] chore: update grpc docs Signed-off-by: Valery Piashchynski --- grpc/interceptors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grpc/interceptors.md b/grpc/interceptors.md index cf620ca..8310848 100644 --- a/grpc/interceptors.md +++ b/grpc/interceptors.md @@ -60,7 +60,7 @@ Make sure every name in `grpc.interceptors` matches a registered interceptor plu ## Execution order -RoadRunner applies configured interceptors in reverse order from the config list. +RoadRunner applies configured interceptors in the same order as the config list. Example: @@ -75,7 +75,7 @@ grpc: Execution order will be: -`third -> second -> first -> handler` +`first -> second -> third -> handler` ## Example plugin From 4cb5d55f4d13b264c0a08ddf8b0bb5d58e8ae401 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Wed, 26 Aug 2026 18:31:00 +0200 Subject: [PATCH 11/21] docs(http): document static middleware cache options New keys: prefixes, cache_ttl, cache_miss_ttl, cache_max_entries. Refresh the intro for the in-memory metadata/miss caching, the GET/HEAD method gate, and the weak-etag description (size + mtime). --- http/static.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/http/static.md b/http/static.md index 19a3e1a..ee42bc2 100644 --- a/http/static.md +++ b/http/static.md @@ -1,10 +1,11 @@ # HTTP β€” Serving static content -The `static` HTTP middleware serves static content using RoadRunner on the main HTTP plugin endpoint. Using this middleware -can slow down the overall performance by up to `~10%`, because RoadRunner has to check the path for each file request. +The `static` HTTP middleware serves static content using RoadRunner on the main HTTP plugin endpoint. + +To avoid a filesystem lookup on every request, the middleware caches file metadata and misses in memory with a short TTL (`10s` by default). A repeated request for the same path is then answered without touching the filesystem, so the overhead of enabling the middleware stays small. The cache trades a few seconds of staleness for that speed: a file added, changed, or removed on disk is picked up within one TTL. Set the TTLs to `0s` to check the filesystem on every request, or run `rr reset static` to flush the cache at once (for example, after a deploy). {% hint style="info" %} -If there is no file to serve, RR will redirect the request back to the PHP worker. +If there is no file to serve, RR forwards the request back to the PHP worker. Only `GET` and `HEAD` requests are served; every other method goes to the worker. {% endhint %} ## Enable HTTP middleware @@ -26,6 +27,15 @@ http: calculate_etag: false weak: false allow: [ ".txt", ".php" ] + # Serve only request paths that start with one of these prefixes. + # Empty (the default) considers every request. + prefixes: [ "/assets/", "/build/" ] + # File metadata cache TTL. "0s" disables it. Default: 10s. + cache_ttl: 10s + # Miss cache TTL. "0s" disables it. Default: 10s. + cache_miss_ttl: 10s + # Maximum entries kept in each cache map. Default: 16384. + cache_max_entries: 16384 request: input: "custom-header" response: @@ -40,8 +50,12 @@ Where: 2. `forbid`: file extensions that should not be served. 3. `allow`: extensions that should be served (empty = serve all except forbidden). If an extension is present in both lists (allow and forbid), it is treated as forbidden. 4. `calculate_etag`: enable etag calculation for the static file. -5. `weak`: use a weak generator (/W); it uses only the filename to generate a CRC32 sum. If false, the entire file content is used to generate the CRC32 sum. -6. `request/response`: custom headers for the static files. +5. `weak`: use a weak generator (`W/`). The weak etag is derived from the file size and modification time. If false, the whole file content is used to produce a strong CRC32 etag. +6. `prefixes`: restrict serving to request paths that start with one of these prefixes. When empty (the default), every path is considered. A path that matches no prefix goes to the worker without a filesystem lookup. The match is a plain prefix, so include the trailing slash (`/assets/`) to scope it to a directory. +7. `cache_ttl`: TTL for the file metadata cache (etag, content type, size, modification time). Active only when `calculate_etag` is enabled. `0s` disables it. Default: `10s`. +8. `cache_miss_ttl`: TTL for the miss cache. A request whose path does not resolve to a file records a miss and skips the filesystem check for this duration, which removes the per-request lookup on dynamic routes that share the URL space with static files. `0s` disables it. Default: `10s`. +9. `cache_max_entries`: upper bound on the number of entries in each cache map (metadata and miss). When a map is full, expired entries are evicted; if none can be freed, the middleware checks the filesystem instead. Default: `16384`. +10. `request/response`: custom headers for the static files. To combine static content with other middleware, use the following sequence (static last, then headers and gzip): @@ -81,9 +95,7 @@ http: ## Fileserver plugin The Fileserver plugin serves static files. It works similarly to the `static` HTTP middleware and has extended functionality. -Static HTTP middleware slows down request processing by `~10%` because RR has to check each request for the -corresponding file. -The file server plugin uses a different port and serves only static files. +The `static` middleware runs on the main HTTP endpoint, while the file server plugin uses a different port and serves only static files. ## File server configuration From 9208183a516aa9a2b09463f8e1a44a1680f67976 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Sun, 6 Sep 2026 12:52:51 +0200 Subject: [PATCH 12/21] docs(http): document zstd middleware --- SUMMARY.md | 1 + grpc/interceptors.md | 20 ++++++++--------- http/http.md | 3 ++- http/zstd.md | 52 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 http/zstd.md diff --git a/SUMMARY.md b/SUMMARY.md index 7334e21..f5b3c12 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -87,6 +87,7 @@ - [X-Sendfile](http/sendfile.md) - [Streaming](http/resp-streaming.md) - [gzip](http/gzip.md) +- [zstd](http/zstd.md) ## ⚑️ gRPC diff --git a/grpc/interceptors.md b/grpc/interceptors.md index 8310848..0cd29c5 100644 --- a/grpc/interceptors.md +++ b/grpc/interceptors.md @@ -21,8 +21,8 @@ Your plugin must implement this interface: ```go type Interceptor interface { - UnaryServerInterceptor() grpc.UnaryServerInterceptor - Name() string + UnaryServerInterceptor() grpc.UnaryServerInterceptor + Name() string } ``` @@ -168,17 +168,17 @@ Edit `container/plugins.go` and add your interceptor plugin to the plugin list: ```go import ( - grpcPlugin "github.com/roadrunner-server/grpc/v5" - grpcInterceptor "github.com/roadrunner-server/samples/grpc_interceptor" + grpcPlugin "github.com/roadrunner-server/grpc/v5" + grpcInterceptor "github.com/roadrunner-server/samples/grpc_interceptor" ) func Plugins() []any { - return []any{ - // ... - &grpcInterceptor.Plugin{}, - &grpcPlugin.Plugin{}, - // ... - } + return []any{ + // ... + &grpcInterceptor.Plugin{}, + &grpcPlugin.Plugin{}, + // ... + } } ``` diff --git a/http/http.md b/http/http.md index 36c30b2..755d3c1 100644 --- a/http/http.md +++ b/http/http.md @@ -36,7 +36,8 @@ http: # Optional, default: false raw_body: false - # Middleware for the HTTP plugin; order is important. Allowed values are: "headers", "gzip", "static", "sendfile", [SINCE 2.6] -> "new_relic", [SINCE 2.6] -> "http_metrics", [SINCE 2.7] -> "cache" + # Middleware for the HTTP plugin; order is important. Allowed values are: "headers", "gzip", "zstd", "static", "sendfile", [SINCE 2.6] -> "new_relic", [SINCE 2.6] -> "http_metrics", [SINCE 2.7] -> "cache" + # The "zstd" middleware requires a build that includes the zstd plugin. # # Default value: [] middleware: [ "headers", "gzip" ] diff --git a/http/zstd.md b/http/zstd.md new file mode 100644 index 0000000..26732ac --- /dev/null +++ b/http/zstd.md @@ -0,0 +1,52 @@ +# HTTP - Zstd middleware + +The `zstd` middleware compresses HTTP response bodies when the client sends `Accept-Encoding: zstd`. It uses a pure Go implementation and does not require CGO. It does not decompress request bodies. + +## Availability + +{% hint style="info" %} +Your RoadRunner build must include the `github.com/roadrunner-server/zstd/v6` plugin. Adding `zstd` to the configuration does not install the plugin. See [Building a Server](../customization/build.md) and [Registering middleware](../customization/middleware.md#registering-middleware) for custom builds. +{% endhint %} + +## Configuration + +Add `zstd` to the middleware list in your existing HTTP configuration: + +{% code title=".rr.yaml" %} + +```yaml +version: "3" + +http: + address: 127.0.0.1:8080 + middleware: [ "zstd" ] +``` + +{% endcode %} + +The middleware does not require a separate configuration section. Eligible responses use `Content-Encoding: zstd`. + +## Compression behavior + +- Clients must request `zstd` explicitly with a nonzero quality value. Missing or unsupported encodings and `zstd;q=0` leave the response uncompressed. +- The default compression level is `zstd.SpeedFastest`. The implementation reuses encoders through a pool. +- The normal minimum response size is 1,024 bytes. A flush can start compression below this limit. +- The middleware skips HEAD requests, empty bodies, responses with an existing `Content-Encoding` or `Content-Range`, and content types excluded by the compression library. +- The middleware adds `Vary: Accept-Encoding`. It removes the original `Content-Length` when it compresses a response. +- The compression library does not change ETags by default. + +The middleware supports OpenTelemetry header propagation when RoadRunner tracing is active. + +## Using gzip and zstd + +The zstd middleware does not provide gzip fallback. The [gzip middleware](gzip.md) is a separate plugin. + +{% hint style="warning" %} +If both gzip and zstd middleware are enabled, their order can determine the selected encoding. The separate plugins do not compare quality values with each other. Do not expect them to select the encoding with the highest quality value across both plugins. +{% endhint %} + +## Documentation + +- [Zstd middleware plugin](https://github.com/roadrunner-server/zstd) +- [MDN Accept-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding) +- [HTTP response streaming](resp-streaming.md) From 606adcef4802059949531ecff4362a789385a8a7 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Sun, 6 Sep 2026 14:32:05 +0200 Subject: [PATCH 13/21] docs: document v6 beta migration --- README.md | 6 +- SUMMARY.md | 2 + app-server/aws-lambda.md | 4 + customization/build.md | 339 ++++++++--------------------------- customization/embedding.md | 55 +++--- customization/jobs-driver.md | 136 +++++++------- customization/middleware.md | 82 +++------ customization/plugin.md | 91 +++++++--- grpc/grpc.md | 43 ++++- grpc/interceptors.md | 145 +-------------- grpc/protoreg.md | 127 +++++++------ http/gzip.md | 7 +- http/headers.md | 6 +- http/http.md | 137 +++++++++----- http/proxy.md | 50 +++--- http/resp-streaming.md | 9 +- http/sendfile.md | 12 +- http/static.md | 90 ++++++---- intro/about.md | 2 +- intro/compatibility.md | 2 + intro/v3-migration.md | 99 ++++++++++ kv/boltdb.md | 14 +- kv/memory.md | 15 +- kv/overview-kv.md | 6 + kv/redis.md | 23 +-- lab/applogger.md | 101 +++-------- lab/health.md | 49 +++-- lab/logger.md | 157 +++++++--------- lab/metrics.md | 12 +- lab/otel.md | 61 ++++--- php/auto-scaling.md | 33 ++-- php/environment.md | 26 ++- php/manual-scaling.md | 12 +- php/pool.md | 35 ++-- php/rpc.md | 23 ++- php/worker.md | 11 +- plugins/centrifuge.md | 9 + plugins/config.md | 6 + plugins/intro.md | 3 +- plugins/locks.md | 2 +- plugins/server.md | 56 +++--- plugins/service.md | 17 +- plugins/tcp.md | 6 + queues/amqp.md | 69 ++++--- queues/beanstalk.md | 8 + queues/boltdb.md | 10 ++ queues/google-pub-sub.md | 18 +- queues/kafka.md | 74 ++++---- queues/nats.md | 27 ++- queues/nsq.md | 79 ++++++++ queues/overview-queues.md | 67 ++++++- queues/sqs.md | 8 +- workflow/temporal.md | 9 + workflow/worker.md | 10 ++ 54 files changed, 1361 insertions(+), 1139 deletions(-) create mode 100644 intro/v3-migration.md create mode 100644 queues/nsq.md diff --git a/README.md b/README.md index 6c86428..e613244 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ of your web applications. RoadRunner includes [PSR-7](https://www.php-fig.org/psr/psr-7), [PSR-17](https://www.php-fig.org/psr/psr-17) compatible HTTP and `HTTP(S)/2/3/fCGI` servers and can be used to replace classic Nginx+FPM setups with much greater performance and flexibility. `HTTP(S)/2/3/fCGI` servers as just one of its many available plugins, but its capabilities extend far beyond: -- Queue drivers: RabbitMQ, Kafka, SQS, Beanstalk, NATS, In-Memory. +- Queue drivers: RabbitMQ, Kafka, SQS, Beanstalk, NATS, Google Pub/Sub, BoltDB, and In-Memory. The v6 beta build also includes [NSQ](queues/nsq.md). - KV drivers: Redis, Memcached, BoltDB, In-Memory. -- OpenTelemetry protocol support (`gRPC`, `http`, `jaeger`). +- OTLP trace export over gRPC or HTTP. - Workflows engine via [Temporal](https://temporal.io) - `gRPC` server. For increased speed, the `protobuf` extension can be used. - `HTTP(S)/2/3` and `fCGI` server features **automatic TLS management**, **103 Early Hints** support and middleware like: Static, Headers, gzip, prometheus (metrics), send (x-sendfile), OTEL, proxy_ip_parser, etc. @@ -39,6 +39,8 @@ RoadRunner includes [PSR-7](https://www.php-fig.org/psr/psr-7), [PSR-17](https:/ - Compatible with Windows, WSL2, FreeBSD, GNU/Linux, etc. - And more πŸ˜‰ +See [v3 Migration](intro/v3-migration.md) for changes in the v6 plugin beta line and the matching development builds. + If you have a feature request in mind, you can check out [GitHub issues](https://github.com/roadrunner-server/roadrunner/issues) page. Here you'll find a list of open feature requests. The RoadRunner community is active and responsive, so feel free to join the discussion on diff --git a/SUMMARY.md b/SUMMARY.md index f5b3c12..31cde14 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -9,6 +9,7 @@ - [Configuration](intro/config.md) - [Contributing](intro/contributing.md) - [Upgrade and Compatibility](intro/compatibility.md) +- [v3 Migration](intro/v3-migration.md) ## πŸ‘· PHP Worker @@ -76,6 +77,7 @@ - [BoltDB](queues/boltdb.md) - [Kafka](queues/kafka.md) - [NATS](queues/nats.md) +- [NSQ](queues/nsq.md) - [SQS](queues/sqs.md) ## πŸ•ΈοΈ HTTP diff --git a/app-server/aws-lambda.md b/app-server/aws-lambda.md index 05cbc15..4cac77e 100644 --- a/app-server/aws-lambda.md +++ b/app-server/aws-lambda.md @@ -54,6 +54,10 @@ composer require spiral/roadrunner-http nyholm/psr7 ### Application +{% hint style="warning" %} +The Go example below uses v4 plugins and the legacy `sdk/v4` pool API. It is not a v6 build recipe. For a v6 build, apply the [plugin import and contract migration](../customization/plugin.md#v6-migration) and test the Lambda adapter with the selected RoadRunner version. +{% endhint %} + We can create a simple application to demonstrate how it works: 1. You need three files: `main.go` with the `Endure` container: diff --git a/customization/build.md b/customization/build.md index cc39dd4..c263aa9 100644 --- a/customization/build.md +++ b/customization/build.md @@ -1,122 +1,44 @@ # Building a Server -Developers can take advantage of the customization options available with RoadRunner to create a server optimized -for their particular project. +Velox builds a RoadRunner binary from the plugins listed in `velox.toml`. Use it to select plugins or build with a custom plugin or fork. -**This can include:** - -- Adding custom plugins. -- Forking existing ones to make changes. -- Building a lightweight server with only the necessary plugins. +{% hint style="warning" %} +**Velox v3 is untagged.** This guide requires development revision `6b71101ce0080143b4927cf2d84ab0ba02189b67`, not a released Velox binary. The example uses RR development source with v6 beta plugins. Use the pinned installation command below. +{% endhint %} -We created a tool called **Velox** that lets developers build a RoadRunner server binary. It uses a configuration file -to determine which plugins and repositories are required for building a RoadRunner server binary. +For an existing v5 build, see the [Velox v2025.1.7 configuration](https://github.com/roadrunner-server/velox/blob/v2025.1.7/velox.toml). That release uses the older format and does not support the configuration below. ## Configuration -The configuration file is written in TOML format and contains a list of repositories to add to the build. For each -repository, you can specify the owner and version. You can also add private repositories from GitHub or GitLab and -authenticate with access tokens. - -{% hint style="info" %} -To download all the required plugins for RoadRunner, you need a GitHub token. If you try to download plugins without a -token, anonymous access is limited to 50 requests per hour. You can read more about these limits on -the [Rate limits for GitHub Apps](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/rate-limits-for-github-apps) -page. -{% endhint %} - -**Here is an example of a configuration file:** +This TOML configuration pins the RR commit and plugin versions. The plugin tags match the requirements in the [pinned RR source](https://github.com/roadrunner-server/roadrunner/blob/b0cccd917f001b6584eafdc04ad6ba69a97cbb69/go.mod). {% code title="velox.toml" %} ```toml [roadrunner] -# ref -> reference, tag, commit or branch -ref = "master" - -# the debug option is used to build RR with debug symbols to profile it with pprof -[debug] -enabled = true +ref = "b0cccd917f001b6584eafdc04ad6ba69a97cbb69" [github] +base_url = "https://github.com" + [github.token] -token = "${RT_TOKEN}" - -# ref -> master, commit or tag -[github.plugins] -# LOGS -appLogger = { ref = "v5.0.2", owner = "roadrunner-server", repository = "app-logger" } -logger = { ref = "v5.0.2", owner = "roadrunner-server", repository = "logger" } -lock = { ref = "v5.0.2", owner = "roadrunner-server", repository = "lock" } -rpc = { ref = "v5.0.2", owner = "roadrunner-server", repository = "rpc" } - -# CENTRIFUGE BROADCASTING PLATFORM -centrifuge = { ref = "v5.0.2", owner = "roadrunner-server", repository = "centrifuge" } - -# WORKFLOWS ENGINE -temporal = { ref = "v5.1.0", owner = "temporalio", repository = "roadrunner-temporal" } - -# METRICS -metrics = { ref = "v5.0.2", owner = "roadrunner-server", repository = "metrics" } - -# HTTP + MIDDLEWARE -http = { ref = "v5.0.2", owner = "roadrunner-server", repository = "http" } -gzip = { ref = "v5.0.2", owner = "roadrunner-server", repository = "gzip" } -prometheus = { ref = "v5.0.1", owner = "roadrunner-server", repository = "prometheus" } -headers = { ref = "v5.0.2", owner = "roadrunner-server", repository = "headers" } -static = { ref = "v5.0.1", owner = "roadrunner-server", repository = "static" } -proxy = { ref = "v5.0.2", owner = "roadrunner-server", repository = "proxy_ip_parser" } -send = { ref = "v5.0.1", owner = "roadrunner-server", repository = "send" } - -# OpenTelemetry -otel = { ref = "v5.0.1", owner = "roadrunner-server", repository = "otel" } - -# SERVER -server = { ref = "v5.0.2", owner = "roadrunner-server", repository = "server" } - -# SERVICE aka lightweight systemd -service = { ref = "v5.0.2", owner = "roadrunner-server", repository = "service" } - -# JOBS -jobs = { ref = "v5.0.2", owner = "roadrunner-server", repository = "jobs" } -amqp = { ref = "v5.0.2", owner = "roadrunner-server", repository = "amqp" } -sqs = { ref = "v5.0.2", owner = "roadrunner-server", repository = "sqs" } -beanstalk = { ref = "v5.0.2", owner = "roadrunner-server", repository = "beanstalk" } -nats = { ref = "v5.0.2", owner = "roadrunner-server", repository = "nats" } -kafka = { ref = "v5.0.2", owner = "roadrunner-server", repository = "kafka" } -googlepubsub = { ref = "v5.0.2", owner = "roadrunner-server", repository = "google-pub-sub" } - -# KV -kv = { ref = "v5.0.2", owner = "roadrunner-server", repository = "kv" } -boltdb = { ref = "v5.0.2", owner = "roadrunner-server", repository = "boltdb" } -memory = { ref = "v5.0.2", owner = "roadrunner-server", repository = "memory" } -redis = { ref = "v5.0.2", owner = "roadrunner-server", repository = "redis" } -memcached = { ref = "v5.0.2", owner = "roadrunner-server", repository = "memcached" } - -# FILESERVER (static files) -fileserver = { ref = "v5.0.1", owner = "roadrunner-server", repository = "fileserver" } - -# gRPC plugin -grpc = { ref = "v5.0.2", owner = "roadrunner-server", repository = "grpc" } - -# HEALTHCHECKS + READINESS CHECKS -status = { ref = "v5.0.2", owner = "roadrunner-server", repository = "status" } - -# TCP for the RAW TCP PAYLOADS -tcp = { ref = "v5.0.2", owner = "roadrunner-server", repository = "tcp" } - -[gitlab] -[gitlab.token] -# api, read-api, read-repo -token = "${GL_TOKEN}" - -[gitlab.endpoint] -endpoint = "https://gitlab.com" - -[gitlab.plugins] -# ref -> master, commit or tag -test_plugin_1 = { ref = "main", owner = "rustatian", repository = "36405203" } -test_plugin_2 = { ref = "main", owner = "rustatian", repository = "36405235" } +token = "${GITHUB_TOKEN}" + +[plugins.logger] +module_name = "github.com/roadrunner-server/logger/v6" +tag = "v6.0.0-beta.4" + +[plugins.server] +module_name = "github.com/roadrunner-server/server/v6" +tag = "v6.0.0-beta.7" + +[plugins.rpc] +module_name = "github.com/roadrunner-server/rpc/v6" +tag = "v6.0.0-beta.6" + +[plugins.http] +module_name = "github.com/roadrunner-server/http/v6" +tag = "v6.0.0-beta.10" [log] level = "info" @@ -125,215 +47,110 @@ mode = "production" {% endcode %} -{% hint style="info" %} -You can find the latest version of the example configuration file in -the [official repository](https://github.com/roadrunner-server/velox/blob/master/velox.toml). -{% endhint %} - -{% hint style="warning" %} -When using official plugins for RoadRunner, it is recommended avoid using the `master` branch as it may contain -unstable code. Instead, use tags with the same major version (e.g., `logger:v4.x.x` + `amqp:v4.x.x`, but -not `logger:v4.0.0` + `amqp:v3.0.5`). Please note that the currently supported plugin version is `v5.x.x`, and the -supported RoadRunner version is `>=v2024.2.x`. - -Failure to follow these guidelines may result in compatibility issues and -other problems. Please pay close attention to your configuration file to ensure proper use of plugins. -{% endhint %} - -You can use environment variables in the configuration file. This is useful when you want to keep the configuration file -in the repository, but you don't want to expose your tokens or just want to pass them as arguments to the `vx` command. +Velox includes `informer` and `resetter` automatically from the downloaded RR `go.mod`. Do not add them to `[plugins]`. Velox warns and ignores those entries. -Here is the list of environment variables from the example above: - -| Variable | Description | -|---------------|----------------------------------------------------------------------------| -| `${GL_TOKEN}` | GitLab token. | -| `${RT_TOKEN}` | GitHub token. | -| `${VERSION}` | RR version to write into the binary (will be shown with `./rr --version`). | -| `${TIME}` | Build time (will be shown with `./rr --version`). | - -{% hint style="info" %} -Keep in mind to set the latest stable version in the `${VERSION}` environment variable. You may also use the `${TIME}` environment -variable to write the build time in the output binary. -{% endhint %} +List each plugin module once. Custom plugins must export a `Plugin` type from the module root. ### Options -| Option | Description | -|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **ref** | Tag, commit hash or branch name. | -| **owner** | Repository owner (might be the user or organization). | -| **repository** | Repository name. | -| **folder** | If the plugin is in some folder in your repository, you may specify it via this configuration option.
For example: `cache = { ref = "v1.6.18", owner = "darkweak", repository = "souin", folder="plugins/roadrunner" }` | -| **replace** | Go.mod [replace directive](https://go.dev/ref/mod#go-mod-file-replace). | +| Key | Meaning | +| --- | --- | +| `roadrunner.ref` | RR semver tag, branch, or 40-character hexadecimal commit SHA. Defaults to `master`. | +| `plugins..module_name` | Full Go module path, including its major-version suffix. | +| `plugins..tag` | Plugin version or ref. Use an exact semver tag to enable version checks. | +| `target_platform.os` | Target `GOOS`. Defaults to the host OS. | +| `target_platform.arch` | Target `GOARCH`. Defaults to the host architecture. | +| `debug.enabled` | Set to `true` to disable optimization and inlining, retain debug symbols, and enable the `debug` build tag. Defaults to `false`. | +| `debug.race` | Set to `true` to build with `-race` and `CGO_ENABLED=1`. Defaults to `false`. | -{% hint style="info" %} +Each missing target platform value defaults independently. Race builds need a C compiler and C libraries for the target platform. Without `debug.race`, Velox sets `CGO_ENABLED=0`. -To replace the module with the local copy or some remote module, use the following `velox.toml` configuration: +### Module replacements + +Append `[[replaces]]` and `[[excludes]]` sections when you need Go module overrides. This example uses a compatible local HTTP fork at `../http` and excludes one dependency version: {% code title="velox.toml" %} -```ini -your_module = { ref = "master", owner = "owner", repository = "repo", replace="github.com/owner2/repo2" } +```toml +[[replaces]] +new = "../http" +old = "github.com/roadrunner-server/http/v6" + +[[excludes]] +module = "github.com/redis/go-redis/v9" +version = "v9.15.0" ``` {% endcode %} +Relative replacement paths resolve against the working directory of `vx`, not the configuration file or downloaded RR directory. A container build must mount the local module at a path available inside the container. -Or with your local copy: - -{% code title="velox.toml" %} +For a remote replacement, use `module@version` in `new`. The `old` value can include `@version` to restrict the replacement to that version. A local path must not have a version suffix. Each `old` value must be unique. -```ini -your_module = { ref = "master", owner = "owner", repository = "repo", replace="../path/to/a/local/dir" } -``` +An excluded version must be canonical semver and match the module path major. Velox applies these directives before Go resolves dependencies. It retains the downloaded RR `go.mod` as the starting point. -{% endcode %} +### Older configuration -{% endhint %} +Velox v3 ignores the old `[github.plugins.*]` and `[gitlab.*]` tables. It does not convert them. Move plugin entries to `[plugins.]` with `module_name` and `tag`. +The old per-plugin `ref`, `owner`, `repository`, `folder`, and inline `replace` fields are also ignored. Use the module path declared in the plugin's `go.mod`, including for plugins in repository subdirectories. Move inline replacements to `[[replaces]]`. ### Private repositories -- Make sure the `ssh-agent` is running and the ssh key has been - added: [link](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) -- Exclude your organization package prefix from the Go environment variables: - -{% code %} +Go downloads plugin modules, including modules hosted on GitLab. Configure SSH or HTTPS credentials for Go module downloads. Set `GOPRIVATE` for your private module prefixes. Replace the organization names in this example: ```bash -go env -w GOPRIVATE="github.com//*,gitlab.com//*" -go env -w GONOSUMDB="github.com//*,gitlab.com//*" +export GOPRIVATE="github.com/your-org/*,gitlab.com/your-org/*" ``` -{% endcode %} - -## Building - -{% tabs %} - -{% tab title="Docker" %} - -Using the Docker image simplifies the build process by automatically building the RoadRunner binary and storing it in -the `/usr/bin/` folder. This eliminates the need to install Golang or other dependencies on your computer. Once the -build is complete, Docker will automatically start the RoadRunner server. +Velox inherits the Go environment, including `GOPRIVATE`, `GOPROXY`, and `GOFLAGS`. The GitHub archive token does not authenticate plugin module downloads. -**Here is an example of Dockerfile:** +### RR archive access -{% code title="Dockerfile" %} +The `[github.token]` section is optional for public RR source. Its `token` value expands environment variables and is sent as a bearer token on the archive request. With the example above, export `GITHUB_TOKEN` when authentication is needed. Exporting the variable without the token configuration does not enable authentication. -```dockerfile -# https://docs.docker.com/buildx/working-with-buildx/ -# TARGETPLATFORM if not empty OR linux/amd64 by default -FROM --platform=${TARGETPLATFORM:-linux/amd64} ghcr.io/roadrunner-server/velox:latest as velox +To download from GitHub Enterprise, set `[github] base_url` to your host, such as `https://ghe.example.com`. The RR mirror must be at `/roadrunner-server/roadrunner`. This setting does not change plugin module hosts. Archive downloads accept redirects and direct HTTP 200 responses. -# app version and build date must be passed during image building (version without any prefix). -# e.g.: `docker build --build-arg "APP_VERSION=1.2.3" --build-arg "BUILD_TIME=$(date +%FT%T%z)" .` -ARG APP_VERSION="undefined" -ARG BUILD_TIME="undefined" - -# copy your configuration into the docker -COPY velox.toml . - -# we don't need CGO -ENV CGO_ENABLED=0 - -# RUN build -RUN vx build -c velox.toml -o /usr/bin/ - -FROM --platform=${TARGETPLATFORM:-linux/amd64} php:8.3-cli - -# copy required files from builder image -COPY --from=velox /usr/bin/rr /usr/bin/rr - -# use roadrunner binary as image entrypoint -CMD ["/usr/bin/rr"] -``` - -{% endcode %} - -{% endtab %} - -{% tab title="Go" %} +## Building -You can use the `go install` command to download Velox. - -{% hint style="warning" %} -To download Velox and build an application server, you need [Golang 1.22+](https://golang.org/dl/) on your local machine. -{% endhint %} +Use Go `1.27.1` for this example. Install the pinned Velox development revision: {% code title="go install" %} ```bash -go install github.com/roadrunner-server/velox/v2025/cmd/vx@latest +go install github.com/roadrunner-server/velox/v3/cmd/vx@6b71101ce0080143b4927cf2d84ab0ba02189b67 ``` {% endcode %} -After the binary has been downloaded, you can build the application server: +Add the Go binary installation directory to `PATH`. Build from the directory that contains `velox.toml`: {% code title="vx build" %} ```bash -vx build -c velox.toml -o ~/Downloads +SOURCE_DATE_EPOCH=1788438954 vx build -c velox.toml -o . ``` {% endcode %} -| Option | Description | -|--------|---------------------------------| -| `-c` | path to the configuration | -| `-o` | path where to put the RR binary | - -{% endtab %} - -{% tab title="Downloading Binary" %} - -To build the application server, you need to download the Velox binary from -the [GitHub releases page](https://github.com/roadrunner-server/velox/releases) and unpack it to your `PATH`. - -{% hint style="warning" %} -To build an application server, you need [Golang 1.22+](https://golang.org/dl/) on your local machine. -{% endhint %} - -After the binary has been downloaded, you can build the application server: - -{% code title="vx build" %} - -```bash -vx build -c velox.toml -o ~/Downloads -``` - -{% endcode %} - -| Option | Description | -|--------|---------------------------------| -| `-c` | path to the configuration | -| `-o` | path where to put the RR binary | - -{% endtab %} - -{% endtabs %} - -## Video tutorials - -### How to write a plugin - -{% embed url="https://www.youtube.com/watch?v=h5PPvc_YOtg" %} +| Option | Meaning | +| --- | --- | +| `-c`, `--config` | Configuration file. Defaults to `velox.toml`. | +| `-o`, `--out` | Output directory. Defaults to the current directory. | -### `v2023.x.x` update +The command produces `./rr`. When the target matches the host OS and architecture, Velox checks `rr --version` before it replaces the output binary. For a cross-build, run that check on the target system. The output directory can be on another filesystem, including a container volume mount. -{% embed url="https://www.youtube.com/watch?v=w_uxFhdinvU" %} +### Reproducible builds -### Velox configuration +`SOURCE_DATE_EPOCH` sets the binary build timestamp in Unix seconds. The value above is the pinned RR commit's timestamp. Without a valid value, Velox uses the current time. The reported binary version comes from `roadrunner.ref`; the old `VERSION` and `TIME` environment variables are not used. -{% embed url="https://www.youtube.com/watch?v=sddi_lh7ePo" %} +Keep the Velox revision, RR commit, plugin tags, Go toolchain, target platform, build flags, and environment fixed for repeated builds. Keep local replacement contents fixed. -## Third-party and deprecated plugins +After Go resolves dependencies, Velox checks semver plugin pins. A different resolved version causes the build to fail. Use `[[replaces]]` to force a version only after you check its compatibility. -- [souin, third-party](https://github.com/darkweak/souin/tree/master/plugins/roadrunner) -- [reload, deprecated](https://github.com/roadrunner-server/reload) +Non-semver plugin refs, including `latest`, branch names, and commit SHAs, skip this version comparison. A same-module replacement compares its replacement version with the requested plugin tag. Local and different-module replacements skip the comparison. ## Known limitations -- At the moment, only GitHub and GitLab repositories are supported. +- Windows targets are rejected, including `target_platform.os = "windows"`. +- The Connect/gRPC build server, `vx server`, and `--address`/`-a` are removed. Use `vx build`. diff --git a/customization/embedding.md b/customization/embedding.md index 01ca077..84eeedf 100644 --- a/customization/embedding.md +++ b/customization/embedding.md @@ -7,16 +7,17 @@ program. Here's an example of how to embed RoadRunner into a Go program with an HTTP handler: -Import the RoadRunner library via the `go get` command into your Go project: +Import the RoadRunner library into your Go project. Replace `REVISION` with the tag or commit you want to embed. For v6 plugins, select a RoadRunner revision that uses v6 plugin modules; do not assume that `@latest` selects that revision. {% code title="main.go" %} ```bash -go get -u github.com/roadrunner-server/roadrunner/v2025/lib +go get github.com/roadrunner-server/roadrunner/v2025/lib@REVISION ``` {% endcode %} +The library import remains `roadrunner/v2025/lib`. `NewRR` returns `(*RR, error)`. Use matching plugin versions and the [v6 plugin contracts](plugin.md#v6-migration); the embedding lifecycle below is unchanged. ## Create an RR instance @@ -25,13 +26,13 @@ go get -u github.com/roadrunner-server/roadrunner/v2025/lib ```go import ( - "github.com/roadrunner-server/roadrunner/v2025/lib" + "github.com/roadrunner-server/roadrunner/v2025/lib" ) func main() { - overrides := []string{} // List of configuration overrides - plugins := lib.DefaultPluginsList() // List of RR plugins to enable - rr, err := lib.NewRR(".rr.yaml", overrides, plugins) + overrides := []string{} // List of configuration overrides + plugins := lib.DefaultPluginsList() // List of RR plugins to enable + rr, err := lib.NewRR(".rr.yaml", overrides, plugins) } ``` @@ -41,41 +42,39 @@ func main() { Here we use the default list of plugins. This is the same list of plugins you would get if you were to run `rr serve` with a stock RoadRunner binary. -You can, however, choose only the plugins you want and add your own private plugins as well: +You can select plugins and add your own plugin. Replace `example.com/my-plugin` with your module path. The omitted entries must include the dependencies required by the selected plugins: {% code title="main.go" %} ```go import ( - "github.com/roadrunner-server/roadrunner/v2025/lib" - httpPlugin "github.com/roadrunner-server/http/v5" - "github.com/roadrunner-server/resetter/v5" - "github.com/roadrunner-server/informer/v5" - "github.com/yourCompay/yourCusomPlugin" // compilation error here, used only as an example. + custom "example.com/my-plugin" + httpPlugin "github.com/roadrunner-server/http/v6" + "github.com/roadrunner-server/informer/v6" + "github.com/roadrunner-server/resetter/v6" + "github.com/roadrunner-server/roadrunner/v2025/lib" ) func main() { - overrides := []string{ - "http.address=127.0.0.1:4444", // example override to set the HTTP address - "http.pool.num_workers=4", // example override of how to set the number of PHP workers - } // List of configuration overrides - - plugins := []interface{}{ - &informer.Plugin{}, - &resetter.Plugin{}, - // ... - &httpPlugin.Plugin{}, - // ... - &yourCustomPlugin.Plugin{}, - } - plugins := lib.DefaultPluginsList() // List of RR plugins to enable - rr, err := lib.NewRR(".rr.yaml", overrides, plugins) + overrides := []string{ + "http.address=127.0.0.1:4444", + "http.pool.num_workers=4", + } + + plugins := []any{ + &informer.Plugin{}, + &resetter.Plugin{}, + // ... + &httpPlugin.Plugin{}, + // ... + &custom.Plugin{}, + } + rr, err := lib.NewRR(".rr.yaml", overrides, plugins) } ``` {% endcode %} - ## Starting & stopping embedded RoadRunner Once everything is ready, we can start the RoadRunner instance: diff --git a/customization/jobs-driver.md b/customization/jobs-driver.md index 641390f..6177ead 100644 --- a/customization/jobs-driver.md +++ b/customization/jobs-driver.md @@ -4,7 +4,7 @@ JOBS drivers are mini-plugins that are connected to the main JOBS plugin and ini ## Architecture -While initializing, the JOBS plugin searches for registered drivers by the `Constructor` interface. The `Constructor` and `Driver` (described below) interfaces are declared in the [RR API repository](https://github.com/roadrunner-server/api/blob/master/plugins/v4/jobs/driver.go). +The Jobs plugin discovers drivers through the `Constructor` interface. The [Jobs contracts](https://github.com/roadrunner-server/api-plugins/blob/v6.0.0-beta.2/jobs/driver.go) are in `github.com/roadrunner-server/api-plugins/v6/jobs`. See [plugin migration](plugin.md#v6-migration) for the shared import and logging changes. Constructor interface: @@ -16,9 +16,9 @@ type Constructor interface { // Name returns the name of the driver Name() string // DriverFromConfig constructs a driver (e.g. kafka, amqp) from the configuration using the provided configKey - DriverFromConfig(configKey string, queue Queue, pipeline Pipeline) (Driver, error) + DriverFromConfig(ctx context.Context, configKey string, queue Queue, pipeline Pipeline) (Driver, error) // DriverFromPipeline constructs a driver (e.g. kafka, amqp) from the pipeline. All configuration is provided by the pipeline - DriverFromPipeline(pipe Pipeline, queue Queue) (Driver, error) + DriverFromPipeline(ctx context.Context, pipe Pipeline, queue Queue) (Driver, error) } ``` @@ -51,8 +51,8 @@ type Driver interface { So every driver should implement the `Constructor` interface to be found by the JOBS plugin. Let's have a look at the methods included in the `Constructor` interface: 1. `Name() string`: This method should return a user-friendly name for the driver. It'll be used later in the pipelines `.driver` option. **It is an important option. The name here and name in the pipeline options should match.** -2. `DriverFromConfig(configKey string, queue Queue, pipeline Pipeline) (Driver, error)`: Returns a `Driver` implementation declared via configuration. RoadRunner, in turn, provides a configuration key (such as `jobs.pipelines.pipeline-name.driver-name.config`), the queue implementation to which messages are pushed, and the pipeline with all information about the pipeline. Later we will look at how to use this. -3. `DriverFromPipeline(pipe Pipeline, queue Queue) (Driver, error)`: Returns a `Driver` implementation declared via the RPC `jobs.Declare` call. It doesn't have configuration, but all info and configuration options are stored in the `pipeline` method argument. +2. `DriverFromConfig(ctx context.Context, configKey string, queue Queue, pipeline Pipeline) (Driver, error)`: Creates a driver from configuration. RoadRunner supplies the context, configuration key, queue, and pipeline. +3. `DriverFromPipeline(ctx context.Context, pipe Pipeline, queue Queue) (Driver, error)`: Creates a driver for an RPC `jobs.Declare` call. The pipeline contains its configuration. Pass the context to backend connection and setup operations. ### Initialization @@ -67,7 +67,7 @@ If required, you may use the `Configurer` plugin to unmarshal global driver conf ### How to create a driver for JOBS -All code from the tutorial is here: [link](https://github.com/roadrunner-server/samples/blob/master/plugins/jobs_driver/) +The [sample driver](https://github.com/roadrunner-server/samples/blob/master/plugins/jobs_driver/) shows the backend structure. The examples below use the v6 contracts. Use your own module path in place of `example.com/jobs-driver`. To create a driver for jobs, you need to create a plugin instance: @@ -77,51 +77,53 @@ To create a driver for jobs, you need to create a plugin instance: package jobs_driver //nolint:revive,stylecheck import ( - "github.com/roadrunner-server/api/v4/plugins/v4/jobs" - "github.com/roadrunner-server/errors" - "github.com/roadrunner-server/samples/plugins/jobs_driver/driver" - "go.uber.org/zap" + "context" + "log/slog" + + "example.com/jobs-driver/driver" + "github.com/roadrunner-server/api-plugins/v6/jobs" + "github.com/roadrunner-server/errors" ) const pluginName string = "my_awesome_driver" +var _ jobs.Constructor = (*Plugin)(nil) + type Configurer interface { - // UnmarshalKey takes a single key and unmarshal it into a Struct. - UnmarshalKey(name string, out any) error - // Has checks if a config section exists. - Has(name string) bool + UnmarshalKey(name string, out any) error + Has(name string) bool } type Logger interface { - NamedLogger(name string) *zap.Logger + NamedLogger(name string) *slog.Logger } type Plugin struct { - log *zap.Logger - cfg Configurer + log *slog.Logger + cfg Configurer } func (p *Plugin) Init(log Logger, cfg Configurer) error { - if !cfg.Has(pluginName) { - return errors.E(errors.Disabled) - } + if !cfg.Has(pluginName) { + return errors.E(errors.Disabled) + } - p.log = log.NamedLogger(pluginName) - p.cfg = cfg - return nil + p.log = log.NamedLogger(pluginName) + p.cfg = cfg + return nil } func (p *Plugin) Name() string { - return pluginName + return pluginName } -func (p *Plugin) DriverFromConfig(configKey string, pq jobs.Queue, pipeline jobs.Pipeline) (jobs.Driver, error) { - return driver.FromConfig(configKey, p.log, p.cfg, pipeline, pq) +func (p *Plugin) DriverFromConfig(ctx context.Context, configKey string, pq jobs.Queue, pipeline jobs.Pipeline) (jobs.Driver, error) { + return driver.FromConfig(ctx, configKey, p.log, p.cfg, pipeline, pq) } -func (p *Plugin) DriverFromPipeline(pipe jobs.Pipeline, pq jobs.Queue) (jobs.Driver, error) { - return driver.FromPipeline(pipe, p.log, p.cfg, pq) -} +func (p *Plugin) DriverFromPipeline(ctx context.Context, pipe jobs.Pipeline, pq jobs.Queue) (jobs.Driver, error) { + return driver.FromPipeline(ctx, pipe, p.log, p.cfg, pq) +} ``` {% endcode %} @@ -130,9 +132,10 @@ This is a simple representation of the RR plugin. It is called driver because it Keep in mind the plugin's name. JOBS plugin will send the following data to the `Constructor` interface methods: + 1. If declared via configuration (`.rr.yaml`) - `configKey`, you may use that key to unmarshal the configuration section related solely to this driver. If the driver was declared via `jobs.Declare` RPC method, all configuration options would be stored in the `jobs.Pipeline` interface. -2. `jobs.Queue`: Priority-Queue, used to push the messages and later process by the PHP workers. +2. `jobs.Queue`: Priority-Queue, used to push the messages and later process by the PHP workers. 3. All other things like logger, `Configurer` plugin which will be used to get the values from the `.rr.yaml` configuration you may pass if you need them from the driver's root (e.g.: `p.log`). Now, let's see the simplified `Driver` implementation: @@ -143,55 +146,54 @@ Now, let's see the simplified `Driver` implementation: package driver import ( - "context" + "context" + "log/slog" - "github.com/roadrunner-server/api/v4/plugins/v4/jobs" - "go.uber.org/zap" + "github.com/roadrunner-server/api-plugins/v6/jobs" ) var _ jobs.Driver = (*Driver)(nil) type Configurer interface { - // UnmarshalKey takes a single key and unmarshal it into a Struct. - UnmarshalKey(name string, out any) error - // Has checks if a config section exists. - Has(name string) bool + UnmarshalKey(name string, out any) error + Has(name string) bool } type Driver struct { + queue jobs.Queue } -func FromConfig(configKey string, log *zap.Logger, cfg Configurer, pipeline jobs.Pipeline, pq jobs.Queue) (*Driver, error) { - return &Driver{}, nil +func FromConfig(ctx context.Context, configKey string, log *slog.Logger, cfg Configurer, pipeline jobs.Pipeline, pq jobs.Queue) (*Driver, error) { + return &Driver{queue: pq}, nil } // FromPipeline initializes consumer from pipeline -func FromPipeline(pipeline jobs.Pipeline, log *zap.Logger, cfg Configurer, pq jobs.Queue) (*Driver, error) { - return &Driver{}, nil +func FromPipeline(ctx context.Context, pipeline jobs.Pipeline, log *slog.Logger, cfg Configurer, pq jobs.Queue) (*Driver, error) { + return &Driver{queue: pq}, nil } func (d *Driver) Push(ctx context.Context, job jobs.Message) error { - return nil + return nil } func (d *Driver) Run(ctx context.Context, p jobs.Pipeline) error { - return nil + return nil } func (d *Driver) State(ctx context.Context) (*jobs.State, error) { - return nil, nil + return &jobs.State{}, nil } func (d *Driver) Pause(ctx context.Context, p string) error { - return nil + return nil } func (d *Driver) Resume(ctx context.Context, p string) error { - return nil + return nil } func (d *Driver) Stop(ctx context.Context) error { - return nil + return nil } ``` @@ -204,38 +206,38 @@ Remember the following things: 3. For pipelines declared via the `jobs.Declare` RPC call, the `jobs.Resume` method should be called instead. ### Pushing jobs into the priority queue -To push a job into the priority queue, you need to slightly transform it to add `Ack`, `Nack`, etc. methods to it. -All interfaces are in the [RR API repository](https://github.com/roadrunner-server/api/blob/master/plugins/v4/jobs/job.go). Let's have a look at the `Job` interface. +To push a job into the priority queue, you need to slightly transform it to add `Ack`, `Nack`, etc. methods to it. +The [Job interface](https://github.com/roadrunner-server/api-plugins/blob/v6.0.0-beta.2/jobs/job.go) includes `jobs.Item`: {% code title="job.go" %} ```go +import "github.com/roadrunner-server/api-plugins/v6/jobs" + // Job represents a binary heap item type Job interface { - pq.Item - // Ack acknowledges the item after processing - Ack() error - // Nack discards the item - Nack() error - // NackWithOptions discards the item with an optional requeue flag - NackWithOptions(requeue bool, delay int) error - // Requeue puts the message back to the queue with an optional delay - Requeue(headers map[string][]string, delay int) error - // Body returns the payload associated with the item - Body() []byte - // Context returns any meta-information associated with the item - Context() ([]byte, error) - // Headers return the metadata for the item - Headers() map[string][]string + jobs.Item + // Ack acknowledges the item after processing + Ack() error + // Nack discards the item + Nack() error + // NackWithOptions discards the item with an optional requeue flag + NackWithOptions(requeue bool, delay int) error + // Requeue puts the message back to the queue with an optional delay + Requeue(headers map[string][]string, delay int) error + // Body returns the payload associated with the item + Body() []byte + // Context returns any meta-information associated with the item + Context() ([]byte, error) + // Headers return the metadata for the item + Headers() map[string][]string } ``` {% endcode %} -The `Job` interface also includes the `pq.Item` interface to satisfy a minimal priority queue requirement. -You may add this (`pq.Item`) interface to any interface and benefit from RoadRunner's priority queue. -So, our driver's `Push` method would be updated as follows: +`jobs.Item` requires `ID() string`, `GroupID() string`, and `Priority() int64`. Update the driver's `Push` method to insert a `jobs.Job` into its queue: {% code title="driver.go" %} diff --git a/customization/middleware.md b/customization/middleware.md index 51bca1b..38706d0 100644 --- a/customization/middleware.md +++ b/customization/middleware.md @@ -63,9 +63,7 @@ func (p *Plugin) Name() string { {% endcode %} {% hint style="info" %} -Middleware must correspond to the -following [interface](https://github.com/roadrunner-server/http/blob/master/common/interfaces.go#L33) and -be [named](https://github.com/roadrunner-server/endure/blob/master/container.go#L47). +The plugin must implement the [HTTP middleware interface](https://github.com/roadrunner-server/http/blob/v6.0.0-beta.10/api/interfaces.go#L36-L40), including `Name() string`. See [plugin migration](plugin.md#v6-migration) for the v6 imports and shared contracts. {% endhint %} ## gRPC @@ -75,12 +73,9 @@ authentication, rate limiting, and logging. To create a custom interceptor for gRPC requests in RoadRunner, follow these steps: -1. Define a struct that implements the `Init()`, `Interceptor()`, and `Name()` methods. The `Init()` method is called - when the plugin is initialized, the `Interceptor()` method is called for each incoming gRPC request, and the `Name()` - method returns the name of the middleware/plugin. +1. Define a struct with `Init()`, `UnaryServerInterceptor()`, and `Name()` methods. `UnaryServerInterceptor()` returns the interceptor used for incoming requests. -2. In the `Interceptor()` method, perform any necessary processing on the incoming gRPC request, and then call the next - interceptor in the pipeline using the `handler(ctx, req)` method. +2. Process the request in the returned function, then call `handler(ctx, req)` to continue execution. {% hint style="warning" %} RoadRunner supports `gRPC` interceptors since version `v2023.2.0`. @@ -94,7 +89,9 @@ Here is an example: package middleware import ( - "net/http" + "context" + + "google.golang.org/grpc" ) const PluginName = "interceptor" @@ -107,7 +104,9 @@ func (p *Plugin) Init() error { } func (p *Plugin) UnaryServerInterceptor() grpc.UnaryServerInterceptor { - // Do something and return interceptor + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + return handler(ctx, req) + } } // Middleware/plugin name. @@ -119,37 +118,41 @@ func (p *Plugin) Name() string { {% endcode %} {% hint style="info" %} -Interceptor must correspond to the -following [interface](https://github.com/roadrunner-server/grpc/blob/master/common/interfaces.go#L14) and -be [named](https://github.com/roadrunner-server/endure/blob/master/container.go#L47). +The plugin must implement the [gRPC interceptor interface](https://github.com/roadrunner-server/grpc/blob/v6.0.0-beta.6/api/interfaces.go#L16-L19), including `Name() string`. {% endhint %} -You can find a lot of examples here: [link](https://github.com/grpc-ecosystem/go-grpc-middleware). Keep in mind that, at -the moment, RR supports only `UnaryServerInterceptor` gRPC interceptors. +See [unary gRPC interceptors](../grpc/interceptors.md) for configuration and [go-grpc-middleware](https://github.com/grpc-ecosystem/go-grpc-middleware) for interceptor examples. ## PSR7 Attributes -PSR7 attributes are a way of attaching metadata to an incoming HTTP request or response. The PSR7 specification defines -a standard interface for HTTP messages, which includes the ability to set and retrieve attributes on both requests and -responses. +PSR-7 server request attributes hold metadata for request processing. They are not response attributes. Attributes can be used to store any kind of metadata that might be useful for processing the request or response. For example, you might use attributes to store information about the authenticated user, the user's IP address, or any other custom data that you want to attach to the request. -The `Psr\Http\Message\ServerRequestInterface->getAttributes()` method can be used to retrieve attributes from an incoming HTTP request, while the `ResponseInterface->withAttribute()` method can be used to set attributes on an outgoing HTTP response. +Use `Psr\Http\Message\ServerRequestInterface::getAttributes()` to read the attributes in PHP. You can safely pass values to a PHP application and retrieve attributes on the PHP side using the `Psr\Http\Message\ServerRequestInterface->getAttributes()` method through the [attributes](https://github.com/roadrunner-server/http/blob/master/attributes/attributes.go) package: {% code title="middleware.go" %} ```go -func (s *Service) Middleware(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +import ( + "net/http" + + "github.com/roadrunner-server/http/v6/attributes" +) + +func (p *Plugin) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r = attributes.Init(r) - attributes.Set(r, "key", "value") + if err := attributes.Set(r, "key", "value"); err != nil { + http.Error(w, "cannot set request attribute", http.StatusInternalServerError) + return + } next.ServeHTTP(w, r) - } + }) } ``` @@ -162,37 +165,11 @@ the `getAttributes()` method. For example, the `nyholm/psr7` package provides a ## Registering middleware -You must register this service in the -[container/plugins.go](https://github.com/roadrunner-server/roadrunner/blob/master/container/plugins.go) file to -properly resolve dependencies: +Include the middleware plugin in your RoadRunner binary. Follow [Building RoadRunner](build.md) for the Velox configuration and build steps. -{% code title="plugin.go" %} +If you maintain the Go entry point yourself, import the middleware module and add `&middleware.Plugin{}` to the existing plugin list in [container/plugins.go](https://github.com/roadrunner-server/roadrunner/blob/master/container/plugins.go). Keep the other required plugins in that list. -```go -package roadrunner - -import ( - "middleware" -) - -func Plugins() []any { - return []any { - // ... - - // middleware - &middleware.Plugin{}, - - // ... -} -``` - -{% endcode %} - -Or you can use the Velox tool to [build the RR binary](./build.md). - -You should also make sure you configure the middleware to be used via -the [config or the command line](../intro/config.md). Otherwise, the plugin will be loaded, but the middleware will not -be used with incoming requests. +Then add the value returned by `Name()` to `http.middleware`. A plugin included in the binary does not handle HTTP requests until it is selected in this list. {% code title=".rr.yaml" %} @@ -209,4 +186,3 @@ http: ### Writing a middleware for HTTP {% embed url="https://www.youtube.com/watch?v=f5fUSYaDKxo" %} - diff --git a/customization/plugin.md b/customization/plugin.md index 6ca18f8..9bd2e49 100644 --- a/customization/plugin.md +++ b/customization/plugin.md @@ -7,18 +7,58 @@ implementation with automatic method injection. **To create a custom plugin, you can follow these steps:** - Define a struct with a public `Init` method that returns an error value. -- Implement the `Service` interface in your struct to provide the `Serve` and `Stop` methods. +- Implement `Serve` and `Stop` only if the plugin starts a service. - Request dependencies using their respective interfaces and inject them using the Endure container. -- Register your plugin with RoadRunner by creating a custom version of the `main.go` file and [building it](build.md). +- Register the plugin in the RoadRunner container and [build the binary](build.md). Below you can find more information about the plugin interface, how to define a plugin, and how to access other plugins. +## v6 migration + +Use the module versions selected by the RoadRunner build. The v6 plugin beta still uses `endure/v2 v2.6.2`. Its lifecycle and dependency injection interfaces do not require a migration. The RoadRunner library module remains `roadrunner/v2025`. + +The API repositories now have separate roles. [api](https://github.com/roadrunner-server/api) contains protobuf source, not a Go module. [api-go](https://github.com/roadrunner-server/api-go) contains generated Go bindings. [api-plugins](https://github.com/roadrunner-server/api-plugins) contains Go plugin contracts, not RPC messages. + +All import paths in this table start with `github.com/roadrunner-server/`: + +| Previous import | v6 plugin beta import | +| --- | --- | +| `/v5` | `/v6` | +| `pool/` | `pool/v2/` | +| `goridge/v3/pkg/` | `goridge/v4/pkg/` | +| `api/v4/build//v1` | `api-go/v6//v1` | +| `api/v4/build/lock/v1beta1` | `api-go/v6/lock/v1` | +| `api/v4/build/status/v1beta1` | `api-go/v6/status/v1` | +| `api/v4/plugins/v4/jobs` | `api-plugins/v6/jobs` | +| `api/v4/plugins/v1/{kv,lock,logger,status}` | `api-plugins/v6/{kv,lock,logger,status}` | +| `api/v4/plugins/v4/priority_queue` | `api-plugins/v6/priority_queue` | + +Generated imports have no `build/` segment. For example: + +```go +import jobsv1 "github.com/roadrunner-server/api-go/v6/jobs/v1" +``` + +Update implementations, local interfaces, and call sites together: + +- **Logging:** use `logger.Named` from `api-plugins/v6/logger` or a local interface with `NamedLogger(string) *slog.Logger`. The old `logger.Log` interface is removed. Pool constructors, worker factories, and logger options also take `*slog.Logger`. Replace `log.Info("started", zap.String("plugin", name))` with `log.Info("started", "plugin", name)`. Slog has no `Fatal`, `Panic`, or `DPanic` methods. +- **Jobs:** add a leading `context.Context` to `DriverFromConfig` and `DriverFromPipeline`. Calls become `constructor.DriverFromConfig(ctx, key, queue, pipeline)` and `constructor.DriverFromPipeline(ctx, pipeline, queue)`. Existing `Driver` methods already take contexts. See the [Jobs driver tutorial](jobs-driver.md). +- **KV:** every `Storage` method now takes a leading context, including `Stop`. Update calls such as `storage.Get(ctx, key)`, `storage.Set(ctx, items...)`, and `storage.Stop(ctx)`. Construction becomes `constructor.KvFromConfig(ctx, key)`. Pass the context to backend operations. See the [KV contracts](https://github.com/roadrunner-server/api-plugins/blob/v6.0.0-beta.2/kv/interface.go). +- **Queues:** lock queue signatures use `lock.Item` and `[]lock.Item`, not the old priority-queue package's named interface. Jobs defines its own `jobs.Item`. Both retain `ID`, `GroupID`, and `Priority`. Update queue type arguments and method signatures. The `priority_queue` package now declares the Go package name `priorityqueue`. +- **Pool defaults:** direct calls to `DynamicAllocationOpts.InitDefaults()` must pass the base worker count: `InitDefaults(cfg.NumWorkers)`. Pool execution and shutdown guidance must match the [pinned pool version](../php/pool.md). +- **Removed helpers:** replace `proxy.Cidrs` from `proxy_ip_parser` with `[]*net.IPNet`. Resetter no longer exposes `Plugin.Reset(string)`; its `resetter.Reset` RPC remains available. OTEL removes `HTTPHandler` and `TemporalHandler`; use `Plugin.Middleware` and `Plugin.WorkerInterceptor`. Temporal no longer exposes `ResetAP`; normal activity-worker replacement is handled by the pool. + +### DTO compatibility + +`api-go/v6 v6.0.0-beta.14` retains the v1 message set. Do not use the v2 DTO packages from earlier betas. The `lock/v1` package defines `Request` and `Response`, not `LockRequest` and `LockResponse`. Regenerated PHP lock DTOs use `RoadRunner\Lock\DTO\V1`. The lock field numbers and types are unchanged; custom descriptor or protobuf `Any` users must account for the package-name change. + +Relocation alone does not change the retained HTTP or Jobs wire fields and does not require a PHP worker-loop rewrite. RPC still uses Goridge and Go `net/rpc`, not Connect. See [RPC compatibility](../php/rpc.md#v6-compatibility) for the MessagePack change. + +Direct Centrifugo DTO users have separate changes. Use typed fields instead of `Command.id/method/params` and `Reply.id/result`, which are removed. The `RateLimit` RPC and its types are removed. `UpdatePushStatusRequest.uid` becomes `analytics_uid` at the same string field number 1; update generated accessors and JSON names. The proxy bindings add experimental `NotifyCacheEmpty`; a custom server must implement it or embed the generated unimplemented server. The RoadRunner plugin forwards this event to PHP; update handlers and DTOs as described in [Centrifuge](../plugins/centrifuge.md). + ## Interface -RoadRunner plugins are implemented using the `Service` interface, which provides the `Serve` and `Stop` methods for -starting and stopping the plugin. Additionally, plugins can implement other optional interfaces -like `Named`, `Provider`, `Weighted`, and `Collector`. These interfaces enable plugins to provide dependencies to other -plugins, define their weight in the plugin's topology, and collect plugins that implement specific interfaces. +A plugin that starts a service implements `Service`, which provides `Serve` and `Stop`. Middleware and other plugins that do not start a service do not need those methods. Optional interfaces such as `Named`, `Provider`, `Weighted`, and `Collector` provide names, dependencies, initialization weights, and dependency collection. **Here is an example:** @@ -144,7 +184,7 @@ interfaces, and a plugin implementing this interface should be registered in RR' package custom import ( - "go.uber.org/zap" + "log/slog" ) type Configurer interface { // <-- config plugin implements @@ -155,7 +195,7 @@ type Configurer interface { // <-- config plugin implements } type Logger interface { // <-- logger plugin implements - NamedLogger(name string) *zap.Logger + NamedLogger(name string) *slog.Logger } type Service struct{} @@ -191,7 +231,8 @@ custom: package custom import ( - "go.uber.org/zap" + "log/slog" + "github.com/roadrunner-server/errors" ) @@ -205,11 +246,12 @@ type Configurer interface { // <-- config plugin implements } type Logger interface { // <-- logger plugin implements - NamedLogger(name string) *zap.Logger + NamedLogger(name string) *slog.Logger } type Plugin struct { cfg *Config + log *slog.Logger } // Init plugin @@ -220,6 +262,8 @@ func (s *Plugin) Init(cfg Configurer, log Logger) error { return errors.E(op, errors.Disabled) } + s.log = log.NamedLogger(PluginName) + // unmarshal initial configuration err := cfg.UnmarshalKey(PluginName, &s.cfg) if err != nil { @@ -236,7 +280,7 @@ func (s *Plugin) Init(cfg Configurer, log Logger) error { {% endcode %} -### Configuration +### Configuration type {% code title="config.go" %} @@ -323,7 +367,7 @@ This is very useful for middlewares or extending plugins with additional functio Let's create an HTTP middleware: -1. Declare a required interface +Declare the required interface: {% code title="middleware.go" %} @@ -336,13 +380,14 @@ import ( // Middleware interface type Middleware interface { - Middleware(f http.Handler) http.HandlerFunc + Middleware(f http.Handler) http.Handler + Name() string } ``` {% endcode %} -2. Implement the `Collects` Endure interface in the plugin where you want to have these dependencies at runtime. +Implement the `Collects` interface in the plugin that accepts the middleware: {% code title="middleware.go" %} @@ -372,15 +417,13 @@ Important notes: ## RPC Methods -Extending your plugin with RPC methods does not change the plugin at all. The only thing you have to do is to create a -file with RPC methods (let's call it `rpc.go`) and add all RPC methods for the plugin without modifying the plugin -itself. +Expose an RPC receiver through `RPC() any`. Its exported methods use the Go `net/rpc` signature: an input argument, a reply pointer, and an `error` result. Do not add a context argument to these RPC methods when updating the Jobs or KV contracts. **Example based on the `informer` plugin:** Suppose we have created a file `rpc.go`. The next step is to create a structure: -1. Create a structure: (logger is optional) +Create the receiver type: {% code title="rpc.go" %} @@ -388,18 +431,20 @@ Suppose we have created a file `rpc.go`. The next step is to create a structure: package custom import ( - "go.uber.org/zap" + "log/slog" ) type rpc struct { plugin *Plugin - log *zap.Logger + log *slog.Logger } ``` {% endcode %} -2. Create a method that you want to expose: +Add an exported method: + +{% code title="rpc.go" %} ```go package custom @@ -414,7 +459,7 @@ func (s *rpc) Hello(input string, output *string) error { {% endcode %} -3. Create a method called `RPC` that accepts nothing and returns `any`: +Add `RPC()` to the plugin: {% code title="rpc.go" %} @@ -422,7 +467,7 @@ func (s *rpc) Hello(input string, output *string) error { package custom func (p *Plugin) RPC() any { - return &rpc{srv: p, log: p.log} + return &rpc{plugin: p, log: p.log} } ``` diff --git a/grpc/grpc.md b/grpc/grpc.md index d7450ab..49a9120 100644 --- a/grpc/grpc.md +++ b/grpc/grpc.md @@ -421,11 +421,13 @@ grpc: key: "server-key.pem" cert: "server-cert.pem" root_ca: "rootCA.pem" - client_auth_type: request_client_cert + client_auth_type: require_and_verify_client_cert ``` {% endcode %} +`require_and_verify_client_cert` requires a client certificate signed by a trusted CA. `request_client_cert` only requests a certificate; it does not require or verify one. + Options for the `client_auth_type` are: - `request_client_cert` @@ -434,6 +436,26 @@ Options for the `client_auth_type` are: - `require_and_verify_client_cert` - `no_client_certs` +## Server reflection + +The gRPC plugin in `v6.0.0-beta.6` enables server reflection on the gRPC listen port. Both the v1 and v1alpha reflection APIs are available without an enable flag. + +Without a descriptor registry, reflection lists registered services but cannot return the file and message descriptors for PHP services. For those descriptors, add [protoreg](./protoreg.md#server-reflection) to a [custom RR build](../customization/build.md). Configure it with the same service definitions used by `grpc.proto`. The stock beta does not include `protoreg`. + +For a listener without TLS, list services with: + +{% code %} + +```bash +grpcurl -plaintext 127.0.0.1:9001 list +``` + +{% endcode %} + +{% hint style="warning" %} +Reflection uses streaming RPCs. Authentication in `grpc.interceptors` applies only to unary RPCs and does not protect reflection. Restrict network access to the gRPC port or require [verified client certificates](#mtls). The plugin has no configuration option to disable reflection. +{% endhint %} + ## Health Checking RoadRunner automatically registers a [`grpc.health.v1.Health`](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) service on the same gRPC listen port. This is the standard gRPC health checking protocol β€” no additional configuration is required. @@ -550,8 +572,8 @@ grpc: # This option is optional. Default value: infinity. max_connection_age: 0s - # MaxConnectionAgeGrace is an additive period after MaxConnectionAge after - # which the connection will be forcibly closed. + # Time allowed for active RPCs to finish after max_connection_age. + # Zero or omitted means unlimited grace. max_connection_age_grace: 0s # Maximal concurrent streams count. @@ -613,6 +635,21 @@ grpc: {% endcode %} +### Connection age grace + +In v6 beta, `max_connection_age_grace` controls how long active RPCs can continue after the connection reaches `max_connection_age`. Zero or omitted grace means unlimited time. Set a finite grace to close the connection after that period. + +{% code title=".rr.yaml" %} + +```yaml +grpc: + max_connection_age: 5m + max_connection_age_grace: 30s +``` + +{% endcode %} + +The v5 plugin used `max_connection_age` as the grace period and ignored `max_connection_age_grace`. To preserve that behavior, explicitly set both values to the same duration. ## OTLP support in the `gRPC` plugin: `[>=2023.3.8]` diff --git a/grpc/interceptors.md b/grpc/interceptors.md index 0cd29c5..22565a9 100644 --- a/grpc/interceptors.md +++ b/grpc/interceptors.md @@ -1,16 +1,16 @@ # gRPC Interceptors -RoadRunner gRPC plugin supports custom unary interceptors. +The RoadRunner gRPC plugin supports custom unary interceptors in both v5.3.0 and v6. -Use interceptors when you want to add cross-cutting behavior around RPC calls, such as: +Use an interceptor to add processing before or after an RPC call, such as: - request/response logging, -- auth checks, +- authentication checks, - rate limiting, - custom metrics. {% hint style="info" %} -At the moment, RoadRunner supports unary gRPC interceptors (`grpc.UnaryServerInterceptor`). +The plugin supports only unary interceptors (`grpc.UnaryServerInterceptor`). Streaming RPCs, including [server reflection](./grpc.md#server-reflection), do not call these interceptors. {% endhint %} ## Interceptor contract @@ -49,13 +49,13 @@ grpc: - "proto/helloworld.proto" interceptors: - - "sample-grpc-interceptor" + - "custom-grpc-interceptor" ``` {% endcode %} {% hint style="warning" %} -Make sure every name in `grpc.interceptors` matches a registered interceptor plugin name. +Each name in `grpc.interceptors` must match the `Name()` of a registered interceptor plugin. RR fails to start if a configured interceptor is missing. {% endhint %} ## Execution order @@ -77,138 +77,11 @@ Execution order will be: `first -> second -> third -> handler` -## Example plugin +## Build a custom binary -Ready-to-use example code is available in the samples repository: +Include your interceptor plugin in a [custom RR build](../customization/build.md). Use plugin versions compatible with the v6 beta. Listing an interceptor in `.rr.yaml` does not add its code to the binary. -- [gRPC interceptor sample](https://github.com/roadrunner-server/samples/tree/master/grpc_interceptor) - -## Build a custom RoadRunner binary with interceptor - -### Build options - -#### Build with Velox - -Use [Velox](../customization/build.md) when you want to build RoadRunner from a `velox.toml` configuration. - -{% code title="velox.toml" %} - -```toml -[roadrunner] -ref = "v2025.1.5" - -[github] -[github.token] -token = "${RT_TOKEN}" - -[github.plugins] -appLogger = { ref = "v5.0.2", owner = "roadrunner-server", repository = "app-logger" } -logger = { ref = "v5.0.2", owner = "roadrunner-server", repository = "logger" } -lock = { ref = "v5.0.2", owner = "roadrunner-server", repository = "lock" } -rpc = { ref = "v5.0.2", owner = "roadrunner-server", repository = "rpc" } -server = { ref = "v5.0.2", owner = "roadrunner-server", repository = "server" } -grpc = { ref = "v5.0.2", owner = "roadrunner-server", repository = "grpc" } -grpcInterceptor = { ref = "master", owner = "roadrunner-server", repository = "samples", folder = "grpc_interceptor" } - -[log] -level = "info" -mode = "production" -``` - -{% endcode %} - -{% code %} - -```bash -go install github.com/roadrunner-server/velox/v2025/cmd/vx@latest -vx build -c velox.toml -o . -``` - -{% endcode %} - -The binary is created as `./rr`. - -#### Build with RoadRunner - -Use this option when you build directly from the RoadRunner repository. - -##### Clone RoadRunner - -{% code %} - -```bash -git clone https://github.com/roadrunner-server/roadrunner.git -cd roadrunner -``` - -{% endcode %} - -##### Add interceptor package - -You can either: - -- copy sample code into your project, or -- import the sample package directly. - -To import the sample package directly: - -{% code %} - -```bash -go get github.com/roadrunner-server/samples/grpc_interceptor@latest -``` - -{% endcode %} - -##### Register plugin in container - -Edit `container/plugins.go` and add your interceptor plugin to the plugin list: - -{% code title="container/plugins.go" %} - -```go -import ( - grpcPlugin "github.com/roadrunner-server/grpc/v5" - grpcInterceptor "github.com/roadrunner-server/samples/grpc_interceptor" -) - -func Plugins() []any { - return []any{ - // ... - &grpcInterceptor.Plugin{}, - &grpcPlugin.Plugin{}, - // ... - } -} -``` - -{% endcode %} - -##### Build binary - -{% code %} - -```bash -make build -``` - -{% endcode %} - -The binary is created as `./rr`. - -### Configure interceptor in `.rr.yaml` - -Use the plugin name returned by `Name()`: - -{% code title=".rr.yaml" %} - -```yaml -grpc: - interceptors: - - "sample-grpc-interceptor" -``` - -{% endcode %} +For an interceptor that reads protobuf descriptors, see the [registry-based example](./protoreg.md#example-grpc-interceptor). Use the plugin's `Name()` in `grpc.interceptors`. ### Run and verify diff --git a/grpc/protoreg.md b/grpc/protoreg.md index 89641ea..4881006 100644 --- a/grpc/protoreg.md +++ b/grpc/protoreg.md @@ -21,7 +21,7 @@ ## Configuration {% hint style="warning" %} -The `protoreg` plugin must be enabled alongside the `grpc` plugin in your configuration. Without the `grpc` plugin enabled, the `protoreg` plugin will not be started. +The stock beta does not include `protoreg`. Add the v6 plugin to a [custom RR build](../customization/build.md). Both `grpc` and `protoreg` configuration sections are required to start it. {% endhint %} The plugin is configured under the `protoreg` section in your `.rr.yaml` configuration file: @@ -74,6 +74,31 @@ files: - user/v1/user.proto # Additional service definitions ``` +## Server reflection + +With gRPC `v6.0.0-beta.6`, RR automatically uses the `protoreg` registry for server reflection. Configure the same service files in `grpc.proto` and `protoreg.files`. Paths in `protoreg.files` are relative to `proto_path`; they do not replace `grpc.proto`. + +For the [Hello World service](./grpc.md#protoc-plugin), use: + +{% code title=".rr.yaml" %} + +```yaml +grpc: + listen: "tcp://127.0.0.1:9001" + proto: + - "proto/helloworld.proto" + +protoreg: + proto_path: + - "proto" + files: + - "helloworld.proto" +``` + +{% endcode %} + +This lets reflection clients retrieve file and message descriptors for the PHP service. Reflection streams do not call unary authentication interceptors. See [reflection access controls](./grpc.md#server-reflection). + ## Example: Project Structure A typical project structure when using this plugin: @@ -138,116 +163,86 @@ protoreg: ## Using the Registry in Your Plugin -Other plugins can depend on `protoreg` to access the registry. Here's how to use it in a custom plugin: +Declare `protoreg.Registry` as an initialization dependency: ```go package myplugin -import ( - "github.com/jhump/protoreflect/desc" - "github.com/jhump/protoreflect/v2/protoresolve" -) - -type Registry interface { - Registry() *protoresolve.Registry - Services() map[string]*desc.ServiceDescriptor - FindMethodByFullPath(method string) (*desc.MethodDescriptor, error) -} +import "github.com/roadrunner-server/protoreg/v6" type Plugin struct { - registry Registry + registry protoreg.Registry } -func (p *Plugin) Init(registry Registry) error { - +func (p *Plugin) Init(registry protoreg.Registry) error { p.registry = registry - - // Access the underlying registry - reg := p.registry.Registry() - - // Get all registered services - services := p.registry.Services() - - // Find a specific method by its full path - method, err := p.registry.FindMethodByFullPath("/service.v1.MyService/Process") - if err != nil { - return err - } - - // Use the method descriptor for reflection - inputType := method.GetInputType() - outputType := method.GetOutputType() - return nil } ``` ### Registry Interface -The plugin exposes the following interface: +The interface uses `protoresolve.Registry` from `github.com/jhump/protoreflect/v2/protoresolve` and descriptors from `github.com/jhump/protoreflect/desc`: ```go type Registry interface { - // Registry returns the underlying protoresolve.Registry that - // contains all the parsed descriptors + // Registry returns the parsed descriptors. Registry() *protoresolve.Registry - // Services returns a map of all registered service descriptors - // keyed by their fully qualified name + // Services maps fully qualified service names to descriptors. Services() map[string]*desc.ServiceDescriptor - // FindMethodByFullPath finds a method descriptor by its full gRPC path - // Format: "/package.Service/Method" or "package.Service/Method" + // The path format is "/package.Service/Method" or "package.Service/Method". FindMethodByFullPath(method string) (*desc.MethodDescriptor, error) } ``` ## Example: gRPC Interceptor -Here's an example of using the registry in a custom gRPC interceptor to log request details: +This unary interceptor logs method descriptors without logging request bodies. Include it with `grpc` and `protoreg` in your custom build. Set `grpc.interceptors: ["descriptor-logger"]` to activate it. ```go package interceptor import ( "context" - "log" + "log/slog" - "github.com/roadrunner-server/protoreg/v5" + "github.com/roadrunner-server/protoreg/v6" "google.golang.org/grpc" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/dynamicpb" ) +type Logger interface { + NamedLogger(name string) *slog.Logger +} + type Plugin struct { registry protoreg.Registry + log *slog.Logger } -func (i *Plugin) UnaryInterceptor( - ctx context.Context, - req interface{}, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, -) (interface{}, error) { - // Find the method descriptor - method, err := i.registry.FindMethodByFullPath(info.FullMethod) - if err != nil { - log.Printf("Method not found in registry: %s", info.FullMethod) - return handler(ctx, req) - } +func (p *Plugin) Init(registry protoreg.Registry, logger Logger) error { + p.registry = registry + p.log = logger.NamedLogger(p.Name()) + return nil +} - // Log method information - log.Printf("Method: %s", method.GetFullyQualifiedName()) - log.Printf("Input type: %s", method.GetInputType().GetFullyQualifiedName()) - log.Printf("Output type: %s", method.GetOutputType().GetFullyQualifiedName()) +func (p *Plugin) Name() string { + return "descriptor-logger" +} - // You can also dynamically inspect the message fields - inputDesc := method.GetInputType() - for _, field := range inputDesc.GetFields() { - log.Printf(" Field: %s (type: %s)", field.GetName(), field.GetType().String()) +func (p *Plugin) UnaryServerInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + method, err := p.registry.FindMethodByFullPath(info.FullMethod) + if err == nil && method != nil { + p.log.Info("grpc method", + "method", method.GetFullyQualifiedName(), + "input_type", method.GetInputType().GetFullyQualifiedName(), + "output_type", method.GetOutputType().GetFullyQualifiedName(), + ) + } + return handler(ctx, req) } - - return handler(ctx, req) } ``` diff --git a/http/gzip.md b/http/gzip.md index d09e6b4..d05fc02 100644 --- a/http/gzip.md +++ b/http/gzip.md @@ -1,11 +1,10 @@ # HTTP β€” Gzip middleware -The gzip middleware supports the `Accept-Encoding: gzip` header and compresses or decompresses the contents of -outgoing and incoming requests. +The gzip middleware can compress HTTP responses for clients that send `Accept-Encoding: gzip`. It does not decompress incoming request bodies. ## Documentation -- MDN [link](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) +- MDN: [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) ## Configuration @@ -25,4 +24,6 @@ http: {% endcode %} +In v6 beta, put `gzip` before `static` or `sendfile` to apply compression to their responses. See [middleware order](./http.md#middleware-order). + The gzip middleware supports OpenTelemetry header propagation. diff --git a/http/headers.md b/http/headers.md index 22d6a01..76101de 100644 --- a/http/headers.md +++ b/http/headers.md @@ -21,7 +21,7 @@ http: # If `allowed_origin_regex` option is set, the content of `allowed_origin` is ignored allowed_origin_regex: "^http://foo" allowed_headers: "*" - allowed_methods: "GET,POST,PUT,DELETE" + allowed_methods: "GET, POST, PUT, DELETE" allow_credentials: true exposed_headers: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma" max_age: 600 @@ -35,6 +35,8 @@ http: > Make sure to declare "headers" middleware. +In v6 beta, `allowed_origin`, `allowed_methods`, `allowed_headers`, and `exposed_headers` ignore whitespace around each comma-separated value. For example, `"GET, POST"` selects both methods. + {% hint style="info" %} Since RoadRunner v2023.2.0, the following changes were made: @@ -48,6 +50,8 @@ Since RoadRunner v2023.2.0, the following changes were made: You can control additional headers for outgoing responses and headers to be added to requests sent to your application. +Put `headers` before `static` in the v6 middleware list to apply headers to static responses. See [middleware order](./http.md#middleware-order). + {% code title=".rr.yaml" %} ```yaml diff --git a/http/http.md b/http/http.md index 755d3c1..ef4afbb 100644 --- a/http/http.md +++ b/http/http.md @@ -13,7 +13,7 @@ version: "3" http: # Host and port to listen on (e.g.: `127.0.0.1:8080`). # - # This option is required. + # Required for plain HTTP. Omit to use only HTTPS or FastCGI. address: 127.0.0.1:8080 # Override HTTP error code for internal RR errors @@ -26,9 +26,9 @@ http: # Default: false access_logs: false - # Maximum incoming request size in megabytes. Zero means no limit. + # Maximum incoming request size in MiB. Zero selects the default limit. # - # Default: 0 + # Default: 1000 max_request_size: 256 # Send raw body (unescaped) to the PHP worker for the application/x-www-form-urlencoded content type @@ -36,24 +36,17 @@ http: # Optional, default: false raw_body: false - # Middleware for the HTTP plugin; order is important. Allowed values are: "headers", "gzip", "zstd", "static", "sendfile", [SINCE 2.6] -> "new_relic", [SINCE 2.6] -> "http_metrics", [SINCE 2.7] -> "cache" + # Middleware names depend on the plugins in the build. Requests run left to right in v6. # The "zstd" middleware requires a build that includes the zstd plugin. # # Default value: [] middleware: [ "headers", "gzip" ] - # Allow incoming requests only from the following subnets (https://en.wikipedia.org/wiki/Reserved_IP_addresses). + # Trust HTTP forwarding headers from these proxy addresses. + # Requires "proxy_ip_parser" in middleware. This is not a network access filter. # - # Default: ["10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10"] - trusted_subnets: [ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10", - ] + # Default: [] (forwarding headers are not trusted) + trusted_subnets: [ "127.0.0.1/32" ] # File uploading settings. uploads: @@ -122,30 +115,30 @@ http: # Settings for "static" middleware. static: - # Path to the directory to serve + # Existing directory to serve. # - # Default: "." (current) + # Required when static middleware is enabled. dir: "." - # File patterns to forbid + # File extensions to forbid # # Default: empty - forbid: [ "" ] + forbid: [ ".php", ".htaccess" ] # ETag calculation (based on the body CRC32) # # Default: false calculate_etag: false - # Weak ETag calculation (based only on the content-length CRC32) + # Weak ETags use the file name in the pinned static beta. # # Default: false weak: false - # Patterns to allow + # File extensions to allow # # Default: empty - allow: [ ".txt", ".php" ] + allow: [ ".txt", ".css", ".js" ] # Request headers # @@ -240,7 +233,7 @@ http: ssl: # Host and port to listen on (e.g.: `127.0.0.1:443`). # - # Default: ":443" + # Default: "127.0.0.1:443" address: "127.0.0.1:443" # Use ACME certificates provider (Let's encrypt) @@ -335,6 +328,8 @@ http: You can enable HTTPS support by adding the `ssl` section to the `http` config. +Use brackets around IPv6 addresses, such as `"[::1]:8443"`. The port must be an unsigned integer from 0 through 65535. HTTP-to-HTTPS redirects preserve IPv6 brackets. + {% code title=".rr.yaml" %} ```yaml @@ -344,7 +339,7 @@ http: address: 127.0.0.1:8080 ssl: - # host and port separated by semicolon (default :443) + # Host and port separated by a colon. address: :8892 redirect: false cert: fixtures/server.crt @@ -483,18 +478,6 @@ http: {% endcode %} -### Upgrade connection from `HTTP/1.1` to `H2C` [`v2.10.2`] - -Connection might be upgraded from the `http/1.1` -to `h2c`: [rfc7540](https://datatracker.ietf.org/doc/html/rfc7540#section-3.4) - -**Headers, which should be sent to upgrade connection:** - -1. `Upgrade`: `h2c` -2. `Connection`: `HTTP2-Settings` -3. `Connection`: `Upgrade` -4. `HTTP2-Settings`: `AAMAAABkAARAAAAAAAIAAAAA` [RFC](https://datatracker.ietf.org/doc/html/rfc7540#section-3.2.1) - ### HTTP/2 Push Resources RoadRunner supports [HTTP/2 push](https://en.wikipedia.org/wiki/HTTP/2_Server_Push) via virtual headers provided by the PHP @@ -517,7 +500,7 @@ HTTP/2 push only works under HTTPS with the `static` service enabled. ### H2C -You can enable HTTP/2 support over non-encrypted TCP connection using H2C: +H2C provides HTTP/2 over an unencrypted TCP connection. In v6 beta, the client must start with HTTP/2 prior knowledge. HTTP/1.1 requests with `Upgrade: h2c` are handled as HTTP/1.1; RR does not upgrade them. {% code title=".rr.yaml" %} @@ -525,7 +508,19 @@ You can enable HTTP/2 support over non-encrypted TCP connection using H2C: version: "3" http: - http2.h2c: true + address: 127.0.0.1:8080 + http2: + h2c: true +``` + +{% endcode %} + +For example, use a curl build with HTTP/2 support: + +{% code %} + +```bash +curl --http2-prior-knowledge http://127.0.0.1:8080/ ``` {% endcode %} @@ -547,6 +542,45 @@ http: {% endcode %} +## Development: PROXY protocol + +{% hint style="warning" %} +This section requires a custom build that includes the untagged HTTP change [8bebd3b](https://github.com/roadrunner-server/http/commit/8bebd3b). The currently pinned HTTP plugin, `v6.0.0-beta.10`, does not include PROXY protocol support. +{% endhint %} + +PROXY protocol v1 and v2 let a TCP proxy supply client addresses. Configure each application listener separately. `http.proxy_protocol` controls plain HTTP, including H2C. `http.ssl.proxy_protocol` controls HTTPS. Omit the corresponding section to disable it. + +The following example enables PROXY protocol on both listeners: + +{% code title=".rr.yaml" %} + +```yaml +http: + address: "0.0.0.0:8080" + proxy_protocol: + trusted_proxies: [ "10.20.0.10" ] + read_header_timeout: 5s + ssl: + address: "0.0.0.0:8443" + cert: "server.crt" + key: "server.key" + proxy_protocol: + trusted_proxies: [ "10.20.0.10" ] + read_header_timeout: 5s +``` + +{% endcode %} + +- `trusted_proxies` must contain at least one IP address or CIDR for an immediate TCP peer. Hostnames are not accepted. Replace the example address with the address of your proxy. +- RR rejects peers outside this list. Trusted peers must send a valid PROXY header. Direct HTTP requests without that header are rejected. +- `read_header_timeout` limits the time to read the PROXY header. The default is `5s`. Zero selects the default; negative values are invalid. This setting does not control HTTP or TLS timeouts. +- For HTTPS, the proxy must send the PROXY header before the TLS handshake. The HTTPS listener still requires certificates or ACME configuration. Temporary ACME challenge listeners are not wrapped. +- Only TCP application listeners support this setting. It does not apply to HTTP/3 or FastCGI. Headers with TCP4 or TCP6 addresses replace the connection addresses; v1 `UNKNOWN` and v2 `LOCAL` retain the socket addresses. + +This trust list is separate from [HTTP forwarding-header trust](./proxy.md). If `proxy_ip_parser` also runs, it checks `trusted_subnets` against the client address supplied by PROXY protocol, not the original TCP peer. + +Route HTTP readiness checks through a trusted proxy that sends a PROXY header. For direct checks, use the status plugin's separate [health and readiness endpoints](../lab/health.md). A successful TCP connection alone does not prove that RR accepted the PROXY header or that a worker is ready. + ## HTTP/3 HTTP/3 support is experimental and might change in the future. Docs are available in the [experimental](../experimental/experimental.md) section. @@ -567,24 +601,37 @@ http: The `http.internal_error_code` is used for `SoftJob`, allocation, TTL, network, and similar errors. For example, a load balancer might require a different code, so you may override the default. +In v6 beta, malformed request bodies that RR cannot parse return `400 Bad Request`. Requests that exceed `max_request_size` return `413 Request Entity Too Large`. `internal_error_code` does not override these responses. + +{% hint style="warning" %} +With `http.pool.debug: true`, internal error responses can contain HTML-escaped error text. Keep debug mode disabled on public production servers. +{% endhint %} + ## Middleware order -Since all middleware components are independent, they can remove or update headers set by -the [previous one](https://github.com/roadrunner-server/roadrunner/issues/1501). +In v6 beta, requests enter middleware from left to right in the configuration list. Responses return through the same middleware in reverse order. A middleware can return a response without calling the remaining handlers. -**Note that the request (imagine) comes from the right:** +The order was reversed in v5. Reverse an existing list to preserve its v5 behavior. -{% code title=".rr.yaml" %} +{% code title="v5 configuration" %} ```yaml http: - middleware: # RESPONSE FROM HERE --> [ "static", "gzip", "sendfile" ] # <-- REQUEST COMES FROM HERE + middleware: [ "static", "headers", "gzip" ] ``` {% endcode %} -So in this case, the request gets into the `sendfile` middleware, then `gzip`, and `static`. And vice versa from the -response. +{% code title="Equivalent v6 configuration" %} + +```yaml +http: + middleware: [ "gzip", "headers", "static" ] +``` + +{% endcode %} + +In the v6 example, requests enter `gzip`, then `headers`, then `static`. Put `headers` and `gzip` before `static` to apply them to static responses. Middleware can replace headers set by an earlier handler. ## Request queues diff --git a/http/proxy.md b/http/proxy.md index beb9855..8caabfd 100644 --- a/http/proxy.md +++ b/http/proxy.md @@ -1,18 +1,12 @@ # Proxy IP parser -This middleware resolves the real client IP from proxy headers when a request arrives -through a trusted subnet. By default it consults, in order: `Forwarded`, `X-Forwarded-For`, -`X-Real-IP`, `True-Client-IP`, and `CF-Connecting-IP`. The set and order of headers can be -customized with `trusted_headers`. +This middleware gets the client address from HTTP forwarding headers when the request comes from a trusted proxy. In v6, `trusted_headers` selects the headers and their order. ## Description -When the immediate peer is within one of the `trusted_subnets`, the middleware resolves the -client IP from the configured headers β€” the first non-empty match wins β€” and sets it as -`RemoteAddr`. Otherwise `RemoteAddr` is left unchanged. +When the immediate peer is within `trusted_subnets`, the middleware uses the first nonempty parsed header value as `RemoteAddr`. Otherwise, it leaves `RemoteAddr` unchanged. This setting controls trust in forwarding headers; it does not block incoming connections. -The middleware is active only when `trusted_subnets` is configured; without it, forwarding -headers are never trusted. +Add `proxy_ip_parser` to `http.middleware` and configure a nonempty `http.trusted_subnets` list to enable it. An omitted or empty subnet list disables it. Each subnet must use CIDR notation, such as `127.0.0.1/32` or `::1/128`. ## Usage @@ -25,16 +19,8 @@ http: middleware: [ "proxy_ip_parser" ] # Middleware uploads: forbid: [ ".php", ".exe", ".bat" ] - trusted_subnets: # Trusted addresses in CIDR format - [ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10" - ] + # Replace this with the immediate proxy's actual CIDR. + trusted_subnets: [ "127.0.0.1/32" ] pool: num_workers: 2 allocate_timeout: 60s @@ -45,27 +31,31 @@ http: ## Trusted headers -`trusted_headers` is an ordered allowlist of the headers used to resolve the client IP. The -middleware checks them in order and uses the first non-empty value; headers that are not -listed are ignored, and custom headers are supported. When `trusted_headers` is omitted, the -default order is used: `Forwarded`, `X-Forwarded-For`, `X-Real-IP`, `True-Client-IP`, -`CF-Connecting-IP`. +`http.trusted_headers` is an ordered allowlist. The middleware ignores headers that are not listed. It removes whitespace around configured header names, compares names without case sensitivity, and removes duplicate names. -For example, to trust only `X-Real-IP` and Cloudflare's `CF-Connecting-IP` while ignoring -`X-Forwarded-*`: +An omitted, empty, or all-blank list uses the default order: `Forwarded`, `X-Forwarded-For`, `X-Real-IP`, `True-Client-IP`, `CF-Connecting-IP`. An empty header list does not disable trust. If a header produces no parsed value, the middleware tries the next header. For example, a `Forwarded` value without `for=` does not prevent use of `X-Forwarded-For`. + +The following example trusts only `X-Real-IP` and `CF-Connecting-IP`: {% code title=".rr.yaml" %} ```yaml http: - trusted_subnets: [ "10.0.0.0/8", "127.0.0.0/8" ] + middleware: [ "proxy_ip_parser" ] + trusted_subnets: [ "10.20.0.10/32" ] trusted_headers: [ "X-Real-IP", "CF-Connecting-IP" ] ``` {% endcode %} {% hint style="info" %} -`X-Forwarded-For` uses the left-most address from its comma-separated list, and `Forwarded` -is parsed per [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239) (`for=`). All other -headers, including custom ones, are taken verbatim. +`X-Forwarded-For` uses the first value before a comma. `Forwarded` uses the first `for=` value and removes its surrounding quotes. All other headers, including custom headers, are used without changing their values. +{% endhint %} + +{% hint style="warning" %} +The parser does not validate that a selected header value is an IP address. Trust only proxy addresses you control. Configure each trusted proxy to overwrite the selected headers so a client cannot supply the address used by the application. {% endhint %} + +## PROXY protocol + +HTTP forwarding headers are separate from the TCP PROXY protocol. The pinned HTTP beta does not support PROXY protocol. For custom development builds, see [Development: PROXY protocol](./http.md#development-proxy-protocol). diff --git a/http/resp-streaming.md b/http/resp-streaming.md index 67c9d37..928b21f 100644 --- a/http/resp-streaming.md +++ b/http/resp-streaming.md @@ -65,7 +65,11 @@ try { ### Sending headers and status codes -You can send headers and status codes (`1XX` multiple times, or others once) to the client during streaming. +You can send multiple informational responses before the final response. Send an empty body and set `endOfStream: false` for each informational response. Send the final status and headers before streaming its body. + +{% hint style="warning" %} +In v6 beta, RR ignores worker responses with status `101 Switching Protocols`. A PHP worker cannot upgrade the connection by sending this status. RR also drops bodies attached to informational responses. Do not send informational responses after the final response has started. +{% endhint %} {% code title="worker.php" %} @@ -101,7 +105,6 @@ $read = static function (): Generator { try { while ($req = $http->waitRequest()) { $http->respond(100, '', headers: ['X-100' => ['100']], endOfStream: false); - $http->respond(101, '', headers: ['X-101' => ['101']], endOfStream: false); $http->respond(102, '', headers: ['X-102' => ['102']], endOfStream: false); $http->respond(103, '', headers: ['Link' => ['; rel=preload; as=style'], 'X-103' => ['103']], endOfStream: false); $http->respond(200, $read(), headers: ['X-200' => ['200']], endOfStream: true); @@ -113,4 +116,4 @@ try { {% endcode %} -In this example, we send five status codes and five headers to the client. You may send a `103 Early Hints` status code (or any `1XX` status code) at any time during streaming (do not forget about `$endOfStream`). +This example sends `100`, `102`, and `103` before the final `200` response. Headers supplied only in an informational response are not copied to the final response. Repeat any headers needed in the final `respond()` call. diff --git a/http/sendfile.md b/http/sendfile.md index 14fc3f9..0e4db4d 100644 --- a/http/sendfile.md +++ b/http/sendfile.md @@ -3,9 +3,7 @@ The `Send` HTTP middleware and the `X-Sendfile` HTTP response header are used to stream large files using RoadRunner. While the file is being streamed with the help of RoadRunner, the PHP worker may accept the next request. -Original issue: [link](https://github.com/roadrunner-server/roadrunner-plugins/issues/9) -The middleware reads the file in 10 MB chunks. For example, for a 5 GB file, only 10 MB of RSS memory is used. If the file -is smaller than 10 MB, the middleware adjusts the buffer to fit the file size. +The middleware reads the file with a buffer of up to 10 MiB. For smaller files, the buffer matches the file size. See the [X-Sendfile proposal](https://github.com/roadrunner-server/roadrunner-plugins/issues/9). ## Similar approaches @@ -33,3 +31,11 @@ http: ``` {% endcode %} + +## File responses + +In v6 beta, a response with `X-Sendfile` uses `Content-Type: application/octet-stream`. This replaces any content type supplied by the PHP worker. Check clients that depend on a specific media type for inline display. Use `Content-Disposition: attachment` for downloads. + +An empty file returns `200 OK` with no body. Paths are normalized before file access. Use paths controlled by the application: this middleware does not restrict access to a configured root directory. + +To apply gzip to file responses, put `gzip` before `sendfile` in the v6 middleware list. See [middleware order](./http.md#middleware-order). diff --git a/http/static.md b/http/static.md index ee42bc2..95cd659 100644 --- a/http/static.md +++ b/http/static.md @@ -2,10 +2,8 @@ The `static` HTTP middleware serves static content using RoadRunner on the main HTTP plugin endpoint. -To avoid a filesystem lookup on every request, the middleware caches file metadata and misses in memory with a short TTL (`10s` by default). A repeated request for the same path is then answered without touching the filesystem, so the overhead of enabling the middleware stays small. The cache trades a few seconds of staleness for that speed: a file added, changed, or removed on disk is picked up within one TTL. Set the TTLs to `0s` to check the filesystem on every request, or run `rr reset static` to flush the cache at once (for example, after a deploy). - {% hint style="info" %} -If there is no file to serve, RR forwards the request back to the PHP worker. Only `GET` and `HEAD` requests are served; every other method goes to the worker. +If there is no file to serve, RR forwards the request to the PHP worker. The pinned static plugin, `v6.0.0-beta.5`, does not include the cache and prefix options described in the [development section](#development-cache-and-prefixes). {% endhint %} ## Enable HTTP middleware @@ -18,24 +16,15 @@ To enable static content serving, use the configuration inside the HTTP section: version: "3" http: - # host and port separated by semicolon + # Host and port separated by a colon. address: 127.0.0.1:44933 middleware: [ "static" ] # Add static to the list of middleware static: dir: "." - forbid: [ "" ] + forbid: [ ".php", ".htaccess" ] calculate_etag: false weak: false - allow: [ ".txt", ".php" ] - # Serve only request paths that start with one of these prefixes. - # Empty (the default) considers every request. - prefixes: [ "/assets/", "/build/" ] - # File metadata cache TTL. "0s" disables it. Default: 10s. - cache_ttl: 10s - # Miss cache TTL. "0s" disables it. Default: 10s. - cache_miss_ttl: 10s - # Maximum entries kept in each cache map. Default: 16384. - cache_max_entries: 16384 + allow: [ ".txt", ".css", ".js" ] request: input: "custom-header" response: @@ -46,18 +35,14 @@ http: Where: -1. `dir`: path to the directory. +1. `dir`: required path to an existing directory. 2. `forbid`: file extensions that should not be served. 3. `allow`: extensions that should be served (empty = serve all except forbidden). If an extension is present in both lists (allow and forbid), it is treated as forbidden. 4. `calculate_etag`: enable etag calculation for the static file. -5. `weak`: use a weak generator (`W/`). The weak etag is derived from the file size and modification time. If false, the whole file content is used to produce a strong CRC32 etag. -6. `prefixes`: restrict serving to request paths that start with one of these prefixes. When empty (the default), every path is considered. A path that matches no prefix goes to the worker without a filesystem lookup. The match is a plain prefix, so include the trailing slash (`/assets/`) to scope it to a directory. -7. `cache_ttl`: TTL for the file metadata cache (etag, content type, size, modification time). Active only when `calculate_etag` is enabled. `0s` disables it. Default: `10s`. -8. `cache_miss_ttl`: TTL for the miss cache. A request whose path does not resolve to a file records a miss and skips the filesystem check for this duration, which removes the per-request lookup on dynamic routes that share the URL space with static files. `0s` disables it. Default: `10s`. -9. `cache_max_entries`: upper bound on the number of entries in each cache map (metadata and miss). When a map is full, expired entries are evicted; if none can be freed, the middleware checks the filesystem instead. Default: `16384`. -10. `request/response`: custom headers for the static files. +5. `weak`: use a weak ETag (`W/`) when `calculate_etag` is enabled. In the pinned beta, this value depends only on the file name, not its contents. With `weak: false`, RR calculates a strong CRC32 ETag from the file contents. +6. `request/response`: custom headers for the static files. -To combine static content with other middleware, use the following sequence (static last, then headers and gzip): +In v6 beta, put `static` after `gzip` and `headers` so they also apply to static responses. See [middleware order](./http.md#middleware-order) when migrating a v5 configuration. {% code title=".rr.yaml" %} @@ -65,9 +50,9 @@ To combine static content with other middleware, use the following sequence (sta version: "3" http: - # host and port separated by semicolon + # Host and port separated by a colon. address: 127.0.0.1:44933 - middleware: [ "static", "headers", "gzip" ] + middleware: [ "gzip", "headers", "static" ] # Settings for "headers" middleware. headers: cors: @@ -80,10 +65,10 @@ http: # Settings for "static" middleware. static: dir: "." - forbid: [ "" ] + forbid: [ ".php", ".htaccess" ] calculate_etag: false weak: false - allow: [ ".txt", ".php" ] + allow: [ ".txt", ".css", ".js" ] request: input: "custom-header" response: @@ -92,6 +77,47 @@ http: {% endcode %} +## Development: cache and prefixes + +{% hint style="warning" %} +This section requires a custom build that includes the untagged static change [030052b](https://github.com/roadrunner-server/static/commit/030052b). The currently pinned static plugin, `v6.0.0-beta.5`, does not include these options or the behavior changes in this section. +{% endhint %} + +The development build serves only `GET` and `HEAD` requests. Other methods go to the PHP worker. It normalizes the URL path before checking prefixes and file extensions. + +{% code title=".rr.yaml" %} + +```yaml +http: + address: 127.0.0.1:44933 + middleware: [ "static" ] + static: + dir: "." + forbid: [ ".php", ".htaccess" ] + allow: [ ".txt", ".css", ".js" ] + calculate_etag: true + weak: false + prefixes: [ "/assets/", "/build/" ] + cache_ttl: 10s + cache_miss_ttl: 10s + cache_max_entries: 16384 +``` + +{% endcode %} + +- `prefixes`: serve only normalized paths that start with a listed prefix. An empty list considers every path. Each prefix must start with `/`. Use a trailing slash, such as `/assets/`, to match a directory. Prefixes are not removed from the file path. +- `cache_ttl`: cache file metadata, including the ETag and content type. This cache is active only when `calculate_etag` is enabled. The default is `10s`. An explicit `0s` disables it. +- `cache_miss_ttl`: cache missing-file and directory results. The default is `10s`. An explicit `0s` disables it. +- `cache_max_entries`: entry limit for each cache. The default is `16384`; zero selects the default. A full cache attempts to remove expired entries. If it cannot free space, RR serves the request without adding a new entry. + +Negative TTLs and negative entry limits are invalid. + +A positive cache hit still opens the file and reads its metadata. RR reuses cached metadata only when the file size and modification time match. It detects deleted files and changed metadata on the next request. A cached miss avoids the filesystem lookup. RR can continue to send requests for a newly created file to PHP until `cache_miss_ttl` expires. + +If a deployment preserves both file size and modification time, RR can reuse an old ETag until `cache_ttl` expires. Run `rr reset static` after such a deployment to clear both caches. Set both TTLs to `0s` to disable caching. + +In this development build, weak ETags use file size and modification time. Strong ETags use CRC32C and are not generated for empty files or files larger than 32 MiB. Treat ETags as opaque values rather than calculating them in a client. + ## Fileserver plugin The Fileserver plugin serves static files. It works similarly to the `static` HTTP middleware and has extended functionality. @@ -99,6 +125,8 @@ The `static` middleware runs on the main HTTP endpoint, while the file server pl ## File server configuration +In v6 beta, startup fails if `address` is empty or `serve` has no entries. Each `prefix` must be nonempty and start with `/`. + {% code title=".rr.yaml" %} ```yaml @@ -107,7 +135,7 @@ fileserver: # # Error on empty address: 127.0.0.1:10101 - # Etag calculation. Request body CRC32. + # ETag calculation from the response body. # # Default: false calculate_etag: true @@ -117,7 +145,7 @@ fileserver: # Default: false weak: false - # Enable body streaming for files more than 4KB + # Stream incoming request bodies. # # Default: false stream_request_body: true @@ -125,7 +153,7 @@ fileserver: serve: # HTTP prefix # - # Error on empty + # Required. Must start with a forward slash. - prefix: "/foo" # Directory to serve @@ -145,7 +173,7 @@ fileserver: # The value for the Cache-Control HTTP-header. Units: seconds # - # Default: 10 seconds + # Default: 0 (no Cache-Control header) max_age: 10 # Enable range requests diff --git a/intro/about.md b/intro/about.md index 0e4d9c0..e158b91 100644 --- a/intro/about.md +++ b/intro/about.md @@ -27,7 +27,7 @@ The following plugins are designed to run workers and handle specific types of r to the PHP application. It supports bidirectional communication, allowing for efficient and seamless interaction between the server and clients. - [**gRPC**](../grpc/grpc.md) - Deals with gRPC requests from clients and passes them on to the PHP application. -- [**TCP**](../plugins/tcp.md) - Handles TCP requests from clients and routes them to the appropriate PHP application. +- [**TCP**](../plugins/tcp.md) - Handles raw TCP requests in custom builds that include the plugin. It is not part of the default RoadRunner container. - [**Temporal**](../workflow/temporal.md) - Manages workflows and activities, allowing for the efficient handling of various tasks and processes. diff --git a/intro/compatibility.md b/intro/compatibility.md index d598572..1e0c6f4 100644 --- a/intro/compatibility.md +++ b/intro/compatibility.md @@ -2,6 +2,8 @@ This section provides information about upgrading your RoadRunner configuration to the latest version. +For the upcoming release, see [v3 Migration](v3-migration.md). It covers the v5-to-v6 plugin changes, beta limitations, and development-only features. The configuration format remains `version: "3"`. + ## Compatibility matrix The compatibility matrix provides information about the supported configuration versions for different RoadRunner diff --git a/intro/v3-migration.md b/intro/v3-migration.md new file mode 100644 index 0000000..ca80d4b --- /dev/null +++ b/intro/v3-migration.md @@ -0,0 +1,99 @@ +# v3 Migration + +This guide compares the v5 plugins in [RoadRunner v2025.1.15](https://github.com/roadrunner-server/roadrunner/blob/v2025.1.15/go.mod) with the v6 beta versions selected by the [RoadRunner development snapshot](https://github.com/roadrunner-server/roadrunner/blob/b0cccd917f001b6584eafdc04ad6ba69a97cbb69/go.mod). It covers application behavior, configuration, and custom Go plugins. + +{% hint style="warning" %} +The v6 plugin line is in beta. These notes do not describe a published RoadRunner v3 binary. Changes that are not in the selected beta versions are listed under [Development Changes](#development-changes). +{% endhint %} + +Keep `version: "3"` in `.rr.yaml`. That value identifies the configuration format. The inspected RR Go module remains `github.com/roadrunner-server/roadrunner/v2025`; its plugins use `/v6`. Source builds require Go 1.27. + +## Upgrade Checks + +| Area | Required check | +| --- | --- | +| HTTP middleware | Requests now enter middleware from left to right. Reverse an existing v5 list to retain its previous execution order. See [Middleware order](../http/http.md#middleware-order). | +| H2C | HTTP/1.1 `Upgrade: h2c` no longer switches protocols. Configure clients for [HTTP/2 prior knowledge](../http/http.md#http2). | +| Logging | Replace `file_logger_options` with `output`. Built-in rotation is removed. Production JSON changes `ts` to `time` and uses uppercase levels. Review parsers and output management in [Logger](../lab/logger.md). | +| Tracing | Replace the removed RR `zipkin` exporter with [OTLP](../lab/otel.md). Middleware spans no longer measure the whole downstream request; use the server span for request latency. | +| RPC | Goridge v4 rejects MessagePack. Use JSON, protobuf, Gob, or raw bytes as appropriate for the method. RPC still uses Goridge and `net/rpc`, not Connect. See [RPC compatibility](../php/rpc.md#v6-compatibility). | +| TCP plugin | The default RR build no longer includes the [TCP plugin](../plugins/tcp.md). A `tcp:` section cannot add it. This does not remove `tcp://` RPC transport. | +| Centrifuge | Remove calls to `centrifuge.RateLimit`; the RPC has no replacement in the plugin. Check [Centrifuge](../plugins/centrifuge.md) and the DTO migration before updating direct clients. | +| Job headers | `pool` is now a routing header. Rename application headers that use this name. External producers must supply a valid pool when [named worker pools](../queues/overview-queues.md#named-worker-pools-v6-beta) are enabled. | +| Environment files | A configured root `envfile` is now loaded without experimental mode. Supply the file or remove an unused setting; a missing file fails startup. See [Environment](../php/environment.md). | +| gRPC reflection | Reflection is registered automatically. Unary interceptors do not protect its streams. Review network access and mTLS in [gRPC](../grpc/grpc.md#server-reflection). | + +## New Features + +- [Jobs worker pools](../queues/overview-queues.md#named-worker-pools-v6-beta): assign pipelines to separate named pools. The existing single `jobs.pool` format remains available; do not configure it together with `jobs.pools`. +- [AMQP version 2 configuration](../queues/amqp.md#version-2-configuration-recommended): separate exchange and queue settings, with controls for declaration and binding. Legacy flat configuration remains supported. Runtime `jobs.Declare` still uses flat keys. +- [NSQ](../queues/nsq.md): a new bundled Jobs driver with topics, channels, discovery, acknowledgements, and delayed delivery. Its retry limit and lack of a dead-letter handoff require application failure handling. +- [gRPC reflection](../grpc/grpc.md#server-reflection): v1 and v1alpha service listing. Full PHP-service descriptors require a [Protoreg plugin](../grpc/protoreg.md). Unary interceptors already existed in v5.3.0; they are not a new v6 feature. +- [Trusted proxy headers](../http/proxy.md): select and order the forwarding headers that a trusted proxy may supply. An empty list restores the defaults; it does not disable header trust. +- [Temporal](../workflow/temporal.md): configurable worker heartbeats and [dynamic workflows](../workflow/worker.md). Worker heartbeats are separate from activity heartbeats. Dynamic registration requires a compatible PHP SDK. +- [Centrifuge](../plugins/centrifuge.md): forwards `NotifyCacheEmpty` events to PHP. Enable this only with matching DTO and handler support. + +## Bug Fixes + +| Component | User-visible change | +| --- | --- | +| HTTP | Informational responses no longer corrupt the final response status or headers. Worker status 101 is ignored. Request-parsing errors return 400, and debug error text is HTML-escaped. Bracketed IPv6 HTTPS addresses and redirects work. | +| gRPC | `max_connection_age_grace` now uses its configured value instead of `max_connection_age`. Check connection-draining settings. Standard `google.rpc` error details are included in logs; check them for sensitive data. | +| X-Sendfile | Empty files no longer enter the read loop. File responses use `application/octet-stream`, even when the worker supplied another content type. | +| Fileserver | Initialization rejects missing addresses, empty route lists, and invalid prefixes. Listener failure and shutdown no longer retain the plugin lock. | +| Jobs | Pipeline restart no longer destroys the replacement pipeline. Empty-queue pollers and shutdown timeout handling no longer prevent clean shutdown. | +| NATS | Stopping a pipeline no longer purges its stream. Retry headers are retained. Configure retention and make workers tolerate redelivery. | +| SQS | FIFO retries use a fresh deduplication ID, so the broker does not discard a retry as the original message. The application job ID stays unchanged. Priority is read from message attributes. | +| Kafka | Explicit partition/offset consumption is now applied. Omit `consumer_options.topics` when using `consume_partitions`. Shutdown releases blocked rebalances. | +| AMQP | Private root CAs are used for server verification, including reconnects. Supported integer priority headers no longer cause type-assertion panics. | +| Beanstalk | Serialized jobs retain headers and trace context. Statistics now describe the pipeline's tube, not the entire server. Old messages cannot recover headers that were never stored. | +| Google Pub/Sub | Pause cancels receiving. Existing-topic startup and dynamically declared dead-letter settings are handled correctly. Existing subscription policies still require separate updates. | +| KV | Unknown drivers fail startup instead of being skipped. Memory deletion no longer blocks on duplicate timer cancellation. Redis expiration and BoltDB commit errors are returned instead of hidden. | +| BoltDB Jobs | Recovery removes stale in-flight records after returning jobs to the queue. Delayed jobs are dispatched after commit. Workers must still tolerate repeated deliveries. | +| Server | Invalid worker users fail initialization. `server.on_init.env` overrides inherited values. Use command sequences for arguments containing spaces; scalar commands do not parse shell quotes. | +| Service | A failed replacement removes the service entry and requests a stop for replacements already started. Fix the command and create the service again. Restart is not rolling or atomic. | +| Temporal | Polling stops before PHP pools are destroyed. An ordinary activity-worker exit no longer resets the whole activity pool. | +| Centrifuge | Missing gRPC metadata no longer causes a panic. A missing worker pool reports unavailable status. | +| Metrics and status | Implicit HTTP success is labeled `200`, not `-1`. Jobs distinguishes successful and requeued jobs, and totals use counters. Negative counter increments return errors. Shutdown readiness uses `unavailable_status_code`. | +| Locks and RPC | Lock wait paths release their mutexes correctly. Malformed RPC offsets and invalid response metadata return errors instead of causing slice or type-assertion panics. | + +Review [Jobs metrics](../lab/metrics.md) before updating dashboards. The request-duration histograms still measure the full handler execution, unlike the shorter middleware spans. + +## Custom Go Plugins + +The API repositories have separate roles: + +| Repository | Purpose | +| --- | --- | +| `api` | Protobuf schema source. It is no longer the Go module imported by plugins. | +| `api-go/v6` | Generated Go messages and gRPC bindings. Imports no longer contain `/build/`. | +| `api-plugins/v6` | Handwritten Go contracts for Jobs, KV, logging, locks, and status. | + +Update custom plugins to `*slog.Logger`, the new import paths, and context-aware constructors and storage methods. The pool module is now `pool/v2`; Goridge is `goridge/v4`. Use the [import and signature migration table](../customization/plugin.md#v6-migration) and the updated [Jobs driver](../customization/jobs-driver.md) and [middleware](../customization/middleware.md) examples. + +API relocation alone does not require a PHP worker-loop rewrite or change the Goridge frame version. Lock DTO namespaces and some Centrifugo messages have separate source-level changes; see [DTO compatibility](../customization/plugin.md#dto-compatibility). Do not use the Connect clients or general v2 DTO packages from intermediate betas. + +RR still pins `endure/v2 v2.6.2` and `tcplisten v1.5.2`, as it did in v2025.1.15. There is no new Endure lifecycle, dependency-injection, or tcplisten caller migration in this comparison. + +## Beta Limits + +| Component | Limitation and action | +| --- | --- | +| `pool/v2 v2.0.0-beta.1` | Scale-up does not retry the request that triggered allocation. That request can fail even after new workers start. Size the base pool and allocation timeout for the expected load. The initial limit is 500 workers; combined base and dynamic capacity is 2048. See [Automatic scaling](../php/auto-scaling.md). | +| `jobs/v6 v6.0.0-beta.10` | An explicit `jobs.pool` with omitted or zero `num_workers` produces only two pollers. Set `num_workers` explicitly; setting `num_pollers` does not override the derived count. | +| BoltDB Jobs | `boltdb/v6 v6.0.0-beta.5` loses the `pool` header during storage, including for newly published jobs. Keep a single `jobs.pool` when consuming BoltDB pipelines with jobs beta.10. Named pools cannot route these jobs. | +| `memory/v6 v6.0.0-beta.5` | `MExpire` with a past deadline or less than one second remaining can replace a value and remove its expiration. Use `Delete` for immediate removal. See [Memory KV](../kv/memory.md). | +| `service/v6 v6.0.0-beta.8` | After an automatic restart, `service.Restart` can start replacements before the old processes finish. Do not rely on it for exclusive process replacement when `remain_after_exit` is enabled. | + +## Development Changes + +These changes are not in the beta dependencies selected by the RR snapshot above. Use a custom build containing the linked change, or wait for a version that includes it. + +| Component | Pending change | +| --- | --- | +| HTTP | [PROXY protocol support](https://github.com/roadrunner-server/http/commit/8bebd3b) for plain HTTP and HTTPS listeners. It requires trusted proxy addresses and changes how load balancers and readiness checks connect. See the [development configuration](../http/http.md#development-proxy-protocol). | +| Static | [Prefix and cache controls](https://github.com/roadrunner-server/static/commit/030052b), normalized path checks, and revised ETags. Positive hits still open and stat files; cached misses can delay newly created files. See [Static files](../http/static.md). | +| Pool | [Allocation and shutdown fixes](https://github.com/roadrunner-server/pool/commit/2ae6f57): retry acquisition after scale-up, cancel spawning during shutdown, reap failed or late workers, and correct supervisor state transitions. Do not assume beta.1 contains these fixes. | +| Temporal | [Shutdown deadlock fix](https://github.com/temporalio/roadrunner-temporal/commit/bcadca6) for concurrent activity-heartbeat RPCs. It is not in `v6.0.0-beta.1`. | +| Velox v3 | New module-based plugin configuration, replacements/exclusions, version-pin checks, and deterministic build inputs. Windows targets and the remote build server are removed. No v3 tag is published; the [build guide](../customization/build.md) pins the development revision. | +| Zstd | A separate [response-compression middleware](../http/zstd.md), not yet in the inspected default RR build. Include and register the plugin before selecting `http.middleware: ["zstd"]`. | diff --git a/kv/boltdb.md b/kv/boltdb.md index 443b860..9a68c25 100644 --- a/kv/boltdb.md +++ b/kv/boltdb.md @@ -63,6 +63,14 @@ identical. ### Interval -`interval`: The interval (in seconds) between checks for the lifetime of the -value in the cache. The meaning and behavior is similar to that used in the -case of the memory driver. +`interval`: The time in seconds between expiration checks. Expired entries can remain readable until the next check. + +## Persistence + +Values remain in the database after a RoadRunner restart. Expiration timestamps are kept only in memory and are lost at restart. This limitation applies to both v5 and the v6 beta. Reapply expiration timestamps from application data after startup if needed. Use [Redis](./redis.md) when expiration must survive a RoadRunner restart. + +Upgrading from v5 to the v6 beta does not require a database format conversion. Stop RoadRunner before backing up the database file. + +## Write Errors + +In the v6 beta, `kv.Set` and `kv.Delete` return database commit errors through RPC. Handle these errors before treating a write or deletion as successful. diff --git a/kv/memory.md b/kv/memory.md index 598c074..2947355 100644 --- a/kv/memory.md +++ b/kv/memory.md @@ -3,8 +3,7 @@ This type of driver is already supported by the RoadRunner and does not require any additional installations. {% hint style="warning" %} -This type of storage, all data is contained in memory and will be destroyed when the RoadRunner Server is restarted. -If you need persistent storage without additional dependencies, then it is recommended to use the boltdb driver. +Restarting RoadRunner removes all data from this storage. For persistent values, see [BoltDB](./boltdb.md#persistence), which does not persist expiration metadata. {% endhint %} ## Configuration @@ -27,4 +26,14 @@ kv: {% endcode %} -There are no additional configuration options for this driver. The `in-memory` driver will automatically create callbacks for items with TTL. \ No newline at end of file +There are no additional configuration options for this driver. The `in-memory` driver will automatically create callbacks for items with TTL. + +## Expiration + +The `kv.MExpire` RPC method takes an absolute RFC 3339 `timeout` timestamp. + +{% hint style="warning" %} +In the memory driver at `v6.0.0-beta.5`, `kv.MExpire` truncates the remaining time to whole seconds. A result of zero or less includes past deadlines and deadlines less than one second ahead when RoadRunner handles the request. In this case, the driver replaces the entry with the request's value and removes its TTL. A request with only a key and timeout can replace an existing value with empty bytes. This is not immediate expiration. +{% endhint %} + +Use `kv.Delete` for immediate removal. Do not use past or subsecond deadlines with `kv.MExpire`. diff --git a/kv/overview-kv.md b/kv/overview-kv.md index 08508b8..b1a4afe 100644 --- a/kv/overview-kv.md +++ b/kv/overview-kv.md @@ -59,6 +59,8 @@ To use the RoadRunner KV plugin, you need to define multiple key-value storages configuration file. Each storage must have a `driver` that indicates the type of connection used by those storages. At the moment, four different types of drivers are available: `boltdb`, `redis`, `memcached`, and `memory`. +In the v6 beta, an unknown driver name in `kv..driver` stops RoadRunner startup with a `no such constructor was registered` error. The v5 driver skipped that storage instead. Check the driver name. Confirm that your RoadRunner binary includes the driver. + {% hint style="info" %} The `memory` and `boltdb` drivers do not require additional binaries and are available immediately, while the others require additional setup. Please see the appropriate documentation for installing [Redis Server](https://redis.io/) @@ -364,6 +366,8 @@ RoadRunner provides an RPC API, which allows you to manage key-value in your app The RPC API provides a set of methods that map to the available methods of the `Spiral\RoadRunner\KeyValue\Cache` class in PHP. +Each request must name a configured storage. In the v6 beta, the `memory`, `redis`, and `memcached` drivers reject empty item lists for `Has`, `MGet`, `Set`, and `Delete`. The `memory` and `redis` drivers also reject empty `TTL` requests. Skip empty batches in direct RPC clients. `Clear` needs a storage name but no items. + #### Has Checks for the presence of one or more keys in the specified storage. @@ -404,6 +408,8 @@ func (r *rpc) MGet(in *kvv1.Request, out *kvv1.Response) error {} Sets the expiration time for one or more keys in the specified storage. +The `timeout` field is an absolute RFC 3339 timestamp, not a duration in seconds. Read the [memory expiration warning](./memory.md#expiration) before sending deadlines near the current time. + {% code %} ```go diff --git a/kv/redis.md b/kv/redis.md index 8e0fa61..16dbab3 100644 --- a/kv/redis.md +++ b/kv/redis.md @@ -95,7 +95,7 @@ kv: # Default: false read_only: false - # Optional section. + # Optional. Remove this section for a non-TLS connection. tls: # Optional section. # Default: "" @@ -184,6 +184,8 @@ suffix". A value of `0` is equivalent to a timeout of 8 milliseconds (`8ms`). A `max_retry_backoff`: Maximum backoff between each retry. Must be in the format of a "numeric value" + "time format suffix". A value of `0` is equivalent to a timeout of 512 milliseconds (`512ms`). A value of `-1` disables backoff. +In the v6 beta, `min_retry_backoff` controls the minimum retry delay independently of `max_retry_backoff`. The v5 driver used the maximum value for both settings. Review both values before upgrading. + ### Pool Size `pool_size`: Maximum number of RoadRunner socket connections. A value of `0` is equivalent to a `10` connections per @@ -192,18 +194,15 @@ cores in your system, then setting the option to 2 you will get 16 connections. ### TLS Configuration -The `tls` section allows you to configure Transport Layer Security (TLS) for secure communication with the Redis server. -If no options are defined in this section, the connection will default to non-TLS. +The `tls` section enables TLS for the Redis connection. Omit the section for a non-TLS connection. In the v6 beta, a TLS configuration without `root_ca` uses the system trust store. + +`cert`: Path to the client certificate file. Set this with `key` when the server requires client authentication. -`cert`: Path to a file containing the client certificate. This certificate is used to authenticate the client -when communicating with the server. +`key`: Path to the client private key file. -`key`: Path to a file containing the client private key. This key is used in conjunction with the client -certificate for mutual authentication. +`root_ca`: Optional path to PEM-encoded CA certificates. The driver adds these certificates to the system trust store to verify the server certificate. This option does not require `cert` or `key`. -`root_ca`: Path to a file containing the Certificate Authority (CA) certificates used to verify the server's certificate. -**Note**: This option can be used independently of the `cert` and `key` options. In cases where the server does not -require client certificate verification, you only need to provide the `root_ca` option. +The v6 beta rejects a CA file that contains no valid PEM certificates. The v5 driver could proceed without TLS in this case. Check the CA file before deployment. An unreadable CA file also prevents startup. ### Other @@ -301,3 +300,7 @@ Where Sentinel's options means: - `sentinel_password`: Sentinel password from "requirepass `password`" (if enabled) in Sentinel configuration. + +## Expiration Errors + +In the v6 beta, `kv.MExpire` returns Redis command errors instead of ignoring them. Handle RPC errors before assuming that an expiration was set. A batch can update some keys before a later key fails. diff --git a/lab/applogger.md b/lab/applogger.md index 7e8a25d..21a947a 100644 --- a/lab/applogger.md +++ b/lab/applogger.md @@ -1,17 +1,10 @@ # Application logger -The RoadRunner server has a useful `app-logger` plugin that allows users to send logs from their applications to the -RoadRunner server using an RPC interface. This plugin is enabled by default and does not require any additional -configurations. It can be used to observe all application and server logs in one place. This is especially useful when -debugging and monitoring applications. - -{% hint style="info" %} -It will send raw messages to the RoadRunner `STDERR` -{% endhint %} +The app-logger plugin accepts log messages over RPC. Level-based methods use the `app` logger channel. The `log()` method writes directly to RoadRunner's standard error. ## Configuration -The `logs` section in the RoadRunner configuration file allows you to configure logging behavior for their application. +Configure the `app` channel in the `logs` section to select the output format and minimum level: {% code title=".rr.yaml" %} @@ -22,21 +15,20 @@ rpc: logs: channels: app: + mode: production level: info ``` {% endcode %} {% hint style="warning" %} -To interact with the RoadRunner app-logger plugin, you will need to have the RPC defined in the rpc configuration -section. You can refer to the documentation page [here](../php/rpc.md) to learn more about the configuration. +Configure [RPC](../php/rpc.md#configuration) to use app-logger. {% endhint %} -The `level` key is used to specify the logging level for this channel. This means that only log messages with a severity -level of info or higher will be sent to this channel. +This example emits JSON records at `info` level or higher and filters out `debug`. These settings do not control raw log calls. {% hint style="info" %} -Read more about logging in the [Logging β€” Logger](./logger.md) section. +See [Logger](./logger.md) for v6 formats, levels, and output destinations. {% endhint %} ## PHP client @@ -81,7 +73,7 @@ $logger->error('Houston, we have a problem!'); {% endcode %} {% hint style="info" %} -You can refer to the documentation page [here](../php/rpc.md) to learn more about creating the RPC connection. +See [RPC connections](../php/rpc.md) for connection setup. {% endhint %} ### Available methods @@ -92,75 +84,28 @@ You can refer to the documentation page [here](../php/rpc.md) to learn more abou - `warning(string): void`: Sends a warning log message to the server - `log(string): void`: Sends a log message directly to the `STDERR` of the server -## API - -### RPC API - -RoadRunner provides an RPC API, which allows you to manage app-logger in your applications using remote -procedure calls. The RPC API provides a set of methods that map to the available methods of -the `RoadRunner\Logger\Logger`class in PHP. - -{% hint style="info" %} -All methods accept a `string` (which will be log message) as a first argument and a `bool` placeholder for the second -arg. -{% endhint %} - -#### Error +### Raw Output -Method sends an `error` log message with the specified message to the RoadRunner server. +The raw RPC methods `app.Log` and `app.LogWithContext` bypass logger levels, formats, and output settings, including `logs.channels.app`. This differs from selecting the logger's `raw` mode, which still uses the configured level and destinations. -{% code %} - -```go -func (r *RPC) Error(in string, _ *bool) error {} -``` - -{% endcode %} - -#### Info +With app-logger v6, `app.Log` appends LF (`\n`) only when the message does not already end with LF. `app.LogWithContext` uses the same rule when there are no attributes. V5 wrote these messages without adding a line ending. Update consumers that depended on concatenated messages without line endings. -Method sends an `info` log message with the specified message to the RoadRunner server. +With attributes, `app.LogWithContext` writes the message, a space, comma-separated `key:value` pairs, and LF. It preserves newlines inside the message. Send the raw RPC message without a trailing newline if you need the attributes on the same line. V6 also retains the complete final attribute value instead of removing its last byte as v5 did. -{% code %} - -```go -func (r *RPC) Info(in string, _ *bool) error {} -``` - -{% endcode %} - -#### Warning - -Method sends a `warning` log message with the specified message to the RoadRunner server. - -{% code %} - -```go -func (r *RPC) Warning(in string, _ *bool) error {} -``` - -{% endcode %} - -#### Debug - -Method sends a `debug` log message with the specified message to the RoadRunner server. - -{% code %} - -```go -func (r *RPC) Debug(in string, _ *bool) error {} -``` - -{% endcode %} +## API -#### Log +### RPC API -Method sends a log message with the specified message directly to the `STDERR` of the RoadRunner server. +The string methods accept a message and a boolean reply placeholder. Call them by their registered RPC names: -{% code %} +| Method | Output | +| --- | --- | +| `app.Error` | Error-level record through the `app` logger. | +| `app.Info` | Info-level record through the `app` logger. | +| `app.Warning` | Warning-level record through the `app` logger. | +| `app.Debug` | Debug-level record through the `app` logger. | +| `app.Log` | Raw standard error output. | -```go -func (r *RPC) Log(in string, _ *bool) error {} -``` +Each method also has a `WithContext` variant, such as `app.InfoWithContext`. These methods accept `LogEntry` and a `Response` placeholder from `github.com/roadrunner-server/api-go/v6/applogger/v1`. The entry carries the message and `LogAttrs` key/value pairs. -{% endcode %} +The plugin's Go RPC receiver is no longer exported as `app.RPC`. Custom containers obtain it through `Plugin.RPC()`; PHP RPC method names are unchanged. diff --git a/lab/health.md b/lab/health.md index 4d15994..1dc38b0 100644 --- a/lab/health.md +++ b/lab/health.md @@ -27,11 +27,12 @@ can change the address to any IP address and port number of your choice. To access the health check, use the following URL: `http://127.0.0.1:2114/health`. This URL will return the health status of all plugins that are enabled and support health probes. To specify a particular plugin, you need to use the `plugin` query parameter: `http://127.0.0.1:2114/health?plugin=http`. In that case, the health status of the `http` plugin will be returned. {% hint style="info" %} -You can specify multiple plugins by separating them with a comma. For example, to check the health status of both the -http and grpc plugins, you can use the following URL: http://127.0.0.1:2114/health?plugin=http&plugin=grpc. +Repeat the `plugin` query parameter to check multiple plugins: `http://127.0.0.1:2114/health?plugin=http&plugin=grpc`. Names that do not support the check are skipped. {% endhint %} -The health check endpoint will return `HTTP 200` if there is at least one worker ready to serve requests. If there are no workers ready to service requests, the endpoint will return `HTTP 503` (or your unavailable status code, which can be set via configuration of the plugin). If there are any other errors, the endpoint will also return `HTTP 503` (or your unavailable status code). The `status` plugin also returns a payload with a list of checked plugins and errors, if any, in the following format: +The `/health` endpoint calls each selected plugin's health check. For worker-pool checks, an active worker can be busy with a request. Use `/ready` to check for idle workers. + +If a checked plugin reports a status of `500` or higher, the HTTP response uses `unavailable_status_code` (`503` by default). The JSON response lists the checked plugins and their reported status or errors: ```json [ @@ -52,9 +53,7 @@ The health check endpoint will return `HTTP 200` if there is at least one worker To access the readiness check, use the following URL: `http://127.0.0.1:2114/ready`. -The readiness check endpoint will return `HTTP 200` if there is at least one worker ready to take the request (i.e., not -currently busy with another request). If there is no worker ready or all workers are busy, the endpoint will return -`HTTP 503` status code (you can override this with the `unavailable_status_code` option). +For worker-pool checks, `/ready` returns `HTTP 200` when each checked plugin has at least one idle worker. If a checked pool has no ready workers, including when all workers are busy, the endpoint returns `unavailable_status_code` (`503` by default). Like the health check, you can target a specific plugin using the `plugin` query parameter: @@ -62,8 +61,7 @@ Like the health check, you can target a specific plugin using the `plugin` query - `http://127.0.0.1:2114/ready?plugin=grpc` {% hint style="info" %} -You can specify multiple plugins by separating them with a comma. For example: -`http://127.0.0.1:2114/ready?plugin=http&plugin=grpc`. +Repeat the `plugin` query parameter to check multiple plugins: `http://127.0.0.1:2114/ready?plugin=http&plugin=grpc`. {% endhint %} The response format is the same JSON structure as the `/health` endpoint. @@ -86,10 +84,15 @@ status: {% endcode %} +## Graceful Shutdown + +During graceful shutdown, `/health` returns `200`. The `/ready` and `/jobs` endpoints return `unavailable_status_code` (`503` by default). These responses contain the text `service is shutting down`, not the usual JSON report. + +Use `/health` for liveness and `/ready` for readiness. This lets the process finish its current work after readiness checks stop new traffic. + ## Check Timeout -The status plugin uses a timeout when checking the status of plugins. By default, this timeout is **60 seconds**. You -can customize it using the `check_timeout` option: +Set `check_timeout` to an integer number of seconds. The default is `60`. This value sets the status server's HTTP request and header read timeouts. It does not set a deadline for `Status()`, `Ready()`, or `JobsState()` execution. {% code title=".rr.yaml" %} @@ -98,7 +101,7 @@ version: "3" status: address: 127.0.0.1:2114 - check_timeout: 30s + check_timeout: 30 ``` {% endcode %} @@ -108,21 +111,33 @@ status: In addition to checking the health status of the workers, you can also examine the pipelines in the Jobs plugin using the following URL: http://127.0.0.1:2114/jobs -This URL will return the status of the pipelines in the Jobs plugin. The output will be in the following format: +This endpoint returns a JSON array of pipeline states: -```log -plugin: jobs: pipeline: test-1 | priority: 13 | ready: true | queue: test-1 | active: 0 | delayed: 0 | reserved: 0 | driver: memory | error: +```json +[ + { + "pipeline": "test-1", + "priority": 13, + "ready": true, + "queue": "test-1", + "active": 0, + "delayed": 0, + "reserved": 0, + "driver": "amqp", + "error_message": "" + } +] ``` +If the Jobs plugin is absent, `/jobs` returns `unavailable_status_code`. The handler passes the HTTP request context to `JobsState()` so the check can respond to request cancellation. + ## Use cases The health check endpoint serves the following purposes: ### Kubernetes Readiness and Liveness Probes -In Kubernetes, you can use readiness and liveness probes to check the health of your application. It can be used as a -readiness or liveness probe to ensure that your application is ready to serve requests. You can configure Kubernetes to -check the health check endpoint and take appropriate action if the endpoint returns an error. +Configure the liveness probe to use `/health` and the readiness probe to use `/ready`. Busy workers can fail readiness without failing liveness. During shutdown, readiness fails while liveness remains successful. **Read more [here](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)** diff --git a/lab/logger.md b/lab/logger.md index 5c810b2..b7db3de 100644 --- a/lab/logger.md +++ b/lab/logger.md @@ -1,11 +1,9 @@ # Logger -Logger Plugin is responsible for collecting logs from server plugins and PHP application workers' `STDERR` and -displaying them in the RoadRunner `STDERR`/`STDOUT`. It comes with a variety of options that allow you to customize the -way your application logs are collected and displayed. +The logger plugin writes logs from RoadRunner plugins and PHP workers to standard error, standard output, or files. The `logs` section controls the format, minimum level, and output destinations. {% hint style="info" %} -PHP workers are internally, mapped to a logger with an INFO log level severity. Use `channels` to map the `server` plugin (responsible for PHP workers) log level to at least `info`: +PHP worker standard error is logged at `info` level through the `server` channel. Set that channel to `info` or `debug` to retain worker output when the root level is higher: {% code title=".rr.yaml" %} @@ -13,11 +11,10 @@ PHP workers are internally, mapped to a logger with an INFO log level severity. version: "3" logs: - encoding: console # default value - level: error # mapped to all plugins - mode: "production" # mapped to all plugins + mode: production + level: error channels: - server: # mapped to only server plugin + server: mode: production level: info ``` @@ -39,39 +36,58 @@ logs: {% endcode %} -There are three available modes: +| Mode | Output | +| --- | --- | +| `production` | JSON records with `time`, `level`, `msg`, and structured attributes. | +| `development` | Key/value text without console colors. This is the default mode. | +| `raw` | The message only. Attributes and groups are discarded. | +| `off`, `none` | No output from this logger. Channel overrides can still enable their own output. | -1. `production` - This mode uses logger settings that are optimized for production usage. -2. `development` - This mode is enabled by default and is designed for use during application development. In - development mode, DPanicLevel logs panic, console colors are used, and logs are written to standard error. Sampling - is disabled, and stack traces are automatically included on logs of WarnLevel and above. -3. `raw` - This mode displays messages as raw output without any formatting. This mode is useful in production - environments where you need to parse logs programmatically. +Unknown modes use the same text format as `development`. -{% hint style="info" %} -Use `production` mode in production environments. It is optimized for production usage. -{% endhint %} +Logger plugin v6 production records use a `time` string in RFC3339 format with up to nanosecond precision. Level names use uppercase, such as `INFO`. In v5, production records used a numeric `ts` value in epoch nanoseconds and lowercase levels. Update log parsers for these changes. Development output also changes from the v5 colored console format to key/value text. + +Example v6 production record: + +```json +{"time":"2026-08-17T12:00:00.123Z","level":"INFO","msg":"worker output","logger":"server"} +``` ### Encoding -Logger supports two types of encoding, `console` and `json`. By default, `console` encoding is used, which outputs logs -in a friendly format. JSON encoding, on the other hand, returns messages in a JSON Structured logging format. This -format presents log messages as JSON objects with key-value pairs representing each log message field, making them more -machine-readable and easier to process programmatically. JSON encoding is also better suited for production usage. +V6 ignores the `encoding` setting. Remove it from the root and channel configurations. Use `mode: production` for JSON or `mode: development` for text. + +### Custom Format + +Use `format` to select a custom text format. It takes precedence over `mode`, except that `off` and `none` still disable the logger. The `time_format` setting uses a [Go time layout](https://pkg.go.dev/time#Layout). Its default is RFC3339, and it applies only to `%time%` in a custom format. {% code title=".rr.yaml" %} ```yaml logs: - encoding: console + level: info + format: "%time% [%level%] %logger% %message% %attrs%" + time_format: "2006-01-02 15:04:05" ``` {% endcode %} +| Placeholder | Value | +| --- | --- | +| `%time%` | Record time, using `time_format`. | +| `%level%` | Level name, such as `INFO`. | +| `%message%` | Log message. | +| `%logger%` | Logger name. | +| `%attrs%` | Attributes as space-separated `key=value` pairs. | +| `%source_file%` | Go source file path. | +| `%source_line%` | Go source line number. | +| `%source_func%` | Go function name. | + +Unknown placeholders remain unchanged. When `%logger%` is present, `%attrs%` omits the `logger` attribute. Custom formats do not escape message or attribute text. Use production mode when you need JSON encoding. + ### Level -The level is used to specify the logging level. This means that only log messages with a severity level will be sent to -this channel. Available levels include `panic`, `error`, `warn`, `info`, and `debug`. +The `level` setting is the minimum severity to emit. Supported values are `debug`, `info`, `warn`, and `error`. The value `warning` also selects `warn`. Values are case-insensitive. {% code title=".rr.yaml" %} @@ -83,13 +99,12 @@ logs: {% endcode %} {% hint style="info" %} -The default level is `debug`. +In v6, an empty or unknown level selects `debug`, including in production and raw channels. Values such as `panic`, `dpanic`, and `fatal` are not supported. Replace these v5 values with a supported level; they do not disable logging or fail configuration validation. {% endhint %} ### Output -By default, RoadRunner sends logs to `STDERR`. However, you can configure RoadRunner to send logs to `STDOUT` by using -the output key. +The logger writes to standard error by default. The `output` list accepts `stderr`, `stdout`, and file paths. The logger writes each enabled record to every destination in the list. Paths are file paths, not URLs. {% code title=".rr.yaml" %} @@ -102,35 +117,27 @@ logs: ### Error Output -You can configure a separate output destination for error-level logs. By default, error logs are sent to `STDERR`. - -{% code title=".rr.yaml" %} - -```yaml -logs: - error_output: [ stderr ] -``` - -{% endcode %} +All enabled levels of a logger use the same output destinations. V6 ignores `err_output`. In v5, this setting controlled internal logger errors, not error-level records. Remove it from your configuration. The `error_output` key is not supported. ### Line Endings -It allows configuring custom line endings for the logger. By default, the plugin uses `\n` as the line ending. Note that the `\n` is a forced default. This means that if the value is empty, RoadRunner will still use `\n`. So no empty line endings are allowed. +Custom formats append `\n` by default. Set `line_ending` to change it. To append nothing, set `skip_line_ending: true`. This takes precedence over `line_ending`. An empty `line_ending` without `skip_line_ending` still selects `\n`. {% code title=".rr.yaml" %} ```yaml logs: + format: "%message%" line_ending: "\r\n" ``` {% endcode %} +These settings require a nonempty `format`. Standard production and development handlers append `\n`. Standard raw mode appends `\n` only when the message does not already end with it. + ### Channels -In addition, you can configure each plugin log messages individually using the `channels` section. It allows you to -customize the logger settings for each plugin independently. You can disable logging for a particular plugin or change -its log mode and output destination. +Use `channels` to configure a plugin logger separately. A channel configuration replaces the root settings for that plugin. Omitted channel settings use their own defaults, not the root values. Plugins without a channel override use the root logger. {% code title=".rr.yaml" %} @@ -138,76 +145,38 @@ its log mode and output destination. version: "3" logs: - encoding: console # default value - level: info - mode: none # disable server logging. Also, `off` can be used. + mode: none channels: http: mode: production + level: info output: [ http.log ] ``` {% endcode %} -## File Logger - -It is possible to redirect channels or the entire log output to a file. To use the file logger, you need to set -the `file_logger_options.log_output` option to the filename where you want to write the logs. - -### Entire log - -{% code title=".rr.yaml" %} - -```yaml -logs: - mode: development - file_logger_options: - log_output: "test.log" - max_size: 10 - max_age: 24 - max_backups: 10 - compress: true -``` - -{% endcode %} +If a channel output cannot be opened, the logger reports the error through the root logger and uses the root settings for that channel. A disabled root logger hides this error. Check the root configuration when a channel file is missing. A root-output open error fails initialization. -### Channel +## File Output -You can also redirect a specific channel to a file. To do this, you need to specify the channel name in the `channels` +Use a file path in `output` to write logs to a file. The plugin creates the file if needed and appends to an existing file. Create its parent directory before starting RoadRunner. {% code title=".rr.yaml" %} ```yaml logs: - mode: development - level: debug - channels: - http: - file_logger_options: - log_output: "test.log" - max_size: 10 - max_age: 24 - max_backups: 10 - compress: true + mode: production + level: info + output: ["rr.log"] ``` {% endcode %} -### Available options +V6 removes `file_logger_options`. Replace the v5 `file_logger_options.log_output` setting with an entry in `output`. For a channel, use `logs.channels..output` as shown above. -1. `log_output`: Filename is the file to write logs to in the same directory. It uses `processname-lumberjack.log` in - `os.TempDir()` if empty. -2. `max_size`: is the maximum size in megabytes of the log file before it gets rotated. It defaults to 100 megabytes. -3. `max_age`: is the maximum number of days to retain old log files based on the timestamp encoded in their filename. - Note that a day is defined as 24 hours and may not exactly correspond to calendar days due to daylight savings, leap - seconds, etc. The default is not to remove old log files based on age. -4. `max_backups`: is the maximum number of old log files to retain. The default is to retain all old log files (though - MaxAge may still cause them to get deleted.) -5. `compress`: determines if the rotated log files should be compressed using gzip. The default is not to perform - compression. -6. `log_ending`: line ending to use in the logger. Default is new line - `\n`. +There is no built-in log rotation, backup retention, or compression in v6. The v5 `max_size`, `max_age`, `max_backups`, and `compress` settings have no effect. The plugin does not reopen files after external rotation. Send logs to stdout or stderr and let a service manager, container runtime, or log collector manage rotation. -### Startup logs +## Startup Logs {% code %} @@ -220,6 +189,8 @@ logs: These logs are not controlled by the logs configuration section. They are emitted directly by the RoadRunner core and can be turned off using the `-s` or `--silent` CLI option. -## ZapLogger +## Go Loggers + +V6 uses Go's [log/slog](https://pkg.go.dev/log/slog) instead of Zap. Named loggers return `*slog.Logger`. Use a `slog.Handler` for custom output. -Feel free to register your own [ZapLogger](https://github.com/uber-go/zap) extensions. +The plugin closes its root and channel file outputs during `Stop`. If you call `Config.BuildLogger()` directly, close each resource in the returned `BuildResult.Closers`. If you construct a `Log` with `NewLogger`, call `Log.Close()` to close its channel outputs. diff --git a/lab/metrics.md b/lab/metrics.md index d626153..d23274b 100644 --- a/lab/metrics.md +++ b/lab/metrics.md @@ -70,6 +70,10 @@ The HTTP metrics provided by the metrics plugin include: - `rr_http_uptime_seconds` - Plugin uptime in seconds. - `rr_http_no_free_workers_total` - Total number of NoFreeWorkers errors. +The request counter and duration histogram have a `status` label. In the v6 Prometheus middleware, a body write without an explicit HTTP status records `status="200"` instead of `status="-1"`. Update queries that use the old value. Metric names and label keys do not change. + +The duration histogram still includes downstream request execution. The shorter middleware spans described in [OpenTelemetry](otel.md) do not change histogram duration. + ### gRPC Metrics The gRPC metrics provided by the metrics plugin include: @@ -95,6 +99,8 @@ The JOBS metrics provided by the metrics plugin include: - `rr_jobs_jobs_err` - Number of jobs that failed while processing in the worker. - `rr_jobs_jobs_ok` - Number of successfully processed jobs. +- `rr_jobs_jobs_requeue` - Number of jobs requeued by worker responses. +- `rr_jobs_push_ok` - Number of successful job pushes. - `rr_jobs_push_err` - Number of jobs that failed to push. - `rr_jobs_push_latency` - Histogram that represents the latency for pushed operation. Available filters: driver, job ( pipeline), source. @@ -103,6 +109,10 @@ The JOBS metrics provided by the metrics plugin include: - `rr_jobs_push_latency_count` - Histogram that represents the latency for pushed operation and the number of processed jobs for the metric. Available filters: driver, job (pipeline), source. +The v6 Jobs plugin exports `rr_jobs_jobs_ok`, `rr_jobs_jobs_err`, `rr_jobs_push_ok`, and `rr_jobs_push_err` as counters instead of gauges. Their names do not change. Use `rate()` or `increase()` for dashboards that measure changes over time. + +Failed and requeued worker responses no longer increment `rr_jobs_jobs_ok`. They increment `rr_jobs_jobs_err` or the new `rr_jobs_jobs_requeue` counter instead. Update success-rate queries to keep these outcomes separate. The job outcome counters measure processing attempts, not unique jobs. + ### Temporal Metrics In temporal each SDK, has its own metrics - RoadRunner retransmits Go SDK metrics to the metrics storage. @@ -297,7 +307,7 @@ the `Spiral\RoadRunner\Metrics\Metrics` class in PHP. #### Add -Method is used to add a new metric to the declared collector. +Add a value to a declared gauge or counter. A negative counter increment returns an RPC error. Use a gauge for values that must decrease. {% code %} diff --git a/lab/otel.md b/lab/otel.md index eb54b90..38e13a7 100644 --- a/lab/otel.md +++ b/lab/otel.md @@ -1,10 +1,10 @@ # OpenTelemetry -RoadRunner offers OTEL (OpenTelemetry) plugin, which provides a unified standard for tracing, logging, and metrics -information. This plugin allows you to send tracing data from RoadRunner to tracing collectors -like [New Relic](https://newrelic.com/), [Zipkin](https://zipkin.io), [Jaeger](https://www.jaegertracing.io/), -[Datadog](https://www.datadoghq.com/), and more. -Starting with `v2023.3`, the `Jaeger` exporter is deprecated. Please use `OTLP` instead: [docs](https://www.jaegertracing.io/docs/1.49/architecture/). +The RoadRunner OpenTelemetry (OTEL) plugin exports tracing data to an OTLP receiver, standard output, or standard error. + +{% hint style="warning" %} +The v6 plugin rejects `exporter: zipkin`. The native Jaeger exporter is also unavailable. Use `exporter: otlp` and an OTLP receiver instead. A Zipkin `/api/v2/spans` endpoint cannot receive OTLP data. +{% endhint %} ![OpenTelemetry](https://user-images.githubusercontent.com/773481/213914208-cd944ca8-f218-4baf-8a54-5a4e42a1ed40.jpg) @@ -12,12 +12,7 @@ Starting with `v2023.3`, the `Jaeger` exporter is deprecated. Please use `OTLP` Read more about OpenTelemetry on the [official site](https://opentelemetry.io/). {% endhint %} -The OpenTelemetry plugin is designed to integrate with various tracing collectors to provide end-to-end tracing of -requests across multiple services. The plugin is built to support the OpenTelemetry standard, which provides a unified -way of collecting telemetry data across various languages and frameworks. - -The plugin supports tracing, logging, and metrics data, but currently only tracing information is stable and safe to use -in production. +This page describes trace export. For Prometheus metrics, see [Metrics](metrics.md). ## Configuration @@ -38,14 +33,17 @@ otel: insecure: true compress: false exporter: otlp + client: grpc endpoint: 127.0.0.1:4317 ``` + {% endcode %} {% hint style="info" %} -Note, that you may also use OTEL envs in the `OTEL` plugin configuration using [Shell-Parameter-Expansion](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html) syntax. For example: +You can use environment variables in the configuration with [shell parameter expansion](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html). This example leaves `client` and `endpoint` unset so the OTLP exporter can use its environment configuration. {% code title=".rr.yaml" %} + ```yaml version: "3" @@ -56,11 +54,12 @@ otel: service_version: "${OTEL_SERVICE_VERSION:-1.0.0}" insecure: "${OTEL_EXPORTER_OTLP_INSECURE:-true}" exporter: "${OTEL_TRACES_EXPORTER:-otlp}" - endpoint: "${OTEL_EXPORTER_OTLP_ENDPOINT:-127.0.0.1:4317}" - ``` + {% endcode %} +When `client` is unset, `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` takes precedence over `OTEL_EXPORTER_OTLP_PROTOCOL`. Supported values are `grpc` and `http/protobuf`. When `endpoint` is unset, the SDK reads its OTLP endpoint environment variables. For example, use `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf` with `OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318`. + {% endhint %} Once the plugin is activated, the `grpc` and `jobs` plugins will use the configuration to send tracing data to the @@ -73,28 +72,32 @@ collector. The `http` plugin requires the `otel` middleware to be added to the m ```yaml .rr.yaml http: address: 127.0.0.1:15389 - middleware: [ gzip, otel ] + middleware: [ otel, gzip ] ``` {% endcode %} +Requests pass through the middleware list from left to right. Put `otel` before the middleware you want to trace. In this example, the HTTP server span starts before `gzip` runs. + +Middleware spans with kind `Internal` measure the middleware's own work. They exclude downstream request time. The HTTP `Server` span covers the full request. + **The `otel` section of the configuration file contains the following options:** -| Option | Description | -|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **insecure** | a boolean that determines whether to use insecure endpoints (HTTP/HTTPS) or insecure gRPC. The default value is `false`. | | -| **compress** | a boolean that determines whether to use gzip to compress the spans. The default value is `false`. | -| **exporter** | a string that provides functionality to emit telemetry to consumers. Possible values are `otlp` (used for New Relic, Datadog, Jaeger), `zipkin`, `stdout` or `stderr`. The default value is `otlp`. | -| **custom_url** | a string that is used for the http client to override the default URL. The default value is `empty`. | -| **client** | a string that determines the client to send the spans. Possible values are http and grpc. The default value is `http`. | -| **endpoint** | a string that specifies the consumer's endpoint. The default value is `127.0.0.1:4318`. | -| **service_name** | a string that specifies the user's service name. The default value is `RoadRunner`. | -| **service_version** | a string that specifies the user's service version. The default value is `1.0.0`. | -| **headers** | a key-value map that contains user-defined headers. The `api-key` for New Relic should be here. | -| **resource** | a key-value map that contains OTEL resource (https://github.com/open-telemetry/opentelemetry-specification/blob/v1.25.0/specification/resource/semantic_conventions/README.md) | +| Option | Description | +| -------- | ------------- | +| **insecure** | Use an OTLP connection without TLS. The configuration default is `false`. | +| **compress** | Compress exported spans with gzip. The configuration default is `false`. | +| **exporter** | Trace exporter: `otlp`, `stdout`, or `stderr`. The default is `otlp`. | +| **custom_url** | Override the HTTP request path, for example `/v1/traces`. This option does not set the receiver address. | +| **client** | OTLP transport: `http` or `grpc`. If unset, the plugin checks the protocol environment variables and defaults to `http`. | +| **endpoint** | OTLP receiver address as `host:port`, without a scheme or path. If unset, the SDK uses its environment configuration or transport default. | +| **service_name** | Deprecated. Use `resource.service_name`. The default resource value is `RoadRunner`. | +| **service_version** | Deprecated. Use `resource.service_version`. The default resource value is `1.0.0`. | +| **headers** | Headers sent to the OTLP receiver, such as an `api-key` header. | +| **resource** | Service attributes: `service_name`, `service_version`, `service_namespace`, and `service_instance_id`. | {% hint style="warning" %} -The OpenTelementy OTLP endpoint can be used with 2 different ports: `4318` and `4317`, [docs](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/). In general, port `4317` is used for the `gRPC` traces (with the `gRPC` OTLP client). While port `4318` is used for the `http` OTLP client. Keep in mind that having a `gRPC` collector with an `http` endpoint will cause a send error. +Match `client` to the receiver protocol. OTLP normally uses port `4317` for `grpc` and port `4318` for `http`. For example, `client: http` requires an HTTP receiver such as `endpoint: 127.0.0.1:4318`. See the [OTLP exporter configuration](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/). {% endhint %} ## Collector @@ -136,7 +139,7 @@ Read more about the OpenTelemetry Collector on the [official site](https://opent The collector is started with the `otel-collector-config.yml` configuration file, which specifies how the collector should receive, process, and export the tracing data. -Here is an example configuration file that sends data to Zipkin and Datadog: +This configuration belongs to the Collector, not RoadRunner. RoadRunner sends OTLP data to the Collector. The Collector can then use its own exporters, including Zipkin when supported by the installed Collector distribution. {% code title="otel-collector-config.yml" %} diff --git a/php/auto-scaling.md b/php/auto-scaling.md index 56d2b9b..026c9ae 100644 --- a/php/auto-scaling.md +++ b/php/auto-scaling.md @@ -1,12 +1,12 @@ -# Auto worker scaling [BETA] +# Automatic worker scaling (beta) ## Beta notice -This feature is still in beta, errors are expected. Do not use it in production environments. + +Automatic scaling is in beta. Do not use it in production environments. This page describes [pool/v2 v2.0.0-beta.1](https://github.com/roadrunner-server/pool/tree/v2.0.0-beta.1), which is used by the v6 plugin beta. ## Introduction -This feature became available starting with the RoadRunner `2024.3` release. -Users can now scale their RoadRunner workers automatically, up to an additional 100 workers. +Automatic scaling has been available since RoadRunner `2024.3`. It adds workers when the pool cannot supply a free worker before the allocation timeout. It removes extra workers when allocation pressure stops. ### Supported plugins @@ -16,13 +16,25 @@ Users can now scale their RoadRunner workers automatically, up to an additional - This feature is unavailable when running RoadRunner in debug mode (`*.pool.debug=true`). - This feature does not scale Temporal workflow workers; only activity workers are scaled. +- The initial `num_workers` value cannot exceed 500. The combined base and additional worker count cannot exceed 2048. ### How it works -RoadRunner uses the `pool.allocate_timeout` option to determine when to start spawning additional workers. If no workers are available to handle the request before the timeout expires, RoadRunner begins dynamically allocating additional workers according to the `spawn_rate`. + +If no worker becomes free within `pool.allocate_timeout`, RoadRunner attempts to add up to `spawn_rate` workers. It does not exceed `max_workers` additional workers or the combined pool limit. + +Only one allocation batch can run at a time. Concurrent triggers do not each start a batch. The next batch can start after a one-second cooldown once the previous batch finishes. + +{% hint style="warning" %} +In beta.1, allocation does not retry the request that triggered it. That request can still fail with `NoFreeWorkers` after new workers are added. Applications must handle this failure. +{% endhint %} + +After an `idle_timeout` interval without allocation triggers, the allocator removes up to `spawn_rate` extra workers per tick. Recent allocation triggers postpone removal, including triggers rejected by the cooldown. This is not a separate idle timer for each worker. + +A removal attempt waits up to 500 ms for a free worker. If none becomes free, the allocator stops that removal batch and tries again on a later tick. It does not interrupt a busy worker to scale down. Stopping a selected worker can take longer than the 500 ms wait. ### Usage -Below is a configuration example demonstrating how to use this new feature: +Configure the allocator in the plugin's `pool` section: {% code title=".rr.yaml" %} @@ -55,7 +67,8 @@ logs: ### Configuration -The new `dynamic_allocator` section has been added to the `*.pool` configuration. It contains the following parameters: -- `max_workers` - the maximum number of workers that can be additionally spawned. -- `spawn_rate` - the number of workers that can be spawned per NoFreeWorkers error (but up to `max_workers`). -- `idle_timeout` - the time after which dynamically allocated workers are considered not needed and will be deallocated. +| Option | Meaning | Default and limit | +| --- | --- | --- | +| `max_workers` | Maximum additional worker count, not the total pool size. | Default: 10. Reduced if necessary so `num_workers + max_workers` does not exceed 2048. | +| `spawn_rate` | Maximum number of workers added per allocation batch or removed per idle tick. | Default: 5. Maximum: 100. | +| `idle_timeout` | Interval for idle checks and minimum time without allocation triggers before removal. | Default: `1m`. Values below `1s` use the default. | diff --git a/php/environment.md b/php/environment.md index 6215794..95e9727 100644 --- a/php/environment.md +++ b/php/environment.md @@ -64,16 +64,34 @@ server: In this example, when RoadRunner starts a PHP worker, it will set the `APP_RUNTIME` environment variable to `prod`. +Values in `server.env` override inherited environment values in the worker processes. They do not change the RoadRunner process environment. Use `server.on_init.env` separately for the [initialization command](../plugins/server.md#server-initialization). + {% hint style="warning" %} -All environment variable keys will be automatically converted to uppercase. +Keys in `server.env` are automatically converted to uppercase. {% endhint %} ## Dotenv -RoadRunner supports reading environment variables from `.env` files, which are typically used to store sensitive or -environment-specific variables outside your codebase. +Use the root `envfile` setting to load a file before environment variable expansion in the main configuration and included files: + +{% code title=".rr.yaml" %} + +```yaml +version: "3" + +envfile: env/.env + +logs: + level: ${RR_LOG_LEVEL:-info} +``` + +{% endcode %} + +With config plugin v6, `envfile` no longer requires experimental mode. Its path is relative to the main configuration file's directory, not the process working directory. In this example, a main configuration at `/var/www/.rr.yaml` loads `/var/www/env/.env`. + +The file supplies variables that are not already present in the RoadRunner process environment. It does not replace existing values. For example, an exported `RR_LOG_LEVEL=error` takes precedence over `RR_LOG_LEVEL=info` in the file. A missing or unreadable file stops startup. -To read environment variables from an `.env` file, you can use the `--dotenv` CLI option when starting RoadRunner. +The existing `--dotenv` CLI option also remains available: {% code %} diff --git a/php/manual-scaling.md b/php/manual-scaling.md index 1e44d13..0fdd458 100644 --- a/php/manual-scaling.md +++ b/php/manual-scaling.md @@ -2,18 +2,17 @@ ## Introduction -This feature became available starting from the RoadRunner `2023.3` release. -Users can now scale their RoadRunner workers dynamically via RPC. -A new class, `Spiral\RoadRunner\WorkerPool`, has been introduced to provide an easy interface to **add** or **remove** -workers from the RoadRunner workers pool. +Manual scaling has been available since RoadRunner `2023.3`. Use `Spiral\RoadRunner\WorkerPool` to add or remove workers through [Goridge RPC](rpc.md). The v6 plugin beta retains these PHP calls. ### Limitations - This feature is not available when running RoadRunner in debug mode (`pool.debug=true`). +- With `pool/v2 v2.0.0-beta.1`, a pool can hold at most 2048 workers. The initial `num_workers` value cannot exceed 500. +- A removal selects a free worker. It does not interrupt an active request or remove the last worker. ### Usage -Below is a brief example demonstrating how to use this new feature: +Add or remove a worker from the HTTP pool: {% code title="worker.php" %} @@ -37,5 +36,4 @@ $pool->removeWorker('http'); - `http`, `grpc`, `temporal`, `centrifuge`, `tcp`, `jobs`. -This provides developers with more control and flexibility over their RoadRunner setup, -allowing for better resource allocation based on the needs of their application. +For automatic allocation, see [automatic worker scaling](auto-scaling.md). diff --git a/php/pool.md b/php/pool.md index 64d15a3..2176ed1 100644 --- a/php/pool.md +++ b/php/pool.md @@ -1,19 +1,22 @@ # Worker pool -RoadRunner uses a worker pool to manage the PHP workers (PHP CLI processes). Internally, the worker pool consists of a worker watcher used to control the workersβ€”releasing, allocating, preventing zombie processes, resetting, destroyingβ€”and an internal stack responsible for manipulating (popping and pushing) already allocated workers. +RoadRunner uses a worker pool to start PHP CLI processes, assign work, and replace stopped workers. The pool also handles worker reset and shutdown. The worker pool is not used in every RoadRunner plugin but only in the `http`, `gRPC`, `tcp`, `roadrunner-temporal`, `jobs`, and `centrifuge` plugins. Additionally, the worker pool contains an internal `supervisor` to control the execution TTL of the workers, overall TTL, and execution time limits. -## Workers pool configuration: +## Worker pool configuration + +The v6 plugin beta uses [pool/v2 v2.0.0-beta.1](https://github.com/roadrunner-server/pool/tree/v2.0.0-beta.1). The configuration below describes that version. {% code title=".rr.yaml" %} ```yaml # Workers pool settings. pool: - # Debug mode for the pool. In this mode, the pool will not pre-allocate the worker. A worker (only 1; num_workers ignored) will be allocated right after a request arrives. + # Start a fresh worker for each request. Do not pre-allocate workers. + # In debug mode, num_workers is ignored. # # Default: false debug: false @@ -23,7 +26,7 @@ Additionally, the worker pool contains an internal `supervisor` to control the e # Default: empty command: "php my-super-app.php" - # How many worker processes will be started. Zero (or nothing) means the number of logical CPUs. + # Initial worker count, at most 500. Zero means the number of logical CPUs. # # Default: 0 num_workers: 0 @@ -34,7 +37,7 @@ Additionally, the worker pool contains an internal `supervisor` to control the e max_jobs: 0 # [2023.3.10] - # Maximum size of the internal requests queue. After reaching the limit, all additional requests would be rejected with error. + # Request admission limit. Concurrent requests can exceed this value. # # Default: 0 (no limit) max_queue_size: 0 @@ -44,7 +47,7 @@ Additionally, the worker pool contains an internal `supervisor` to control the e # Default: 60s allocate_timeout: 60s - # Timeout for the reset operation. Zero means 60s. + # Wait for active work before stopping workers during reset. Zero means 60s. # # Default: 60s reset_timeout: 60s @@ -54,12 +57,12 @@ Additionally, the worker pool contains an internal `supervisor` to control the e # Default: 60s stream_timeout: 60s - # Timeout for worker destroying before process killing. Zero means 60s. + # Wait for active work before stopping workers during shutdown. Zero means 60s. # # Default: 60s destroy_timeout: 60s - # Dynamic allocator settings. + # Dynamic allocator settings. Base and additional workers share a limit of 2048. # # Default: empty dynamic_allocator: @@ -99,14 +102,20 @@ Additionally, the worker pool contains an internal `supervisor` to control the e {% endcode %} -Tips and tricks: +## Timeouts and admission -{% hint style="info" %} -The worker pool has an internal queue that holds requests waiting for execution, which can be limited with the `pool.max_queue_size` option. -{% endhint %} +`allocate_timeout` limits worker allocation and the wait for a free worker. It does not limit PHP request execution. Set `supervisor.exec_ttl` to limit execution time. Without `exec_ttl`, canceling the caller's context does not stop normal pool execution in PHP. + +For a stream, `exec_ttl` applies separately to each read, not to the entire stream. `stream_timeout` applies to stream cancellation. + +`reset_timeout` and `destroy_timeout` limit the wait for active work before the pool starts stopping workers. They are not strict limits on the entire operation. A worker stop has its own ten-second grace period. + +`max_queue_size` checks the number of active pool `Exec` calls before another call is registered. This includes calls waiting for a worker and calls executing a request. Concurrent calls can pass the check together, so the value is not a strict queue-capacity guarantee. Zero disables the check. + +Tips and tricks: {% hint style="info" %} -Workers can be dynamically scaled: [link](auto-scaling.md) +See [automatic worker scaling](auto-scaling.md) for allocation batches, capacity limits, and timeout behavior. {% endhint %} {% hint style="info" %} diff --git a/php/rpc.md b/php/rpc.md index f3e01e6..210873a 100644 --- a/php/rpc.md +++ b/php/rpc.md @@ -22,6 +22,16 @@ composer require spiral/goridge {% endcode %} +## v6 compatibility + +The v6 plugin beta still uses Goridge with Go `net/rpc`. Keep the existing TCP or Unix listener address and `plugin.Method` calls. No Connect client migration is required. + +The Go transport, `goridge/v4 v4.0.0-beta.3`, no longer supports MessagePack. PHP clients that select a MessagePack codec must switch to a supported codec, even if their PHP Goridge version still offers MessagePack. The server supports JSON, protobuf, Gob, and raw bytes. Use JSON for JSON RPC arguments and protobuf for methods that accept protobuf DTOs. + +The Go module version does not change the frame protocol version. The base header, payload-length encoding, CRC calculation, and frame version remain compatible. This does not add a new frame-size limit or make MessagePack calls compatible. + +RoadRunner protobuf source, generated Go bindings, and Go plugin contracts now have separate repositories. This relocation alone does not require a PHP worker-loop rewrite. See [plugin migration](../customization/plugin.md#v6-migration) for the Go imports and DTO exceptions. + ## Configuration You can change the RPC port from the default (`127.0.0.1:6001`) using the following configuration: @@ -134,12 +144,13 @@ the RPC Go definitions for these plugins in the following repositories: You can use `Spiral\Goridge\RPC\AsyncRPCInterface` and an implementation with multiple relays to offer non-blocking I/O for RoadRunner communication. -The interface provides the following new methods: - - `callIgnoreResponse(string $method, mixed $payload): void` - Invoke the remote RoadRunner service method using the given payload (free form) non-blocking and ignore the response. - - `callAsync(string $method, mixed $payload): int` - Invokes the specified method with the specified payload and returns an integer identifier that can be used to retrieve the response when it's ready. - - `hasResponse(int $seq): bool, getResponse(int $seq, mixed $options = null): mixed` - - `hasResponses(array $seqs): array` - - `getResponses(array $seqs, mixed $options = null): iterable` - methods to check for and retrieve one or more results of executed requests. +The interface provides these methods: + +- `callIgnoreResponse(string $method, mixed $payload): void` - Invoke the method without waiting for its response. +- `callAsync(string $method, mixed $payload): int` - Invoke the method and return an identifier for its response. +- `hasResponse(int $seq): bool, getResponse(int $seq, mixed $options = null): mixed` +- `hasResponses(array $seqs): array` +- `getResponses(array $seqs, mixed $options = null): iterable` - Retrieve responses for the supplied identifiers. The `callIgnoreResponse` method can be used to invoke RPC methods without waiting for a response. If you don't need a response, this can greatly improve performance. For example, consider sending metric data. diff --git a/php/worker.md b/php/worker.md index 632c768..e8f9dd2 100644 --- a/php/worker.md +++ b/php/worker.md @@ -11,10 +11,10 @@ RoadRunner server. ### Worker types -RoadRunner provides several plugins that use workers to receive requests, -including [HTTP](https://github.com/roadrunner-php/http), [Jobs](https://github.com/roadrunner-php/jobs), -[Centrifuge](https://github.com/roadrunner-php/centrifugo), [gRPC](https://github.com/roadrunner-php/grpc), -[TCP](https://github.com/roadrunner-php/tcp), and [Temporal](https://legacy-documentation-sdks.temporal.io/php/workers). +RoadRunner provides worker plugins for [HTTP](https://github.com/roadrunner-php/http), [Jobs](https://github.com/roadrunner-php/jobs), [Centrifuge](https://github.com/roadrunner-php/centrifugo), [gRPC](https://github.com/roadrunner-php/grpc), and [Temporal](../workflow/worker.md). + +[TCP workers](../plugins/tcp.md) require a custom build that includes the TCP plugin. The default RoadRunner container no longer includes it. + You should choose the appropriate plugin based on the requirements of your application. In the examples below, we will explore the creation of an HTTP worker and a simple implementation of an entry point that can handle several types of requests. @@ -136,8 +136,7 @@ composer require spiral/roadrunner-http spiral/roadrunner-jobs nyholm/psr7 {% endcode %} -Let's start by creating an enum to enumerate the possible operating modes of RoadRunner. In this example, we will only -require the values **http** and **jobs**, but we will list all available modes: +The following enum lists worker modes. The `Tcp` mode requires a custom build with the TCP plugin. This example uses only `Http` and `Jobs`. {% code title="RoadRunnerMode.php" %} diff --git a/plugins/centrifuge.md b/plugins/centrifuge.md index 1b7f980..1f0c0a3 100644 --- a/plugins/centrifuge.md +++ b/plugins/centrifuge.md @@ -252,12 +252,18 @@ specifications and proxies these events to the PHP worker. To determine what proxy method was called inside the PHP, RR adds a `type` : `endpoint` metadata. For example, if the `Subscribe` method was called, RR will add `type`:`subscribe` metadata to the worker's context. +The proxy supports the unary events listed below. Unidirectional and bidirectional subscription streams are not implemented. + ### RPC You may also use RPC methods to communicate with Centrifugo server. RR follows the official [Centrifugo proto API](https://github.com/centrifugal/centrifugo/blob/master/internal/apiproto/api.proto). Official documentation available [here](https://centrifugal.dev/docs/server/server_api#grpc-api) +{% hint style="warning" %} +The v6 plugin no longer exposes `centrifuge.RateLimit`. Remove calls to this RPC before upgrading. The plugin does not provide a replacement method. +{% endhint %} + ### Proxy events With the incoming payload, RoadRunner also adds the type of the proxied request to the headers before sending it to the PHP worker. The key in the headers is called `type`. Here is the complete list of types supported by RoadRunner: @@ -268,8 +274,11 @@ With the incoming payload, RoadRunner also adds the type of the proxied request - `publish`: Publish proxy request. - `rpc`: RPC proxy request. - `subrefresh`: Subscription refresh proxy request. +- `notifycacheempty`: Notify cache empty proxy request (`NotifyCacheEmpty`). - `notifychannelstate`: Notify channel state proxy request. +Before enabling `NotifyCacheEmpty` in Centrifugo, verify that the PHP DTO package and worker request handler support this method. RoadRunner forwards the event with `type: notifycacheempty`; an older PHP client can reject the request type. + ## Metrics RoadRunner has a [metrics plugin](../lab/metrics.md) that provides metrics for the Centrifuge plugin, which can be used diff --git a/plugins/config.md b/plugins/config.md index 9748a9a..156d1cf 100644 --- a/plugins/config.md +++ b/plugins/config.md @@ -29,6 +29,12 @@ version: '3' Version numbers are strings, not numbers. For example, `version: "3"` is correct, but `version: 3` is not. {% endhint %} +## Environment Files + +The root `envfile` setting loads a file before the config plugin expands environment variables. With config plugin v6, it no longer requires experimental mode. A setting that was ignored without experimental mode in v5 is now active. + +Remove an unused `envfile` setting. If you need it, supply the file it names. A missing or unreadable file stops initialization. See [Dotenv](../php/environment.md#dotenv) for configuration examples, relative paths, and environment precedence. + ## Tips 1. By default, `.rr.yaml` used as the configuration, located in the same directory with RR binary. diff --git a/plugins/intro.md b/plugins/intro.md index 0304923..c4a2f31 100644 --- a/plugins/intro.md +++ b/plugins/intro.md @@ -18,13 +18,14 @@ development process. Some of the most notable plugins include: - [**Server**](./server.md): Core server functionality and lifecycle management. - [**Service**](./service.md): Start and monitor services like a supervisor. - [**Locks**](./locks.md): Distributed locking mechanisms for concurrency control. -- [**TCP**](./tcp.md): High-performance TCP server for custom networking solutions. - [**Metrics**](../lab/metrics.md): Application-level metrics and monitoring. - [**KV**](../kv/overview-kv.md): Key-value store interface for storage and retrieval of data. - [**Jobs**](../queues/overview-queues.md): Background job processing and management. - [**HealthChecks**](../lab/health.md): Health monitoring and reporting for system components. - [**OpenTelemetry (OTEL)**](../lab/otel.md): Distributed tracing and observability with OpenTelemetry integration. +The [TCP plugin](tcp.md) is no longer in the default RoadRunner container. Its reference applies to custom builds that explicitly include it. + ## Custom plugins In addition, RoadRunner encourages developers to create their own custom plugins, tailored to meet specific requirements diff --git a/plugins/locks.md b/plugins/locks.md index 201b995..e66c650 100644 --- a/plugins/locks.md +++ b/plugins/locks.md @@ -200,7 +200,7 @@ To make it easy to use the Lock proto API in PHP, we provide a [GitHub repository](https://github.com/roadrunner-php/roadrunner-api-dto), that contains all the generated PHP DTO classes proto files, making it easy to work with these files in your PHP application. -- [API](https://buf.build/roadrunner-server/api/file/main:lock/v1beta1/lock.proto) +- [Lock protobuf API](https://github.com/roadrunner-server/api/blob/25217e9/roadrunner/api/lock/v1/lock.proto) ### RPC API diff --git a/plugins/server.md b/plugins/server.md index 115b24d..b10a0b7 100644 --- a/plugins/server.md +++ b/plugins/server.md @@ -25,17 +25,15 @@ server: # Default: "" user: "" - # Script execute timeout - # - # Default: 60s [60m, 60h], if used w/o units its means - NANOSECONDS. + # Timeout after the command starts. Default: 60s. Include a unit. exec_timeout: 20s - # Environment variables for the worker processes. + # Environment variables for the initialization command. # # Default: env: - - SOME_KEY: "SOME_VALUE" - - SOME_KEY2: "SOME_VALUE2" + SOME_KEY: "SOME_VALUE" + SOME_KEY2: "SOME_VALUE2" # Exit RR if the `on_init` command fails or exceeds the `exec_timeout`. exit_on_error: false @@ -50,17 +48,12 @@ server: # Default: "" user: "" - # Group name (not GID) for the worker processes. An empty value means to use the RR process user. - # - # Default: "" - group: "" - # Environment variables for the worker processes. # # Default: env: - - SOME_KEY: "SOME_VALUE" - - SOME_KEY2: "SOME_VALUE2" + SOME_KEY: "SOME_VALUE" + SOME_KEY2: "SOME_VALUE2" relay: pipes ``` @@ -84,10 +77,12 @@ The `on_init` section is used for application initialization or warming up befor a command script that will be executed before starting the workers. You can also set environment variables to pass to this script. +With server plugin v6, `on_init.env` values override inherited process environment values. In v5, inherited values took precedence for this command. Check for conflicting variable names when upgrading. `server.env` does not configure the initialization command. + +The `on_init.exec_timeout` interval starts after the command starts successfully. Use a duration with a unit, such as `20s`. + {% hint style="info" %} -If the `on_init` command fails (i.e., returns a non-zero exit code), RoadRunner will log the error but continue -execution. This ensures that a failure during initialization does not interrupt the application's operation. -Use `on_init.exit_on_error: true` to stop RoadRunner if `on_init` command fails or exceeds the `exec_timeout`. +By default, RoadRunner logs an `on_init` command error and continues startup. Set `on_init.exit_on_error: true` to stop RoadRunner if the command fails or exceeds `exec_timeout`. {% endhint %} ### Worker starting command @@ -98,21 +93,30 @@ The `server.command` option is required and is used to start the worker pool for This option can be overridden by plugins with a pool section, such as the `http.pool.command`, or in general `.pool.command`. {% endhint %} -The `user` and `group` options allow you to set the user and group that will start and own the worker process. This -feature provides an additional layer of security and control over the application's execution environment. +In v6, a scalar command or a one-element sequence is split at whitespace. Repeated spaces and tabs do not create empty arguments. This is not shell parsing: quotes inside a scalar do not keep words in one argument. Use a sequence with one element for the executable and one for each argument when an argument contains spaces: -{% hint style="info" %} -An empty value means to use the RoadRunner process user. -{% endhint %} +{% code title=".rr.yaml" %} + +```yaml +server: + command: ["php", "worker.php", "--label", "my worker"] +``` + +{% endcode %} + +The same argument rules apply to `server.on_init.command` and pool command overrides. + +### Worker User + +Set `server.user` to an account name, not a numeric UID. An empty value keeps the RoadRunner process user. The server plugin resolves the selected account's UID and GID during initialization. `server.group` does not override that GID. + +In v6, a failed account lookup or an invalid numeric UID or GID stops initialization. Correct the account name before restarting RoadRunner. A nonempty `server.user` is not supported on Windows and also stops initialization; remove that setting on Windows. {% hint style="warning" %} -RoadRunner must be started from the root user. Root access is needed only to fork the process under a -different user. Once the worker process is started, it will run with the specified user and group permissions, -providing a secure and controlled execution environment for the application. All temporary files (`http` for example) -would be created with the provided user/group +On Unix, RoadRunner needs permission to change process credentials when `server.user` selects a different account. {% endhint %} -The `env` option allows you to set environment variables to pass to the worker script. +Use `server.env` to set [worker environment variables](../php/environment.md#setting-environment-variables). ## PHP Client diff --git a/plugins/service.md b/plugins/service.md index 176095f..bcff17b 100644 --- a/plugins/service.md +++ b/plugins/service.md @@ -37,7 +37,7 @@ service: remain_after_exit: true service_name_in_log: false env: - - foo: "BAR" + foo: "BAR" restart_sec: 1 ``` @@ -57,11 +57,14 @@ The following are the available configuration settings for each service: | **timeout_stop_sec** | The maximum allowed time to wait for the process to stop. | | **remain_after_exit** | If set to `true`, the process will remain after exit. For example, if you need to restart the process every 10 seconds, exec_timeout should be set to `10s`, and `remain_after_exit` should be set to `true`. Note that if you kill the process from outside and `remain_after_exit` is `true`, the process will be restarted. | | restart_sec | The delay between process stop and restart. The default value is 30 seconds. | -| service_name_in_log | If set to `true`, the service name will be shown in the log in the form `%plugin%.%service_name%`. The default value is `false`. | +| service_name_in_log | If `true`, adds a `service` log attribute with the service name. The logger name remains `service`. The default is `false`. | | env | Environment variables to pass to the underlying process from the config. | | user | Username (not UID) for the Service process. An empty value means to use the RR process user. | + Services will be started when RoadRunner starts and will be stopped when RoadRunner stops. +Service plugin v5 used `service_name_in_log` to change the logger name to `service.NAME`. In v6, update log filters to use the separate `service` attribute. Use production mode or a custom [logger format](../lab/logger.md#custom-format) with `%attrs%` to retain that attribute; raw mode discards it. + ## PHP client The RoadRunner Service Plugin PHP Client Library allows you to manage processes in PHP application using the Service @@ -150,6 +153,12 @@ try { To restart a service, use the `restart` method: +In v6, restart calls stop for each old process before starting replacements. It is not a rolling or atomic restart. Plan for an interval with no running process in that service. + +{% hint style="warning" %} +In `v6.0.0-beta.8`, a service with `remain_after_exit: true` can start replacements before old processes finish if an automatic restart occurred earlier. Do not rely on `service.Restart` for exclusive process replacement in this case. +{% endhint %} + {% code title="app.php" %} ```php @@ -168,6 +177,8 @@ try { {% endcode %} +If a replacement fails to start, RoadRunner requests a stop for replacements that already started and removes the service from its registry. It does not restore the old processes. Fix the reported startup error. Then call `create` with the required service settings. Another `restart` call cannot recover a service that is no longer registered. + #### Terminating a Service To terminate a service, use the `terminate` method: @@ -223,4 +234,4 @@ To make it easy to use the Service proto API in PHP, we provide a [GitHub repository](https://github.com/roadrunner-php/roadrunner-api-dto), that contains all the generated PHP DTO classes proto files, making it easy to work with these files in your PHP application. -- [API](https://github.com/roadrunner-server/api/blob/master/proto/service/v1/service.proto) +- [Service protobuf API](https://github.com/roadrunner-server/api/blob/25217e9/roadrunner/api/service/v1/service.proto) diff --git a/plugins/tcp.md b/plugins/tcp.md index 73d2b60..f449fbb 100644 --- a/plugins/tcp.md +++ b/plugins/tcp.md @@ -1,5 +1,11 @@ # TCP +{% hint style="warning" %} +The default RoadRunner build with v6 plugins no longer includes the TCP plugin. The [TCP plugin repository](https://github.com/roadrunner-server/tcp) remains available. This removal does not affect `tcp://` transport for RPC or worker relays. +{% endhint %} + +This page is a reference for [custom builds](../customization/build.md) that explicitly include a compatible TCP plugin. A `tcp` configuration section does not load a missing plugin. Before upgrading a TCP workload, verify that the target binary includes the plugin and starts the required listeners. + The RoadRunner TCP plugin helps you handle TCP requests. You can use this plugin to make your own servers like an SMTP server, and send TCP requests directly to PHP workers for handling. diff --git a/queues/amqp.md b/queues/amqp.md index bf632ad..c8e2c76 100644 --- a/queues/amqp.md +++ b/queues/amqp.md @@ -14,25 +14,26 @@ To install and configure the RabbitMQ, use the corresponding [documentation page](https://www.rabbitmq.com/download.html). {% hint style="info" %} -Every message pushed to the RabbitMQ server uses publisher confirms. A message is considered sent only after the server confirms it. This is a reliable way to ensure delivery to the server. +Immediate publishes wait for a publisher confirm from RabbitMQ. Delayed publishes do not wait for that confirmation before returning. A publisher confirm does not prove that a message reached a queue. {% endhint %} - After that, you should configure the connection to the server in the `amqp` section. This configuration section contains exactly one `addr` key with a [connection DSN](https://www.rabbitmq.com/uri-spec.html). The `TLS` configuration sits in the `amqp.tls` section and consists of the following options: - `key`: path to a key file. - `cert`: path to a certificate file. - `root_ca`: path to Root CAs used by the AMQP client to trust and verify the broker/server certificate during TLS dial. -- `client_auth_type`: also known as `mTLS`. Possible values are: `no_client_cert`, `request_client_cert`, `require_any_client_cert`, `verify_client_cert_if_given`, `require_and_verify_client_cert`. +- `client_auth_type`: possible values are `no_client_cert`, `request_client_cert`, `require_any_client_cert`, `verify_client_cert_if_given`, `require_and_verify_client_cert`. This setting does not control broker certificate verification. You should also configure `rabbitMQ` with `TLS` support: [link](https://www.rabbitmq.com/ssl.html). +In the AMQP v6 beta line (`v6.0.0-beta.9`), `root_ca` adds trust roots for broker certificate verification. Reconnects reuse the configured TLS settings. Use an `amqps://` address with valid `key` and `cert` files. A `root_ca`-only TLS block is not supported. Omit `tls` when using a plain `amqp://` connection. + {% code title=".rr.yaml" %} ```yaml amqp: - addr: amqp://guest:guest@127.0.0.1:5672 + addr: amqps://guest:guest@rabbitmq.example.com:5671/ # AMQPS TLS configuration # @@ -41,19 +42,19 @@ amqp: # Path to the key file # # This option is required - key: "" + key: /etc/rr/tls/client.key # Path to the certificate # # This option is required - cert: "" + cert: /etc/rr/tls/client.crt # Path to Root CAs used by the AMQP client to trust and verify the broker/server certificate during TLS dial. # # This option is optional - root_ca: "" + root_ca: /etc/rr/tls/ca.crt - # Client auth type (mTLS, peer verification). + # Legacy client_auth_type setting. # # This option is optional. Default value: no_client_cert. Possible values: no_client_cert, request_client_cert, require_any_client_cert, verify_client_cert_if_given, require_and_verify_client_cert client_auth_type: no_client_cert @@ -66,14 +67,14 @@ the queue settings, including those specific to AMQP. ## Pipeline Configuration Formats -AMQP pipeline configuration is selected by `jobs.pipelines..config.version`: +These versioned formats apply to the AMQP v6 beta line (`v6.0.0-beta.9`). AMQP v5 uses the legacy flat format. In v6 beta, `jobs.pipelines..config.version` selects the parser: - `0`, `1`, or omitted: legacy flat parser. - `2`: nested parser with `exchange` and `queue` sections (recommended). ## Version 2 Configuration (Recommended) -Set `jobs.pipelines..config.version: 2` and define `exchange` and `queue` sections. +Set `jobs.pipelines..config.version: 2`. At least one of the nested `exchange` or `queue` sections is required. Missing sections receive default values. ### Options in `config` @@ -81,6 +82,8 @@ Set `jobs.pipelines..config.version: 2` and define `exchange` and `queue` - `prefetch`: RabbitMQ QoS prefetch. Default: `10`. - `redial_timeout`: reconnect timeout in seconds. Default: `60`. +Zero or negative values for these three options use their defaults. + ### Exchange settings - `name`: exchange name. Default: `amqp.default`. @@ -112,7 +115,7 @@ Producer-only pipeline: `queue.name` can be empty, `push` works, but `run`, `res {% endhint %} {% hint style="info" %} -If `exchange.type` is not `fanout`, `queue.routing_key` must be set. +If `exchange.type` is not `fanout`, `queue.routing_key` must be set when RR initializes or declares the pipeline. This also applies to consume-only pipelines and pipelines with declarations disabled. {% endhint %} {% hint style="info" %} @@ -126,11 +129,6 @@ version: "3" amqp: addr: amqp://guest:guest@127.0.0.1:5672/ - tls: - key: "" - cert: "" - root_ca: "" - client_auth_type: no_client_cert jobs: pipelines: @@ -166,12 +164,16 @@ jobs: ## Read-only RabbitMQ Permissions -If the RabbitMQ user does not have `configure` permission, disable declarations in pipeline configuration: +In AMQP v6 beta, use `config.version: 2` to disable declarations when the RabbitMQ user has no `configure` permission: - `jobs.pipelines..config.exchange.declare: false` - `jobs.pipelines..config.queue.declare: false` -This avoids startup declaration failures and passive queue-check failures in restricted environments. +Create the exchange, queue and binding before starting RR. The broker user still needs `read` permission to consume and `write` permission to publish. Keep `queue.delete_on_stop: false` to avoid a queue deletion request. + +With `queue.declare: false`, RR skips both queue binding and passive queue inspection. `jobs.Stat` reports `active: 0` without querying queue depth. This does not mean the queue is empty. + +Delayed publishing and delayed requeue still declare and bind temporary queues. These operations still require `configure` permission. {% code title=".rr.yaml" %} @@ -208,10 +210,10 @@ jobs: Dynamic pipeline declaration over RPC remains flat. It does not use nested `config.exchange` / `config.queue` sections. -Declaration control keys in `jobs.Declare` payload: +In AMQP v6 beta, the `jobs.Declare` payload accepts these declaration controls as strings: -- `exchange_declare` -- `queue_declare` +- `exchange_declare`: `"true"` (default) or `"false"`. +- `queue_declare`: `"true"` (default) or `"false"`. ## Legacy Flat Configuration (`version: 0|1`) @@ -276,14 +278,31 @@ In legacy format, `queue` can be omitted for push-only pipelines, but `run`, `re ## Migration -- Prefer `config.version: 2` with nested `exchange` and `queue` sections for new configurations. -- Existing flat configurations remain compatible through `config.version: 0`, `config.version: 1`, or omitted `config.version`. -- For restricted RabbitMQ permissions, set `exchange.declare: false` and `queue.declare: false`. +When moving from AMQP v5 to v6 beta, existing flat configurations can keep `config.version` omitted or set it to `0` or `1`. Both formats now reject exchange types outside `direct`, `fanout`, `topic` and `headers`. Non-fanout exchanges require a routing key during pipeline initialization, not only on push. + +To use version 2, set `config.version: 2` and move the entity settings below. All paths are relative to `jobs.pipelines..config`. + +| Legacy Flat Key | Version 2 Key | +| --- | --- | +| `exchange` | `exchange.name` | +| `exchange_type` | `exchange.type` | +| `exchange_durable` | `exchange.durable` | +| `exchange_auto_delete` | `exchange.auto_delete` | +| `queue` | `queue.name` | +| `durable` | `queue.durable` | +| `queue_auto_delete` | `queue.auto_delete` | +| `delete_queue_on_stop` | `queue.delete_on_stop` | +| `queue_headers` | `queue.headers` | +| `routing_key`, `exclusive`, `consumer_id`, `multiple_ack`, `requeue_on_fail` | Same key under `queue` | + +Keep `priority`, `prefetch` and `redial_timeout` directly in `config`. Do not mix flat and nested entity settings. Version 2 ignores legacy flat entity settings; for example, a flat `durable: true` does not set `queue.durable`. + +For restricted RabbitMQ permissions, set `exchange.declare: false` and `queue.declare: false`. Review the [declaration limitations](#read-only-rabbitmq-permissions) before using delayed jobs. ## What's Next? 1. [Queues and Jobs overview](overview-queues.md) - Review the full jobs pipeline model before configuring AMQP in production. 2. [Read-only RabbitMQ permissions](https://www.rabbitmq.com/docs/access-control) - See declaration flags for restricted users and review the Runtime / RPC (`jobs.Declare`) section on this page for flat declaration keys. 3. [Pipeline configuration formats](../intro/config.md) - Confirm when to use `config.version: 2` versus legacy `0|1`, and review the general RoadRunner configuration structure. -4. [Exchange settings](kafka.md) and [Queue settings](sqs.md) - Compare exchange and queue responsibilities when tuning routing and consumption behavior. +4. [Exchange settings](#exchange-settings) and [Queue settings](#queue-settings) - Review routing and consumption settings. 5. [Allocate Timeout](../known-issues/allocate-timeout.md) and [CRC validation failed](../known-issues/stdout-crc.md) - Use these troubleshooting references when workers fail to process queue jobs as expected. diff --git a/queues/beanstalk.md b/queues/beanstalk.md index a83daf9..16d736e 100644 --- a/queues/beanstalk.md +++ b/queues/beanstalk.md @@ -78,3 +78,11 @@ value should not exceed `int32` size. ### Tube `tube` - The name of the inner "tube" specific to the Beanstalk driver. + +## Headers + +The v6 beta stores headers with the job body and preserves them on delivery and requeue. Upgrade producers and consumers before relying on headers for retry counts or tracing. Existing queued jobs need no conversion, but headers that v5 did not store cannot be recovered. + +## Statistics + +In the v6 beta, queue counters describe the configured tube, not all tubes on the server. An unused tube reports zero jobs. Review dashboards that assumed server-wide counts in v5. Pipelines that share a tube report the same tube counters. diff --git a/queues/boltdb.md b/queues/boltdb.md index 03b5834..0902808 100644 --- a/queues/boltdb.md +++ b/queues/boltdb.md @@ -7,6 +7,10 @@ Data in this driver is stored in the boltdb database file. You can't use the sam pipelines or for the for KV plugin and Jobs plugin. This is a boltdb limitation on simultaneous access of 2 processes to the same file. +{% hint style="warning" %} +In `v6.0.0-beta.5`, stored jobs do not retain application headers, including the `pool` routing header. Use a single `jobs.pool`, not [named worker pools](overview-queues.md#named-worker-pools-v6-beta), when consuming BoltDB pipelines with jobs `v6.0.0-beta.10`. This restriction applies to new jobs as well as existing data. +{% endhint %} + ## Configuration {% code title=".rr.yaml" %} @@ -71,3 +75,9 @@ Default: `rr.db`. ### Permissions `permissions` - Permissions for the boltdb database file. Default: `0755`. + +## Recovery + +Persisted jobs that were not acknowledged can be delivered again after a restart. This behavior also exists in v5. Workers must tolerate repeated deliveries. + +Keep the same database file when upgrading if you need to retain queued jobs. Stop RoadRunner before backing up the file. The v6 beta reads the existing v5 job format; no conversion is required. diff --git a/queues/google-pub-sub.md b/queues/google-pub-sub.md index f795f1f..6cd2822 100644 --- a/queues/google-pub-sub.md +++ b/queues/google-pub-sub.md @@ -41,7 +41,7 @@ jobs: project_id: test topic: rrTopic1 dead_letter_topic: "dead-letter-topic" - max_delivery_attempts: 3 + max_delivery_attempts: 10 ``` {% endcode %} @@ -66,14 +66,24 @@ from `pipe1` have been processed. ### Dead letter topic -`dead_letter_topic` - optional, string. Should be used with `max_delivery_attempts`. If the Pub/Sub service attempts to deliver a message but the subscriber can't acknowledge it, Pub/Sub can forward the undeliverable message to a dead-letter topic. For more information, see: [link](https://cloud.google.com/pubsub/docs/handling-failures#dead_letter_topic) +`dead_letter_topic`: Optional topic ID. Use it with `max_delivery_attempts` to configure forwarding of messages that cannot be acknowledged. See [Pub/Sub dead-letter topics](https://cloud.google.com/pubsub/docs/handling-failures#dead_letter_topic). ### Max delivery attempts -`max_delivery_attempts` - optional, int. Should be used with `dead_letter_topic`. The maximum number of delivery attempts for a message. For more information, see: [link](https://cloud.google.com/pubsub/docs/handling-failures#dead_letter_topic) +`max_delivery_attempts`: Optional delivery-attempt setting for dead-letter forwarding. RoadRunner defaults to `10` when `dead_letter_topic` is set. See [Pub/Sub dead-letter configuration](https://cloud.google.com/pubsub/docs/handling-failures#dead_letter_topic). + +## Subscriptions + +The v6 beta can reuse existing topics and subscriptions. The pipeline name is the subscription ID. Keep the project, topic, and pipeline names unchanged during an upgrade to use the same subscription. + +The v6 beta applies `dead_letter_topic` and `max_delivery_attempts` when it creates subscriptions for both YAML pipelines and dynamically declared pipelines. These settings do not update an existing subscription. Use Pub/Sub administration to change an existing subscription's dead-letter policy. Do not delete a subscription with pending messages just to apply new settings. + +## Pause and Resume + +In the v6 beta, pausing a pipeline cancels its receive operation and stops new message pulls. Publishing remains available while paused. Jobs already received by RoadRunner can still complete. Resume starts receiving from the same subscription again. ## Limitations 1. TLS is not supported at the moment. -2. Metrics are not supported at the moment. +2. Use Google Pub/Sub monitoring for queue counts. In the v6 beta, RR statistics return pipeline identity only. Zero counts and `ready: false` do not report the broker's actual state. 3. Telemetry and Authentication are not supported at the moment. Use `insecure: true` to test this driver. diff --git a/queues/kafka.md b/queues/kafka.md index 1fc96c3..996907c 100644 --- a/queues/kafka.md +++ b/queues/kafka.md @@ -14,6 +14,8 @@ Version `2023.2.0` update: ## Configuration +For direct partition consumption, use the [v6 beta example](#direct-partitions-v6-beta) instead of the topics and group configuration below. + {% code title=".rr.yaml" %} ```yaml @@ -231,7 +233,8 @@ jobs: # topics: adds topics to consume # - # Default: empty (produces an error); possible to use regexp if `consume_regexp` is set to true. + # Default: empty. Required unless consume_partitions is set. + # Regex is supported when consume_regexp is true. topics: [ "foo", "bar", "^[a-zA-Z0-9._-]+$" ] # consume_regexp sets the client to parse all topics passed to `topics` as regular expressions. @@ -257,35 +260,6 @@ jobs: # Optional, default: 1. min_fetch_message_size: 1 - # consume_partitions sets partitions to consume from directly and the offsets to start consuming those partitions from. - # This option is basically a way to explicitly consume from subsets of partitions in topics, or to consume at exact offsets. - # - # NOTE: This option is not compatible with group consuming and regex consuming. - # - # Optional, default: null - consume_partitions: - - # Topic for consume_partitions - # - # At least one topic is required. - foo: - - # Partition for the topic. - # - # At least one partition is required. - 0: - - # Partition offset. - # - # Required if all options are used. No default; error on empty. - # Possible values: AtEnd, At, AfterMilli, AtStart, Relative, WithEpoch - type: AtStart - - # Value for the At, AfterMilli, Relative, and WithEpoch offsets. - # - # Optional, default: 0. - value: 1 - # consumer_offset sets the offset to start consuming from, or, if OffsetOutOfRange is seen while fetching, # to restart consuming from. # @@ -326,3 +300,43 @@ jobs: ``` {% endcode %} + +## Direct Partitions (v6 Beta) + +The Kafka v6 beta line (`v6.0.0-beta.7`) applies `consumer_options.consume_partitions` to the client. Kafka `v5.2.5` accepted this setting but did not apply it. + +A nonempty `consumer_options.topics` list takes precedence: RR ignores `consume_partitions` in that case. For direct partition consumption, omit `topics` and `group_options`. Leave `consume_regexp` unset or `false`. + +This example consumes partition `0` of the existing `orders` topic, starting at offset `100`: + +{% code title=".rr.yaml" %} + +```yaml +version: "3" + +server: + command: php consumer.php + relay: pipes + +kafka: + brokers: ["127.0.0.1:9092"] + +jobs: + pool: + num_workers: 2 + consume: ["orders"] + pipelines: + orders: + driver: kafka + config: + consumer_options: + consume_partitions: + orders: + 0: + type: At + value: 100 +``` + +{% endcode %} + +Each topic maps partition numbers to offsets. The offset `type` is required: `At`, `AfterMilli`, `AtEnd`, `AtStart`, `Relative` or `WithEpoch`. The `value` defaults to `0` and is used by `At`, `AfterMilli`, `Relative` and `WithEpoch`. diff --git a/queues/nats.md b/queues/nats.md index 9560397..99097a4 100644 --- a/queues/nats.md +++ b/queues/nats.md @@ -43,10 +43,12 @@ jobs: # The consumer will only start receiving messages that were created after the consumer was created # Default: false (deliver all messages from the stream beginning) + # Not applied in v6.0.0-beta.5. deliver_new: true # Consumer rate-limiter in bytes https://docs.nats.io/jetstream/concepts/consumers#ratelimit # Default: 1000 + # Not applied in v6.0.0-beta.5. rate_limit: 100 # Delete the stream after the pipeline is stopped @@ -56,11 +58,12 @@ jobs: # Delete message from the stream after successful acknowledge # Default: false delete_after_ack: false - - # Time in seconds after which NATS will redeliver the message if no ACK received - # Default: 30 - ack_wait: 30 + + # v6 beta: time before redelivery if no acknowledgment is received. + # Default: 30s + ack_wait: 30s ``` + {% endcode %} ## Configuration options @@ -83,13 +86,21 @@ To prevent duplicate message consumption, ensure that each pipeline is configure `deliver_new` - the consumer will only start receiving messages that were created after the consumer was created. +In NATS `v6.0.0-beta.5`, this setting is parsed but not applied to the consumer. It does not prevent replay after a consumer restart. + ### Rate limit `rate_limit` - NATS rate [limiter](https://docs.nats.io/jetstream/concepts/consumers#ratelimit). +In NATS `v6.0.0-beta.5`, this setting is parsed but not applied to the consumer. + ### Delete stream on stop -`delete_stream_on_stop` - delete the whole stream when pipeline stopped. +`delete_stream_on_stop` deletes the whole stream when the pipeline stops. Default: `false`. + +In the NATS v6 beta line (`v6.0.0-beta.5`), stopping an active pipeline with this option set to `false` no longer purges the stream. Messages remain subject to the stream's retention policy. Setting this option to `true` still deletes the stream and its messages. + +RR creates a new consumer on each run or resume. Retained messages can be delivered again, including messages acknowledged by an earlier consumer when `delete_after_ack` is `false`. Review stream retention before upgrading. Make job handlers safe to process the same job more than once. ### Delete after ack @@ -97,4 +108,8 @@ To prevent duplicate message consumption, ensure that each pipeline is configure ### Ack wait -`ack_wait` - time in seconds after which NATS will redeliver the message if no ACK received. +In the NATS v6 beta line (`v6.0.0-beta.5`), `ack_wait` sets the time before JetStream redelivers an unacknowledged message. NATS v5 ignored this setting. + +YAML uses a Go duration string, such as `ack_wait: 30s` or `ack_wait: 2m`. The default is `30s`. Use a duration longer than the expected queueing and processing time. + +The `jobs.Declare` RPC payload instead uses integer seconds encoded as a string: `"ack_wait": "30"`. Do not send a duration string such as `"30s"` through RPC. diff --git a/queues/nsq.md b/queues/nsq.md new file mode 100644 index 0000000..5eeee1d --- /dev/null +++ b/queues/nsq.md @@ -0,0 +1,79 @@ +# NSQ Driver + +RoadRunner publishes jobs to an NSQ topic and consumes them through an NSQ channel. + +{% hint style="info" %} +NSQ is newly bundled in the RoadRunner development binary that uses v6 plugins. It is not included in RoadRunner `v2025.1.15`. This page describes the `nsq` driver at `v6.0.0-beta.1`. +{% endhint %} + +## Configuration + +Start `nsqd` before RoadRunner. The example uses a [Jobs consumer](./overview-queues.md) in `consumer.php`. + +{% code title=".rr.yaml" %} + +```yaml +version: "3" + +rpc: + listen: tcp://127.0.0.1:6001 + +server: + command: php consumer.php + relay: pipes + +nsq: + addr: tcp://127.0.0.1:4150 + lookupd_poll_interval: 5s + +jobs: + consume: [tasks] + pipelines: + tasks: + driver: nsq + config: + topic: tasks + channel: workers + prefetch: 10 + priority: 10 + max_attempts: 5 +``` + +{% endcode %} + +The global `nsq` section is required. Put connection settings in this section. Put pipeline settings under `jobs.pipelines..config`. Global values override matching fields in YAML pipeline configurations. + +- `addr`: The nsqd TCP address. Both `host:port` and `tcp://host:port` are accepted. The default is `127.0.0.1:4150`. The producer always uses this address, including when consumer discovery is enabled. Driver initialization fails if the producer cannot connect. +- `lookupd`: An optional list of nsqlookupd HTTP addresses, such as `["127.0.0.1:4161"]`. With this option, consumers discover nsqd servers through nsqlookupd. Without it, consumers connect to `addr`. +- `lookupd_poll_interval`: The duration between discovery polls. It also controls the delay before a direct consumer connection reconnects. The default is `60s`. +- `dial_timeout`: The connection timeout. The default is `1s`. Duration settings use a unit suffix, such as `5s`. + +## Pipeline Options + +- `topic`: The topic to publish to and consume from. It defaults to the pipeline name. +- `channel`: The consumer channel. It defaults to `default`. Consumers on the same topic and channel share the work. Separate channels each receive a copy of the topic's messages. +- `prefetch`: The maximum number of messages in flight for the consumer. It defaults to `10`. Zero or negative values also select `10`. +- `priority`: The default job priority in RoadRunner. It defaults to `10`. Lower numbers have higher priority. This affects RR's priority queue, not NSQ delivery order. +- `max_attempts`: The delivery-attempt limit. See [Retry Limit](#retry-limit) before changing it. + +## Delivery Behavior + +- Acknowledgement completes the NSQ message. Auto-ack completes it before PHP processes it. Do not enable auto-ack when failed jobs must be retried. +- Job delays and explicit retry delays use seconds. A negative acknowledgement can requeue the original message. Disabling requeue completes the message without another attempt. +- A retry that changes headers publishes a new message to the topic before acknowledging the original. The two operations are not atomic. Workers must tolerate repeated deliveries. +- Pause stops requesting new messages without closing the consumer connection. Jobs already received can still run. Publishing remains available while paused. Resume restores the configured prefetch limit. +- The driver can consume raw messages from other producers. It passes the raw body to PHP with the job name `deduced_by_rr`. + +## Retry Limit + +{% hint style="warning" %} +In `v6.0.0-beta.1`, omitting `max_attempts` or setting it to `0` keeps the client default of five attempts. Zero does not enable unlimited retries. On a delivery beyond the limit, the client acknowledges the message without sending it to PHP. The driver does not forward it to a dead-letter queue. +{% endhint %} + +Use a positive `max_attempts` value for a different limit. A retry that publishes a new message starts a new broker attempt count. This setting is not a total retry limit across those new messages. Applications that must retain failed jobs need their own failure storage. + +## Limitations + +RR statistics report pipeline identity, listener readiness, and a locally tracked delayed-job count. They do not report the broker backlog. Use [nsqadmin](https://nsq.io/components/nsqadmin.html) for topic and channel counts. + +The beta driver does not expose TLS or authentication settings. diff --git a/queues/overview-queues.md b/queues/overview-queues.md index f036c79..a744221 100644 --- a/queues/overview-queues.md +++ b/queues/overview-queues.md @@ -85,12 +85,9 @@ jobs: {% endcode %} -Above is a complete list of all possible common Jobs settings. Let's now figure out what they are responsible for. +Common Jobs settings: -- `num_pollers`: The number of threads that are simultaneously reading from the priority - queue and send payloads to the workers. There is no optimal number, it depends - heavily on the performance of the PHP worker. For example, echo workers - can process over 300k jobs per second within 64 pollers (on a 32 core CPU). `num_pollers` should not be less than the number of workers to properly load all of them with the jobs. +- `num_pollers`: RR derives the number of queue pollers from the worker count and ignores this setting in jobs v5 and v6 beta. With an explicit worker count, a single pool uses `num_workers + 2` pollers. Named pools in v6 beta use the total worker count plus two. - `timeout`: The internal Golang context timeouts (in seconds). For example, if the connection was disconnected or your push was in the middle of a @@ -98,7 +95,7 @@ Above is a complete list of all possible common Jobs settings. Let's now figure or the queue is full. If the timeout is exceeded, your call will be rejected with an error. Default: 60 (seconds). -- `options.parallelism`: The number of goroutines that process jobs from the binary heap priority queue simultaneously. Default: 10. +- `options.parallelism`: Limits concurrent pipeline initialization and destruction. Default: `10` when `options` is omitted; `5` when `options.parallelism` is zero. - `pipeline_size`: The binary heaps priority queue (PQ) settings. The priority queue stores jobs in order of priority. The priority can be set @@ -116,6 +113,8 @@ driver queue until after Ack. - `pool`: All settings in this section are similar to the worker pool settings described on the [configuration page](https://roadrunner.dev/docs/intro-config). +- `pools`: Named worker pools in jobs v6 beta. Use this instead of `pool`. See [Named Worker Pools](#named-worker-pools-v6-beta). + - `consume`: Contains an array of the names of all queues specified in the `"pipelines"` section, which should be processed by the concierge specified in the global `"server"` section (see the [PHP worker's settings](../php/worker.md)). @@ -124,6 +123,60 @@ driver queue until after Ack. RoadRunner. The key is a unique *queue identifier*, and the value is an object of the driver-specific configuration (we will talk about this later). +{% hint style="warning" %} +In jobs `v6.0.0-beta.10`, an explicit `jobs.pool` with omitted or zero `num_workers` creates only two pollers, even after the pool selects its default worker count. Set `jobs.pool.num_workers` to a value greater than zero. Setting `num_pollers` does not correct this. +{% endhint %} + +### Named Worker Pools (v6 Beta) + +The jobs v6 beta line (`v6.0.0-beta.10`) supports named worker pools. Jobs v5 does not support this configuration. Use `jobs.pools` instead of `jobs.pool`. Setting both is an error. + +Set each pipeline's `pool` to a configured pool name: + +{% code title=".rr.yaml" %} + +```yaml +version: "3" + +rpc: + listen: tcp://127.0.0.1:6001 + +server: + command: php consumer.php + relay: pipes + +jobs: + pools: + default: + num_workers: 4 + reports: + num_workers: 1 + consume: ["emails", "reports"] + pipelines: + emails: + driver: memory + pool: default + config: + prefetch: 10 + reports: + driver: memory + pool: reports + config: + prefetch: 10 +``` + +{% endcode %} + +On `jobs.Push` and `jobs.PushBatch`, RR copies the pipeline's `pool` setting into the `pool` job header. This replaces any existing value. The consumer selects the named pool from the first header value. + +Treat `pool` as a reserved header. An unknown pool name causes a negative acknowledgment, even in single-pool mode. In multi-pool mode, a missing or empty `pool` header also causes a negative acknowledgment. A pool named `default` is not an automatic fallback. + +Producers that publish directly to a broker must supply the `pool` header. The pipeline setting alone does not route incoming jobs. Process existing jobs without this header before switching to named pools. + +{% hint style="warning" %} +BoltDB `v6.0.0-beta.5` does not preserve the `pool` header in stored jobs. With jobs `v6.0.0-beta.10`, an RR instance that consumes BoltDB pipelines must retain `jobs.pool`; do not switch it to `jobs.pools`. This also affects newly published jobs, so draining old jobs does not remove the restriction. +{% endhint %} + ## PHP Client (Producer) ### Installation @@ -354,6 +407,8 @@ IP address, the user's token or session id, etc. Headers can only contain string values and are not serialized in any way during transmission, so be careful when specifying them. +In jobs v6 beta, `pool` is [reserved for worker-pool routing](#named-worker-pools-v6-beta). + In the case to add a new header to the task, you can use methods [similar to PSR-7](https://www.php-fig.org/psr/psr-7/). **That is:** diff --git a/queues/sqs.md b/queues/sqs.md index 62446c5..37a1498 100644 --- a/queues/sqs.md +++ b/queues/sqs.md @@ -131,7 +131,7 @@ jobs: config: # Optional section. - # Default: 10 + # Default: 1 prefetch: 10 # Get queue URL only @@ -185,6 +185,12 @@ process the jobs in correct order. 3. You should make sure to either use an explicit non-zero `visibility_timeout` **or** make sure that the default visibility configuration attribute on your queue (`VisibilityTimeout`) is non-zero. +### Retries (v6 Beta) + +In the SQS v6 beta line (`v6.0.0-beta.6`), a republished FIFO retry uses a new SQS deduplication ID. This prevents SQS from suppressing the retry as a duplicate of the original message. The application job ID does not change. Initial sends still use the job ID for deduplication within SQS's five-minute deduplication window. + +`retain_failed_jobs: true` applies to native Nack handling. An explicit requeue still republishes the job, even with this option enabled, and can change FIFO processing order. The new deduplication behavior does not add exactly-once job processing. Job handlers must remain safe to retry. + ## Configuration Options ### Prefetch diff --git a/workflow/temporal.md b/workflow/temporal.md index 5c085da..e0afdf2 100644 --- a/workflow/temporal.md +++ b/workflow/temporal.md @@ -17,6 +17,7 @@ server: temporal: address: "127.0.0.1:7233" + worker_heartbeat_interval: 10s activities: num_workers: 10 @@ -26,6 +27,14 @@ logs: temporal.level: error ``` +## Worker Heartbeats + +The `temporal.worker_heartbeat_interval` option sets how often SDK workers report their state to the Temporal server. Use a duration from `1s` through `60s`. If the option is omitted or set to `0s`, the SDK uses its default of `60s`. + +Positive intervals below `1s` are clamped to `1s`. Intervals above `60s` are clamped to `60s`. The plugin logs a warning when it clamps a value. + +The plugin supplies host CPU and memory usage and the PHP SDK identity for these heartbeats. This option does not control activity heartbeats or the activity heartbeat timeout. + ## Example Integrated workflow server provides the ability to create very complex, long-running activities. diff --git a/workflow/worker.md b/workflow/worker.md index 3008c80..b262de2 100644 --- a/workflow/worker.md +++ b/workflow/worker.md @@ -39,6 +39,16 @@ $factory->run(); Read more about Temporal configuration and usage on the [official website](https://docs.temporal.io/develop/php/core-application#run-a-dev-worker). +## Dynamic Workflows + +A dynamic workflow handles workflow types that have no named registration in that SDK worker. Register at most one dynamic workflow per worker. Multiple dynamic registrations cause worker initialization to fail. + +Use a PHP SDK with dynamic workflow support. The SDK must set the `dynamic` field to `true` in the workflow registration metadata. This is a worker registration option, not a RoadRunner YAML setting. + +## Worker Recovery + +After an activity worker exits, the pool replaces that worker without an explicit activity-pool reset. A workflow-worker exit still triggers a full reset and clears the sticky workflow cache so Temporal can replay workflow history. + ## Multi-worker environment To serve both HTTP and Temporal from the same worker, use the `getMode()` option of `Environment`: From d879946e17266b4d5ab5cbd157e7d5e4ce72b9e6 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Sun, 6 Sep 2026 17:42:23 +0200 Subject: [PATCH 14/21] docs: correct runtime guidance and examples --- app-server/aws-lambda.md | 138 +++++++++++++++++++++++++++++------ app-server/docker.md | 2 +- app-server/nginx-with-rr.md | 1 + customization/events-bus.md | 32 ++++---- customization/plugin.md | 43 ++++++----- experimental/experimental.md | 39 ++-------- http/headers.md | 6 +- http/http.md | 13 +++- intro/about.md | 6 +- intro/config.md | 4 +- intro/quick-start.md | 9 +++ kv/boltdb.md | 15 +--- kv/overview-kv.md | 15 ++-- kv/redis.md | 5 +- lab/metrics.md | 16 ++-- php/rpc.md | 9 ++- plugins/centrifuge.md | 56 +++++++++----- plugins/locks.md | 84 +++++++++++++++------ queues/kafka.md | 10 +++ queues/overview-queues.md | 9 ++- 20 files changed, 338 insertions(+), 174 deletions(-) diff --git a/app-server/aws-lambda.md b/app-server/aws-lambda.md index 4cac77e..ee018b2 100644 --- a/app-server/aws-lambda.md +++ b/app-server/aws-lambda.md @@ -47,7 +47,7 @@ Name this file `handler.php` and put it in the root of your project. Make sure t {% code %} ```bash -composer require spiral/roadrunner-http nyholm/psr7 +composer require 'spiral/roadrunner-http:^4.1' nyholm/psr7 ``` {% endcode %} @@ -153,6 +153,10 @@ func main() { 2. `plugin.go` with the plugin implementation: +The adapter uses API Gateway HTTP API payload format `2.0` and the JSON protocol supported by [`spiral/roadrunner-http` 4.1](https://github.com/roadrunner-php/http/blob/v4.1.0/src/HttpWorker.php). HTTP metadata goes in `Payload.Context`. Raw body bytes go in `Payload.Body`. The adapter returns base64 response bodies so API Gateway can restore text and binary data. Form bodies remain raw; the PHP application must parse them if needed. + +The execution context sets a 10-second budget for worker acquisition and the initial response. A nonzero `Supervisor.ExecTTL` gives each stream read a separate 10-second timeout. Cleanup cancels the execution context and drains the result channel. A stream read already in progress can continue until its timeout expires. The SDK then kills the worker, reaps the process, and starts a replacement. The handler reserves 10 seconds for cleanup plus a 1-second margin before the Lambda deadline. If too little time remains, the handler returns an error without executing PHP. To allow the full execution budget, set the Lambda timeout above 21 seconds, for example 30 seconds, with additional time for initialization and response delivery. + {% code title="plugin.go" %} ```go @@ -160,6 +164,10 @@ package main import ( "context" + "encoding/base64" + "net/http" + "net/url" + "strings" "sync" "time" @@ -178,6 +186,7 @@ import ( const ( pluginName string = "lambda" + executionTimeout = 10 * time.Second ) type Plugin struct { @@ -241,6 +250,10 @@ func (p *Plugin) Serve() chan error { NumWorkers: 4, AllocateTimeout: time.Second * 20, DestroyTimeout: time.Second * 20, + StreamTimeout: time.Second, + Supervisor: &pool.SupervisorConfig{ + ExecTTL: executionTimeout, + }, }, nil, nil) if err != nil { errCh <- errors.E(op, err) @@ -268,12 +281,62 @@ func (p *Plugin) Stop(ctx context.Context) error { func (p *Plugin) handler() func(ctx context.Context, request events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { return func(ctx context.Context, request events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { - requestJSON, err := json.Marshal(request) - if err != nil { - return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil + deadline := time.Now().Add(executionTimeout) + if d, ok := ctx.Deadline(); ok { + d = d.Add(-executionTimeout - time.Second) + if d.Before(deadline) { + deadline = d + } + } + ctx, cancel := context.WithDeadline(ctx, deadline) + defer cancel() + if err := ctx.Err(); err != nil { + return events.APIGatewayV2HTTPResponse{}, err + } + + body := []byte(request.Body) + if request.IsBase64Encoded { + var err error + body, err = base64.StdEncoding.DecodeString(request.Body) + if err != nil { + return events.APIGatewayV2HTTPResponse{StatusCode: 400}, nil + } + } + + headers := make(http.Header, len(request.Headers)) + for name, value := range request.Headers { + headers.Set(name, value) + } + if len(request.Cookies) != 0 { + headers.Set("Cookie", strings.Join(request.Cookies, "; ")) + } + + cookies := make(map[string]string) + for _, cookie := range (&http.Request{Header: headers}).Cookies() { + if value, err := url.QueryUnescape(cookie.Value); err == nil { + cookies[cookie.Name] = value + } } - ctxJSON, err := json.Marshal(ctx) + host := headers.Get("Host") + if host == "" { + host = request.RequestContext.DomainName + } + uri := "https://" + host + request.RawPath + if request.RawQueryString != "" { + uri += "?" + request.RawQueryString + } + + metadata, err := json.Marshal(map[string]any{ + "remoteAddr": request.RequestContext.HTTP.SourceIP, + "protocol": request.RequestContext.HTTP.Protocol, + "method": request.RequestContext.HTTP.Method, + "uri": uri, + "headers": headers, + "cookies": cookies, + "rawQuery": request.RawQueryString, + "parsed": false, + }) if err != nil { return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil } @@ -281,37 +344,65 @@ func (p *Plugin) handler() func(ctx context.Context, request events.APIGatewayV2 pld := p.getPld() defer p.putPld(pld) - pld.Body = requestJSON - pld.Context = ctxJSON + pld.Body = body + pld.Context = metadata - re, err := p.wrkPool.Exec(ctx, pld, nil) + stopCh := make(chan struct{}) + re, err := p.wrkPool.Exec(ctx, pld, stopCh) if err != nil { return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil } + defer func() { + // StreamCancel uses ctx. ExecTTL bounds a stream read already in progress. + cancel() + close(stopCh) + for range re { + } + }() var r *payload.Payload select { - case pl := <-re: + case <-ctx.Done(): + return events.APIGatewayV2HTTPResponse{}, ctx.Err() + case pl, ok := <-re: + if !ok || pl == nil { + return events.APIGatewayV2HTTPResponse{Body: "worker empty response", StatusCode: 500}, nil + } if pl.Error() != nil { return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil } - // streaming is not supported - if pl.Payload().Flags&frame.STREAM != 0 { + r = pl.Payload() + if r == nil { + return events.APIGatewayV2HTTPResponse{Body: "worker empty response", StatusCode: 500}, nil + } + if r.Flags&frame.STREAM != 0 { return events.APIGatewayV2HTTPResponse{Body: "streaming is not supported", StatusCode: 500}, nil } - - // assign the payload - r = pl.Payload() - default: - return events.APIGatewayV2HTTPResponse{Body: "worker empty response", StatusCode: 500}, nil } - var response events.APIGatewayV2HTTPResponse - err = json.Unmarshal(r.Body, &response) - if err != nil { + var responseMetadata struct { + Status int `json:"status"` + Headers map[string][]string `json:"headers"` + } + err = json.Unmarshal(r.Context, &responseMetadata) + if err != nil || responseMetadata.Status < 100 || responseMetadata.Status >= 600 { return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil } + + response := events.APIGatewayV2HTTPResponse{ + StatusCode: responseMetadata.Status, + Headers: make(map[string]string, len(responseMetadata.Headers)), + Body: base64.StdEncoding.EncodeToString(r.Body), + IsBase64Encoded: true, + } + for name, values := range responseMetadata.Headers { + if strings.EqualFold(name, "Set-Cookie") { + response.Cookies = append(response.Cookies, values...) + } else { + response.Headers[name] = strings.Join(values, ", ") + } + } return response, nil } } @@ -355,18 +446,21 @@ endure: Here you can take full advantage of RoadRunner: you can include any plugin here and configure it with the embedded config (within reasonable limits). -To build and package your Lambda function, run: +Build the binary with a supported Go release that includes current security fixes. If your project has no `go.mod`, run `go mod init example.com/lambda` from the project root. + +The build uses `-mod=readonly` because Composer's `vendor` directory does not contain Go modules. AWS Lambda requires an executable named [`bootstrap`](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html#runtimes-custom-bootstrap) at the root of the deployment package. To build and package your Lambda function, run these commands from the project root: {% code title="build.sh" %} ```bash -CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s" -o bootstrap-amd64 main.go plugin.go +go mod tidy +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=readonly -trimpath -ldflags "-s" -o bootstrap main.go plugin.go zip main.zip * -r ``` {% endcode %} -You can now upload and invoke your handler using a simple string event. +You can now upload the function and connect it to an API Gateway HTTP API with payload format `2.0`. For a direct Lambda test, use an API Gateway v2 HTTP request event, not a string event. API Gateway v2 omits the custom-domain API mapping prefix from `rawPath`; this adapter uses the path supplied in the event. ## Repository with the full example diff --git a/app-server/docker.md b/app-server/docker.md index 53d8b15..5277054 100644 --- a/app-server/docker.md +++ b/app-server/docker.md @@ -43,7 +43,7 @@ RUN composer install --optimize-autoloader --no-dev COPY ./app . # Run the RoadRunner server -CMD ./rr serve -c .rr.yaml +CMD ["/usr/local/bin/rr", "serve", "-c", ".rr.yaml"] ``` {% endcode %} diff --git a/app-server/nginx-with-rr.md b/app-server/nginx-with-rr.md index d462a65..73f6ec6 100644 --- a/app-server/nginx-with-rr.md +++ b/app-server/nginx-with-rr.md @@ -261,6 +261,7 @@ Do not forget the `composer.json` file: "minimum-stability": "dev", "prefer-stable": true, "require": { + "nyholm/psr7": "^1.8", "spiral/roadrunner-http": "^3.0", "spiral/goridge": "^4.0" } diff --git a/customization/events-bus.md b/customization/events-bus.md index 67766fe..23cc12d 100644 --- a/customization/events-bus.md +++ b/customization/events-bus.md @@ -31,34 +31,33 @@ import ( func foo() { // Get the (global) instance of the event bus. Make sure to // unsubscribe the event handler when you don't need it anymore: eh.Unsubscribe(id). - eh, id := events.Bus() + eh, id := events.NewEventBus() defer eh.Unsubscribe(id) // Create an events channel. ch := make(chan events.Event, 100) - // Subscribe to the events that fit your pattern (e.g., `http.EventJobOK`). - err := eh.SubscribeP(id, "http.EventJobOK", ch) + // Subscribe to worker errors from the HTTP plugin. + err := eh.SubscribeP(id, "http.EventWorkerError", ch) if err != nil { panic(err) } // Send an event to the channel. - eh.Send(events.NewEvent(events.EventJobOK, "http", "foo")) + eh.Send(events.NewEvent(events.EventWorkerError, "http", "worker failed")) // Receive an event from the channel. evt := <-ch - // evt.Message() -> "foo" + // evt.Message() -> "worker failed" // evt.Plugin() -> "http" - // evt.Type().String() -> "EventJobOK" + // evt.Type().String() -> "EventWorkerError" } ``` {% endcode %} {% hint style="info" %} -If you use only `eh.Send` events bus function, you don't need to unsubscribe, so, you may simplify the declaration to -the `eh, _ := events.Bus()`. +If you only send events, use `eh, _ := events.NewEventBus()`. You do not need to unsubscribe when you have no subscriptions. {% endhint %} ### Event Payload @@ -70,9 +69,9 @@ Let's take a closer look at each of these properties: | Property | Description | |-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Message** | The message is a custom, user-defined string that can be used to convey additional information about the event. This can be useful for logging purposes or providing extra context to the subscriber when an event is received. In the examples provided earlier, the message is set to "foo" when sending the event. | +| **Message** | The message is a custom, user-defined string that can be used to convey information about the event. This can be useful for logging purposes or providing extra context to the subscriber when an event is received. In the examples provided earlier, the message is set to "worker failed" when sending the event. | | **Plugin** | The plugin property indicates the source plugin that raised the event. This information can be helpful in identifying the origin of the event and can be used for filtering or processing events based on their source. In the examples, the plugin is set to "http" when creating the event. | -| **Type** | The event type is a custom or RoadRunner (RR) defined identifier that categorizes the event. This identifier is used when subscribing to events and can help subscribers determine how to process the event or decide if they are interested in it. In the examples, the event type is set to `events.EventJobOK`. | +| **Type** | The event type is a custom or RoadRunner defined identifier that categorizes the event. This identifier is used when subscribing to events and can help subscribers determine how to process the event or decide if they are interested in it. In the examples, the event type is set to `events.EventWorkerError`. | When receiving an event, you can access these properties using the following methods: @@ -99,7 +98,7 @@ import ( ) func foo() { - eh, id := events.Bus() + eh, id := events.NewEventBus() defer eh.Unsubscribe(id) ch := make(chan events.Event, 100) @@ -110,18 +109,17 @@ func foo() { panic(err) } - eh.Send(events.NewEvent(events.EventJobOK, "http", "foo")) + eh.Send(events.NewEvent(events.EventWorkerError, "http", "worker failed")) evt := <-ch - // evt.Message() -> "foo" + // evt.Message() -> "worker failed" // evt.Plugin() -> "http" - // evt.Type().String() -> "EventJobOK" + // evt.Type().String() -> "EventWorkerError" } ``` {% endcode %} -In this example, we've changed the subscription pattern from `http.EventJobOK` to `http.*`, allowing the subscription to -match any event from the HTTP plugin. +The `http.*` subscription matches any event from the HTTP plugin, including `http.EventWorkerError`. ## How to implement a custom event @@ -212,7 +210,7 @@ import ( ) func foo() { - eh, id := events.Bus() + eh, id := events.NewEventBus() defer eh.Unsubscribe(id) ch := make(chan events.Event, 100) diff --git a/customization/plugin.md b/customization/plugin.md index 9bd2e49..a4b6322 100644 --- a/customization/plugin.md +++ b/customization/plugin.md @@ -303,8 +303,9 @@ func (cfg *Config) InitDefaults() { ## Serving -Create `Serve` and `Stop` methods in your structure to let RoadRunner start and stop your service. You may also use the -context from the `Stop` method to let RR force your plugin to stop after a specified timeout in the configuration. +Endure calls `Serve()` synchronously. Start blocking work in a goroutine owned by the plugin, then return an error channel promptly. A blocking `Serve()` prevents the remaining plugins from starting and prevents container shutdown. + +`Stop(ctx)` must stop the service cooperatively and respect the context deadline. The `endure.grace_period` setting determines this deadline. Endure does not terminate plugin goroutines when the deadline expires. {% code title=".rr.yaml" %} @@ -321,6 +322,8 @@ endure: ### Plugin +This example starts a local HTTP server. Its `Stop` method uses `http.Server.Shutdown(ctx)` to wait for active requests until the context expires. + {% code title="plugin.go" %} ```go @@ -328,37 +331,41 @@ package custom import ( "context" + "net/http" ) -type Plugin struct{} +type Plugin struct { + server *http.Server +} + +func (s *Plugin) Init() error { + s.server = &http.Server{ + Addr: "127.0.0.1:8088", + Handler: http.NotFoundHandler(), + } + return nil +} func (s *Plugin) Serve() chan error { - const op = errors.Op("custom_plugin_serve") errCh := make(chan error, 1) - err := s.DoSomeWork() - if err != nil { - errCh <- errors.E(op, err) - return errCh - } + go func() { + if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + errCh <- err + } + }() - return nil + return errCh } func (s *Plugin) Stop(ctx context.Context) error { - return s.stopServing() -} - -func (s *Plugin) DoSomeWork() error { - return nil + return s.server.Shutdown(ctx) } ``` {% endcode %} -The `Serve` method is thread-safe. It runs in a separate goroutine managed by the `Endure` container. -One note is that you should unblock it when calling `Stop` on the container. -Otherwise, the service will be killed after the timeout (which can be set in Endure). +`http.ErrServerClosed` is the normal result of HTTP shutdown, so the example does not send it to Endure. Other errors are sent through the buffered channel. Endure does not make plugin code thread-safe; the plugin must synchronize access to shared state. ## Collecting dependencies at runtime diff --git a/experimental/experimental.md b/experimental/experimental.md index d89df57..2ebc5c2 100644 --- a/experimental/experimental.md +++ b/experimental/experimental.md @@ -46,32 +46,13 @@ envfile: .env ### Support for the HTTP/3 server: `[>=2023.3.8]` -In `v2023.3.8`, we added experimental support for an HTTP/3 server. It can work with the ACME provider to generate certificates for the HTTP/3 server automatically. +In `v2023.3.8`, we added experimental support for an HTTP/3 server. -Sample `.rr.yaml` file: - -{% code title=".rr.yaml" %} - -```yaml -version: "3" +{% hint style="warning" %} +The pinned HTTP plugin, `v6.0.0-beta.10`, requires existing certificate and private key files in `http.http3.cert` and `http.http3.key`. The HTTP/3 listener does not use certificates from `http.ssl.acme`. +{% endhint %} -server: - command: "php worker.php" - relay: pipes - -http: - address: 127.0.0.1:15389 - pool: - num_workers: 2 - http3: - address: 127.0.0.1:34555 - key: "localhost+2-key.pem" - cert: "localhost+2.pem" -``` - -{% endcode %} - -Or if you use an ACME provider: +Sample `.rr.yaml` file: {% code title=".rr.yaml" %} @@ -90,16 +71,6 @@ http: address: 127.0.0.1:34555 key: "localhost+2-key.pem" cert: "localhost+2.pem" - ssl: - acme: - certs_dir: rr_le_certs - email: you-email-here@email - alt_http_port: 80 - alt_tlsalpn_port: 443 - challenge_type: http-01 - use_production_endpoint: false - domains: - - your-cool-domains.here ``` {% endcode %} diff --git a/http/headers.md b/http/headers.md index 76101de..2b298fc 100644 --- a/http/headers.md +++ b/http/headers.md @@ -17,9 +17,9 @@ http: # ... headers: cors: - allowed_origin: "*" + allowed_origin: "https://foo.example.com" # If `allowed_origin_regex` option is set, the content of `allowed_origin` is ignored - allowed_origin_regex: "^http://foo" + allowed_origin_regex: "^https://foo[.]example[.]com$" allowed_headers: "*" allowed_methods: "GET, POST, PUT, DELETE" allow_credentials: true @@ -35,6 +35,8 @@ http: > Make sure to declare "headers" middleware. +Replace the example origin in both settings with your trusted origin. Keep `^` and `$` in the regex. Use `[.]` to match literal dots. Do not use `allowed_origin: "*"` with `allow_credentials: true`. + In v6 beta, `allowed_origin`, `allowed_methods`, `allowed_headers`, and `exposed_headers` ignore whitespace around each comma-separated value. For example, `"GET, POST"` selects both methods. {% hint style="info" %} diff --git a/http/http.md b/http/http.md index ef4afbb..a389cf7 100644 --- a/http/http.md +++ b/http/http.md @@ -55,9 +55,11 @@ http: # Default: "" dir: "/tmp" - # Deny files with the following extensions to upload. + # Deny uploads with these file extensions. + # Configure forbid or allow for your application's extension restrictions. + # Without either list, RR accepts all file extensions. # - # Default: [".php", ".exe", ".bat"] + # Default: [] forbid: [ ".php", ".exe", ".bat", ".sh" ] # [SINCE 2.6] Allow files with the following extensions to upload @@ -72,9 +74,10 @@ http: # feature disabling. cors: # Controls "Access-Control-Allow-Origin" header value (docs: https://mzl.la/2OgD4Qf). + # Replace this example with your application's trusted origin. # # Default: "" - allowed_origin: "*" + allowed_origin: "https://foo.example.com" # Controls "Access-Control-Allow-Headers" header value (docs: https://mzl.la/2OzDVvk). # @@ -486,11 +489,13 @@ response. {% code title="script.php" %} ```php -return $response->withAddedHeader('http2-push', '/test.js'); +return $response->withAddedHeader('Http2-Push', '/test.js'); ``` {% endcode %} +In v6 beta, this virtual header requires the exact name `Http2-Push`. + Note that the path of the resource must be related to the public application directory and must include `/` at the beginning. diff --git a/intro/about.md b/intro/about.md index e158b91..5dc783a 100644 --- a/intro/about.md +++ b/intro/about.md @@ -34,11 +34,9 @@ The following plugins are designed to run workers and handle specific types of r By utilizing these plugins, RoadRunner ensures that your PHP application can handle a wide range of requests and communication protocols, delivering optimal performance and flexibility. -## (g)RPC interface +## Goridge RPC interface -RoadRunner also provides a customized gRPC interface for communication between the application and the server, which plays a -significant role in enhancing the interaction between the two components. This interface is particularly useful when -working with the various plugins that support RPC communication, such as: +Applications call these RoadRunner plugin services through [Goridge RPC](../php/rpc.md). The transport uses Go `net/rpc`, not gRPC. The services include: - [**KV**](../kv/overview-kv.md) - A cache service that allows for efficient storage and retrieval of cached data. - [**Locks**](../plugins/locks.md) - Offers a convenient means to manage distributed locks, ensuring resource access diff --git a/intro/config.md b/intro/config.md index e108972..47aeecf 100644 --- a/intro/config.md +++ b/intro/config.md @@ -79,7 +79,7 @@ exec /var/www/rr \ -w /var/www \ -o http.pool.num_workers=${RR_NUM_WORKERS:-8} \ -o http.pool.max_jobs=${RR_MAX_JOBS:-16} \ - -o http.pool.supervisor.max_worker_memory=${RR_MAX_WORKER_MEMORY:-512} + -o http.pool.supervisor.max_worker_memory=${RR_MAX_WORKER_MEMORY:-512} \ serve ``` @@ -184,4 +184,4 @@ You may use any number of the included configuration files via CLI command, in q 1. [Server Commands](../app-server/cli.md) - learn how to start the server. 2. [PHP Workers β€” Environment variables](../php/environment.md) - learn how to configure PHP workers environment. -3. [Config plugin](../plugins/config.md) - learn more about the Config plugin. \ No newline at end of file +3. [Config plugin](../plugins/config.md) - learn more about the Config plugin. diff --git a/intro/quick-start.md b/intro/quick-start.md index 5f717b1..03357b8 100644 --- a/intro/quick-start.md +++ b/intro/quick-start.md @@ -10,6 +10,12 @@ To begin, you need to download RR for your platform. Visit the [RR installation RR requires PHP to run. If you don't have PHP installed, you can download it from the [official PHP website](https://www.php.net/downloads.php) and follow the installation instructions for your operating system. +Install [Composer](https://getcomposer.org/download/). Enable the PHP `sockets` extension. Run this command in the project directory to install the worker dependencies: + +```bash +composer require spiral/roadrunner-http nyholm/psr7 +``` + ## Step 3: Create a simple configuration Next, you need to create a simple configuration file for RR. Open a text editor and create a new file called `.rr.yaml`. Add the following content to the file: @@ -87,6 +93,9 @@ Now start the server. You should have the following files in the current folder: - `.rr.yaml` - `psr-worker.php` +- `composer.json` +- `composer.lock` +- `vendor/` - `rr` binary Then, open a terminal window in the current folder and run the following command: diff --git a/kv/boltdb.md b/kv/boltdb.md index 9a68c25..b9d8b1b 100644 --- a/kv/boltdb.md +++ b/kv/boltdb.md @@ -27,10 +27,6 @@ kv: # Default: 0777 permissions: 0777 - # Optional section. - # Default: "rr" - bucket: "rr" - # Optional section. # Default: 60 interval: 60 @@ -49,18 +45,15 @@ own at startup. Note that this must be an existing directory, otherwise a "The s error will occur, indicating that the full database pathname is invalid. Might be a full path with file: `/foo/bar/rr1.db`. Default: `rr.db`. +Use a separate database file for each KV storage. Each storage opens the file with an exclusive lock. Sharing a file between storage instances causes startup to fail. + +Both v5 and the v6 beta use the fixed internal bucket `default`. The driver does not support a `bucket` configuration option. + ### Permissions `permissions`: The file permissions in UNIX format of the database file, set at the time of its creation. If the file already exists, the permissions will not be changed. -### Bucket - -`bucket`: The bucket name. You can create several boltdb connections by specifying different buckets and in this case -the data stored in one bucket will -not intersect with the data stored in the other, even if the database file and other settings are completely -identical. - ### Interval `interval`: The time in seconds between expiration checks. Expired entries can remain readable until the next check. diff --git a/kv/overview-kv.md b/kv/overview-kv.md index b1a4afe..10c46bd 100644 --- a/kv/overview-kv.md +++ b/kv/overview-kv.md @@ -291,9 +291,11 @@ use Spiral\Goridge\RPC\RPC; use Spiral\RoadRunner\KeyValue\Factory; use Spiral\RoadRunner\KeyValue\Serializer\IgbinarySerializer; -$storage = (new Factory($rpc) +$rpc = RPC::create('tcp://127.0.0.1:6001'); + +$storage = (new Factory($rpc)) ->withSerializer(new IgbinarySerializer()) - ->select('storage'); + ->select('example'); ``` {% endcode %} @@ -331,13 +333,14 @@ After generating the keypair, you can use it to encrypt and decrypt the data. use Spiral\Goridge\RPC\RPC; use Spiral\RoadRunner\KeyValue\Factory; use Spiral\RoadRunner\KeyValue\Serializer\SodiumSerializer; -use Spiral\RoadRunner\KeyValue\Serializer\DefaultSerializer; -$storage = new Factory($rpc); - ->select('storage'); +$rpc = RPC::create('tcp://127.0.0.1:6001'); + +$storage = (new Factory($rpc)) + ->select('example'); // Encrypted serializer -$key = file_get_contents(__DIR__ . '/path/to/keypair.key'); +$key = file_get_contents(__DIR__ . '/keypair.key'); $encrypted = new SodiumSerializer($storage->getSerializer(), $key); // Storing public data diff --git a/kv/redis.md b/kv/redis.md index 16dbab3..c5454bf 100644 --- a/kv/redis.md +++ b/kv/redis.md @@ -48,7 +48,7 @@ kv: dial_timeout: 0 # Optional section. - # Default: 0 (equivalent to the default value of 3 retries) + # Default: 0 (3 command retries after the initial attempt). max_retries: 0 # Optional section. @@ -175,8 +175,7 @@ disables idle timeout check. ### Retries -`max_retries`: Maximum number of retries before giving up. Specifying `0` is equivalent to the default (`3` attempts). -If you need to specify an infinite number of connection attempts, specify the value `-1`. +`max_retries`: Maximum number of command retries after the initial attempt. A value of `0` selects the default of three retries. A value of `-1` disables command retries. It does not enable unlimited connection attempts. `min_retry_backoff`: Minimum backoff between each retry. Must be in the format of a "numeric value" + "time format suffix". A value of `0` is equivalent to a timeout of 8 milliseconds (`8ms`). A value of `-1` disables backoff. diff --git a/lab/metrics.md b/lab/metrics.md index d23274b..6fe63b0 100644 --- a/lab/metrics.md +++ b/lab/metrics.md @@ -160,6 +160,9 @@ Prometheus. To do this, you need to register collectors in your configuration fi ```yaml version: "3" +rpc: + listen: tcp://127.0.0.1:6001 + metrics: address: 127.0.0.1:2112 collect: @@ -183,18 +186,21 @@ You can also use tagged (labels) metrics to group values: ```yaml version: "3" +rpc: + listen: tcp://127.0.0.1:6001 + metrics: address: 127.0.0.1:2112 collect: registered_users: - type: histogram + type: counter help: "Total registered users." labels: [ "type", "is_admin" ] ``` {% endcode %} -In the example below we will show you how to send metrics into `registered_users` collector. +Choose either the basic configuration or this labeled configuration for `registered_users`. Every update to the labeled collector must include two label values, in the order `type`, `is_admin`. ### PHP client @@ -218,7 +224,7 @@ composer require spiral/roadrunner-metrics After the installation, you can create an instance of the `Spiral\RoadRunner\Metrics\Metrics` class, which will allow you to use the available class methods. -**Here is an example:** +Use the [basic configuration](#application-metrics) for this example. Its collector has no labels: {% code title="metrics.php" %} @@ -247,7 +253,7 @@ grouping, and aggregating the data. - **Simplified querying:** You can use labels to filter and aggregate your metric data, making it easier to extract meaningful insights from the data. -You can also specify labels for your metrics by passing an array of labels to the `add` method: +Use the [tagged configuration](#tagged-metrics) for this call. Pass one value for `type` and one for `is_admin`, in that order: {% code title="metrics.php" %} @@ -283,7 +289,7 @@ $metrics->add('earned_money', 100_000_000); {% endcode %} -You can also declare labeled metrics: +You can also declare the labeled collector in PHP instead of YAML. For this example, remove `registered_users` from `metrics.collect` before starting RoadRunner. A declaration does not change the labels of an existing collector. Use the two-label `add()` call above after this declaration: {% code title="metrics.php" %} diff --git a/php/rpc.md b/php/rpc.md index 210873a..eedf6b5 100644 --- a/php/rpc.md +++ b/php/rpc.md @@ -262,9 +262,10 @@ final class AsyncCache public function commitAsync(): bool { try { - $this->rpc->getResponses($this->responses, Response::class); - } catch (ServiceException $e) { - // ... + foreach ($this->rpc->getResponses($this->responses, Response::class) as $response) { + // Read each response to detect RPC errors. + } + } catch (ServiceException) { return false; } finally { $this->responses = []; @@ -293,6 +294,8 @@ final class AsyncCache {% endcode %} +`getResponses()` returns a lazy iterator. `commitAsync()` returns `true` only after it reads all responses. It returns `false` on the first server-side RPC error; other exceptions propagate. It does not undo completed operations or read the remaining responses after an error. + Usage: {% code %} diff --git a/plugins/centrifuge.md b/plugins/centrifuge.md index 1f0c0a3..7835ec2 100644 --- a/plugins/centrifuge.md +++ b/plugins/centrifuge.md @@ -50,11 +50,13 @@ quite feasible to run: {% code title=".rr.yaml" %} ```yaml +version: "3" + rpc: listen: tcp://127.0.0.1:6001 server: - command: "php app.php" + command: "php centrifuge-worker.php" relay: pipes centrifuge: @@ -107,7 +109,6 @@ For example: "allowed_origins": [ "*" ], - "token_hmac_secret_key": "test", "proxy_publish": true, "proxy_subscribe": true, "allow_subscribe_for_client": true, @@ -119,8 +120,6 @@ For example: "proxy_subscribe_timeout": "10s", "proxy_refresh_endpoint": "grpc://127.0.0.1:30000", "proxy_refresh_timeout": "10s", - "proxy_sub_refresh_endpoint": "grpc://127.0.0.1:30000", - "proxy_sub_refresh_timeout": "1s", "proxy_rpc_endpoint": "grpc://127.0.0.1:30000", "proxy_rpc_timeout": "10s" } @@ -129,13 +128,23 @@ For example: {% endcode %} {% hint style="info" %} -`proxy_connect_endpoint`, `proxy_publish_endpoint`, `proxy_subscribe_endpoint`, `proxy_refresh_endpoint`, `proxy_sub_refresh_endpoint`, `proxy_rpc_endpoint` - +`proxy_connect_endpoint`, `proxy_publish_endpoint`, `proxy_subscribe_endpoint`, `proxy_refresh_endpoint`, `proxy_rpc_endpoint` - endpoint address of roadrunner server with activated centrifuge plugin. {% endhint %} ### PHP worker example -Here is an example of a PHP worker: +This worker authenticates one configured service account with a bearer token. Set `APP_CENTRIFUGO_USER` to that account's ID in the RoadRunner process environment. Generate a token with the following command: + +```bash +php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;' +``` + +Set `APP_CENTRIFUGO_TOKEN` to the command output in the RoadRunner process environment. Give the token only to that account. Send it in the Centrifugo client's connection `data` as `{"token": ""}`. Use WSS outside local tests. Do not put the token in public JavaScript or logs. The worker obtains the user ID from server configuration, not from client data. It rejects missing or incorrect credentials. + +Connections expire after five minutes. The refresh handler marks them as expired, so clients must reconnect and authenticate again. To revoke the token, replace it. Then restart RoadRunner. Existing connections remain valid until they expire or you disconnect them through the Centrifugo API. + +For multiple users, validate a separate credential for each user against your application's session or token store. The subscribe, publish, and RPC handlers below are examples, not per-channel authorization rules. Replace the sample admin and API credentials in the Centrifugo configuration before exposing the server. {% code title="centrifuge-worker.php" %} @@ -150,6 +159,12 @@ use RoadRunner\Centrifugo\Request; use RoadRunner\Centrifugo\Request\RequestFactory; use Spiral\RoadRunner\Worker; +$authToken = (string) getenv('APP_CENTRIFUGO_TOKEN'); +$authUser = (string) getenv('APP_CENTRIFUGO_USER'); +if (strlen($authToken) < 64 || $authUser === '') { + throw new \RuntimeException('Configure APP_CENTRIFUGO_TOKEN and APP_CENTRIFUGO_USER.'); +} + $worker = Worker::create(); $requestFactory = new RequestFactory($worker); @@ -171,11 +186,24 @@ while ($request = $centrifugoWorker->waitRequest()) { continue; } + if ($request instanceof Request\Connect) { + $token = $request->getData()['token'] ?? null; + if (!is_string($token) || !hash_equals($authToken, $token)) { + $request->error(1000, 'Invalid credentials.'); + continue; + } + + $request->respond(new Payload\ConnectResponse( + user: $authUser, + expireAt: time() + 300, + )); + continue; + } + if ($request instanceof Request\Refresh) { try { - // Do something $request->respond(new Payload\RefreshResponse( - // ... + expired: true, )); } catch (\Throwable $e) { $request->error($e->getCode(), $e->getMessage()); @@ -191,8 +219,7 @@ while ($request = $centrifugoWorker->waitRequest()) { // ... )); - // You can also disconnect connection - $request->disconnect('500', 'Connection is not allowed.'); + // Use disconnect() instead of respond() to reject a connection. } catch (\Throwable $e) { $request->error($e->getCode(), $e->getMessage()); } @@ -207,8 +234,7 @@ while ($request = $centrifugoWorker->waitRequest()) { // ... )); - // You can also disconnect connection - $request->disconnect('500', 'Connection is not allowed.'); + // Use disconnect() instead of respond() to reject a connection. } catch (\Throwable $e) { $request->error($e->getCode(), $e->getMessage()); } @@ -218,12 +244,8 @@ while ($request = $centrifugoWorker->waitRequest()) { if ($request instanceof Request\RPC) { try { - $response = $router->handle( - new Request(uri: $request->method, data: $request->data), - ); // ['user' => ['id' => 1, 'username' => 'john_smith']] - $request->respond(new Payload\RPCResponse( - data: $response + data: $request->getData(), )); } catch (\Throwable $e) { $request->error($e->getCode(), $e->getMessage()); diff --git a/plugins/locks.md b/plugins/locks.md index e66c650..e692d88 100644 --- a/plugins/locks.md +++ b/plugins/locks.md @@ -59,20 +59,38 @@ The `RoadRunner\Lock\Lock` class provides four methods that allow you to manage #### Acquire lock -Locks a resource so that it can be accessed by one process at a time. When a resource is locked, other processes that -attempt to lock the same resource will be blocked until the lock is released. +Attempts to acquire an exclusive lock on a resource. Set a positive `waitTTL` to wait for a conflicting lock to be released. The method returns a lock ID on success or `false` on failure. Check the result before accessing the protected resource. + +The PHP SDK uses seconds for numeric `ttl` and `waitTTL` values. It converts these values to microseconds for RPC. In PHP SDK 1.0.x, `waitTTL` defaults to `0`, which sends a zero RPC wait. The server then uses a one-millisecond acquisition window. Set `waitTTL` explicitly when you need a longer wait. {% code title="app.php" %} ```php -$id = $lock->lock('pdf:create'); +$id = $lock->lock('pdf:create', ttl: 10, waitTTL: 5); +if ($id === false) { + throw new \RuntimeException('Could not acquire pdf:create.'); +} + +try { + // Access the protected resource here. +} finally { + $lock->release('pdf:create', $id); +} +``` + +{% endcode %} -// Acquire lock with ttl - 10 microseconds +These calls show alternative arguments. Use the same success check and release handling for each call: + +{% code title="app.php" %} + +```php +// Set a ten-second TTL. $id = $lock->lock('pdf:create', ttl: 10); // or $id = $lock->lock('pdf:create', ttl: new \DateInterval('PT10S')); -// Acquire lock and wait 5 microseconds until lock will be released +// Wait for at most five seconds to acquire the lock. $id = $lock->lock('pdf:create', waitTTL: 5); // or $id = $lock->lock('pdf:create', waitTTL: new \DateInterval('PT5S')); @@ -85,18 +103,36 @@ $id = $lock->lock('pdf:create', id: '14e1b600-9e97-11d8-9f32-f2801f1b9fd1'); #### Acquire read lock -Locks a resource for shared access, allowing multiple processes to access the resource simultaneously. When a resource -is locked for shared access, other processes that attempt to lock the resource for exclusive access will be blocked -until all shared locks are released. +Attempts to acquire a shared lock on a resource. Multiple readers can hold shared locks. An exclusive acquisition must wait until all readers release their locks or its wait expires. Set a positive `waitTTL` to wait for an existing exclusive lock. `lockRead()` returns a lock ID on success or `false` on failure. The PHP time units and default wait are the same as for `lock()`. + +{% code title="app.php" %} + +```php +$id = $lock->lockRead('pdf:create', ttl: 10, waitTTL: 5); +if ($id === false) { + throw new \RuntimeException('Could not acquire a read lock on pdf:create.'); +} + +try { + // Read the protected resource here. +} finally { + $lock->release('pdf:create', $id); +} +``` + +{% endcode %} + +These calls show alternative arguments. Check each result before reading the resource: {% code title="app.php" %} ```php -$id = $lock->lockRead('pdf:create', ttl: 100000); +// Set a ten-second TTL. +$id = $lock->lockRead('pdf:create', ttl: 10); // or $id = $lock->lockRead('pdf:create', ttl: new \DateInterval('PT10S')); -// Acquire lock and wait 5 microseconds until lock will be released +// Wait for at most five seconds to acquire the read lock. $id = $lock->lockRead('pdf:create', waitTTL: 5); // or $id = $lock->lockRead('pdf:create', waitTTL: new \DateInterval('PT5S')); @@ -143,15 +179,20 @@ if($status) { #### Update TTL -Updates the time-to-live (TTL) for the locked resource. +Replaces the remaining TTL with a new duration from the time the server applies the update. It does not add time to the previous expiry. Numeric PHP values use seconds. A shorter value can release the lock before the protected operation finishes. Check the update result. Stop accessing the resource if the update fails. Finish the operation or renew the lock before its TTL expires. {% code title="app.php" %} ```php -// Add 10 microseconds to lock ttl -$lock->updateTTL('pdf:create', $id, 10); -// or -$lock->updateTTL('pdf:create', $id, new \DateInterval('PT10S')); +// Set the remaining TTL to ten seconds. +if (!$lock->updateTTL('pdf:create', $id, 10)) { + throw new \RuntimeException('Could not renew pdf:create.'); +} + +// Alternative: use a DateInterval for the same duration. +if (!$lock->updateTTL('pdf:create', $id, new \DateInterval('PT10S'))) { + throw new \RuntimeException('Could not renew pdf:create.'); +} ``` {% endcode %} @@ -207,10 +248,13 @@ PHP DTO classes proto files, making it easy to work with these files in your PHP RoadRunner provides an RPC API, which allows you to manage locks in your applications using remote procedure calls. The RPC API provides a set of methods that map to the available methods of the `RoadRunner\Lock\Lock` class in PHP. +Raw RPC requests use microseconds for `Request.ttl` and `Request.wait`. For example, `wait: 5000000` allows an acquisition wait of five seconds. If `wait` is omitted or zero, the server uses a one-millisecond acquisition window, not an unlimited wait. PHP SDK arguments use seconds and are converted before the RPC call. + +For `Lock` and `LockRead`, require `Response.Ok == true` before accessing the resource. A completed RPC call with no error can still return `Ok == false`, including when the acquisition wait expires. + ### Lock -Acquires an exclusive lock on a resource so that it can be accessed by one process at a time. When a resource is locked, -other processes that attempt to lock the same resource will be blocked until the lock is released. +Attempts to acquire an exclusive lock. If the resource has a conflicting lock, the call waits until that lock is released or the acquisition deadline expires. Check `Response.Ok` before entering the protected section. {% code %} @@ -222,9 +266,7 @@ func (r *rpc) Lock(req *lockApi.Request, resp *lockApi.Response) error {} ### LockRead -Acquires a read lock on a resource, allowing multiple processes to access the resource simultaneously. When a resource -is locked for shared access, other processes that attempt to lock the resource for exclusive access will be blocked -until all shared locks are released. +Attempts to acquire a shared lock. Readers can share the resource, but an existing exclusive lock prevents acquisition. The call waits until the conflict ends or the acquisition deadline expires. Check `Response.Ok` before reading the protected resource. An exclusive acquisition also has a finite wait when readers hold the resource. {% code %} @@ -272,7 +314,7 @@ func (r *rpc) Exists(req *lockApi.Request, resp *lockApi.Response) error {} #### UpdateTTL -Updates the time-to-live (TTL) for the locked resource. +Replaces the remaining TTL with `Request.ttl` microseconds from the time the server applies the update. It does not add to the previous expiry. Check `Response.Ok` to detect a failed update. {% code %} diff --git a/queues/kafka.md b/queues/kafka.md index 996907c..5846173 100644 --- a/queues/kafka.md +++ b/queues/kafka.md @@ -301,6 +301,16 @@ jobs: {% endcode %} +## Acknowledgments + +In Kafka `v5.2.5` and `v6.0.0-beta.7`, pipelines with `group_options.group_id` set mark acknowledged records for automatic offset commits. A commit advances the consumer group's position for a partition. RR does not wait for all earlier records in that partition to complete. + +{% hint style="warning" %} +Workers can complete jobs out of order. If offset `101` is acknowledged while offset `100` is unfinished in the same partition, a commit can advance the group to `102`. After a crash, the group then skips offset `100`, although that job was not acknowledged. Do not assume that every unacknowledged job will be delivered again. +{% endhint %} + +Direct partition consumption without `group_options` does not use these consumer-group commits. + ## Direct Partitions (v6 Beta) The Kafka v6 beta line (`v6.0.0-beta.7`) applies `consumer_options.consume_partitions` to the client. Kafka `v5.2.5` accepted this setting but did not apply it. diff --git a/queues/overview-queues.md b/queues/overview-queues.md index a744221..704606c 100644 --- a/queues/overview-queues.md +++ b/queues/overview-queues.md @@ -104,10 +104,11 @@ Common Jobs settings: is then blocked until the workers have processed all the jobs in it. **Lower number means higher priority.** {% hint style="info" %} -Blocked PQ means that you can push the job into the driver, but RoadRunner -will not read that job until PQ is empty. If RoadRunner is running -with jobs in the PQ, they won't be lost because jobs are not removed from the driver's -driver queue until after Ack. +A full PQ blocks further inserts. Publishing can continue if the driver has capacity. + +The PQ does not guarantee recovery after a crash. Recovery depends on the driver's persistence, broker retention, and acknowledgment settings. The memory driver loses jobs when RoadRunner stops. Auto-ack can acknowledge a job before PHP processes it. + +Kafka pipelines with a configured consumer group use partition offset commits. A later acknowledgment can cause unfinished earlier records in the same partition to be skipped after a crash. See [Kafka acknowledgments](./kafka.md#acknowledgments). {% endhint %} - `pool`: All settings in this section are similar to the worker pool settings From 40583c007aa71e25f3ccf275e5728f47e3fdc84d Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Sun, 6 Sep 2026 19:58:33 +0200 Subject: [PATCH 15/21] docs: update examples for v6 source builds --- README.md | 4 +- app-server/aws-lambda.md | 154 ++++++++++++++++++------------------ app-server/docker.md | 47 +++++++++-- app-server/nginx-with-rr.md | 18 ++--- customization/build.md | 2 - customization/embedding.md | 8 +- grpc/grpc.md | 87 ++++---------------- intro/install.md | 118 ++++++++------------------- intro/quick-start.md | 4 +- php/debugging.md | 17 ++-- 10 files changed, 194 insertions(+), 265 deletions(-) diff --git a/README.md b/README.md index e613244..f7e79bc 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@

- + - + All releases diff --git a/app-server/aws-lambda.md b/app-server/aws-lambda.md index ee018b2..af5e48f 100644 --- a/app-server/aws-lambda.md +++ b/app-server/aws-lambda.md @@ -4,17 +4,12 @@ RoadRunner can run PHP as an AWS Lambda function. ## PHP Worker -The PHP worker does not require any specific configuration to run inside a Lambda function. We can use the default snippet with -an internal counter to demonstrate how workers are reused: +Use PHP `8.5` with the `sockets` extension and Composer 2. This worker returns an HTTP response for each invocation: {% code title="handler.php" %} ```php = 600 { return events.APIGatewayV2HTTPResponse{Body: "", StatusCode: 500}, nil } response := events.APIGatewayV2HTTPResponse{ - StatusCode: responseMetadata.Status, + StatusCode: int(responseMetadata.Status), Headers: make(map[string]string, len(responseMetadata.Headers)), Body: base64.StdEncoding.EncodeToString(r.Body), IsBase64Encoded: true, } - for name, values := range responseMetadata.Headers { + for name, header := range responseMetadata.Headers { + values := make([]string, 0, len(header.GetValue())) + for _, value := range header.GetValue() { + values = append(values, string(value)) + } if strings.EqualFold(name, "Set-Cookie") { response.Cookies = append(response.Cookies, values...) } else { @@ -446,21 +437,34 @@ endure: Here you can take full advantage of RoadRunner: you can include any plugin here and configure it with the embedded config (within reasonable limits). -Build the binary with a supported Go release that includes current security fixes. If your project has no `go.mod`, run `go mod init example.com/lambda` from the project root. +Use Go `1.27.1`. If your project has no `go.mod`, run `go mod init example.com/lambda` from the project root. Select the dependencies before building: + +```bash +go get github.com/aws/aws-lambda-go@v1.55.0 \ + github.com/roadrunner-server/api-go/v6@v6.0.0-beta.14 \ + github.com/roadrunner-server/config/v6@v6.0.0-beta.4 \ + github.com/roadrunner-server/endure/v2@v2.6.2 \ + github.com/roadrunner-server/errors@v1.5.0 \ + github.com/roadrunner-server/goridge/v4@v4.0.0-beta.3 \ + github.com/roadrunner-server/logger/v6@v6.0.0-beta.4 \ + github.com/roadrunner-server/pool/v2@v2.0.0-beta.1 \ + github.com/roadrunner-server/server/v6@v6.0.0-beta.7 \ + google.golang.org/protobuf@v1.36.12 +``` -The build uses `-mod=readonly` because Composer's `vendor` directory does not contain Go modules. AWS Lambda requires an executable named [`bootstrap`](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html#runtimes-custom-bootstrap) at the root of the deployment package. To build and package your Lambda function, run these commands from the project root: +The build uses `-mod=readonly` because Composer's `vendor` directory does not contain Go modules. AWS Lambda requires an executable named [`bootstrap`](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html#runtimes-custom-bootstrap) at the root of the deployment package. Include a Linux PHP `8.5` executable named `php` and its shared libraries in `lib/`, built for Amazon Linux 2023 and `x86_64`. Run these commands from the project root: {% code title="build.sh" %} ```bash go mod tidy CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=readonly -trimpath -ldflags "-s" -o bootstrap main.go plugin.go -zip main.zip * -r +zip -r main.zip bootstrap php lib handler.php vendor ``` {% endcode %} -You can now upload the function and connect it to an API Gateway HTTP API with payload format `2.0`. For a direct Lambda test, use an API Gateway v2 HTTP request event, not a string event. API Gateway v2 omits the custom-domain API mapping prefix from `rawPath`; this adapter uses the path supplied in the event. +Use the Lambda `provided.al2023` runtime and `x86_64` architecture. Upload the package and connect it to an API Gateway HTTP API with payload format `2.0`. For a direct Lambda test, use an API Gateway v2 HTTP request event, not a string event. API Gateway v2 omits the custom-domain API mapping prefix from `rawPath`; this adapter uses the path supplied in the event. ## Repository with the full example diff --git a/app-server/docker.md b/app-server/docker.md index 5277054..7c816ab 100644 --- a/app-server/docker.md +++ b/app-server/docker.md @@ -1,12 +1,43 @@ # Docker Images -The following Docker images are available: +Build a local image with v6 plugins from the same RoadRunner revision as the [installation guide](../intro/install.md). The application, Nginx, and debugging examples use this image. -| Description | Links | Status | -|------------------------------------------|-----------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Official RR image** | [Link](https://github.com/roadrunner-server/roadrunner/pkgs/container/roadrunner) | ![Latest Stable Version](https://img.shields.io/github/v/release/roadrunner-server/roadrunner.svg?maxAge=30) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) | -| **Third-party image from `n1215`** | [Link](https://github.com/n1215/roadrunner-docker-skeleton) | [![License](https://poser.pugx.org/n1215/roadrunner-docker-skeleton/license)](https://packagist.org/packages/n1215/roadrunner-docker-skeleton) | -| **Third-party image from `spacetab-io`** | [Link](https://github.com/spacetab-io/docker-roadrunner-php) | ![Latest Stable Version](https://img.shields.io/github/v/release/spacetab-io/docker-roadrunner-php) ![License](https://img.shields.io/github/license/spacetab-io/docker-roadrunner-php) | +## Build the RoadRunner Image + +Create `Dockerfile.rr` in the build directory: + +{% code title="Dockerfile.rr" %} + +```dockerfile +FROM --platform=$BUILDPLATFORM golang:1.27.1 AS build + +ARG TARGETOS +ARG TARGETARCH + +WORKDIR /src + +ADD https://github.com/roadrunner-server/roadrunner/archive/b0cccd917f001b6584eafdc04ad6ba69a97cbb69.tar.gz /tmp/rr.tar.gz + +RUN tar -xzf /tmp/rr.tar.gz --strip-components=1 -C /src \ + && CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -mod=readonly -trimpath \ + -ldflags "-s -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.version=dev-b0cccd9" \ + -o /rr ./cmd/rr + +FROM scratch +COPY --from=build /rr /usr/bin/rr +``` + +{% endcode %} + +Build the image for the same target platform as your PHP application image: + +```bash +docker build -f Dockerfile.rr -t roadrunner:v6-b0cccd9 . +``` + +`roadrunner:v6-b0cccd9` is a local image that supplies the compiled binary. It does not contain PHP. For a cross-build, pass the same `--platform` value to this command and the application image build. + +## Build the Application Image Here is an example of a `Dockerfile` that can be used to build a Docker image with RoadRunner for a PHP application: @@ -17,9 +48,9 @@ Note that this example utilizes a folder named `app` for your application. If yo {% code title="Dockerfile" %} ```dockerfile -FROM ghcr.io/roadrunner-server/roadrunner:2024 as roadrunner +FROM roadrunner:v6-b0cccd9 AS roadrunner -FROM php:8.3-alpine +FROM php:8.5-cli-alpine # https://github.com/mlocati/docker-php-extension-installer # https://github.com/docker-library/docs/tree/0fbef0e8b8c403f581b794030f9180a68935af9d/php#how-to-install-more-php-extensions diff --git a/app-server/nginx-with-rr.md b/app-server/nginx-with-rr.md index 73f6ec6..6b564f4 100644 --- a/app-server/nginx-with-rr.md +++ b/app-server/nginx-with-rr.md @@ -153,11 +153,13 @@ In this example, we will demonstrate how to use RoadRunner with Nginx in a Docke ### Dockerfile +Build the [local v6 RoadRunner image](docker.md#build-the-roadrunner-image) before building this application image. + {% code title="docker/app/Dockerfile" %} -```docker -FROM --platform=${TARGETPLATFORM:-linux/amd64} ghcr.io/roadrunner-server/roadrunner:latest as roadrunner -FROM --platform=${TARGETPLATFORM:-linux/amd64} php:8.3-alpine +```dockerfile +FROM roadrunner:v6-b0cccd9 AS roadrunner +FROM php:8.5-cli-alpine COPY --from=roadrunner /usr/bin/rr /usr/local/bin/rr COPY --from=mlocati/php-extension-installer:2 /usr/bin/install-php-extensions /usr/local/bin/ @@ -177,12 +179,6 @@ ENTRYPOINT ["rr"] {% endcode %} -{% hint style="warning" %} - -Consider using the direct version in production. The `latest` image tag might be used in development environments only. - -{% endhint %} - ### RoadRunner configuration Create a `.rr.yaml` configuration file to specify how RoadRunner should interact with your PHP application. @@ -258,11 +254,9 @@ Do not forget the `composer.json` file: ```json { - "minimum-stability": "dev", - "prefer-stable": true, "require": { "nyholm/psr7": "^1.8", - "spiral/roadrunner-http": "^3.0", + "spiral/roadrunner-http": "^4.1", "spiral/goridge": "^4.0" } } diff --git a/customization/build.md b/customization/build.md index c263aa9..dd3950f 100644 --- a/customization/build.md +++ b/customization/build.md @@ -6,8 +6,6 @@ Velox builds a RoadRunner binary from the plugins listed in `velox.toml`. Use it **Velox v3 is untagged.** This guide requires development revision `6b71101ce0080143b4927cf2d84ab0ba02189b67`, not a released Velox binary. The example uses RR development source with v6 beta plugins. Use the pinned installation command below. {% endhint %} -For an existing v5 build, see the [Velox v2025.1.7 configuration](https://github.com/roadrunner-server/velox/blob/v2025.1.7/velox.toml). That release uses the older format and does not support the configuration below. - ## Configuration This TOML configuration pins the RR commit and plugin versions. The plugin tags match the requirements in the [pinned RR source](https://github.com/roadrunner-server/roadrunner/blob/b0cccd917f001b6584eafdc04ad6ba69a97cbb69/go.mod). diff --git a/customization/embedding.md b/customization/embedding.md index 84eeedf..99fd71e 100644 --- a/customization/embedding.md +++ b/customization/embedding.md @@ -7,17 +7,17 @@ program. Here's an example of how to embed RoadRunner into a Go program with an HTTP handler: -Import the RoadRunner library into your Go project. Replace `REVISION` with the tag or commit you want to embed. For v6 plugins, select a RoadRunner revision that uses v6 plugin modules; do not assume that `@latest` selects that revision. +Use Go `1.27.1`. Import the RoadRunner library at the same revision as the [source installation guide](../intro/install.md#build-from-source): -{% code title="main.go" %} +{% code title="Install the library" %} ```bash -go get github.com/roadrunner-server/roadrunner/v2025/lib@REVISION +go get github.com/roadrunner-server/roadrunner/v2025/lib@b0cccd917f001b6584eafdc04ad6ba69a97cbb69 ``` {% endcode %} -The library import remains `roadrunner/v2025/lib`. `NewRR` returns `(*RR, error)`. Use matching plugin versions and the [v6 plugin contracts](plugin.md#v6-migration); the embedding lifecycle below is unchanged. +The library imports v6 plugins and uses the [v6 plugin contracts](plugin.md#v6-migration). `NewRR` returns `(*RR, error)`. ## Create an RR instance diff --git a/grpc/grpc.md b/grpc/grpc.md index 49a9120..ac1bbe2 100644 --- a/grpc/grpc.md +++ b/grpc/grpc.md @@ -51,61 +51,20 @@ The `php_namespace` and `php_metadata_namespace` options allow you to specify th ### Generating PHP code -After defining the proto file, you need to generate the PHP files using the `protoc` compiler and the `protoc-gen-php-grpc` plugin. You can install the plugin binary using Composer or download a pre-built binary from the GitHub releases page. - -{% tabs %} - -{% tab title="Prebuilt Binary" %} - -The simplest way to get the latest version of `protoc-gen-php-grpc` plugin is to download one of the pre-built release binaries on the GitHub [releases page](https://github.com/roadrunner-server/roadrunner/releases). - -Just download the appropriate archive from the release page and extract it into your desired application directory. - -{% endtab %} - -{% tab title="Composer" %} - -If you use Composer to manage your PHP dependencies, you can install the `spiral/roadrunner-cli` package to download the latest version of `protoc-gen-php-grpc` plugin to your project's root directory. - -**Install the package** - -{% code %} +Use `protoc` `36.1` and Go `1.27.1`. Install the RoadRunner generator from its pinned source revision: ```bash -composer require spiral/roadrunner-cli +go install github.com/roadrunner-server/grpc/protoc_plugins/v5/protoc-gen-php-grpc@4965bf6d7e43 ``` -{% endcode %} - -And run the following command to download the latest version of the plugin - -{% code %} - -```bash -./vendor/bin/rr download-protoc-binary -``` - -{% endcode %} - -Server binary will be available at the root of your project. - -{% hint style="warning" %} -PHP's extensions `php-curl` and `php-zip` are required. Check with `php --modules` your installed extensions. -{% endhint %} - -{% endtab %} - -{% endtabs %} - -Once the plugin is installed, you can use the `protoc` command to compile the proto file into PHP files. +Add the Go binary installation directory to `PATH`. Create the `generated` directory before running `protoc`. **Here's an example command:** {% code %} ```bash -protoc --plugin=protoc-gen-php-grpc \ - --php_out=./generated \ +protoc --php_out=./generated \ --php-grpc_out=./generated \ proto/helloworld.proto ``` @@ -149,31 +108,19 @@ Here's an example of a `buf.yaml` file: ```yaml version: v2 -deps: - - buf.build/googleapis/googleapis:fb98f92554c17ec159a0b35ea8ffca71bac14385 - -name: buf.build// +modules: + - path: proto lint: use: - - DEFAULT - except: - - FIELD_NOT_REQUIRED - - PACKAGE_NO_IMPORT_CYCLE + - STANDARD breaking: use: - FILE - except: - - EXTENSION_NO_DELETE - - FIELD_SAME_DEFAULT ``` {% endcode %} -Note that you need to optionally replace `` and `` with your organization and project names created on the [BUF](https://login.buf.build/u/signup) website. - -In the `deps` section, you can specify the dependencies that your `.proto` file relies on. In this example, we're using a dependency from the Google APIs repository. - -Also, you may configure the linting and breaking changes rules. +The module contains the `.proto` files in `proto/`. If your files import external schemas, add their Buf modules under `deps`, run `buf dep update`, and keep the resulting `buf.lock` with your source files. Here's an example of a `buf.gen.yaml` file: @@ -182,31 +129,31 @@ Here's an example of a `buf.gen.yaml` file: ```yaml version: v2 plugins: - - remote: buf.build/protocolbuffers/php:v26.1 + - remote: buf.build/protocolbuffers/php:v36.1 out: generated/php - - remote: buf.build/community/roadrunner-server-php-grpc:v4.8.0 + - remote: buf.build/community/roadrunner-server-php-grpc:v5.3.0 out: generated/php - - remote: buf.build/protocolbuffers/go:v1.32.0 + - remote: buf.build/protocolbuffers/go:v1.36.12 out: generated/go opt: paths=source_relative - - remote: buf.build/grpc/go:v1.3.0 + - remote: buf.build/grpc/go:v1.6.2 out: generated/go opt: - paths=source_relative - require_unimplemented_servers=false - - remote: buf.build/grpc/python:v1.63.0 + - remote: buf.build/grpc/python:v1.83.1 out: generated/python - - remote: buf.build/protocolbuffers/python + - remote: buf.build/protocolbuffers/python:v36.1 out: generated/python - - remote: buf.build/protocolbuffers/pyi + - remote: buf.build/protocolbuffers/pyi:v36.1 out: generated/python ``` {% endcode %} -As you can see, the `buf.gen.yaml` file specifies the plugins that will be used to generate the code. In this example, we're using the `buf.build/community/roadrunner-server-php-grpc` plugin to generate PHP gRPC services. +Run `buf generate` from the directory that contains `buf.yaml` and `buf.gen.yaml`. The configuration pins each generator version and uses the RoadRunner plugin to generate PHP gRPC services. -Also, you may generate code for other languages like Go and Python. +For Buf output, set the `GRPC\\` Composer autoload path to `generated/php/GRPC`. The example also generates Go and Python code. ## PHP Client diff --git a/intro/install.md b/intro/install.md index ce034c4..6c50e49 100644 --- a/intro/install.md +++ b/intro/install.md @@ -1,106 +1,56 @@ -# RoadRunner β€” Installation +# RoadRunner Installation -There are several ways to install RoadRunner, depending on your needs and preferences. +Build RoadRunner with v6 plugins from revision [`b0cccd917f001b6584eafdc04ad6ba69a97cbb69`](https://github.com/roadrunner-server/roadrunner/tree/b0cccd917f001b6584eafdc04ad6ba69a97cbb69). This revision and its plugin dependencies are the target for these guides. -## Pre-built Binaries +## Requirements -The simplest way to get the latest version of RoadRunner is to download one of the pre-built release binaries, which are -available for various operating systems, including macOS, Linux, FreeBSD, and Windows. You can find these binaries on -the GitHub [releases page](https://github.com/roadrunner-server/roadrunner/releases). +- Go `1.27.1` for the source build. +- PHP `8.5` with the `sockets` extension for the examples. +- Composer 2 for PHP dependencies. +- `curl` and `tar` to download and extract the source archive. -To install RoadRunner, just download the appropriate archive from the releases page and extract it into your desired -application directory. +Check the installed versions and PHP extensions: -## Docker - -If you prefer to use RoadRunner inside a Docker container, you can use the official RoadRunner Docker -image `ghcr.io/roadrunner-server/roadrunner:latest`. - -{% hint style="info" %} -More information about available tags can be -found [here](https://github.com/roadrunner-server/roadrunner/pkgs/container/roadrunner). -{% endhint %} - -**Here is an example of usage:** - -```dockerfile -FROM php:8.x-cli -COPY --from=ghcr.io/roadrunner-server/roadrunner:2025.X.X /usr/bin/rr /usr/local/bin/rr - -# Install and configure your application -# ... - -CMD rr serve -c .rr.yaml -``` - -{% hint style="warning" %} -Don't forget to replace `2025.X.X` with the desired version of RoadRunner. -{% endhint %} - -## Composer - -If you use Composer to manage your PHP dependencies, you can install the `spiral/roadrunner-cli` package to download the -latest version of RoadRunner to your project's root directory. - -**Install the package** - -```terminal -composer require spiral/roadrunner-cli -``` - -Run the following command to download the latest version of RoadRunner: - -```terminal -./vendor/bin/rr get-binary +```bash +go version +php --version +php --modules +composer --version ``` -The server binary will be available at the root of your project. - -{% hint style="warning" %} -The `php-curl` and `php-zip` extensions are required to download RoadRunner automatically. -The `php-sockets` extension needs to be installed to run RoadRunner. -Check your installed extensions with `php --modules`. -{% endhint %} +## Build From Source -## Debian Package - -For Debian-based operating systems such as **Ubuntu**, **Mint**, and **MX**, you can download the `.deb` package from -the RoadRunner GitHub releases page and install it using dpkg. - -**Just run the following commands:** +Run these commands from your application directory. The build uses the plugin versions in the downloaded `go.mod` and produces `./rr` for the host operating system and architecture. ```bash -wget https://github.com/roadrunner-server/roadrunner/releases/download/v2024.X.X/roadrunner-2024.X.X-linux-amd64.deb -sudo dpkg -i roadrunner-2024.X.X-linux-amd64.deb +curl --fail --location --output rr-source.tar.gz \ + https://github.com/roadrunner-server/roadrunner/archive/b0cccd917f001b6584eafdc04ad6ba69a97cbb69.tar.gz +mkdir rr-source +tar -xzf rr-source.tar.gz --strip-components=1 -C rr-source +CGO_ENABLED=0 go -C rr-source build -mod=readonly -trimpath \ + -ldflags "-s -X github.com/roadrunner-server/roadrunner/v2025/internal/meta.version=dev-b0cccd9" \ + -o ../rr ./cmd/rr +./rr --version ``` -{% hint style="warning" %} -Don't forget to replace `2024.X.X` with the desired version of RoadRunner. -{% endhint %} - -## macOS package using [Homebrew](https://brew.sh/): -```terminal -brew install roadrunner -``` +Inspect the compiled module versions with `go version -m ./rr`. Keep the source revision and `go.sum` with your build inputs. To select a different set of plugins, use the [Velox build guide](../customization/build.md). -## Windows using [Chocolatey](https://community.chocolatey.org/): -```bash -choco install roadrunner -``` +## Docker -## CURL +Use the [Docker source build](../app-server/docker.md#build-the-roadrunner-image) to create a local RoadRunner image from the same revision. That guide also shows how to copy the binary into a PHP application image. -You can also install RoadRunner using curl and the `download-latest.sh` script from the RoadRunner GitHub repository. +## Composer -**Just run the following commands:** +Install the PHP packages needed by your worker. For an HTTP worker, run: ```bash -curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/roadrunner-server/roadrunner/master/download-latest.sh | sh +composer require spiral/roadrunner-http nyholm/psr7 ``` -## What's Next? +Composer installs the PHP libraries. The source build above supplies the RoadRunner binary. -After you have installed RoadRunner, you can proceed to the next steps and configure it for your needs. +## What's Next? -1. [RoadRunner β€” Configuration](./config.md). -2. [Developer Mode](../php/developer.md). +1. [Quick Start Guide](quick-start.md). +2. [RoadRunner Configuration](config.md). +3. [Developer Mode](../php/developer.md). diff --git a/intro/quick-start.md b/intro/quick-start.md index 03357b8..716d129 100644 --- a/intro/quick-start.md +++ b/intro/quick-start.md @@ -2,9 +2,9 @@ This guide walks you through getting started with RoadRunner. You'll learn how to install RoadRunner and configure it for your project. -## Step 1: Download RR for your platform +## Step 1: Build RoadRunner -To begin, you need to download RR for your platform. Visit the [RR installation guide](install.md) and download the appropriate version for your operating system. +Follow the [source installation guide](install.md#build-from-source) to build RoadRunner with v6 plugins. Keep the resulting `rr` binary in your project directory. ## Step 2: Install PHP diff --git a/php/debugging.md b/php/debugging.md index 1b2f3ed..b228169 100644 --- a/php/debugging.md +++ b/php/debugging.md @@ -18,6 +18,8 @@ To enable Xdebug in your application, set the environment variable `XDEBUG_SESSI {% code title=".rr.yaml" %} ```yaml +version: "3" + rpc: listen: tcp://127.0.0.1:6001 @@ -54,6 +56,8 @@ php -dvariables_order=EGPCS artisan octane:start --max-requests=250 --server=roa ## Xdebug for RoadRunner in Docker +Build the [local v6 RoadRunner image](../app-server/docker.md#build-the-roadrunner-image) before building the application image below. + First, create a `docker-compose.yml` file in your project root, or copy the `environment` and `extra_hosts` sections into your existing `docker-compose.yml`: {% code title="docker-compose.yml" %} @@ -81,11 +85,13 @@ services: {% endcode %} -Next, create a `.rr.yaml` file in your project root or copy the following content: +Create `api/public/` for the static middleware. Put the PHP worker and its Composer dependencies in `api/`, which is mounted at `/app`. Create `api/.rr.yaml` with this configuration: {% code title=".rr.yaml" %} ```yaml +version: "3" + rpc: listen: 'tcp://127.0.0.1:6001' http: @@ -119,12 +125,11 @@ Next, create a `Dockerfile` in your project root or copy the following content: {% code title="Dockerfile" %} ```dockerfile -FROM ghcr.io/roadrunner-server/roadrunner:2025.1.1 AS roadrunner +FROM roadrunner:v6-b0cccd9 AS roadrunner -FROM php:8.4-cli-alpine3.21 +FROM php:8.5-cli-alpine -# change version to latest if you want to use the latest version of xDebug, see https://xdebug.org -ENV XDEBUG_VERSION=3.4.4 +ENV XDEBUG_VERSION=3.5.3 RUN apk add --no-cache autoconf g++ make postgresql-dev coreutils --update linux-headers \ && pecl install xdebug-$XDEBUG_VERSION \ @@ -154,7 +159,7 @@ USER app {% endcode %} -Important: Make sure to change `XDEBUG_VERSION` to the version you want to use (see xdebug.org). Note that the example container runs as the `app` user. +The example uses Xdebug `3.5.3` and runs as the `app` user. Next, create an `xdebug.ini` file in your project root or copy the following content: From 744f414155c7855ee04ef00a5523c69267635af3 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Sun, 6 Sep 2026 23:45:02 +0200 Subject: [PATCH 16/21] docs: document per-listener UNIX socket attributes --- app-server/nginx-with-rr.md | 27 ++++++++++++++++++++++++ grpc/grpc.md | 17 +++++++++++++++ http/http.md | 23 ++++++++++++++++++++ http/static.md | 20 +++++++++++++++++- intro/config.md | 42 ++++++++++++++++++++++++++++++++++--- intro/v3-migration.md | 1 + php/rpc.md | 17 +++++++++++++++ plugins/centrifuge.md | 17 +++++++++++++++ plugins/server.md | 18 ++++++++++++++++ plugins/tcp.md | 21 +++++++++++++++++++ 10 files changed, 199 insertions(+), 4 deletions(-) diff --git a/app-server/nginx-with-rr.md b/app-server/nginx-with-rr.md index 6b564f4..b74ecf9 100644 --- a/app-server/nginx-with-rr.md +++ b/app-server/nginx-with-rr.md @@ -55,6 +55,33 @@ Consider using `fastcgi_pass` instead of `proxy_pass`: Using the `fastcgi_pass` performance in certain configurations. {% endhint %} +#### Development: Unix Socket + +The development HTTP plugin can give Nginx group access to a FastCGI socket without changing application file permissions: + +{% code title=".rr.yaml fragment" %} + +```yaml +http: + fcgi: + address: "unix:///run/roadrunner/fcgi.sock" + unix_socket: + mode: "0660" + gid: 2000 +``` + +{% endcode %} + +Replace the `fastcgi_pass` directive in the Nginx example with: + +```nginx +fastcgi_pass unix:/run/roadrunner/fcgi.sock; +``` + +Replace `2000` with the numeric group shared by RoadRunner and the Nginx worker processes. A non-root RoadRunner process must belong to this group to set the socket group. Nginx workers must also belong to this group to connect through the group permissions. The socket owner stays unchanged because `uid` is omitted. + +Create the parent directory first. RoadRunner needs permission to create the socket there. Nginx needs search permission, not write permission, on every parent directory. See [Unix socket attributes](../intro/config.md#unix-socket-attributes) for availability and startup access limits. The Docker example below does not include this development feature or a shared socket directory. + ### Proxy RoadRunner can be configured to listen for HTTP requests on a specific port. diff --git a/grpc/grpc.md b/grpc/grpc.md index ac1bbe2..ae262e1 100644 --- a/grpc/grpc.md +++ b/grpc/grpc.md @@ -582,6 +582,23 @@ grpc: {% endcode %} +### Development: Unix Socket + +The development gRPC plugin supports [Unix socket attributes](../intro/config.md#unix-socket-attributes). Keep the other gRPC settings from the preceding example: + +{% code title=".rr.yaml fragment" %} + +```yaml +grpc: + listen: "unix:///run/roadrunner/grpc.sock" + unix_socket: + mode: "0660" +``` + +{% endcode %} + +Configure clients to use the same Unix socket. These options do not change gRPC TLS credentials or worker credentials. + ### Connection age grace In v6 beta, `max_connection_age_grace` controls how long active RPCs can continue after the connection reaches `max_connection_age`. Zero or omitted grace means unlimited time. Set a finite grace to close the connection after that period. diff --git a/http/http.md b/http/http.md index a389cf7..b87023e 100644 --- a/http/http.md +++ b/http/http.md @@ -547,6 +547,29 @@ http: {% endcode %} +## Development: Unix Sockets + +The development HTTP plugin supports independent [Unix socket attributes](../intro/config.md#unix-socket-attributes) for plain HTTP and FastCGI: + +{% code title=".rr.yaml fragment" %} + +```yaml +http: + address: "unix:///run/roadrunner/http.sock" + unix_socket: + mode: "0600" + fcgi: + address: "unix:///run/roadrunner/fcgi.sock" + unix_socket: + mode: "0660" +``` + +{% endcode %} + +`http.unix_socket` also applies when H2C uses the plain HTTP listener. It does not configure FastCGI, HTTPS, HTTP/3, or ACME challenge listeners. Keep `http.fcgi.unix_socket` separate. PROXY protocol still requires TCP and cannot be used on these Unix listeners. + +See [Nginx group access](../app-server/nginx-with-rr.md#development-unix-socket) to let a web server connect without changing application file permissions. + ## Development: PROXY protocol {% hint style="warning" %} diff --git a/http/static.md b/http/static.md index 95cd659..30d6976 100644 --- a/http/static.md +++ b/http/static.md @@ -121,7 +121,7 @@ In this development build, weak ETags use file size and modification time. Stron ## Fileserver plugin The Fileserver plugin serves static files. It works similarly to the `static` HTTP middleware and has extended functionality. -The `static` middleware runs on the main HTTP endpoint, while the file server plugin uses a different port and serves only static files. +The `static` middleware runs on the main HTTP endpoint. The Fileserver plugin uses a separate listener and serves only static files. ## File server configuration @@ -191,3 +191,21 @@ fileserver: ``` {% endcode %} + +### Development: Unix Socket + +The development Fileserver plugin supports [Unix socket attributes](../intro/config.md#unix-socket-attributes). These options belong to `fileserver`, not `http.static`: + +{% code title=".rr.yaml fragment" %} + +```yaml +fileserver: + address: "unix:///run/roadrunner/files.sock" + unix_socket: + mode: "0660" + serve: + - prefix: "/" + root: "public" +``` + +{% endcode %} diff --git a/intro/config.md b/intro/config.md index 47aeecf..f419028 100644 --- a/intro/config.md +++ b/intro/config.md @@ -10,15 +10,51 @@ as . ## Configuration reference -The most recent configuration reference with all available options can be found in the `.rr.yaml` file in the RoadRunner -GitHub repository: +Use the configuration reference for the RoadRunner revision you build. The following file matches the snapshot used by the [installation guide](install.md): -- [**.rr.yaml**](https://github.com/roadrunner-server/roadrunner/blob/master/.rr.yaml) +- [**.rr.yaml**](https://github.com/roadrunner-server/roadrunner/blob/b0cccd917f001b6584eafdc04ad6ba69a97cbb69/.rr.yaml) {% hint style="warning" %} We use dots as level separators, e.g.: `http.pool`, you can't use dots in section names, queue names, etc. You can find out more about it [here](https://github.com/roadrunner-server/roadrunner/issues/1529). {% endhint %} +## Unix Socket Attributes + +{% hint style="warning" %} +Development only. These options require a build that includes the corresponding v6 plugin changes. RoadRunner `v2025.1.15` and the beta dependencies selected by the installation guide do not include them. See [Development Changes](v3-migration.md#development-changes). +{% endhint %} + +Configure each filesystem Unix listener separately. Omit its options object to keep the existing socket defaults. + +| Options object | Listener address | +| --- | --- | +| `http.unix_socket` | `http.address`, including H2C | +| `http.fcgi.unix_socket` | `http.fcgi.address` | +| `rpc.unix_socket` | `rpc.listen` | +| `grpc.unix_socket` | `grpc.listen` | +| `tcp.servers..unix_socket` | `tcp.servers..addr` | +| `centrifuge.proxy_socket` | `centrifuge.proxy_address`, the incoming proxy listener | +| `fileserver.unix_socket` | `fileserver.address` | +| `server.relay_socket` | `server.relay`, the worker communication listener | + +All objects use the same optional fields: + +| Field | Meaning | +| --- | --- | +| `mode` | Quoted octal string from `"0000"` through `"0777"`. Omitted or empty means no mode change. | +| `uid` | Numeric socket owner ID. Omit it to keep the default owner. | +| `gid` | Numeric socket group ID. Omit it to keep the default group. | + +UID and GID must be integers from `0` through `4294967294` that fit the platform's Go `int` type. Zero is an explicit ID, not an omitted value. Account names, booleans, fractional numbers, and empty ID strings are invalid. An unset environment variable used for an ID causes a configuration error; omit the field or provide a valid value. + +These options require a filesystem `unix://` address on Linux, macOS, or FreeBSD. They do not apply to TCP addresses such as `0.0.0.0:8000`, the `pipes` worker relay, Windows, or Linux abstract sockets. They do not change RoadRunner or PHP worker credentials, the process umask, or application file permissions. + +Create the parent directory before startup. RoadRunner needs permission to create the socket there. Clients need search (`x`) permission on every parent directory. The process also needs permission to apply the requested ownership changes. An unprivileged socket owner can change the socket group only to a group to which its process belongs. + +Ownership is applied before mode, after the socket starts listening. Clients can connect before these operations finish. The settings specify final attributes, not access control during startup. Existing directory permissions and umask must restrict initial access. Parent directories must prevent untrusted path replacement. If an attribute change fails, RoadRunner closes that listener and reports the error. + +See [Nginx group access](../app-server/nginx-with-rr.md#development-unix-socket) for a FastCGI example. + ## Configuration file The RoadRunner looks for a configuration file named `.rr.yaml` in the same directory as the server binary. diff --git a/intro/v3-migration.md b/intro/v3-migration.md index ca80d4b..67c3d8c 100644 --- a/intro/v3-migration.md +++ b/intro/v3-migration.md @@ -91,6 +91,7 @@ These changes are not in the beta dependencies selected by the RR snapshot above | Component | Pending change | | --- | --- | +| Unix sockets | Optional [mode, owner, and group settings](config.md#unix-socket-attributes) for HTTP, FastCGI, RPC, gRPC, named TCP servers, Centrifuge proxy listeners, Fileserver, and worker relays. Requires the corresponding plugin changes with `tcplisten v1.6.0`. Existing defaults remain unchanged. | | HTTP | [PROXY protocol support](https://github.com/roadrunner-server/http/commit/8bebd3b) for plain HTTP and HTTPS listeners. It requires trusted proxy addresses and changes how load balancers and readiness checks connect. See the [development configuration](../http/http.md#development-proxy-protocol). | | Static | [Prefix and cache controls](https://github.com/roadrunner-server/static/commit/030052b), normalized path checks, and revised ETags. Positive hits still open and stat files; cached misses can delay newly created files. See [Static files](../http/static.md). | | Pool | [Allocation and shutdown fixes](https://github.com/roadrunner-server/pool/commit/2ae6f57): retry acquisition after scale-up, cancel spawning during shutdown, reap failed or late workers, and correct supervisor state transitions. Do not assume beta.1 contains these fixes. | diff --git a/php/rpc.md b/php/rpc.md index eedf6b5..05c6f36 100644 --- a/php/rpc.md +++ b/php/rpc.md @@ -47,6 +47,23 @@ rpc: {% endcode %} +### Development: Unix Socket + +The development RPC plugin can set [Unix socket attributes](../intro/config.md#unix-socket-attributes) independently of worker credentials: + +{% code title=".rr.yaml fragment" %} + +```yaml +rpc: + listen: "unix:///run/roadrunner/rpc.sock" + unix_socket: + mode: "0600" +``` + +{% endcode %} + +Direct clients must use the same listener address. PHP workers can read it through the environment-based client shown below. Keep RPC permissions separate from sockets used by a web server. + ## Connecting to RoadRunner Once you have installed Goridge, you can connect to the RoadRunner server. To do so, create an instance of diff --git a/plugins/centrifuge.md b/plugins/centrifuge.md index 7835ec2..80f2bc6 100644 --- a/plugins/centrifuge.md +++ b/plugins/centrifuge.md @@ -132,6 +132,23 @@ For example: endpoint address of roadrunner server with activated centrifuge plugin. {% endhint %} +### Development: Unix Socket + +The development Centrifuge plugin supports [Unix socket attributes](../intro/config.md#unix-socket-attributes) for its incoming proxy listener: + +{% code title=".rr.yaml fragment" %} + +```yaml +centrifuge: + proxy_address: "unix:///run/roadrunner/centrifuge.sock" + proxy_socket: + mode: "0660" +``` + +{% endcode %} + +Configure Centrifugo to connect to the same Unix socket. `proxy_socket` does not configure `grpc_api_address` or the TLS client used for outgoing API calls. + ### PHP worker example This worker authenticates one configured service account with a bearer token. Set `APP_CENTRIFUGO_USER` to that account's ID in the RoadRunner process environment. Generate a token with the following command: diff --git a/plugins/server.md b/plugins/server.md index b10a0b7..01067a6 100644 --- a/plugins/server.md +++ b/plugins/server.md @@ -71,6 +71,24 @@ processes. It does not require any network connections or external libraries, ma Use `on_init.user` option to execute the on_init command under a different user. {% endhint %} +### Development: Unix Relay + +The development server plugin supports [Unix socket attributes](../intro/config.md#unix-socket-attributes) for the shared worker relay: + +{% code title=".rr.yaml fragment" %} + +```yaml +server: + command: "php worker.php" + relay: "unix:///run/roadrunner/relay.sock" + relay_socket: + mode: "0600" +``` + +{% endcode %} + +`relay_socket` controls only the socket file. It does not select the [worker user](#worker-user) or `on_init.user`. Its permissions must allow the configured workers to connect. Omit this object when using the default `pipes` relay or TCP. + ### Server initialization The `on_init` section is used for application initialization or warming up before starting workers. It allows you to set diff --git a/plugins/tcp.md b/plugins/tcp.md index f449fbb..e1ba1f1 100644 --- a/plugins/tcp.md +++ b/plugins/tcp.md @@ -125,6 +125,27 @@ tcp: - `pool`: Configuration for the PHP worker pool for the TCP servers. See https://docs.roadrunner.dev/docs/php-worker/pool for available parameters. +### Development: Unix Sockets + +The development TCP plugin supports [Unix socket attributes](../intro/config.md#unix-socket-attributes) on each named server. Keep the worker command and pool configuration from the preceding example: + +{% code title=".rr.yaml fragment" %} + +```yaml +tcp: + servers: + local: + addr: "unix:///run/roadrunner/tcp.sock" + unix_socket: + mode: "0660" + network: + addr: "tcp://127.0.0.1:8889" +``` + +{% endcode %} + +`unix_socket` applies only to the named server that contains it. Do not set it on a TCP address or at the `tcp` root. The plugin still requires a custom RoadRunner build. + ## PHP client The RoadRunner TCP plugin comes with a convenient PHP package that simplifies the process of integrating the plugin with From fbe02d2220b6240154f6aed6ffef37518fb1f897 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Mon, 7 Sep 2026 11:50:33 +0200 Subject: [PATCH 17/21] docs: clarify UNIX socket ID conversion --- intro/config.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/intro/config.md b/intro/config.md index f419028..9da8453 100644 --- a/intro/config.md +++ b/intro/config.md @@ -45,7 +45,9 @@ All objects use the same optional fields: | `uid` | Numeric socket owner ID. Omit it to keep the default owner. | | `gid` | Numeric socket group ID. Omit it to keep the default group. | -UID and GID must be integers from `0` through `4294967294` that fit the platform's Go `int` type. Zero is an explicit ID, not an omitted value. Account names, booleans, fractional numbers, and empty ID strings are invalid. An unset environment variable used for an ID causes a configuration error; omit the field or provide a valid value. +Use numeric UID and GID values from `0` through `4294967294` that fit the platform's Go `int` type. Zero is an explicit ID, not an omitted value. Account names are not supported. + +The configuration provider converts values before socket validation. Viper can convert booleans and fractional numbers to integer IDs. An unset or empty environment variable can become ID `0`, rather than cause an error. Set ID variables explicitly, or omit the fields to keep ownership unchanged. These options require a filesystem `unix://` address on Linux, macOS, or FreeBSD. They do not apply to TCP addresses such as `0.0.0.0:8000`, the `pipes` worker relay, Windows, or Linux abstract sockets. They do not change RoadRunner or PHP worker credentials, the process umask, or application file permissions. From fa70ef818edcba480aca1754c553515a0523ee62 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Mon, 7 Sep 2026 16:56:33 +0200 Subject: [PATCH 18/21] docs: clarify UNIX socket configuration and shutdown --- http/http.md | 4 ++++ intro/config.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/http/http.md b/http/http.md index b87023e..6ad22b0 100644 --- a/http/http.md +++ b/http/http.md @@ -568,6 +568,10 @@ http: `http.unix_socket` also applies when H2C uses the plain HTTP listener. It does not configure FastCGI, HTTPS, HTTP/3, or ACME challenge listeners. Keep `http.fcgi.unix_socket` separate. PROXY protocol still requires TCP and cannot be used on these Unix listeners. +A socket options object does not enable a listener. Set its `address` to a filesystem Unix socket. Omit its socket options when the listener is disabled. + +During shutdown, the HTTP plugin closes the FastCGI listener. This stops new connections. It does not close FastCGI connections that the server already accepted. + See [Nginx group access](../app-server/nginx-with-rr.md#development-unix-socket) to let a web server connect without changing application file permissions. ## Development: PROXY protocol diff --git a/intro/config.md b/intro/config.md index 9da8453..487fd9a 100644 --- a/intro/config.md +++ b/intro/config.md @@ -26,6 +26,8 @@ Development only. These options require a build that includes the corresponding Configure each filesystem Unix listener separately. Omit its options object to keep the existing socket defaults. +The configuration provider can discard an empty options object (`{}`). It then behaves like omission, including on TCP addresses or the `pipes` worker relay. + | Options object | Listener address | | --- | --- | | `http.unix_socket` | `http.address`, including H2C | @@ -51,6 +53,8 @@ The configuration provider converts values before socket validation. Viper can c These options require a filesystem `unix://` address on Linux, macOS, or FreeBSD. They do not apply to TCP addresses such as `0.0.0.0:8000`, the `pipes` worker relay, Windows, or Linux abstract sockets. They do not change RoadRunner or PHP worker credentials, the process umask, or application file permissions. +The plugin validates the decoded socket options during initialization, before it opens that listener. Invalid modes, out-of-range IDs, and incompatible listener addresses fail initialization. This validation does not check filesystem permissions. + Create the parent directory before startup. RoadRunner needs permission to create the socket there. Clients need search (`x`) permission on every parent directory. The process also needs permission to apply the requested ownership changes. An unprivileged socket owner can change the socket group only to a group to which its process belongs. Ownership is applied before mode, after the socket starts listening. Clients can connect before these operations finish. The settings specify final attributes, not access control during startup. Existing directory permissions and umask must restrict initial access. Parent directories must prevent untrusted path replacement. If an attribute change fails, RoadRunner closes that listener and reports the error. From aa916957467f31b5c6552366a33786971f2ab629 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Mon, 7 Sep 2026 19:57:17 +0200 Subject: [PATCH 19/21] docs: prepare v3 migration guide for release --- README.md | 2 +- intro/compatibility.md | 2 +- intro/config.md | 2 +- intro/v3-migration.md | 51 +++++++++++++++------------------------ queues/boltdb.md | 2 +- queues/overview-queues.md | 8 +++--- 6 files changed, 28 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index f7e79bc..c7e29a0 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ RoadRunner includes [PSR-7](https://www.php-fig.org/psr/psr-7), [PSR-17](https:/ - Compatible with Windows, WSL2, FreeBSD, GNU/Linux, etc. - And more πŸ˜‰ -See [v3 Migration](intro/v3-migration.md) for changes in the v6 plugin beta line and the matching development builds. +See [v3 Migration](intro/v3-migration.md) for the v5-to-v6 plugin changes and upgrade checks for RoadRunner v3. If you have a feature request in mind, you can check out [GitHub issues](https://github.com/roadrunner-server/roadrunner/issues) page. Here you'll find a list of open diff --git a/intro/compatibility.md b/intro/compatibility.md index 1e0c6f4..9ff6473 100644 --- a/intro/compatibility.md +++ b/intro/compatibility.md @@ -2,7 +2,7 @@ This section provides information about upgrading your RoadRunner configuration to the latest version. -For the upcoming release, see [v3 Migration](v3-migration.md). It covers the v5-to-v6 plugin changes, beta limitations, and development-only features. The configuration format remains `version: "3"`. +For RoadRunner v3, see [v3 Migration](v3-migration.md). It covers the v5-to-v6 plugin changes, upgrade checks, and known limits. The configuration format remains `version: "3"`. ## Compatibility matrix diff --git a/intro/config.md b/intro/config.md index 487fd9a..b6cf22f 100644 --- a/intro/config.md +++ b/intro/config.md @@ -21,7 +21,7 @@ We use dots as level separators, e.g.: `http.pool`, you can't use dots in sectio ## Unix Socket Attributes {% hint style="warning" %} -Development only. These options require a build that includes the corresponding v6 plugin changes. RoadRunner `v2025.1.15` and the beta dependencies selected by the installation guide do not include them. See [Development Changes](v3-migration.md#development-changes). +RoadRunner v3 supports these options through its v6 plugins. RoadRunner `v2025.1.15` does not include them. See [New Features](v3-migration.md#new-features). {% endhint %} Configure each filesystem Unix listener separately. Omit its options object to keep the existing socket defaults. diff --git a/intro/v3-migration.md b/intro/v3-migration.md index 67c3d8c..fa09656 100644 --- a/intro/v3-migration.md +++ b/intro/v3-migration.md @@ -1,12 +1,8 @@ # v3 Migration -This guide compares the v5 plugins in [RoadRunner v2025.1.15](https://github.com/roadrunner-server/roadrunner/blob/v2025.1.15/go.mod) with the v6 beta versions selected by the [RoadRunner development snapshot](https://github.com/roadrunner-server/roadrunner/blob/b0cccd917f001b6584eafdc04ad6ba69a97cbb69/go.mod). It covers application behavior, configuration, and custom Go plugins. +This guide covers migration from [RoadRunner v2025.1.15](https://github.com/roadrunner-server/roadrunner/blob/v2025.1.15/go.mod) with v5 plugins to RoadRunner v3 with v6 plugins. It covers application behavior, configuration, and custom Go plugins. -{% hint style="warning" %} -The v6 plugin line is in beta. These notes do not describe a published RoadRunner v3 binary. Changes that are not in the selected beta versions are listed under [Development Changes](#development-changes). -{% endhint %} - -Keep `version: "3"` in `.rr.yaml`. That value identifies the configuration format. The inspected RR Go module remains `github.com/roadrunner-server/roadrunner/v2025`; its plugins use `/v6`. Source builds require Go 1.27. +Keep `version: "3"` in `.rr.yaml`. That value identifies the configuration format, not the RoadRunner release. The plugins use `/v6` Go module paths. Source builds require Go 1.27. ## Upgrade Checks @@ -19,19 +15,23 @@ Keep `version: "3"` in `.rr.yaml`. That value identifies the configuration forma | RPC | Goridge v4 rejects MessagePack. Use JSON, protobuf, Gob, or raw bytes as appropriate for the method. RPC still uses Goridge and `net/rpc`, not Connect. See [RPC compatibility](../php/rpc.md#v6-compatibility). | | TCP plugin | The default RR build no longer includes the [TCP plugin](../plugins/tcp.md). A `tcp:` section cannot add it. This does not remove `tcp://` RPC transport. | | Centrifuge | Remove calls to `centrifuge.RateLimit`; the RPC has no replacement in the plugin. Check [Centrifuge](../plugins/centrifuge.md) and the DTO migration before updating direct clients. | -| Job headers | `pool` is now a routing header. Rename application headers that use this name. External producers must supply a valid pool when [named worker pools](../queues/overview-queues.md#named-worker-pools-v6-beta) are enabled. | +| Job headers | `pool` is now a routing header. Rename application headers that use this name. External producers must supply a valid pool when [named worker pools](../queues/overview-queues.md#named-worker-pools) are enabled. | | Environment files | A configured root `envfile` is now loaded without experimental mode. Supply the file or remove an unused setting; a missing file fails startup. See [Environment](../php/environment.md). | | gRPC reflection | Reflection is registered automatically. Unary interceptors do not protect its streams. Review network access and mTLS in [gRPC](../grpc/grpc.md#server-reflection). | ## New Features -- [Jobs worker pools](../queues/overview-queues.md#named-worker-pools-v6-beta): assign pipelines to separate named pools. The existing single `jobs.pool` format remains available; do not configure it together with `jobs.pools`. +- [Jobs worker pools](../queues/overview-queues.md#named-worker-pools): assign pipelines to separate named pools. The existing single `jobs.pool` format remains available; do not configure it together with `jobs.pools`. - [AMQP version 2 configuration](../queues/amqp.md#version-2-configuration-recommended): separate exchange and queue settings, with controls for declaration and binding. Legacy flat configuration remains supported. Runtime `jobs.Declare` still uses flat keys. - [NSQ](../queues/nsq.md): a new bundled Jobs driver with topics, channels, discovery, acknowledgements, and delayed delivery. Its retry limit and lack of a dead-letter handoff require application failure handling. - [gRPC reflection](../grpc/grpc.md#server-reflection): v1 and v1alpha service listing. Full PHP-service descriptors require a [Protoreg plugin](../grpc/protoreg.md). Unary interceptors already existed in v5.3.0; they are not a new v6 feature. - [Trusted proxy headers](../http/proxy.md): select and order the forwarding headers that a trusted proxy may supply. An empty list restores the defaults; it does not disable header trust. - [Temporal](../workflow/temporal.md): configurable worker heartbeats and [dynamic workflows](../workflow/worker.md). Worker heartbeats are separate from activity heartbeats. Dynamic registration requires a compatible PHP SDK. - [Centrifuge](../plugins/centrifuge.md): forwards `NotifyCacheEmpty` events to PHP. Enable this only with matching DTO and handler support. +- [Unix socket attributes](config.md#unix-socket-attributes): configure mode, owner, and group independently for HTTP, FastCGI, RPC, gRPC, named TCP servers, Centrifuge proxy listeners, Fileserver, and worker relays. Omitted settings keep the existing defaults. +- [PROXY protocol](../http/http.md#development-proxy-protocol): accept trusted proxy addresses on plain HTTP and HTTPS listeners. Configure load balancers and readiness checks to send the required PROXY header. +- [Static file controls](../http/static.md): configure URL prefixes, cache lifetimes, and cache limits. The middleware normalizes paths before access checks and uses revised ETags. Positive cache hits still open and stat files. Cached misses can delay newly created files. +- [Zstd middleware](../http/zstd.md): add response compression with Zstandard. Include and register the plugin before selecting `http.middleware: ["zstd"]`. ## Bug Fixes @@ -41,6 +41,7 @@ Keep `version: "3"` in `.rr.yaml`. That value identifies the configuration forma | gRPC | `max_connection_age_grace` now uses its configured value instead of `max_connection_age`. Check connection-draining settings. Standard `google.rpc` error details are included in logs; check them for sensitive data. | | X-Sendfile | Empty files no longer enter the read loop. File responses use `application/octet-stream`, even when the worker supplied another content type. | | Fileserver | Initialization rejects missing addresses, empty route lists, and invalid prefixes. Listener failure and shutdown no longer retain the plugin lock. | +| Pool | Worker acquisition retries after dynamic scale-up. Shutdown cancels worker allocation. Allocation cleanup reaps failed and late workers. Supervisor state transitions are atomic. | | Jobs | Pipeline restart no longer destroys the replacement pipeline. Empty-queue pollers and shutdown timeout handling no longer prevent clean shutdown. | | NATS | Stopping a pipeline no longer purges its stream. Retry headers are retained. Configure retention and make workers tolerate redelivery. | | SQS | FIFO retries use a fresh deduplication ID, so the broker does not discard a retry as the original message. The application job ID stays unchanged. Priority is read from message attributes. | @@ -52,7 +53,7 @@ Keep `version: "3"` in `.rr.yaml`. That value identifies the configuration forma | BoltDB Jobs | Recovery removes stale in-flight records after returning jobs to the queue. Delayed jobs are dispatched after commit. Workers must still tolerate repeated deliveries. | | Server | Invalid worker users fail initialization. `server.on_init.env` overrides inherited values. Use command sequences for arguments containing spaces; scalar commands do not parse shell quotes. | | Service | A failed replacement removes the service entry and requests a stop for replacements already started. Fix the command and create the service again. Restart is not rolling or atomic. | -| Temporal | Polling stops before PHP pools are destroyed. An ordinary activity-worker exit no longer resets the whole activity pool. | +| Temporal | Polling stops before PHP pools are destroyed. An ordinary activity-worker exit no longer resets the whole activity pool. Activity-heartbeat RPCs no longer deadlock with shutdown. | | Centrifuge | Missing gRPC metadata no longer causes a panic. A missing worker pool reports unavailable status. | | Metrics and status | Implicit HTTP success is labeled `200`, not `-1`. Jobs distinguishes successful and requeued jobs, and totals use counters. Negative counter increments return errors. Shutdown readiness uses `unavailable_status_code`. | | Locks and RPC | Lock wait paths release their mutexes correctly. Malformed RPC offsets and invalid response metadata return errors instead of causing slice or type-assertion panics. | @@ -71,30 +72,18 @@ The API repositories have separate roles: Update custom plugins to `*slog.Logger`, the new import paths, and context-aware constructors and storage methods. The pool module is now `pool/v2`; Goridge is `goridge/v4`. Use the [import and signature migration table](../customization/plugin.md#v6-migration) and the updated [Jobs driver](../customization/jobs-driver.md) and [middleware](../customization/middleware.md) examples. -API relocation alone does not require a PHP worker-loop rewrite or change the Goridge frame version. Lock DTO namespaces and some Centrifugo messages have separate source-level changes; see [DTO compatibility](../customization/plugin.md#dto-compatibility). Do not use the Connect clients or general v2 DTO packages from intermediate betas. - -RR still pins `endure/v2 v2.6.2` and `tcplisten v1.5.2`, as it did in v2025.1.15. There is no new Endure lifecycle, dependency-injection, or tcplisten caller migration in this comparison. - -## Beta Limits +API relocation alone does not require a PHP worker-loop rewrite or change the Goridge frame version. Lock DTO namespaces and some Centrifugo messages have separate source-level changes; see [DTO compatibility](../customization/plugin.md#dto-compatibility). Use the v1 DTO packages listed in the migration table. -| Component | Limitation and action | -| --- | --- | -| `pool/v2 v2.0.0-beta.1` | Scale-up does not retry the request that triggered allocation. That request can fail even after new workers start. Size the base pool and allocation timeout for the expected load. The initial limit is 500 workers; combined base and dynamic capacity is 2048. See [Automatic scaling](../php/auto-scaling.md). | -| `jobs/v6 v6.0.0-beta.10` | An explicit `jobs.pool` with omitted or zero `num_workers` produces only two pollers. Set `num_workers` explicitly; setting `num_pollers` does not override the derived count. | -| BoltDB Jobs | `boltdb/v6 v6.0.0-beta.5` loses the `pool` header during storage, including for newly published jobs. Keep a single `jobs.pool` when consuming BoltDB pipelines with jobs beta.10. Named pools cannot route these jobs. | -| `memory/v6 v6.0.0-beta.5` | `MExpire` with a past deadline or less than one second remaining can replace a value and remove its expiration. Use `Delete` for immediate removal. See [Memory KV](../kv/memory.md). | -| `service/v6 v6.0.0-beta.8` | After an automatic restart, `service.Restart` can start replacements before the old processes finish. Do not rely on it for exclusive process replacement when `remain_after_exit` is enabled. | +The Endure lifecycle and dependency-injection contracts are unchanged. Existing `tcplisten.CreateListener` callers need no changes. Use `tcplisten.CreateListenerWithOptions` from `tcplisten v1.6.0` or later to configure filesystem Unix socket attributes. -## Development Changes +Velox v3 uses [module-based build configuration](../customization/build.md) with replacements, exclusions, version-pin checks, and deterministic build inputs. Windows targets and the remote build server are removed. -These changes are not in the beta dependencies selected by the RR snapshot above. Use a custom build containing the linked change, or wait for a version that includes it. +## Known Limits -| Component | Pending change | +| Component | Limitation and action | | --- | --- | -| Unix sockets | Optional [mode, owner, and group settings](config.md#unix-socket-attributes) for HTTP, FastCGI, RPC, gRPC, named TCP servers, Centrifuge proxy listeners, Fileserver, and worker relays. Requires the corresponding plugin changes with `tcplisten v1.6.0`. Existing defaults remain unchanged. | -| HTTP | [PROXY protocol support](https://github.com/roadrunner-server/http/commit/8bebd3b) for plain HTTP and HTTPS listeners. It requires trusted proxy addresses and changes how load balancers and readiness checks connect. See the [development configuration](../http/http.md#development-proxy-protocol). | -| Static | [Prefix and cache controls](https://github.com/roadrunner-server/static/commit/030052b), normalized path checks, and revised ETags. Positive hits still open and stat files; cached misses can delay newly created files. See [Static files](../http/static.md). | -| Pool | [Allocation and shutdown fixes](https://github.com/roadrunner-server/pool/commit/2ae6f57): retry acquisition after scale-up, cancel spawning during shutdown, reap failed or late workers, and correct supervisor state transitions. Do not assume beta.1 contains these fixes. | -| Temporal | [Shutdown deadlock fix](https://github.com/temporalio/roadrunner-temporal/commit/bcadca6) for concurrent activity-heartbeat RPCs. It is not in `v6.0.0-beta.1`. | -| Velox v3 | New module-based plugin configuration, replacements/exclusions, version-pin checks, and deterministic build inputs. Windows targets and the remote build server are removed. No v3 tag is published; the [build guide](../customization/build.md) pins the development revision. | -| Zstd | A separate [response-compression middleware](../http/zstd.md), not yet in the inspected default RR build. Include and register the plugin before selecting `http.middleware: ["zstd"]`. | +| Pool | The initial `num_workers` limit is 500 per pool. Base and additional workers cannot exceed 2048 in total. `dynamic_allocator.max_workers` counts additional workers. | +| Jobs | An explicit `jobs.pool` with omitted or zero `num_workers` produces only two pollers. Set a positive `num_workers` value. Setting `num_pollers` does not override the derived count. | +| BoltDB Jobs | Storage does not preserve the `pool` header, including for newly published jobs. Use a single `jobs.pool` with BoltDB pipelines. Named pools cannot route these jobs. | +| Memory KV | `kv.MExpire` with a past deadline or less than one second remaining can replace a value and remove its expiration. Use `kv.Delete` for immediate removal. See [Memory KV](../kv/memory.md). | +| Service | After an automatic restart, `service.Restart` can start replacements before the old processes finish. Do not rely on it for exclusive process replacement when `remain_after_exit` is enabled. | diff --git a/queues/boltdb.md b/queues/boltdb.md index 0902808..4a3b631 100644 --- a/queues/boltdb.md +++ b/queues/boltdb.md @@ -8,7 +8,7 @@ pipelines or for the for KV plugin and Jobs plugin. This is a boltdb limitation the same file. {% hint style="warning" %} -In `v6.0.0-beta.5`, stored jobs do not retain application headers, including the `pool` routing header. Use a single `jobs.pool`, not [named worker pools](overview-queues.md#named-worker-pools-v6-beta), when consuming BoltDB pipelines with jobs `v6.0.0-beta.10`. This restriction applies to new jobs as well as existing data. +Stored jobs do not retain application headers, including the `pool` routing header. Use a single `jobs.pool` when consuming BoltDB pipelines. [Named worker pools](overview-queues.md#named-worker-pools) cannot route these jobs. This restriction applies to new jobs as well as existing data. {% endhint %} ## Configuration diff --git a/queues/overview-queues.md b/queues/overview-queues.md index 704606c..8d4b0f6 100644 --- a/queues/overview-queues.md +++ b/queues/overview-queues.md @@ -114,7 +114,7 @@ Kafka pipelines with a configured consumer group use partition offset commits. A - `pool`: All settings in this section are similar to the worker pool settings described on the [configuration page](https://roadrunner.dev/docs/intro-config). -- `pools`: Named worker pools in jobs v6 beta. Use this instead of `pool`. See [Named Worker Pools](#named-worker-pools-v6-beta). +- `pools`: Named worker pools in jobs v6. Use this instead of `pool`. See [Named Worker Pools](#named-worker-pools). - `consume`: Contains an array of the names of all queues specified in the `"pipelines"` section, which should be processed by the concierge specified in @@ -128,9 +128,9 @@ Kafka pipelines with a configured consumer group use partition offset commits. A In jobs `v6.0.0-beta.10`, an explicit `jobs.pool` with omitted or zero `num_workers` creates only two pollers, even after the pool selects its default worker count. Set `jobs.pool.num_workers` to a value greater than zero. Setting `num_pollers` does not correct this. {% endhint %} -### Named Worker Pools (v6 Beta) +### Named Worker Pools -The jobs v6 beta line (`v6.0.0-beta.10`) supports named worker pools. Jobs v5 does not support this configuration. Use `jobs.pools` instead of `jobs.pool`. Setting both is an error. +Jobs v6 supports named worker pools. Jobs v5 does not support this configuration. Use `jobs.pools` instead of `jobs.pool`. Setting both is an error. Set each pipeline's `pool` to a configured pool name: @@ -408,7 +408,7 @@ IP address, the user's token or session id, etc. Headers can only contain string values and are not serialized in any way during transmission, so be careful when specifying them. -In jobs v6 beta, `pool` is [reserved for worker-pool routing](#named-worker-pools-v6-beta). +In jobs v6, `pool` is [reserved for worker-pool routing](#named-worker-pools). In the case to add a new header to the task, you can use methods [similar to PSR-7](https://www.php-fig.org/psr/psr-7/). From cfad5d7f27d863588b4c7003092095f52aea4e2d Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Tue, 8 Sep 2026 09:13:52 +0200 Subject: [PATCH 20/21] docs(http): document bundled rate limiter --- SUMMARY.md | 1 + http/http.md | 3 ++ http/rate-limiter.md | 99 +++++++++++++++++++++++++++++++++++++++++++ intro/v3-migration.md | 1 + 4 files changed, 104 insertions(+) create mode 100644 http/rate-limiter.md diff --git a/SUMMARY.md b/SUMMARY.md index 31cde14..69ab078 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -85,6 +85,7 @@ - [Intro into HTTP](http/http.md) - [Headers and CORS](http/headers.md) - [Proxy IP parser](http/proxy.md) +- [Rate limiter](http/rate-limiter.md) - [Static files](http/static.md) - [X-Sendfile](http/sendfile.md) - [Streaming](http/resp-streaming.md) diff --git a/http/http.md b/http/http.md index 6ad22b0..48ee347 100644 --- a/http/http.md +++ b/http/http.md @@ -2,6 +2,8 @@ HTTP plugin is used to pass `HTTP`/`HTTPS`/`fCGI`/`HTTP2(h2c)`/`HTTP3` requests to the PHP worker. +The upcoming bundle adds [HTTP rate limiting](rate-limiter.md) with global, IP, or header keys. The pinned source build does not yet include this middleware; see its [availability](rate-limiter.md#availability). + ## Configuration reference {% code title=".rr.yaml" %} @@ -38,6 +40,7 @@ http: # Middleware names depend on the plugins in the build. Requests run left to right in v6. # The "zstd" middleware requires a build that includes the zstd plugin. + # The upcoming "rate_limiter" middleware uses http.rate_limiter settings. # # Default value: [] middleware: [ "headers", "gzip" ] diff --git a/http/rate-limiter.md b/http/rate-limiter.md new file mode 100644 index 0000000..54672f8 --- /dev/null +++ b/http/rate-limiter.md @@ -0,0 +1,99 @@ +# HTTP - Rate Limiter + +The `rate_limiter` middleware limits requests before they reach PHP. It uses one process-local token bucket policy with a global, IP, or header key. + +## Availability + +{% hint style="info" %} +This middleware is planned for the RoadRunner bundle in the upcoming v3 release with v6 plugins. The pinned [RR source build](../intro/install.md) at `b0cccd917f001b6584eafdc04ad6ba69a97cbb69` does not include it. The implementation is in [rate-limiter PR #1](https://github.com/roadrunner-server/rate-limiter/pull/1). +{% endhint %} + +## Configuration + +Add `rate_limiter` to `http.middleware`. Define its policy under `http.rate_limiter`: + +{% code title=".rr.yaml" %} + +```yaml +version: "3" + +server: + command: "php worker.php" + relay: pipes + +http: + address: "127.0.0.1:8080" + middleware: ["rate_limiter"] + rate_limiter: + key: ip + rate: 10 + interval: 1s + burst: 20 + max_entries: 10000 +``` + +{% endcode %} + +| Field | Default | Meaning | +| --- | --- | --- | +| `key` | `ip` | One of `global`, `ip`, or `header`. | +| `rate` | Required | Positive integer. Tokens added per `interval`. | +| `interval` | `1s` | Positive Go duration, such as `250ms`, `1s`, or `1m`. | +| `burst` | `1` | Positive integer. Bucket capacity and initial token count. | +| `max_entries` | `10000` | Positive integer. Maximum number of stored buckets. | +| `header` | Empty | A valid HTTP header name is required for `key: header`. It must be empty in other modes. | + +Omitted fields use their defaults. An explicit zero or negative value for `rate`, `interval`, `burst`, or `max_entries` is invalid. Invalid configuration fails initialization. An absent `http.rate_limiter` section disables the plugin. A configured policy has no effect unless the middleware list selects it. + +## Token Accounting + +The middleware uses the [`golang.org/x/time/rate` token bucket](https://pkg.go.dev/golang.org/x/time/rate#Limiter). Each new bucket starts with `burst` tokens. Each allowed request uses one token. Tokens refill continuously at `rate / interval`, up to `burst`. Rejected requests do not reserve future tokens. + +For example, `rate: 60`, `interval: 1m`, and `burst: 10` allow 10 requests immediately and add one token per second. This is not a fixed-window limit of 60 requests per minute. The bucket does not reset at minute boundaries, and `burst` can exceed `rate`. + +## Client Identity + +| Key | Bucket identity | +| --- | --- | +| `global` | All requests share one bucket. | +| `ip` | The normalized IP from `RemoteAddr`, without its port. IPv4-mapped IPv6 addresses share the IPv4 bucket. | +| `header` | The trimmed value of the configured header. Values are case-sensitive. The plugin stores a SHA-256 hash, not the raw value. For `Host`, it reads Go's `Request.Host` field. | + +For header mode, set `key: header` and `header: X-Client-ID`. A header value is not proof of identity. For per-user limits, a trusted upstream must supply a stable, verified identity and overwrite client-supplied values. A client that controls the selected value can obtain new buckets and fill the map. + +IP mode reads `RemoteAddr`. It does not read `Forwarded` or `X-Forwarded-For` directly. Behind a proxy, configure the [Proxy IP parser](proxy.md) and place it before the limiter. The proxy must overwrite the selected forwarding headers. Without this setup, IP mode can limit the proxy address or use a forged client identity. Use `global` or `header` mode when the transport provides no client IP. + +## Responses + +| Condition | Response | +| --- | --- | +| A token is available | Call the next handler without changing its response. | +| The selected header is missing or empty after trimming | `400 Bad Request`. | +| `RemoteAddr` has no valid IP in IP mode | `400 Bad Request`. | +| The bucket has no token | `429 Too Many Requests` with `Retry-After`. | +| A new identity arrives when the map is full | `503 Service Unavailable`. Existing identities still use their buckets. | + +The middleware sets `Cache-Control: no-store` on its error responses. `Retry-After` gives the delay until the next token, rounded up to whole seconds, with a minimum of `1`. It does not reserve capacity; other requests can consume the token first. Rejected requests do not reach PHP. Rejected HEAD requests have no response body. + +See [RFC 6585, section 4](https://www.rfc-editor.org/rfc/rfc6585.html#section-4) for `429` and [RFC 9110, section 10.2.3](https://www.rfc-editor.org/rfc/rfc9110.html#section-10.2.3) for `Retry-After`. + +## State And Cleanup + +All HTTP listeners and PHP workers in one RoadRunner process share the policy state. Separate RoadRunner processes have independent quotas. A process restart clears the state. An HTTP worker reset does not clear it. + +`max_entries` bounds the number of buckets. Cleanup runs during requests, at most once per minute. It removes only fully refilled buckets. The plugin does not evict depleted buckets to admit new identities. Idle entries can remain until a later request starts cleanup. There is no background cleanup worker. + +The middleware provides HTTP admission control only. It has no RPC API, PHP-supplied policy updates, database lookups, path rules, or distributed storage. + +## Middleware Order + +In v6, requests enter middleware from left to right. Place `proxy_ip_parser` before the limiter to resolve client IPs from trusted proxy headers. Place `headers` before it when error responses need CORS headers. Place `http_metrics` and `otel` before it to include rejected requests in metrics and tracing. + +```yaml +http: + middleware: ["otel", "proxy_ip_parser", "headers", "http_metrics", "rate_limiter"] +``` + +A handler that answers before the limiter consumes no token. This includes CORS preflight responses when `headers` comes first. Place the limiter before `static` to count static-file requests. Place `static` first to let its responses bypass the limit. + +The built-in HTTP access logger runs inside the configured middleware chain. It does not record limiter rejections. The limiter's internal OpenTelemetry span ends before the next handler starts. The HTTP server span includes downstream request time. diff --git a/intro/v3-migration.md b/intro/v3-migration.md index fa09656..7730221 100644 --- a/intro/v3-migration.md +++ b/intro/v3-migration.md @@ -32,6 +32,7 @@ Keep `version: "3"` in `.rr.yaml`. That value identifies the configuration forma - [PROXY protocol](../http/http.md#development-proxy-protocol): accept trusted proxy addresses on plain HTTP and HTTPS listeners. Configure load balancers and readiness checks to send the required PROXY header. - [Static file controls](../http/static.md): configure URL prefixes, cache lifetimes, and cache limits. The middleware normalizes paths before access checks and uses revised ETags. Positive cache hits still open and stat files. Cached misses can delay newly created files. - [Zstd middleware](../http/zstd.md): add response compression with Zstandard. Include and register the plugin before selecting `http.middleware: ["zstd"]`. +- [HTTP rate limiting](../http/rate-limiter.md): upcoming bundled middleware with global, IP, or header keys, bounded process-local state, and `429` responses with `Retry-After`. The pinned source build does not include it. ## Bug Fixes From f51ffcfa9a82ccef77e75fc417863a3acc68dbe7 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Tue, 8 Sep 2026 10:54:59 +0200 Subject: [PATCH 21/21] docs(amqp): document named connections and nested configuration --- intro/v3-migration.md | 13 ++- queues/amqp.md | 263 ++++++++++++++++++++++++------------------ 2 files changed, 165 insertions(+), 111 deletions(-) diff --git a/intro/v3-migration.md b/intro/v3-migration.md index 7730221..0f3878a 100644 --- a/intro/v3-migration.md +++ b/intro/v3-migration.md @@ -16,13 +16,14 @@ Keep `version: "3"` in `.rr.yaml`. That value identifies the configuration forma | TCP plugin | The default RR build no longer includes the [TCP plugin](../plugins/tcp.md). A `tcp:` section cannot add it. This does not remove `tcp://` RPC transport. | | Centrifuge | Remove calls to `centrifuge.RateLimit`; the RPC has no replacement in the plugin. Check [Centrifuge](../plugins/centrifuge.md) and the DTO migration before updating direct clients. | | Job headers | `pool` is now a routing header. Rename application headers that use this name. External producers must supply a valid pool when [named worker pools](../queues/overview-queues.md#named-worker-pools) are enabled. | +| AMQP (development/unreleased) | Move global broker settings to named connections. Set `config.connection` on every AMQP pipeline. Use nested exchange and queue settings without `config.version`. See [AMQP migration](#amqp-configuration-development). | | Environment files | A configured root `envfile` is now loaded without experimental mode. Supply the file or remove an unused setting; a missing file fails startup. See [Environment](../php/environment.md). | | gRPC reflection | Reflection is registered automatically. Unary interceptors do not protect its streams. Review network access and mTLS in [gRPC](../grpc/grpc.md#server-reflection). | ## New Features - [Jobs worker pools](../queues/overview-queues.md#named-worker-pools): assign pipelines to separate named pools. The existing single `jobs.pool` format remains available; do not configure it together with `jobs.pools`. -- [AMQP version 2 configuration](../queues/amqp.md#version-2-configuration-recommended): separate exchange and queue settings, with controls for declaration and binding. Legacy flat configuration remains supported. Runtime `jobs.Declare` still uses flat keys. +- [AMQP pipeline configuration](../queues/amqp.md#pipeline-configuration): separate exchange and queue settings, with controls for declaration and binding. The development configuration requires nested sections and named connections. Runtime `jobs.Declare` remains a flat string map. - [NSQ](../queues/nsq.md): a new bundled Jobs driver with topics, channels, discovery, acknowledgements, and delayed delivery. Its retry limit and lack of a dead-letter handoff require application failure handling. - [gRPC reflection](../grpc/grpc.md#server-reflection): v1 and v1alpha service listing. Full PHP-service descriptors require a [Protoreg plugin](../grpc/protoreg.md). Unary interceptors already existed in v5.3.0; they are not a new v6 feature. - [Trusted proxy headers](../http/proxy.md): select and order the forwarding headers that a trusted proxy may supply. An empty list restores the defaults; it does not disable header trust. @@ -34,6 +35,16 @@ Keep `version: "3"` in `.rr.yaml`. That value identifies the configuration forma - [Zstd middleware](../http/zstd.md): add response compression with Zstandard. Include and register the plugin before selecting `http.middleware: ["zstd"]`. - [HTTP rate limiting](../http/rate-limiter.md): upcoming bundled middleware with global, IP, or header keys, bounded process-local state, and `429` responses with `Retry-After`. The pinned source build does not include it. +## AMQP Configuration (Development) + +Named connections and nested-only static configuration are development/unreleased changes for the next major release. The pinned [RR source build](install.md) at `b0cccd9` uses AMQP `v6.0.0-beta.9` and includes neither change. That beta allowed both flat and nested static configuration. Select an AMQP dependency with both changes before using the new configuration. + +Move `amqp.addr` to `amqp..addr`. Move optional `amqp.tls` to `amqp..tls`. Each connection requires an explicit address. Every YAML AMQP pipeline requires `config.connection` with a configured name. Top-level `amqp.addr` and `amqp.tls` are not supported. There is no implicit default connection or localhost fallback. + +Keep root `version: "3"`. Remove AMQP `config.version`. Static AMQP configuration uses only nested `exchange` and `queue` sections. Scalar `exchange` or `queue` values fail to decode. Flat flags do not set nested values. Move old flat entity settings with the [AMQP migration table](../queues/amqp.md#migration). + +Runtime `jobs.Declare` stays a flat string map. Send `connection`, or use `queueHeaders: ['rr_connection' => 'brokerB']` with the existing PHP `AMQPCreateInfo` API. No PHP package change is required. See [AMQP runtime declarations](../queues/amqp.md#runtime--rpc-jobsdeclare) for precedence and reserved-key removal, and [named connections](../queues/amqp.md#named-connections-development) for separate consume and publish pipelines. + ## Bug Fixes | Component | User-visible change | diff --git a/queues/amqp.md b/queues/amqp.md index c8e2c76..7a416d2 100644 --- a/queues/amqp.md +++ b/queues/amqp.md @@ -17,72 +17,83 @@ corresponding [documentation page](https://www.rabbitmq.com/download.html). Immediate publishes wait for a publisher confirm from RabbitMQ. Delayed publishes do not wait for that confirmation before returning. A publisher confirm does not prove that a message reached a queue. {% endhint %} -After that, you should configure the connection to the server in the `amqp` section. This configuration section -contains exactly one `addr` key with a [connection DSN](https://www.rabbitmq.com/uri-spec.html). The `TLS` configuration sits in the `amqp.tls` section and consists of the following options: +## Named Connections (Development) + +{% hint style="warning" %} +Named connections and nested-only static configuration are development/unreleased changes for the next major release. The pinned [RR source build](../intro/install.md) at `b0cccd9` uses AMQP `v6.0.0-beta.9` and includes neither change. That beta allowed both flat and nested static configuration. All YAML examples below require an AMQP dependency with both changes. +{% endhint %} + +Define each connection under `amqp.`. Each entry requires an explicit `addr` with a [connection DSN](https://www.rabbitmq.com/uri-spec.html). Each entry can also have an optional `tls` section. Every YAML AMQP pipeline must set `config.connection` to a configured name. + +RR rejects pipeline creation if the connection selector is missing or empty. An unknown connection name also causes an error. RR also rejects a selected connection with a missing or empty `addr`. + +There is no implicit default connection or localhost fallback. Top-level `amqp.addr` and `amqp.tls` are not supported. Connection names select configuration. Each pipeline keeps its own broker sockets. + +Connection names are separate from [named worker pools](overview-queues.md#named-worker-pools). Connection selection does not add a message header. + +TLS options belong under `amqp..tls`: - `key`: path to a key file. - `cert`: path to a certificate file. - `root_ca`: path to Root CAs used by the AMQP client to trust and verify the broker/server certificate during TLS dial. - `client_auth_type`: possible values are `no_client_cert`, `request_client_cert`, `require_any_client_cert`, `verify_client_cert_if_given`, `require_and_verify_client_cert`. This setting does not control broker certificate verification. -You should also configure `rabbitMQ` with `TLS` support: [link](https://www.rabbitmq.com/ssl.html). +Configure [RabbitMQ TLS support](https://www.rabbitmq.com/ssl.html) on the broker. In the AMQP v6 beta line (`v6.0.0-beta.9`), `root_ca` adds trust roots for broker certificate verification. Reconnects reuse the configured TLS settings. Use an `amqps://` address with valid `key` and `cert` files. A `root_ca`-only TLS block is not supported. Omit `tls` when using a plain `amqp://` connection. -{% code title=".rr.yaml" %} +{% code title=".rr.yaml (development/unreleased)" %} ```yaml -amqp: - addr: amqps://guest:guest@rabbitmq.example.com:5671/ - - # AMQPS TLS configuration - # - # This section is optional - tls: - # Path to the key file - # - # This option is required - key: /etc/rr/tls/client.key - - # Path to the certificate - # - # This option is required - cert: /etc/rr/tls/client.crt +version: "3" - # Path to Root CAs used by the AMQP client to trust and verify the broker/server certificate during TLS dial. - # - # This option is optional - root_ca: /etc/rr/tls/ca.crt +amqp: + brokerA: + addr: amqps://guest:guest@rabbitmq.example.com:5671/ - # Legacy client_auth_type setting. + # AMQPS TLS configuration # - # This option is optional. Default value: no_client_cert. Possible values: no_client_cert, request_client_cert, require_any_client_cert, verify_client_cert_if_given, require_and_verify_client_cert - client_auth_type: no_client_cert + # This section is optional + tls: + # Path to the key file + # + # This option is required + key: /etc/rr/tls/client.key + + # Path to the certificate + # + # This option is required + cert: /etc/rr/tls/client.crt + + # Path to Root CAs used by the AMQP client to trust and verify the broker/server certificate during TLS dial. + # + # This option is optional + root_ca: /etc/rr/tls/ca.crt + + # Legacy client_auth_type setting. + # + # This option is optional. Default value: no_client_cert. Possible values: no_client_cert, request_client_cert, require_any_client_cert, verify_client_cert_if_given, require_and_verify_client_cert + client_auth_type: no_client_cert ``` {% endcode %} -Upon establishing a connection to the server, you can create a new queue that utilizes this connection and encompasses -the queue settings, including those specific to AMQP. - -## Pipeline Configuration Formats +Configure each pipeline's exchange and queue as shown below. -These versioned formats apply to the AMQP v6 beta line (`v6.0.0-beta.9`). AMQP v5 uses the legacy flat format. In v6 beta, `jobs.pipelines..config.version` selects the parser: +## Pipeline Configuration -- `0`, `1`, or omitted: legacy flat parser. -- `2`: nested parser with `exchange` and `queue` sections (recommended). +Static AMQP configuration uses only nested `exchange` and `queue` sections under `jobs.pipelines..config`. At least one section is required. Missing sections receive default values. -## Version 2 Configuration (Recommended) - -Set `jobs.pipelines..config.version: 2`. At least one of the nested `exchange` or `queue` sections is required. Missing sections receive default values. +AMQP `config.version` is removed. Keep the root `version: "3"`. Use the [migration table](#migration) to move old flat entity settings. ### Options in `config` +- `connection`: required connection name from `amqp` in the development configuration. - `priority`: pipeline priority. If a job has priority `0`, it inherits the pipeline priority. Default: `10`. - `prefetch`: RabbitMQ QoS prefetch. Default: `10`. - `redial_timeout`: reconnect timeout in seconds. Default: `60`. -Zero or negative values for these three options use their defaults. +Zero or negative values for `priority`, `prefetch`, and `redial_timeout` use their defaults. ### Exchange settings @@ -90,7 +101,7 @@ Zero or negative values for these three options use their defaults. - `type`: exchange type. Supported: `direct`, `fanout`, `topic`, `headers`. Default: `direct`. - `durable`: durable exchange flag. Default: `false`. - `auto_delete`: auto-delete exchange when last queue is unbound. Default: `false`. -- `declare`: declare exchange on startup. Default: `true`. +- `declare`: declare exchange during pipeline creation. Default: `true`. ### Queue settings @@ -104,7 +115,9 @@ Zero or negative values for these three options use their defaults. - `multiple_ack`: ACK this and prior unacked deliveries on the same channel. Default: `false`. - `requeue_on_fail`: use RabbitMQ requeue on failure (Nack). Default: `false`. - `headers`: queue declaration arguments (for example, `x-queue-mode: lazy`). -- `declare`: declare and bind queue on startup. Default: `true`. +- `declare`: declare and bind queue when `Run` or `Resume` starts consumption. Default: `true`. + +For both entities, an omitted `declare` has the same effect as `true`. Explicit `false` disables declaration. Pipeline creation does not declare or bind the queue. {% hint style="info" %} See also [AMQP model](https://www.rabbitmq.com/tutorials/amqp-concepts.html#amqp-model) documentation section. @@ -122,20 +135,33 @@ If `exchange.type` is not `fanout`, `queue.routing_key` must be set when RR init Read more about Nack in RabbitMQ official docs: https://www.rabbitmq.com/confirms.html#consumer-nacks-requeue {% endhint %} -{% code title=".rr.yaml" %} +This development example consumes from `brokerA` through `consume-a`. It publishes to `brokerB` through `publish-b`. + +{% code title=".rr.yaml (development/unreleased)" %} ```yaml version: "3" +rpc: + listen: tcp://127.0.0.1:6001 + +server: + command: php worker.php + relay: pipes + amqp: - addr: amqp://guest:guest@127.0.0.1:5672/ + brokerA: + addr: amqp://guest:guest@broker-a:5672/ + brokerB: + addr: amqp://guest:guest@broker-b:5672/ jobs: + consume: ["consume-a"] pipelines: - p1: + consume-a: driver: amqp config: - version: 2 + connection: brokerA priority: 10 prefetch: 10 redial_timeout: 60 @@ -146,8 +172,8 @@ jobs: auto_delete: false declare: true queue: - name: p1-queue - routing_key: p1 + name: team-a-queue + routing_key: team-a durable: false auto_delete: false exclusive: false @@ -158,13 +184,53 @@ jobs: headers: x-queue-mode: lazy declare: true + publish-b: + driver: amqp + config: + connection: brokerB + exchange: + name: team-b-exchange + type: direct + queue: + routing_key: team-b +``` + +{% endcode %} + +Only `consume-a` is in `jobs.consume`. The producer-only `publish-b` pipeline has no queue name. Create a destination queue on broker B before publishing. Bind it to `team-b-exchange` with routing key `team-b`. + +{% code title="worker.php" %} + +```php +connect('publish-b'); +$consumer = new Consumer(); + +while ($task = $consumer->waitTask()) { + try { + $destination->dispatch($destination->create($task->getName(), $task->getPayload())); + $task->ack(); + } catch (\Throwable $e) { + $task->nack($e, redelivery: true); + } +} ``` {% endcode %} +RR does not provide a cross-broker transaction. A failure after publishing to B but before acknowledging on A can cause duplicate messages on B. Make the destination handler safe to repeat. + ## Read-only RabbitMQ Permissions -In AMQP v6 beta, use `config.version: 2` to disable declarations when the RabbitMQ user has no `configure` permission: +Disable declarations when the RabbitMQ user has no `configure` permission: - `jobs.pipelines..config.exchange.declare: false` - `jobs.pipelines..config.queue.declare: false` @@ -175,20 +241,21 @@ With `queue.declare: false`, RR skips both queue binding and passive queue inspe Delayed publishing and delayed requeue still declare and bind temporary queues. These operations still require `configure` permission. -{% code title=".rr.yaml" %} +{% code title=".rr.yaml (development/unreleased)" %} ```yaml version: "3" amqp: - addr: amqp://readonly:readonly@127.0.0.1:5675/TEST + brokerA: + addr: amqp://readonly:readonly@127.0.0.1:5675/TEST jobs: pipelines: readonly: driver: amqp config: - version: 2 + connection: brokerA exchange: name: test-1-exchange type: fanout @@ -208,81 +275,55 @@ jobs: ## Runtime / RPC (`jobs.Declare`) -Dynamic pipeline declaration over RPC remains flat. It does not use nested `config.exchange` / `config.queue` sections. +Dynamic pipeline declaration over RPC remains a flat string map. It is separate from static YAML configuration. It does not use nested `config.exchange` or `config.queue` sections. -In AMQP v6 beta, the `jobs.Declare` payload accepts these declaration controls as strings: +The `jobs.Declare` payload accepts these declaration controls as strings: - `exchange_declare`: `"true"` (default) or `"false"`. - `queue_declare`: `"true"` (default) or `"false"`. -## Legacy Flat Configuration (`version: 0|1`) - -Legacy flat keys remain supported under `jobs.pipelines..config` when `config.version` is `0`, `1`, or omitted: - -- `priority` -- `prefetch` -- `queue` -- `exchange` -- `exchange_type` -- `routing_key` -- `exclusive` -- `multiple_ack` -- `requeue_on_fail` -- `queue_headers` -- `durable` -- `delete_queue_on_stop` -- `redial_timeout` -- `exchange_durable` -- `exchange_auto_delete` -- `queue_auto_delete` -- `consumer_id` - -{% code title=".rr.yaml" %} +For named connections (development/unreleased), set the flat `connection` field to a configured name such as `"brokerB"`. Do not send a DSN or TLS settings in the RPC payload. -```yaml -version: "3" +The existing PHP 4.x [AMQPCreateInfo API](https://github.com/roadrunner-php/jobs/blob/4.x/src/Queue/AMQPCreateInfo.php) accepts `queueHeaders`. `Jobs` serializes this map as JSON in the flat `queue_headers` field. Set the reserved `rr_connection` key in that map to select a connection. No PHP package change is required. -amqp: - addr: amqp://guest:guest@127.0.0.1:5672/ +RR reads `rr_connection` only when the flat `connection` field is absent. The flat field takes precedence whenever it is present, even when it is empty. An empty value causes an error. RR removes the reserved key from queue arguments before broker declaration and configuration storage. Other queue arguments stay unchanged. This key is RPC configuration metadata, not a message header. YAML pipelines must still set `config.connection`. -jobs: - pipelines: - p1: - driver: amqp - config: - version: 1 - priority: 10 - prefetch: 10 - queue: p1-queue - exchange: amqp.default - exchange_type: direct - routing_key: p1 - exclusive: false - multiple_ack: false - requeue_on_fail: false - queue_headers: {} # optional, e.g. { x-queue-mode: lazy } - durable: false - delete_queue_on_stop: false - redial_timeout: 60 - exchange_durable: false - exchange_auto_delete: false - queue_auto_delete: false - consumer_id: "" +This runtime example creates the `runtime-b` pipeline on `brokerB` and declares its exchange. It does not declare or bind the queue. PHP 4.x `Jobs` requires an RPC instance: + +{% code title="create.php (development/unreleased AMQP)" %} + +```php +use Spiral\Goridge\RPC\RPC; +use Spiral\RoadRunner\Jobs\Jobs; +use Spiral\RoadRunner\Jobs\Queue\AMQPCreateInfo; + +$jobs = new Jobs(RPC::create('tcp://127.0.0.1:6001')); +$queue = $jobs->create(new AMQPCreateInfo( + name: 'runtime-b', + queue: 'team-b-queue', + exchange: 'team-b-exchange', + routingKey: 'team-b', + queueHeaders: ['rr_connection' => 'brokerB'], +)); ``` {% endcode %} -{% hint style="info" %} -In legacy format, `queue` can be omitted for push-only pipelines, but `run`, `resume`, and `pause` require a queue name. Also, `routing_key` is required when `exchange_type != fanout`. -{% endhint %} +Call `$jobs->resume('runtime-b')` after creation to declare and bind the queue and start consumption. For a producer-only pipeline, omit this call. Its destination queue and binding must exist before publishing. ## Migration -When moving from AMQP v5 to v6 beta, existing flat configurations can keep `config.version` omitted or set it to `0` or `1`. Both formats now reject exchange types outside `direct`, `fanout`, `topic` and `headers`. Non-fanout exchanges require a routing key during pipeline initialization, not only on push. +To use the development/unreleased configuration: + +1. Select an AMQP dependency with named connections and nested-only static configuration. +2. Move `amqp.addr` to `amqp..addr`. Move optional `amqp.tls` to `amqp..tls`. Set an explicit address for each connection. +3. Set `config.connection` on every YAML AMQP pipeline. There is no implicit default connection or localhost fallback. +4. Remove `config.version` from every YAML AMQP pipeline. Keep the root `version: "3"`. +5. For runtime declarations, send flat `connection` or use the existing PHP `queueHeaders` map with `rr_connection`. The flat field takes precedence even when empty. RR removes the reserved key before broker declaration and configuration storage. -To use version 2, set `config.version: 2` and move the entity settings below. All paths are relative to `jobs.pipelines..config`. +Move old flat entity settings to the nested fields below. All paths are relative to `jobs.pipelines..config`. -| Legacy Flat Key | Version 2 Key | +| Old Field | New Field | | --- | --- | | `exchange` | `exchange.name` | | `exchange_type` | `exchange.type` | @@ -295,7 +336,9 @@ To use version 2, set `config.version: 2` and move the entity settings below. Al | `queue_headers` | `queue.headers` | | `routing_key`, `exclusive`, `consumer_id`, `multiple_ack`, `requeue_on_fail` | Same key under `queue` | -Keep `priority`, `prefetch` and `redial_timeout` directly in `config`. Do not mix flat and nested entity settings. Version 2 ignores legacy flat entity settings; for example, a flat `durable: true` does not set `queue.durable`. +Keep `priority`, `prefetch` and `redial_timeout` directly in `config`. + +The configuration schema rejects `config.version` and removed flat keys. The normal config provider can ignore unknown keys, but old scalar `exchange` or `queue` values fail to decode. Flat flags do not set nested values. For example, a flat `durable: true` does not set `queue.durable`. Migrate all entity settings before starting RR. For restricted RabbitMQ permissions, set `exchange.declare: false` and `queue.declare: false`. Review the [declaration limitations](#read-only-rabbitmq-permissions) before using delayed jobs. @@ -303,6 +346,6 @@ For restricted RabbitMQ permissions, set `exchange.declare: false` and `queue.de 1. [Queues and Jobs overview](overview-queues.md) - Review the full jobs pipeline model before configuring AMQP in production. 2. [Read-only RabbitMQ permissions](https://www.rabbitmq.com/docs/access-control) - See declaration flags for restricted users and review the Runtime / RPC (`jobs.Declare`) section on this page for flat declaration keys. -3. [Pipeline configuration formats](../intro/config.md) - Confirm when to use `config.version: 2` versus legacy `0|1`, and review the general RoadRunner configuration structure. +3. [Pipeline configuration](#pipeline-configuration) - Use nested AMQP entity settings. See [RoadRunner configuration](../intro/config.md) for the general configuration structure. 4. [Exchange settings](#exchange-settings) and [Queue settings](#queue-settings) - Review routing and consumption settings. 5. [Allocate Timeout](../known-issues/allocate-timeout.md) and [CRC validation failed](../known-issues/stdout-crc.md) - Use these troubleshooting references when workers fail to process queue jobs as expected.