Skip to content

Commit baf5e74

Browse files
Surface claim-failed age on dw system:operator-metrics
Renders the workflow-v2 task-path claim-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 claim error?" — the primary lease-conflict and duplicate-risk age indicator on the claim path — directly from `dw system:operator-metrics`. `OperatorMetricsCommand::renderTasks()` now emits two new rows right after the existing oldest-dispatch-overdue rows: Oldest claim-failed age: <ms> Oldest claim failed at: <ISO-8601> Both rows degrade gracefully when the snapshot predates the contract (workflow alpha < 2.0.0-alpha.17, where 6089777 "Surface claim-failed age on operator metrics and task_transport health" is not yet on a published tag): the renderer omits the rows and the existing `claim failed <count>` cell on the duplicate-risk roll-up still renders. Pins `tasks.oldest_claim_failed_at` and `tasks.max_claim_failed_age_ms` on `schemas/output/operator-metrics.schema.json` next to the existing `oldest_dispatch_overdue_since` / `max_dispatch_overdue_age_ms` pair, and adds `test_operator_metrics_schema_pins_claim_failed_age_keys` plus `test_operator_metrics_command_omits_claim_failed_age_when_snapshot_predates_contract` to guard the schema declaration and the renderer fallback.
1 parent 7eb66e3 commit baf5e74

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
@@ -39,6 +39,8 @@
3939
"max_ready_due_age_ms": { "type": ["integer", "null"] },
4040
"oldest_dispatch_overdue_since": { "type": ["string", "null"] },
4141
"max_dispatch_overdue_age_ms": { "type": ["integer", "null"] },
42+
"oldest_claim_failed_at": { "type": ["string", "null"] },
43+
"max_claim_failed_age_ms": { "type": ["integer", "null"] },
4244
"unhealthy": { "type": ["integer", "null"] }
4345
},
4446
"additionalProperties": true

src/Commands/SystemCommand/OperatorMetricsCommand.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,15 @@ private function renderTasks(OutputInterface $output, array $tasks): void
162162
if (is_string($tasks['oldest_dispatch_overdue_since'] ?? null)) {
163163
$output->writeln(sprintf(' Oldest dispatch-overdue since: %s', $tasks['oldest_dispatch_overdue_since']));
164164
}
165+
if (array_key_exists('max_claim_failed_age_ms', $tasks)) {
166+
$output->writeln(sprintf(
167+
' Oldest claim-failed age: %d ms',
168+
(int) ($tasks['max_claim_failed_age_ms'] ?? 0),
169+
));
170+
}
171+
if (is_string($tasks['oldest_claim_failed_at'] ?? null)) {
172+
$output->writeln(sprintf(' Oldest claim failed at: %s', $tasks['oldest_claim_failed_at']));
173+
}
165174
$output->writeln('');
166175
}
167176

tests/Commands/SystemCommandTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,8 @@ public function test_operator_metrics_command_renders_rollout_safety_signals():
522522
self::assertStringContainsString('Oldest ready-due at: 2026-04-24T11:29:45Z', $display);
523523
self::assertStringContainsString('Oldest dispatch-overdue age: 55000 ms', $display);
524524
self::assertStringContainsString('Oldest dispatch-overdue since: 2026-04-24T11:29:05Z', $display);
525+
self::assertStringContainsString('Oldest claim-failed age: 65000 ms', $display);
526+
self::assertStringContainsString('Oldest claim failed at: 2026-04-24T11:28:55Z', $display);
525527

526528
self::assertStringContainsString('Runnable tasks: 7', $display);
527529
self::assertStringContainsString('Delayed tasks: 3', $display);
@@ -664,6 +666,20 @@ public function test_operator_metrics_schema_pins_dispatch_overdue_age_keys(): v
664666
self::assertSame(['integer', 'null'], $tasks['max_dispatch_overdue_age_ms']['type']);
665667
}
666668

669+
public function test_operator_metrics_schema_pins_claim_failed_age_keys(): void
670+
{
671+
$schema = json_decode(
672+
(string) file_get_contents(__DIR__.'/../../schemas/output/operator-metrics.schema.json'),
673+
true,
674+
flags: JSON_THROW_ON_ERROR,
675+
);
676+
677+
$tasks = $schema['properties']['operator_metrics']['properties']['tasks']['properties'];
678+
679+
self::assertSame(['string', 'null'], $tasks['oldest_claim_failed_at']['type']);
680+
self::assertSame(['integer', 'null'], $tasks['max_claim_failed_age_ms']['type']);
681+
}
682+
667683
public function test_operator_metrics_schema_pins_run_wait_age_keys(): void
668684
{
669685
$schema = json_decode(
@@ -714,6 +730,27 @@ public function test_operator_metrics_schema_pins_activities_retrying_age_keys()
714730
self::assertSame(['integer', 'null'], $activities['max_attempt_count']['type']);
715731
}
716732

733+
public function test_operator_metrics_command_omits_claim_failed_age_when_snapshot_predates_contract(): void
734+
{
735+
$payload = self::operatorMetricsPayload();
736+
unset(
737+
$payload['operator_metrics']['tasks']['oldest_claim_failed_at'],
738+
$payload['operator_metrics']['tasks']['max_claim_failed_age_ms'],
739+
);
740+
741+
$command = new OperatorMetricsCommand();
742+
$command->setServerClient(new SystemFakeClient($payload));
743+
744+
$tester = new CommandTester($command);
745+
self::assertSame(Command::SUCCESS, $tester->execute([]));
746+
747+
$display = $tester->getDisplay();
748+
749+
self::assertStringNotContainsString('Oldest claim-failed age', $display);
750+
self::assertStringNotContainsString('Oldest claim failed at', $display);
751+
self::assertStringContainsString('claim failed 3', $display);
752+
}
753+
717754
public function test_operator_metrics_command_omits_activities_block_when_payload_lacks_it(): void
718755
{
719756
$payload = self::operatorMetricsPayload();
@@ -801,6 +838,8 @@ private static function operatorMetricsPayload(): array
801838
'max_ready_due_age_ms' => 15000,
802839
'oldest_dispatch_overdue_since' => '2026-04-24T11:29:05Z',
803840
'max_dispatch_overdue_age_ms' => 55000,
841+
'oldest_claim_failed_at' => '2026-04-24T11:28:55Z',
842+
'max_claim_failed_age_ms' => 65000,
804843
'unhealthy' => 11,
805844
],
806845
'backlog' => [

0 commit comments

Comments
 (0)