Skip to content

Commit e7a0f78

Browse files
Require the published workflow health contract in Waterline
Require durable-workflow/workflow ^2.0.0-alpha.27 so Waterline can rely on the namespace-scoped v2 health snapshot and the current schedule mutation signatures. Remove compatibility shims for older workflow alphas and extend the API-floor test coverage to assert the HealthCheck snapshot namespace parameter.
1 parent 65f3153 commit e7a0f78

5 files changed

Lines changed: 37 additions & 118 deletions

File tree

app/Http/Controllers/V2HealthController.php

Lines changed: 4 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
use Workflow\V2\Enums\TaskStatus;
1010
use Workflow\V2\Enums\TaskType;
1111
use Workflow\V2\Support\HealthCheck;
12-
use Workflow\V2\Support\OperatorMetrics;
1312
use Workflow\V2\Support\StandaloneWorkerVisibility;
14-
use Workflow\V2\Support\StructuralLimits;
1513
use Workflow\V2\Support\TaskRepairPolicy;
1614
use Workflow\V2\Models\WorkflowTask;
1715

@@ -577,74 +575,15 @@ private function queueVisibilityAlerts(array $queueVisibility): array
577575
}
578576

579577
/**
580-
* Keep Waterline compatible with both the released alpha package and the
581-
* newer workflow branch while health snapshots gain namespace scoping.
578+
* Waterline relies on the namespace-scoped v2 health snapshot contract.
579+
* WorkflowPackageApiFloor asserts the required signature at boot whenever
580+
* the resolved engine source is v2.
582581
*
583582
* @return array<string, mixed>
584583
*/
585584
private function snapshotForConfiguredNamespace(): array
586585
{
587-
$namespace = $this->namespace();
588-
$now = now();
589-
590-
if ((new \ReflectionMethod(HealthCheck::class, 'snapshot'))->getNumberOfParameters() >= 2) {
591-
return HealthCheck::snapshot($now, $namespace);
592-
}
593-
594-
$metrics = OperatorMetrics::snapshot($now, $namespace);
595-
$checks = [
596-
self::invokeLegacyHealthCheck('backendCheck', $metrics['backend'] ?? []),
597-
self::invokeLegacyHealthCheck('runSummaryProjectionCheck', $metrics['projections']['run_summaries'] ?? []),
598-
self::invokeLegacyHealthCheck('selectedRunProjectionCheck', $metrics['projections'] ?? []),
599-
self::invokeLegacyHealthCheck('historyRetentionInvariantCheck', $metrics['history'] ?? []),
600-
self::invokeLegacyHealthCheck('commandContractCheck', $metrics['command_contracts'] ?? []),
601-
self::invokeLegacyHealthCheck('taskTransportCheck', $metrics['tasks'] ?? [], $metrics['backlog'] ?? []),
602-
];
603-
604-
if (self::legacyHealthCheckExists('routingHealthCheck')) {
605-
$checks[] = self::invokeLegacyHealthCheck(
606-
'routingHealthCheck',
607-
$metrics['tasks'] ?? [],
608-
$metrics['backlog'] ?? [],
609-
$metrics['matching_role'] ?? [],
610-
$metrics['workers'] ?? [],
611-
);
612-
}
613-
614-
$checks[] = self::invokeLegacyHealthCheck(
615-
'durableResumePathCheck',
616-
$metrics['backlog'] ?? [],
617-
$metrics['repair'] ?? [],
618-
$metrics['runs'] ?? [],
619-
);
620-
$checks[] = self::invokeLegacyHealthCheck('workerCompatibilityCheck', $metrics['workers'] ?? []);
621-
$checks[] = self::invokeLegacyHealthCheck('schedulerRoleCheck', $metrics['schedules'] ?? []);
622-
$checks[] = self::invokeLegacyHealthCheck('longPollWakeAccelerationCheck');
623-
$status = self::invokeLegacyHealthCheck('status', $checks);
624-
625-
return [
626-
'generated_at' => $metrics['generated_at'] ?? $now->toJSON(),
627-
'status' => $status,
628-
'healthy' => $status !== 'error',
629-
'checks' => $checks,
630-
'categories' => self::invokeLegacyHealthCheck('categorySummary', $checks),
631-
'operator_metrics' => $metrics,
632-
'structural_limits' => StructuralLimits::snapshot(),
633-
];
634-
}
635-
636-
private static function invokeLegacyHealthCheck(string $method, mixed ...$args): mixed
637-
{
638-
return \Closure::bind(
639-
static fn (string $method, array $args): mixed => HealthCheck::$method(...$args),
640-
null,
641-
HealthCheck::class,
642-
)($method, $args);
643-
}
644-
645-
private static function legacyHealthCheckExists(string $method): bool
646-
{
647-
return method_exists(HealthCheck::class, $method);
586+
return HealthCheck::snapshot(now(), $this->namespace());
648587
}
649588

650589
/**

app/Repositories/Workflow/Infrastructure/V2WorkflowRepository.php

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ private function annotateOperatorMetrics(mixed $metrics, ?string $namespace): mi
172172
}
173173

174174
$metrics['workers'] = $this->scopedWorkerMetrics($metrics['workers'] ?? null, $namespace);
175-
$metrics['matching_role'] = $this->matchingRoleMetrics($metrics['matching_role'] ?? null);
176175

177176
return $metrics;
178177
}
@@ -229,38 +228,6 @@ private function scopedWorkerMetrics(mixed $workers, ?string $namespace): mixed
229228
return $workers;
230229
}
231230

232-
/**
233-
* The task-matching contract freezes the wake owner, partition
234-
* primitives, and lease-based backpressure model even though older
235-
* workflow alphas did not yet expose the full matching-role block on
236-
* OperatorMetrics::snapshot().
237-
*
238-
* @param mixed $matchingRole
239-
* @return mixed
240-
*/
241-
private function matchingRoleMetrics(mixed $matchingRole): mixed
242-
{
243-
if (! is_array($matchingRole)) {
244-
return $matchingRole;
245-
}
246-
247-
if (! array_key_exists('wake_owner', $matchingRole)) {
248-
$matchingRole['wake_owner'] = ($matchingRole['queue_wake_enabled'] ?? false) === true
249-
? 'worker_loop'
250-
: 'dedicated_repair_pass';
251-
}
252-
253-
if (! array_key_exists('partition_primitives', $matchingRole)) {
254-
$matchingRole['partition_primitives'] = ['connection', 'queue', 'compatibility', 'namespace'];
255-
}
256-
257-
if (! array_key_exists('backpressure_model', $matchingRole)) {
258-
$matchingRole['backpressure_model'] = 'lease_ownership';
259-
}
260-
261-
return $matchingRole;
262-
}
263-
264231
/**
265232
* @param array<string, mixed> $snapshot
266233
* @return array<string, mixed>

app/Support/WorkflowPackageApiFloor.php

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,19 @@
1111
* Enforces the minimum `durable-workflow/workflow` API surface Waterline
1212
* relies on when the v2 operator bridge is active.
1313
*
14-
* Waterline's composer constraint (`^1.0 || ^2.0`) intentionally covers a
15-
* wide range because Waterline ships with a v1 fallback. The API floor
16-
* here only matters when the resolved engine source is v2 — a v1 install
17-
* or an `auto`-mode install that falls back to v1 never calls the v2
18-
* schedule mutation signatures and must boot cleanly even when the v2
19-
* classes are absent.
14+
* Waterline now requires the published v2 alpha line of
15+
* `durable-workflow/workflow`, but the API floor still only matters when the
16+
* resolved engine source is v2. A v1 install or an `auto`-mode install that
17+
* falls back to v1 never calls the v2 schedule mutation signatures and must
18+
* boot cleanly even when the v2 classes are absent.
2019
*
21-
* Inside the v2 band, however, specific methods that carry
22-
* `CommandContext` for audit attribution landed only in workflow commit
23-
* `e59e6f2`. An older v2 install that predates that commit lacks the
24-
* context-accepting signatures and fails the Waterline schedule
25-
* pause/resume/trigger/backfill/delete routes with unknown-named-parameter
26-
* or argument-count errors. `assertIfActive()` is called at boot so those
27-
* broken pairings surface with a clear diagnostic instead of a 500 the
28-
* first time an operator clicks "Pause" in the UI.
20+
* Inside the v2 band, however, Waterline now depends on specific schedule
21+
* mutation signatures and the namespace-scoped v2 health snapshot. Older
22+
* v2 installs that predate those contracts fail schedule mutation routes
23+
* with unknown-named-parameter or argument-count errors, or silently lose
24+
* namespace scoping on the operator health surface. `assertIfActive()` is
25+
* called at boot so those broken pairings surface with a clear diagnostic
26+
* instead of a 500 or a cross-namespace health payload at runtime.
2927
*/
3028
final class WorkflowPackageApiFloor
3129
{
@@ -43,6 +41,7 @@ final class WorkflowPackageApiFloor
4341
[\Workflow\V2\Support\ScheduleManager::class, 'triggerDetailed', 'context'],
4442
[\Workflow\V2\Support\ScheduleManager::class, 'backfill', 'context'],
4543
[\Workflow\V2\Support\ScheduleManager::class, 'delete', 'context'],
44+
[\Workflow\V2\Support\HealthCheck::class, 'snapshot', 'namespace'],
4645
];
4746

4847
public const COMMAND_CONTEXT_CLASS = \Workflow\V2\CommandContext::class;
@@ -92,7 +91,7 @@ public static function assertAgainst(
9291
throw new RuntimeException(sprintf(
9392
"Installed durable-workflow/workflow package is older than the API floor Waterline requires. "
9493
."Missing: %s. Upgrade the workflow package to a v2 snapshot that includes CommandContext "
95-
.'and the context-accepting schedule mutation signatures (see repos/workflow commit e59e6f2).',
94+
.'plus the context-accepting schedule mutation and namespace-scoped health snapshot signatures.',
9695
implode(', ', $missing),
9796
));
9897
}

composer.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"require": {
1515
"php": "^8.0.2",
1616
"illuminate/support": "^9.0|^10.0|^11.0|^12.0|^13.0",
17-
"durable-workflow/workflow": "^2.0@dev"
17+
"durable-workflow/workflow": "^2.0.0-alpha.27"
1818
},
1919
"require-dev": {
2020
"fakerphp/faker": "^1.9.1",
@@ -23,9 +23,6 @@
2323
"orchestra/workbench": "^7.29",
2424
"phpunit/phpunit": "^9.5.10|^12.5.12"
2525
},
26-
"conflict": {
27-
"durable-workflow/workflow": "<2.0.0-alpha.27"
28-
},
2926
"autoload": {
3027
"psr-4": {
3128
"Waterline\\": "app/"

tests/Unit/Support/WorkflowPackageApiFloorTest.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Waterline\Support\WorkflowPackageApiFloor;
88
use Waterline\Tests\TestCase;
99
use Workflow\V2\CommandContext;
10+
use Workflow\V2\Support\HealthCheck;
1011
use Workflow\V2\Support\ScheduleManager;
1112

1213
/**
@@ -58,6 +59,22 @@ public function test_find_missing_returns_empty_list_on_current_workflow_package
5859
$this->assertSame([], WorkflowPackageApiFloor::findMissing());
5960
}
6061

62+
public function test_health_check_snapshot_accepts_namespace_parameter(): void
63+
{
64+
$reflection = new ReflectionClass(HealthCheck::class);
65+
$reflectionMethod = $reflection->getMethod('snapshot');
66+
67+
$this->assertTrue($reflectionMethod->isPublic(), 'snapshot is not public');
68+
$this->assertTrue($reflectionMethod->isStatic(), 'snapshot is not static');
69+
70+
$parameterNames = array_map(
71+
static fn ($parameter): string => $parameter->getName(),
72+
$reflectionMethod->getParameters(),
73+
);
74+
75+
$this->assertContains('namespace', $parameterNames, 'snapshot does not declare a $namespace parameter');
76+
}
77+
6178
public function test_find_missing_reports_missing_command_context_class(): void
6279
{
6380
$missing = WorkflowPackageApiFloor::findMissing(
@@ -142,7 +159,7 @@ public function test_assert_against_throws_with_aggregated_diagnostic_when_floor
142159
$this->expectExceptionMessage('older than the API floor Waterline requires');
143160
$this->expectExceptionMessage('NonExistentCommandContext');
144161
$this->expectExceptionMessage('thisMethodDoesNotExist');
145-
$this->expectExceptionMessage('context-accepting schedule mutation signatures');
162+
$this->expectExceptionMessage('namespace-scoped health snapshot signatures');
146163

147164
WorkflowPackageApiFloor::assertAgainst(
148165
contextClass: 'Waterline\\Tests\\Unit\\Support\\NonExistentCommandContext',

0 commit comments

Comments
 (0)