Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3f7fae5
Prepare optional Laravel service HTTP hosting example
rmcdaniel Oct 14, 2026
f8aa08a
Verify eager HTTP resolution and request log isolation
rmcdaniel Oct 14, 2026
827417c
Add published-runtime lifecycle controls to HTTP example
rmcdaniel Oct 14, 2026
221dcee
Preserve lifecycle observer deadline across status reads
rmcdaniel Oct 14, 2026
a121ff4
Send SIGTERM to the independent service worker
rmcdaniel Oct 14, 2026
6f4c670
Document lifecycle qualification and worker stop signal
rmcdaniel Oct 14, 2026
5b50e9e
Prepare original timer checkpoint for CLI recovery qualification
rmcdaniel Oct 14, 2026
7113527
docs: record original-run worker replacement qualification
rmcdaniel Oct 14, 2026
8303c95
Pin the HTTP example to qualified PHP SDK 2.4.1 diagnostics
rmcdaniel Oct 14, 2026
50259e6
Add published cache and connection recovery checks to the HTTP example
rmcdaniel Oct 14, 2026
1857a89
Add bounded k6 service HTTP comparison workload
rmcdaniel Oct 14, 2026
aa21cd5
Audit original load results and count terminal failures explicitly
rmcdaniel Oct 14, 2026
12ed289
Record HTTP worker identities during the Laravel load profile
rmcdaniel Oct 14, 2026
7110a50
Merge remote-tracking branch 'origin/main' into docs/laravel-service-…
rmcdaniel Oct 14, 2026
e907a97
Bound load offers before admission and preallocate the declared clien…
rmcdaniel Oct 14, 2026
78320ab
Merge current SDK and Sonar configuration into HTTP preparation
rmcdaniel Oct 15, 2026
1d90256
chore: clarify Laravel HTTP example configuration
rmcdaniel Oct 15, 2026
eb77794
Merge remote-tracking branch 'origin/main' into docs/laravel-service-…
rmcdaniel Oct 16, 2026
08a2a33
Keep mixed query requests within the original observation budget
rmcdaniel Oct 16, 2026
989acc6
Expose transport timing for Laravel example diagnostics
rmcdaniel Oct 16, 2026
55cd083
Use an HTTP-only RoadRunner profile in Laravel example
rmcdaniel Oct 16, 2026
67b311a
Bind the Octane control RPC to loopback explicitly
rmcdaniel Oct 16, 2026
f027697
Qualify the Laravel HTTP example against current published packages
rmcdaniel Oct 16, 2026
e81b7c8
Size example HTTP pools for concurrent query waits and graceful reloads
rmcdaniel Oct 16, 2026
db5d53a
Document interim HTTP hosting results and remaining backend gate
rmcdaniel Oct 16, 2026
8df5e88
test: select corrected published Server for HTTP qualification
rmcdaniel Oct 17, 2026
f6ac71a
fix(qualification): drain original HTTP observers before final failure
rmcdaniel Oct 17, 2026
79b0a6f
Prepare bounded CLI worker pool for HTTP qualification
rmcdaniel Oct 17, 2026
530c98b
Fix hosting probe readiness and reload reserve
rmcdaniel Oct 18, 2026
af741d1
Reserve context users through the declared request limit
rmcdaniel Oct 18, 2026
8ef08b8
Merge current documentation anchor into Laravel example
rmcdaniel Oct 18, 2026
516d13a
Document qualified Laravel FPM and Octane profiles
rmcdaniel Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,22 @@ jobs:
WORKFLOW_REQUIREMENT: ${{ matrix.workflow }}
run: scripts/ci/laravel-service-transition-smoke.sh

load-driver-drain:
name: Laravel HTTP load-driver drain
runs-on: ubuntu-latest
timeout-minutes: 5
env:
LOAD_DRAIN_RUN: dw-load-drain-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Preserve original observers after an independent HTTP failure
run: bash examples/laravel-http/qualification/check-load-drain.sh
- name: Remove only this test's HTTP fixture, generator and network
if: always()
run: bash examples/laravel-http/qualification/check-load-drain.sh cleanup

analyse:
runs-on: ubuntu-latest
timeout-minutes: 10
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ for portable payloads and accepts any PSR-18 HTTP client.
| Plain PHP client or worker | [First workflow](https://php.durable-workflow.com/getting-started/first-workflow/) |
| Laravel v1 or v2 embedded | [Choose a Laravel ownership model](https://php.durable-workflow.com/frameworks/laravel/) |
| Laravel service mode | [Laravel service-mode bridge](https://php.durable-workflow.com/frameworks/laravel/#service-mode) |
| Laravel HTTP hosting | [FPM and optional Octane example](examples/laravel-http/) |
| Symfony service mode | [Symfony bundle](https://php.durable-workflow.com/frameworks/symfony/) |
| Runtime authentication | [Authentication guide](https://php.durable-workflow.com/operate/authentication/) |

Expand Down
14 changes: 14 additions & 0 deletions docs/portal/frameworks/laravel.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,20 @@ neither a running Server nor source inspection.

Use the same fake assertions for a concurrent application start; concurrency is an implementation detail of the registered handler, so the class-shaped start contract does not change. For handler-level coverage, build the registered Laravel worker in the test container and inspect `WorkerTestHarness::runWorkflow()`: every declared leaf must appear in the same command sequence with one group/path identity. Replay mixed-order or partial history through the same method to assert declaration-order results without starting a Server.

## HTTP hosting and durable workers

Use the [FPM and optional Octane example](https://github.com/durable-workflow/sdk-php/tree/main/examples/laravel-http)
for a complete Laravel HTTP application with a normal Composer lock, explicit
resource limits, request-context controls and a separately supervised Artisan
worker. The recipe records its runtime/tool images and qualification outcomes.

HTTP workers submit and inspect workflows. Run `durable-workflow:worker` in its
own process; Server or Cloud owns durable state, timers and task leases. Keep
each process's endpoint, namespace and credential authority fixed, and pass
client and worker credentials only to their respective roles outside cached
configuration. The CLI worker's history cache retains history and still replays
workflow instructions.

## Qualification boundary

Clean compatibility jobs install this SDK in every supported Laravel/PHP cell
Expand Down
10 changes: 10 additions & 0 deletions examples/laravel-http/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/vendor/
/rr
/.env
/data/
/storage/
/bootstrap/cache/*
!/bootstrap/cache/.gitignore
/receipts/
!/composer.lock
.rr.yaml
266 changes: 266 additions & 0 deletions examples/laravel-http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
# Laravel service HTTP example

This application runs with nginx/PHP-FPM or optional Laravel Octane and
RoadRunner. The [qualification report](qualification/report.md) records the
tested configuration, resource costs and limits.

Start with FPM for this recipe. Both documented two-CLI-worker profiles pass
functional/recovery controls, three short comparison pairs, a baseline recheck
and mixed-load reload soaks on the pinned tuple. Octane improves small context
reads but shows no consistent application CPU or workflow-latency gain, while
using about 185 MiB more charged memory. Production capacity remains
workload-specific. The [report](qualification/report.md) gives the measured
costs, limits and earlier failed runs.

The application uses the normally published SDK. Durable workflow and activity
polling runs in the separate `durable-workflow:worker` CLI container. HTTP
workers only submit and inspect runtime operations. Server or Cloud owns durable
history, timers, task leases and recovery.

## Installation

Check out the runnable application:

```bash
git clone --depth 1 https://github.com/durable-workflow/sdk-php.git
cd sdk-php/examples/laravel-http
```

The exact PHP SDK, Laravel and Octane versions live in `composer.json` and the
resolved dependencies in `composer.lock`. The image uses PHP 8.3.35. Preparation
downloads RoadRunner 2025.1.15 through its standard installer, then runs
`octane:install` and `config:cache`. Octane remains an application dependency.

Use a disposable self-hosted Server namespace with separate application-client
and worker role credentials. Put the following Compose values in a private file
outside this application directory. Do not put runtime credentials in Laravel
configuration or commit them.

```text
APP_KEY=<base64 application key>
DURABLE_WORKFLOW_RUNTIME_URL=<Server origin or complete Cloud runtime URL>
DURABLE_WORKFLOW_NAMESPACE=<one fixed namespace>
DURABLE_WORKFLOW_CLIENT_TOKEN=<application client-role credential>
DURABLE_WORKFLOW_WORKER_TOKEN=<worker-role credential>
EXAMPLE_STATE_DIRECTORY=<absolute writable local state directory>
```

The checkout and state directory must be writable by UID/GID `1000:1000`, used
by these containers. Start the FPM profile with:

```bash
docker compose --env-file /private/example.env -f compose.yaml -f compose.fpm.yaml -f compose.worker-pool.yaml up --build -d
```

The HTTP container receives only the client token. The CLI container receives
only the worker token. The preparation container receives neither. Change the
runtime address, namespace or credential by replacing the relevant process.
Never rewrite a shared client using request headers or an authenticated user's
token. Multiple runtime authorities need independently configured clients or
separate application processes.

For the optional Octane profile:

```bash
docker compose --env-file /private/example.env -f compose.yaml -f compose.worker-pool.yaml up --build -d
```

Both commands use four HTTP workers, two durable CLI workers and nginx with the same
OPcache and cached configuration. The application allocation totals two CPUs
and 1 GiB with no swap: HTTP 0.9 CPU/640 MiB, each CLI 0.5 CPU/160 MiB, nginx
0.1 CPU/64 MiB. Provision and report the separate Server/database allocation.
These limits are qualification settings, not a general capacity promise.

The `compose.worker-pool.yaml` override selects the two-worker configuration
used in the current results. Each process has its own registration and history
cache. Omitting that override uses the base recipe's one CLI worker at
1 CPU/320 MiB, preserving the same combined application allocation. Its earlier
load outcomes are recorded separately. See the report before choosing a profile.

Synchronous query requests occupy HTTP workers while awaiting their result.
Size the HTTP pool for that waiting time as well as request CPU. The FPM
configuration gives in-flight requests a bounded 35-second process-control
grace period during reload. The durable CLI worker remains independent.

The checked-in `runtime/roadrunner.yaml` enables the HTTP pool and local control
RPC. The generated starter configuration also starts a jobs pool, which this
HTTP-only application does not use. Durable polling stays in the CLI worker.
`qualification/inputs.json` records the pinned runtime and tool images for the
comparison; application dependencies remain in the normal Composer lock.

## Exercise the application

The two `example-alpha` and `example-beta` bearer values are synthetic application
identities. Replace this middleware with your application's authorization before
exposing the example. It binds workflow IDs to the current user and refuses a
different `X-Namespace` before calling the SDK.

```bash
curl -f http://localhost:8088/api/workflows \
-H 'Authorization: Bearer example-alpha' \
-H 'X-Request-Id: start-1' -H 'Content-Type: application/json' \
-d '{"id":"first","kind":"short"}'
curl -f http://localhost:8088/api/workflows/first \
-H 'Authorization: Bearer example-alpha' -H 'X-Request-Id: read-1'
```

Kinds `short`, `timer`, `signal` and `cancel` share one echo activity. A timer
waits one second. A signal workflow waits for `POST /api/workflows/{id}/signal`.
Query it with `POST /api/workflows/{id}/query`. Cancel a waiting original with
`POST /api/workflows/{id}/cancel`. Status reads return canonical status and the
typed result, including an integer above JavaScript's exact-number range, null
and Unicode. Decode that integer with an appropriate client.

Every API request requires a fresh `X-Request-Id`. Alternating requests omit or
set `X-Optional-Context` and inspect response headers plus application logs.
Request context is removed in the middleware's `finally` block. Set
`EXAMPLE_EAGER=true` before starting a fresh application to resolve the SDK at
boot; the default resolves it lazily. Both processes keep a fixed authority.

`qualification/context.mjs` checks 40 alternating requests and three auth,
namespace and invalid-header refusals through `/api/context`. That diagnostic
route resolves the normal SDK client without sending a runtime request. It
checks HTTP request isolation only; it cannot prove workflow execution or
runtime namespace authorization.

With the real runtime and CLI worker ready, choose a fresh original prefix and
run `qualification/lifecycle.mjs` with `EXAMPLE_ORIGIN` and `EXAMPLE_RUN_PREFIX`.
It admits five originals once, observes four completions and one cancellation,
and checks signal/query and a foreign-user refusal. Status observation budgets
are 35 seconds; individual HTTP calls are capped at the remaining budget.
`qualification/verify-results.php` reads those same original results through the
installed PHP SDK and checks the exact int64/null/Unicode types. Run the drivers
in a separate tool container so their resources are excluded from the fixed
application allocation.

The durable worker explicitly uses Docker's SIGTERM stop signal. The FPM image's
default SIGQUIT belongs to the HTTP process; use SIGTERM for the CLI worker so
its normal SDK shutdown can deregister.

The separate `recovery-timer` kind waits ten seconds after its echo activity.
It provides a checkpoint before testing CLI replacement. Use the returned
original workflow/run IDs with `qualification/recovery-checkpoint.php before`,
writing a temporary file under the private state directory. The check requires
one completed activity and one unfired persisted timer. An interruption control
must preserve the same original IDs and accepted 600/900-second deadlines, use
the declared observation budget, and never seed a replacement workflow or
manually repair history. After the replacement completes, `after` requires the
whole original prefix, unchanged deadline fields and exact typed result. Delete
the temporary checkpoint when the control ends.

Actual [two-worker FPM replacement, cache and connection controls](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6243887984)
preserve the same original runs, complete history prefixes and unchanged
deadlines. API connection loss replaces the two CLI processes three and six
times, respectively, so it does not establish recovery within one process.

History-cache defaults remain 100 entries/300 seconds. The existing SDK validates
capacity and TTL, with a 3600-second TTL ceiling. `EXAMPLE_HISTORY_CAPACITY` and
`EXAMPLE_HISTORY_TTL` select this process's settings before startup. A cache hit
retains history and still replays instructions. HTTP reuse does not retain a
workflow Fiber or avoid execution replay.

### Cache and connection checks

Run `qualification/cache.php` in a separate tool container with only the client
credential and a private writable checkpoint path. Start a fresh CLI process
with the settings below before admitting each profile. The driver fixes a
90-second observation, 600-second run and 900-second execution budget, checks the
whole pre-release history prefix and requires exact typed output once.

| Mode | Capacity / TTL | Action |
| --- | --- | --- |
| `expiry` | 100 / 1 second | One waiting original; idle three seconds, then release it. |
| `eviction` | 1 / 300 seconds | Two waiting originals; release the target and its neighbor. |
| `checkpoint`, then `resume` | 100 / 1 second | Preserve one waiting original across a bounded test-runtime outage. |

```bash
php qualification/cache.php expiry fresh-expiry /private/expiry.json
php qualification/cache.php eviction fresh-eviction /private/eviction.json
php qualification/cache.php checkpoint fresh-outage /private/outage.json
# Interrupt only your isolated test runtime, restore it, and verify readiness.
# The original observation cutoff continues while it is unavailable.
php qualification/cache.php resume fresh-outage /private/outage.json
```

For the two-worker pool's capacity control, checkpoint four fresh originals
before releasing any of them, then resume those same four. This fills both
independent caches. The two-original `eviction` command above exercises one
CLI worker.

Do not run the three profiles together or reuse a checkpoint. The driver does
not change the CLI's cache settings, stop services, restart workers or provision
credentials. Use the normal supervisor for connection recovery and record actual
worker replacements. Require an observed SDK transport error before claiming
connection-error recovery; a network configuration change alone is insufficient.

After normal CLI shutdown, read the actual `worker.stopped` diagnostic's
`sticky_cache` counters from the logger or framework event: `hit`, `miss`,
`eviction`, and `forced_cold_replay`. Missing counters fail the observation.
Require a real eviction for expiry/capacity tests and a forced cold replay for
the capacity test. A complete runtime history can yield zero forced cold replays
after TTL expiry. Audit original task attempts/repairs and deregistration through
the isolated runtime, then delete temporary checkpoints and test resources.

## Qualification before recommendation

Freeze the normal dependency lock, RoadRunner binary hash, runtime image digest,
role configuration, offered schedule, original deadlines and resource limits
before admitting workflows. Qualify eager/lazy resolution, alternating users and
namespace refusals, headers/log context, signal/query/cancel, cache expiry and
eviction, HTTP reload/recycling, CLI SIGKILL/cold replacement and runtime
connection loss first. Inspect canonical histories for duplicate effects and
unchanged identities/deadlines.

`qualification/load.js` uses the open-source k6 constant-arrival-rate executor:
six authenticated context reads and one new original workflow per second. Run
the tool separately from the fixed application allocation. Preallocate 64 context
and sixteen workflow virtual users; a missed offer still fails the run. The
context reserve covers six requests per second through their existing ten-second
transport limit. It does not increase the offered rate or extend a request.
The request sequence is finite. A generator tick at the window boundary is
counted separately and cannot admit an extra workflow or context read. A late
planned offer fails before HTTP admission. Each original has
the existing 35-second observer; the outer run allows 36 seconds to drain after
the last offer. Every expected admission, terminal result and context read is
counted. Dropped offers, HTTP/check failures, incomplete originals, OOM or
restarts invalidate a successful comparison.

A query request may use up to 30 seconds, capped by the original workflow's
remaining observation budget. Other requests retain their ten-second transport
cap. No request resets or extends the original 35-second observer.

Set `EXAMPLE_ORIGIN`, `DURABLE_WORKFLOW_NAMESPACE`, a fresh `EXAMPLE_RUN_PREFIX`
and `EXAMPLE_SUMMARY_PATH` in the tool container. Freeze its image digest and
limits as well as application/backend roles. `EXAMPLE_DURATION_SECONDS` selects
5–300 seconds; the planned warmup/window/soak are 30/120/300 seconds. The normal
window uses one-activity workflows. `EXAMPLE_MIXED=true` cycles two short,
one timer, one queried/signalled and one queried/cancelled original. Wire checks
preserve the exact int64 digits; normal installed PHP readers and native task
records independently verify types, attempts, histories and repairs afterward.
Run `php qualification/audit-load.php` with those same original environment
values and only the client credential. It reads each selected original through
the installed SDK, checks canonical activity/terminal counts and exact PHP
types, and reads actual timer due/fired timestamps without inventing a clock.

Use fresh projects/databases for three pairs with order FPM/Octane,
Octane/FPM, FPM/Octane, then an FPM drift recheck and mixed soak. Separate warmup
from measured offers and keep CLI settings identical. Report context/start/status latency separately from the
poll-observed workflow latency; the latter includes the declared 250ms status
poll interval. Sample every app/backend/tool role through drain and verification.
Both proxies emit JSON request IDs and connection/header/response timings without
request bodies or authorization headers. A failed driver request records the
same ID, status, error code and k6 transport timings. Correlate these records with
component CPU, throttling, memory and queue state before attributing a timeout to
the application, backend or load generator. These diagnostics do not relax the
offered schedule or original deadlines.
Include offered/completed/failed/unfinished operations, measured drain, timer
due-to-resume latency, per-completion CPU, aggregate steady/peak memory, history
bytes/requests and cache diagnostics. Reject OOM, swapping or growing backlog as
a successful measurement. Exercise recycling/reload during the mixed soak.
Keep the small k6 summary and readable report; delete raw samples, histories,
databases, dependencies and temporary hosts after their immediate audit. Report
gains and tradeoffs in the owning issue, including no consistent benefit.

Stop the Compose project and delete only this example's state when finished.
This application does not provision or retire your runtime namespace or its
credentials. Retire those through your Server or Cloud's normal admin path.
8 changes: 8 additions & 0 deletions examples/laravel-http/app/Http/Controllers/Controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace App\Http\Controllers;

abstract class Controller
{
//
}
Loading
Loading