Skip to content

Commit 48c2ff6

Browse files
Surface dispatch-failed age on dw system:operator-metrics
Renders the workflow-v2 task-path dispatch-failed age pair on the operator-metrics CLI surface so HTTP-only operators can read "how long has the worst-case task been sitting with an uncleared dispatch error?" — the primary transport-failure age indicator on the dispatch path — directly from `dw system:operator-metrics`. `OperatorMetricsCommand::renderTasks()` now emits two new rows right after the existing claim-failed age rows: Oldest dispatch-failed age: <ms> Oldest dispatch failed at: <ISO-8601> Both rows degrade gracefully when the snapshot predates the contract: the renderer omits the rows and the existing `dispatch failed <count>` cell on the duplicate-risk roll-up still renders. Pins `tasks.oldest_dispatch_failed_at` and `tasks.max_dispatch_failed_age_ms` on `schemas/output/operator-metrics.schema.json` next to the existing `oldest_claim_failed_at` / `max_claim_failed_age_ms` pair, and adds `test_operator_metrics_schema_pins_dispatch_failed_age_keys` plus `test_operator_metrics_command_omits_dispatch_failed_age_when_snapshot_predates_contract` to guard the schema declaration and the renderer fallback.
1 parent baf5e74 commit 48c2ff6

3 files changed

Lines changed: 50 additions & 0 deletions

File tree

schemas/output/operator-metrics.schema.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
"max_dispatch_overdue_age_ms": { "type": ["integer", "null"] },
4242
"oldest_claim_failed_at": { "type": ["string", "null"] },
4343
"max_claim_failed_age_ms": { "type": ["integer", "null"] },
44+
"oldest_dispatch_failed_at": { "type": ["string", "null"] },
45+
"max_dispatch_failed_age_ms": { "type": ["integer", "null"] },
4446
"unhealthy": { "type": ["integer", "null"] }
4547
},
4648
"additionalProperties": true

src/Commands/SystemCommand/OperatorMetricsCommand.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,15 @@ private function renderTasks(OutputInterface $output, array $tasks): void
171171
if (is_string($tasks['oldest_claim_failed_at'] ?? null)) {
172172
$output->writeln(sprintf(' Oldest claim failed at: %s', $tasks['oldest_claim_failed_at']));
173173
}
174+
if (array_key_exists('max_dispatch_failed_age_ms', $tasks)) {
175+
$output->writeln(sprintf(
176+
' Oldest dispatch-failed age: %d ms',
177+
(int) ($tasks['max_dispatch_failed_age_ms'] ?? 0),
178+
));
179+
}
180+
if (is_string($tasks['oldest_dispatch_failed_at'] ?? null)) {
181+
$output->writeln(sprintf(' Oldest dispatch failed at: %s', $tasks['oldest_dispatch_failed_at']));
182+
}
174183
$output->writeln('');
175184
}
176185

tests/Commands/SystemCommandTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,8 @@ public function test_operator_metrics_command_renders_rollout_safety_signals():
524524
self::assertStringContainsString('Oldest dispatch-overdue since: 2026-04-24T11:29:05Z', $display);
525525
self::assertStringContainsString('Oldest claim-failed age: 65000 ms', $display);
526526
self::assertStringContainsString('Oldest claim failed at: 2026-04-24T11:28:55Z', $display);
527+
self::assertStringContainsString('Oldest dispatch-failed age: 135000 ms', $display);
528+
self::assertStringContainsString('Oldest dispatch failed at: 2026-04-24T11:27:45Z', $display);
527529

528530
self::assertStringContainsString('Runnable tasks: 7', $display);
529531
self::assertStringContainsString('Delayed tasks: 3', $display);
@@ -680,6 +682,20 @@ public function test_operator_metrics_schema_pins_claim_failed_age_keys(): void
680682
self::assertSame(['integer', 'null'], $tasks['max_claim_failed_age_ms']['type']);
681683
}
682684

685+
public function test_operator_metrics_schema_pins_dispatch_failed_age_keys(): void
686+
{
687+
$schema = json_decode(
688+
(string) file_get_contents(__DIR__.'/../../schemas/output/operator-metrics.schema.json'),
689+
true,
690+
flags: JSON_THROW_ON_ERROR,
691+
);
692+
693+
$tasks = $schema['properties']['operator_metrics']['properties']['tasks']['properties'];
694+
695+
self::assertSame(['string', 'null'], $tasks['oldest_dispatch_failed_at']['type']);
696+
self::assertSame(['integer', 'null'], $tasks['max_dispatch_failed_age_ms']['type']);
697+
}
698+
683699
public function test_operator_metrics_schema_pins_run_wait_age_keys(): void
684700
{
685701
$schema = json_decode(
@@ -751,6 +767,27 @@ public function test_operator_metrics_command_omits_claim_failed_age_when_snapsh
751767
self::assertStringContainsString('claim failed 3', $display);
752768
}
753769

770+
public function test_operator_metrics_command_omits_dispatch_failed_age_when_snapshot_predates_contract(): void
771+
{
772+
$payload = self::operatorMetricsPayload();
773+
unset(
774+
$payload['operator_metrics']['tasks']['oldest_dispatch_failed_at'],
775+
$payload['operator_metrics']['tasks']['max_dispatch_failed_age_ms'],
776+
);
777+
778+
$command = new OperatorMetricsCommand();
779+
$command->setServerClient(new SystemFakeClient($payload));
780+
781+
$tester = new CommandTester($command);
782+
self::assertSame(Command::SUCCESS, $tester->execute([]));
783+
784+
$display = $tester->getDisplay();
785+
786+
self::assertStringNotContainsString('Oldest dispatch-failed age', $display);
787+
self::assertStringNotContainsString('Oldest dispatch failed at', $display);
788+
self::assertStringContainsString('dispatch failed 2', $display);
789+
}
790+
754791
public function test_operator_metrics_command_omits_activities_block_when_payload_lacks_it(): void
755792
{
756793
$payload = self::operatorMetricsPayload();
@@ -840,6 +877,8 @@ private static function operatorMetricsPayload(): array
840877
'max_dispatch_overdue_age_ms' => 55000,
841878
'oldest_claim_failed_at' => '2026-04-24T11:28:55Z',
842879
'max_claim_failed_age_ms' => 65000,
880+
'oldest_dispatch_failed_at' => '2026-04-24T11:27:45Z',
881+
'max_dispatch_failed_age_ms' => 135000,
843882
'unhealthy' => 11,
844883
],
845884
'backlog' => [

0 commit comments

Comments
 (0)