Skip to content

Commit d7db4a2

Browse files
authored
Configure legacy overlap-lock release delay (#903)
Add optional nonnegative overlapReleaseAfter with zero-default and serialized-options compatibility. Preserve legacy lock ownership and inheritance; keep V2 portable behavior unchanged. Refs #897.
1 parent aca7928 commit d7db4a2

20 files changed

Lines changed: 545 additions & 27 deletions

‎.github/workflows/php.yml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,20 @@ jobs:
394394
tests/Unit/V2/ProjectionSqliteContentionTest.php \
395395
--fail-on-warning --log-junit build/test-results/pr-database-queue-recovery.xml
396396
397+
- name: Run legacy overlap releases on a real database queue
398+
env:
399+
DB_CONNECTION: sqlite
400+
DB_DATABASE: ${{ runner.temp }}/workflow-overlap-release.sqlite
401+
CACHE_DRIVER: array
402+
CACHE_STORE: array
403+
QUEUE_CONNECTION: sync
404+
REDIS_HOST: ''
405+
run: |
406+
touch "$DB_DATABASE"
407+
vendor/bin/phpunit tests/Unit/WorkflowOverlapReleaseTest.php \
408+
tests/Unit/WorkflowOptionsTest.php \
409+
tests/Unit/V2/LegacyOverlapOptionsBoundaryTest.php --testdox --fail-on-warning
410+
397411
- name: Run retained execution authority and process-loss controls
398412
run: >-
399413
vendor/bin/phpunit tests/Unit/V2/EmbeddedContinuationTest.php

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 2.11.0 - 2026-10-22
4+
5+
- Add the opt-in `WorkflowOptions::overlapReleaseAfter` delay for legacy
6+
workflow, activity, signal and child-completion jobs contending on overlap
7+
locks. Preserve the zero default, stored options, lock ownership and expiry.
8+
- Preserve the delay through child inheritance and continue-as-new. Queue
9+
releases still count toward Laravel attempt limits.
10+
311
## 2.10.2 - 2026-10-22
412

513
- Preserve recorded activity lease expiry and worker deregistration in canonical

‎composer.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
"dev-main": "2.0.x-dev"
8888
},
8989
"durable-workflow": {
90-
"product-train": "2.10.2",
90+
"product-train": "2.11.0",
9191
"laravel-embedded-upgrade-contract": "resources/laravel-embedded-upgrade-contract.json",
9292
"laravel-dependency-security-policy": "resources/laravel-dependency-security-policy.json"
9393
},

‎docs/architecture/routing-precedence.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,11 @@ The contract authorities are:
133133
`RoutingResolver::activityConnection`, and
134134
`RoutingResolver::activityQueue`. No other class is allowed to
135135
compute `(connection, queue)` from the same inputs.
136-
- **`WorkflowOptions`** — the durable wire value for workflow start
137-
routing overrides. The type carries only `?string $connection` and
138-
`?string $queue`. Any additional fields on `WorkflowOptions` are a
139-
protocol change.
136+
- **`WorkflowOptions`** — workflow start routing overrides are
137+
`?string $connection` and `?string $queue`. The optional nonnegative
138+
`int $overlapReleaseAfter` controls only the legacy `Workflow\\Workflow`
139+
queue middleware. It defaults to zero, is omitted from stored metadata at
140+
that default, and does not change V2 routing or the portable service protocol.
140141
- **`ActivityOptions`** — the durable wire value for activity
141142
scheduling overrides. Routing fields on `ActivityOptions` are
142143
`?string $connection` and `?string $queue`; retry and timeout

‎docs/legacy-overlap-release.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Legacy overlap release delay
2+
3+
Applications using `Workflow\Workflow` can back off queue jobs that cannot
4+
acquire an overlap lock:
5+
6+
```php
7+
use Workflow\WorkflowOptions;
8+
use Workflow\WorkflowStub;
9+
10+
$workflow = WorkflowStub::make(MyWorkflow::class);
11+
$workflow->start('argument', new WorkflowOptions(overlapReleaseAfter: 5));
12+
```
13+
14+
`WorkflowOptions::set(['overlapReleaseAfter' => 5])` accepts the same setting.
15+
The delay is a nonnegative integer in seconds. Its default is zero, preserving
16+
existing behavior and stored options. It applies to contended workflow,
17+
activity, signal and child-completion queue jobs. It does not change lock
18+
expiry, semaphore ownership, or activity exception backoff.
19+
20+
The setting is saved with the run. Children and continue-as-new inherit it
21+
when no explicit `WorkflowOptions` is supplied. An explicit options object
22+
replaces inherited options, including resetting this delay to zero if omitted.
23+
24+
Laravel counts queue reservations as attempts even when middleware releases
25+
the job before its handler executes. A positive delay reduces rapid retries;
26+
it does not make a low `$tries`, `--tries`, or database queue attempt limit
27+
safe for arbitrary contention. Choose the delay and retry budget for the
28+
expected lock duration. Keep queue visibility and worker timeout settings
29+
consistent with activity duration.
30+
31+
This option belongs to the legacy API. `Workflow\V2\Workflow` uses durable
32+
task claims and does not use this overlap middleware.

‎src/Activity.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ public function middleware()
138138
new WithoutOverlappingMiddleware(
139139
$this->storedWorkflow->id,
140140
WithoutOverlappingMiddleware::ACTIVITY,
141-
0,
141+
$this->storedWorkflow->workflowOptions()
142+
->overlapReleaseAfter,
142143
$this->timeout
143144
),
144145
new ActivityMiddleware(),

‎src/ChildWorkflow.php‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,13 @@ public function handle()
7373
public function middleware()
7474
{
7575
return [
76-
new WithoutOverlappingMiddleware($this->parentWorkflow->id, WithoutOverlappingMiddleware::ACTIVITY, 0, 15),
76+
new WithoutOverlappingMiddleware(
77+
$this->parentWorkflow->id,
78+
WithoutOverlappingMiddleware::ACTIVITY,
79+
$this->parentWorkflow->workflowOptions()
80+
->overlapReleaseAfter,
81+
15
82+
),
7783
];
7884
}
7985
}

‎src/ChildWorkflowStub.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public static function make($workflow, ...$arguments): PromiseInterface
121121
if (! $hasOptions) {
122122
$options = $context->storedWorkflow->workflowOptions();
123123

124-
if ($options->connection !== null || $options->queue !== null) {
124+
if ($options->connection !== null || $options->queue !== null || $options->overlapReleaseAfter !== 0) {
125125
$arguments[] = $options;
126126
}
127127
}

‎src/Signal.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public function middleware()
4646
new WithoutOverlappingMiddleware(
4747
$this->storedWorkflow->id,
4848
WithoutOverlappingMiddleware::WORKFLOW,
49-
0,
49+
$this->storedWorkflow->workflowOptions()
50+
->overlapReleaseAfter,
5051
15
5152
),
5253
];

‎src/Traits/Continues.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static function continueAsNew(...$arguments): PromiseInterface
6767
if (! collect($arguments)->contains(static fn ($argument): bool => $argument instanceof WorkflowOptions)) {
6868
$options = $context->storedWorkflow->workflowOptions();
6969

70-
if ($options->connection !== null || $options->queue !== null) {
70+
if ($options->connection !== null || $options->queue !== null || $options->overlapReleaseAfter !== 0) {
7171
$arguments[] = $options;
7272
}
7373
}

0 commit comments

Comments
 (0)