Skip to content

Commit 65f3153

Browse files
Revert "Rely on the workflow v2 health snapshot contract in Waterline"
This reverts commit b45d56d.
1 parent b45d56d commit 65f3153

5 files changed

Lines changed: 119 additions & 37 deletions

File tree

app/Http/Controllers/V2HealthController.php

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

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

577579
/**
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.
580+
* Keep Waterline compatible with both the released alpha package and the
581+
* newer workflow branch while health snapshots gain namespace scoping.
581582
*
582583
* @return array<string, mixed>
583584
*/
584585
private function snapshotForConfiguredNamespace(): array
585586
{
586-
return HealthCheck::snapshot(now(), $this->namespace());
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);
587648
}
588649

589650
/**

app/Repositories/Workflow/Infrastructure/V2WorkflowRepository.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ 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);
175176

176177
return $metrics;
177178
}
@@ -228,6 +229,38 @@ private function scopedWorkerMetrics(mixed $workers, ?string $namespace): mixed
228229
return $workers;
229230
}
230231

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+
231264
/**
232265
* @param array<string, mixed> $snapshot
233266
* @return array<string, mixed>

app/Support/WorkflowPackageApiFloor.php

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,21 @@
1111
* Enforces the minimum `durable-workflow/workflow` API surface Waterline
1212
* relies on when the v2 operator bridge is active.
1313
*
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.
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.
1920
*
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.
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.
2729
*/
2830
final class WorkflowPackageApiFloor
2931
{
@@ -41,7 +43,6 @@ final class WorkflowPackageApiFloor
4143
[\Workflow\V2\Support\ScheduleManager::class, 'triggerDetailed', 'context'],
4244
[\Workflow\V2\Support\ScheduleManager::class, 'backfill', 'context'],
4345
[\Workflow\V2\Support\ScheduleManager::class, 'delete', 'context'],
44-
[\Workflow\V2\Support\HealthCheck::class, 'snapshot', 'namespace'],
4546
];
4647

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

composer.json

Lines changed: 5 additions & 1 deletion
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.0-alpha.27"
17+
"durable-workflow/workflow": "^2.0@dev"
1818
},
1919
"require-dev": {
2020
"fakerphp/faker": "^1.9.1",
@@ -23,6 +23,9 @@
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+
},
2629
"autoload": {
2730
"psr-4": {
2831
"Waterline\\": "app/"
@@ -87,5 +90,6 @@
8790
"pestphp/pest-plugin": true
8891
}
8992
},
93+
"minimum-stability": "dev",
9094
"prefer-stable": true
9195
}

tests/Unit/Support/WorkflowPackageApiFloorTest.php

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

1312
/**
@@ -59,22 +58,6 @@ public function test_find_missing_returns_empty_list_on_current_workflow_package
5958
$this->assertSame([], WorkflowPackageApiFloor::findMissing());
6059
}
6160

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-
7861
public function test_find_missing_reports_missing_command_context_class(): void
7962
{
8063
$missing = WorkflowPackageApiFloor::findMissing(
@@ -159,7 +142,7 @@ public function test_assert_against_throws_with_aggregated_diagnostic_when_floor
159142
$this->expectExceptionMessage('older than the API floor Waterline requires');
160143
$this->expectExceptionMessage('NonExistentCommandContext');
161144
$this->expectExceptionMessage('thisMethodDoesNotExist');
162-
$this->expectExceptionMessage('namespace-scoped health snapshot signatures');
145+
$this->expectExceptionMessage('context-accepting schedule mutation signatures');
163146

164147
WorkflowPackageApiFloor::assertAgainst(
165148
contextClass: 'Waterline\\Tests\\Unit\\Support\\NonExistentCommandContext',

0 commit comments

Comments
 (0)