Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
58c016c
feat: prototype committed embedded queue continuations
durable-workflow-ops Oct 17, 2026
257375f
test: fence retained frames across loss and commit failures
durable-workflow-ops Oct 17, 2026
5d8e44d
feat: keep retained execution inside the existing dispatcher
durable-workflow-ops Oct 17, 2026
690446d
fix: validate retained cursors and fence cross-build publication
durable-workflow-ops Oct 17, 2026
44fab36
test: qualify a persistent daemon and reject bound workflow services
durable-workflow-ops Oct 17, 2026
e38aef7
test: route positive continuation evidence to its queue consumer
durable-workflow-ops Oct 17, 2026
a0e7ee1
refactor: separate workflow task dispatch handlers
durable-workflow-ops Oct 17, 2026
da2079d
refactor: clarify condition phases and parallel state
durable-workflow-ops Oct 17, 2026
415c667
test: recover retained execution after worker SIGKILL
durable-workflow-ops Oct 17, 2026
edb705d
fix: invalidate retained execution when run model changes
durable-workflow-ops Oct 17, 2026
288a809
test: cover retained execution fallback and cold dependency scopes
durable-workflow-ops Oct 17, 2026
f09dee2
test: prepare real-worker continuation growth comparison
durable-workflow-ops Oct 17, 2026
b2c2ad3
test: pin timeout-capable continuation profiling image
durable-workflow-ops Oct 17, 2026
af19fc5
test: measure continuation observer overhead and record growth results
durable-workflow-ops Oct 17, 2026
5cfe33c
test: allow workers without history reads in continuation probes
durable-workflow-ops Oct 17, 2026
a260ce2
test: drain continuation transport jobs within the original bound
durable-workflow-ops Oct 17, 2026
9907b9b
Record failed large-history pilot and observer controls
durable-workflow-ops Oct 17, 2026
857c421
Merge remote-tracking branch 'origin/main' into feature/embedded-exec…
durable-workflow-ops Oct 17, 2026
00e6b7b
refactor: separate workflow replay resume paths
durable-workflow-ops Oct 17, 2026
5b50e4e
Merge current main into experimental retained execution
durable-workflow-ops Oct 18, 2026
66fc3dc
Verify Nexus cold fallback after retained activity prefix
durable-workflow-ops Oct 18, 2026
9ad7d9b
Assert cold replay at Nexus backend admission
durable-workflow-ops Oct 18, 2026
7713209
Record current-source continuation observer comparison
durable-workflow-ops Oct 18, 2026
178ad63
Merge current main and preserve entry binding safety in retained exec…
durable-workflow-ops Oct 20, 2026
0648f9f
Run staged continuation pairs in the existing hosted profiling workflow
durable-workflow-ops Oct 20, 2026
9a4e0e9
Fence retained frames across database reconnects and verify repair re…
durable-workflow-ops Oct 20, 2026
ae29003
Record current-main small growth originals and allow a bounded hosted…
durable-workflow-ops Oct 20, 2026
b7515ed
Expose cold entry and retained prefix work counters
durable-workflow-ops Oct 20, 2026
2b1c1f1
Document opt-in embedded execution and measured handoff costs
durable-workflow-ops Oct 20, 2026
39314ae
Fence warm execution to the generation captured by its task claim
durable-workflow-ops Oct 20, 2026
70ed2c2
Distinguish transport redispatch from recovered execution in growth a…
durable-workflow-ops Oct 20, 2026
6f087ee
Measure actual executor and Fiber CPU with an opt-in observer
durable-workflow-ops Oct 20, 2026
e3f148b
Count actual retained entries removed by pressure and clear
durable-workflow-ops Oct 20, 2026
92bb602
Pair CPU observer controls on one host and retain the large pilot
durable-workflow-ops Oct 20, 2026
9ee8d64
Invalidate retained execution when configured model classes change
durable-workflow-ops Oct 20, 2026
0d476b2
Record executor CPU attribution and installed-package acceptance
durable-workflow-ops Oct 20, 2026
48d664f
Discard retained execution when broker acknowledgement raises a worke…
durable-workflow-ops Oct 20, 2026
64b306a
Assert the generation returned with the configured workflow lease
durable-workflow-ops Oct 20, 2026
5dd594e
Prepare embedded minor qualification and retain failed growth originals
durable-workflow-ops Oct 20, 2026
9f70643
Verify committed workflow handoff after worker memory-limit exit
durable-workflow-ops Oct 20, 2026
fa5d230
Keep object-capable builtin entry signatures on cold replay
durable-workflow-ops Oct 20, 2026
662d1ae
Describe the bounded profile and its measured operating limits
durable-workflow-ops Oct 20, 2026
d4555f8
Record failed large admission pilots and bounded source comparisons
durable-workflow-ops Oct 20, 2026
82acd9b
Publish all three bounded single-worker large-history pairs
durable-workflow-ops Oct 20, 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
106 changes: 106 additions & 0 deletions .github/workflows/embedded-replay-profile.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,51 @@
name: Published embedded replay profile
run-name: ${{ inputs.execution == 'continuation' && format('Embedded continuation {0} events / {1} workers / {2} runs / observers {3}', inputs.events, inputs.workers, inputs.runs, inputs.observers) || 'Published embedded replay profile' }}

on:
workflow_dispatch:
inputs:
execution:
description: 'Published cold bridge or candidate real-queue continuation comparison'
type: choice
options: [published, continuation]
default: published
history_sizes:
description: 'Event targets: all (10/100/1000), small (10/100), or large (1000)'
type: choice
options: [all, small, large]
default: all
events:
description: 'Continuation profile: events per original'
type: choice
options: ['10', '100', '1000']
default: '10'
workers:
description: 'Continuation profile: persistent workers within 1 GiB'
type: choice
options: ['1', '4']
default: '1'
runs:
description: 'Continuation profile: concurrent original workflows'
type: choice
options: ['1', '8']
default: '1'
observers:
description: 'CPU/SQL/scheduling observers; compare uses 100 events, 1 worker/run with drift controls'
type: choice
options: ['on', 'off', 'compare']
default: 'on'
repetitions:
description: 'Continuation profile: one bounded pilot or all three measured repetitions'
type: choice
options: ['all', '1']
default: 'all'

permissions:
contents: read

jobs:
profile:
if: ${{ inputs.execution != 'continuation' }}
runs-on: ubuntu-24.04
timeout-minutes: 120
strategy:
Expand Down Expand Up @@ -147,3 +179,77 @@ jobs:
if test -d benchmarks/embedded-replay/vendor; then
rm -r -- benchmarks/embedded-replay/vendor
fi

continuation:
if: ${{ inputs.execution == 'continuation' }}
runs-on: ubuntu-24.04
timeout-minutes: 135
strategy:
fail-fast: false
matrix:
repetition: ${{ fromJSON(inputs.repetitions == '1' && '[1]' || '[1,2,3]') }}
env:
DWF_PROFILE_IMAGE: dw-continuation-profile:qualification
DWF_GROWTH_OBSERVERS: ${{ inputs.observers }}
PROFILE_EVENTS: ${{ inputs.events }}
PROFILE_WORKERS: ${{ inputs.workers }}
PROFILE_RUNS: ${{ inputs.runs }}
PROFILE_REPETITION: ${{ matrix.repetition }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false

- name: Record host conditions and prepare the locked source consumer
timeout-minutes: 8
run: |
uname -srmo
lscpu | grep -E 'Architecture:|Model name:|CPU\(s\):'
docker ps --format '{{.Names}} {{.Image}}'
docker build --tag "$DWF_PROFILE_IMAGE" tests/Performance/continuation
cp tests/Performance/continuation/COMPOSER-INPUT.lock composer.lock
root_version=$(jq -r '.extra["durable-workflow"]["product-train"]' composer.json)
docker run --rm --user "$(id -u):$(id -g)" \
--volume "$PWD:/app" --workdir /app \
--env COMPOSER_HOME=/tmp/composer --env "COMPOSER_ROOT_VERSION=$root_version" \
"$DWF_PROFILE_IMAGE" composer install --prefer-dist --no-interaction --no-progress

- name: Compare fresh replay and retained originals
run: |
if [[ "$DWF_GROWTH_OBSERVERS" == compare ]]; then
[[ "$PROFILE_EVENTS/$PROFILE_WORKERS/$PROFILE_RUNS" == 100/1/1 ]]
observer_order='on off on'
if [[ "$PROFILE_REPETITION" == 2 ]]; then observer_order='off on off'; fi
observer_block=0
observer_outcome=0
for observer_setting in $observer_order; do
observer_block=$((observer_block + 1))
echo "CONTINUATION_OBSERVER_BLOCK=$observer_block setting=$observer_setting"
if DWF_GROWTH_OBSERVERS="$observer_setting" \
bash scripts/profile-embedded-continuation.sh \
"$PROFILE_EVENTS" "$PROFILE_WORKERS" "$PROFILE_RUNS" "$PROFILE_REPETITION"; then
:
else
observer_outcome=1
fi
done
exit "$observer_outcome"
fi
bash scripts/profile-embedded-continuation.sh \
"$PROFILE_EVENTS" "$PROFILE_WORKERS" "$PROFILE_RUNS" "$PROFILE_REPETITION"

- name: Record the scope and remove disposable installations
if: always()
run: |
{
echo '## Embedded continuation source comparison'
echo "Source: $GITHUB_SHA"
echo "Events: $PROFILE_EVENTS; workers: $PROFILE_WORKERS; runs: $PROFILE_RUNS"
echo "Repetition: $PROFILE_REPETITION; observers: $DWF_GROWTH_OBSERVERS"
echo 'One fresh container per mode, 1 CPU / 1 GiB aggregate memory / no swap.'
echo 'Original 3,600-second workflow and observation bounds remain unchanged.'
echo 'Scalar outcome, CPU, RSS, SQL, cache, queue and timer measurements are in the log.'
echo 'A failed or unfinished original is not acceptance. No database/history archive is uploaded.'
} >> "$GITHUB_STEP_SUMMARY"
if test -d vendor; then rm -r -- vendor; fi
if test -f composer.lock; then rm -- composer.lock; fi
26 changes: 26 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,32 @@ jobs:
tests/Unit/V2/ProjectionSqliteContentionTest.php \
--fail-on-warning --log-junit build/test-results/pr-database-queue-recovery.xml

- name: Run retained execution authority and process-loss controls
run: >-
vendor/bin/phpunit tests/Unit/V2/EmbeddedContinuationTest.php
tests/Unit/V2/WorkflowContinuationCacheTest.php
tests/Unit/V2/EmbeddedContinuationProcessTest.php
--testdox --fail-on-warning
--log-junit build/test-results/pr-retained-execution.xml

- name: Run growing continuation worker setup controls
env:
PROFILE_EMBEDDED_CONTINUATION: setup
run: >-
vendor/bin/phpunit tests/Performance/EmbeddedContinuationGrowthProfileTest.php
--testdox --fail-on-warning --display-phpunit-deprecations
--log-junit build/test-results/pr-continuation-growth.xml

- name: Run minimally observed continuation setup controls
env:
PROFILE_EMBEDDED_CONTINUATION: setup
DWF_GROWTH_OBSERVERS: 'off'
run: >-
vendor/bin/phpunit tests/Performance/EmbeddedContinuationGrowthProfileTest.php
--filter '10-events_(1-workers_1-runs|4-workers_8-runs)_(replay|retained)_repeat-1'
--testdox --fail-on-warning --fail-on-empty-test-suite
--log-junit build/test-results/pr-continuation-minimal-observers.xml

- name: Resolve bounded pull-request test selection
run: >-
php scripts/ci/select-pr-tests.php
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Server or Cloud does not reinterpret existing embedded history.

- [Embedded installation](https://durable-workflow.com/docs/2.0/installation/)
- [Embedded feature guides](https://durable-workflow.com/docs/2.0/category/embedded/)
- [Embedded live execution](docs/workflow/embedded-live-execution.md)
- [Configuration reference](https://durable-workflow.com/docs/2.0/configuration/options/)
- [Deployment modes](https://durable-workflow.com/docs/2.0/polyglot/deployment-modes/)
- [Monitoring with Waterline](https://durable-workflow.com/docs/2.0/monitoring/)
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"dev-main": "2.0.x-dev"
},
"durable-workflow": {
"product-train": "2.7.3",
"product-train": "2.8.0",
"laravel-embedded-upgrade-contract": "resources/laravel-embedded-upgrade-contract.json",
"laravel-dependency-security-policy": "resources/laravel-dependency-security-policy.json"
},
Expand Down
134 changes: 134 additions & 0 deletions docs/workflow/embedded-live-execution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Embedded live execution

Persistent embedded PHP workers can keep a bounded suspended workflow between
database-queue jobs. An eligible continuation skips its completed instruction
prefix while accepting the same committed durable outcomes. The database
remains authoritative. Ordinary cold replay is the default.

The [qualification record](https://github.com/durable-workflow/workflow/issues/748)
describes the measured workloads, failed capacity cells and operating limits.
These observations do not establish general production performance or capacity.
Service-mode warm execution has a separate [PHP SDK implementation](https://github.com/durable-workflow/sdk-php/issues/113).

## Enable a selected workflow

In the application's published `config/workflows.php`, set an explicit allowlist
and a build identity that changes whenever the workflow deployment changes:

```php
'v2' => [
// Other application settings remain here.
'live_execution' => [
'workflows' => [App\Workflows\OrderWorkflow::class],
'build' => env('APP_RELEASE_ID'),
],
],
```

Set the application's `APP_RELEASE_ID` to its immutable release identifier.
Restart workers when deploying code or changing connections, schemas, bindings
or allowlists. An empty allowlist or missing/empty build identity selects cold
replay. Each process owns its cache; another worker can always cold replay.
There is no wait for a previous worker or extension of a task lease.

Use an asynchronous Laravel database queue and a persistent `queue:work`
process. Match its connection/queue to the application's existing workflow
routing. Octane is optional and is not part of this cache's lifetime.
Run workers under a process supervisor that restarts memory-limit exits.
An ordinary memory stop can occur between committed jobs; a fresh worker
continues from durable state with an empty cache. Retention does not keep
workers alive or replace supervision.

## Initial execution profile

The retained profile supports sequential remote activities and positive ordinary
timers. Activities continue to run through their durable jobs. Timer deadlines
remain persisted and use the workflow's recorded time.

Allowlist workflows only when their suspended state contains deterministic
workflow data. Keep external I/O and container services in activities. Do not
retain request/scoped services, logging contexts or transient custom-model state
in workflow locals across durable calls. Ordinary replay remains available for
workflows that need those lifetimes.

The runtime automatically selects cold replay for constructor or method object
injection, explicit workflow bindings, decorated methods/classes, unsupported
history, fake/synchronous jobs and caller-owned transactions. Query/update/signal
handlers, parallel/selection/child operations, conditions, continue-as-new,
version markers, side effects and other excluded operations retain their cold
contract. An excluded operation encountered after a retained prefix triggers
cold replay before its durable handler. Repair/redrive history can also select
cold replay. Recorded successful business effects are not repeated.
The initial retained profile requires declared scalar or array entry-parameter
types. Untyped, union/intersection, object, mixed, callable and iterable
parameters select cold replay, including when their current argument is null.

Tracing integration is still an explicit qualification gate: a retained Fiber
must never carry an ended processing span or a previous job's task context.
The tracing-enabled cold fallback and context-toggle controls must pass before
the integrated feature is released.

## Bounds and recovery

Each worker keeps at most four entries. An entry's 30-second monotonic age is
not renewed by hits. Publication and lookup check a 128 MiB worker-resident-memory
threshold. This includes the worker's other memory and is not an isolated frame
measurement or an OS memory limit. Missing resident-memory observations select
cold replay. Use the normal worker/process supervisor limits as well.

Before a resume, the worker verifies the backend/session, namespace/run/model,
configured model classes, definition/build, fresh lease, original run fields and complete contiguous
committed history prefix. Reconnects, changed identities, eviction, expiry,
memory pressure and worker loss select authoritative replay.

An advanced frame becomes reusable only after root commit. Rollback, failed
execution or uncertain acknowledgement discards it. A worker job exception also
clears retained state, including a broker acknowledgement failure after commit.
The ordinary repair and
watchdog paths recover durable work with the original deadlines. Failed task
records remain available for diagnosis. A lost timer transport delivery still
uses the existing stale-dispatch interval; retention does not shorten it.

Shutdown and eviction discard suspended execution using the existing Fiber
disposal contract. They do not request workflow cancellation or create a new
durable cleanup outcome. External cancellation and termination remain durable
commands and are checked before resuming any frame.

## Worker diagnostics

The worker exposes a process-local diagnostic snapshot:

```php
$snapshot = app(Workflow\V2\Support\WorkflowContinuationCache::class)->snapshot();
```

It reports current entry count, nullable worker resident bytes and cumulative
counters for hits, misses, retention publications, eviction, invalidation and cold-fallback
reasons. `cold_entry` counts fresh authoring starts within the observed profile.
Eviction counters count removed entries; `evict_clear` covers explicit clears
and normal worker shutdown. A repeated pressure check on an empty cache is not
another eviction.
`evict_job_exception` counts entries cleared after a worker job exception.
`reused_committed_operations` counts completed durable operations before the
retained suspension point, and `validated_history_events` counts the fresh
history prefixes checked before reuse. These counts are not PHP instruction
counts or CPU-saving estimates. The diagnostic class is internal.
Collect it through the application's existing worker metrics mechanism.
A web request or separate Artisan process cannot observe another worker's
cache. Counters are sparse and reset when that process exits; resident bytes
cover the whole worker and must not be labelled retained-frame bytes.

The [reproducible growth probe](../../tests/Performance/EMBEDDED-CONTINUATION.md)
separates cold test-worker startup, full replay and retained execution. Its
reports state actual cache hits/misses, queue/timer delay, CPU, SQL, memory,
sample counts, failures and unfinished work. Choose retention from measurements
of the actual application workload and its supported lifetime constraints.
The [1,000-event comparison](../../tests/Performance/results/issue-748-20261020/GROWTH-1000.md)
passes three single-worker pairs but fails all six four-worker originals in its
one-CPU / 1 GiB shared-SQLite configuration, with unfinished work, SQLite lock
errors and container or worker memory exits in both modes. Its eight-run large
pilots also miss their original observation deadlines in both modes; remaining
large concurrent repetitions were not admitted. Retention does not establish a
larger safe operating envelope. Those failures remain under review.
The [later small and concurrent comparisons](../../tests/Performance/results/issue-748-20261020/CURRENT-SOURCE.md)
report actual cache misses and workload-specific CPU results.
56 changes: 56 additions & 0 deletions scripts/profile-embedded-continuation.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail

events=${1:?Expected 10, 100 or 1000 events}
workers=${2:?Expected 1 or 4 workers}
runs=${3:?Expected 1 or 8 concurrent runs}
repetition=${4:?Expected repetition 1, 2 or 3}
case "$events" in 10|100|1000) ;; *) exit 2 ;; esac
case "$workers" in 1|4) ;; *) exit 2 ;; esac
case "$runs" in 1|8) ;; *) exit 2 ;; esac
case "$repetition" in 1|2|3) ;; *) exit 2 ;; esac
: "${DWF_PROFILE_IMAGE:?Build tests/Performance/continuation first}"

cd "$(dirname "$0")/.."
git diff --exit-code -- src resources tests scripts composer.json
cmp tests/Performance/continuation/COMPOSER-INPUT.lock composer.lock
git rev-parse HEAD HEAD^{tree}
sha256sum composer.lock
docker image inspect "$DWF_PROFILE_IMAGE" --format '{{.Id}}'

container=''
cleanup() {
if [[ -n "$container" ]]; then docker container rm --force "$container" >/dev/null; fi
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
outcome=0
modes='replay retained'
if [[ "$repetition" == 2 ]]; then modes='retained replay'; fi
for mode in $modes; do
container="dw-continuation-${GITHUB_RUN_ID:-local}-${repetition}-${mode}-$$"
filter="${events}-events_${workers}-workers_${runs}-runs_${mode}_repeat-${repetition}"
echo "CONTINUATION_CELL=$filter"
if timeout --signal=TERM --kill-after=30s 62m \
docker run --name "$container" --network none \
--user "$(id -u):$(id -g)" --cpus 1 --memory 1g --memory-swap 1g \
--volume "$PWD:/app" --workdir /app \
--env PROFILE_EMBEDDED_CONTINUATION=measure \
--env "DWF_GROWTH_OBSERVERS=${DWF_GROWTH_OBSERVERS:-on}" \
"$DWF_PROFILE_IMAGE" \
php vendor/bin/phpunit tests/Performance/EmbeddedContinuationGrowthProfileTest.php \
--filter "$filter" --no-coverage --do-not-cache-result \
--fail-on-empty-test-suite --fail-on-skipped --fail-on-warning; then
status=0
else
status=$?
outcome=1
fi
echo "CONTINUATION_CELL_EXIT=$status"
docker container inspect "$container" --format \
'CONTINUATION_CONTAINER={{json .Name}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}} cpu={{.HostConfig.NanoCpus}} memory={{.HostConfig.Memory}} memory_swap={{.HostConfig.MemorySwap}}'
cleanup
container=''
done
exit "$outcome"
Loading
Loading