Skip to content

Commit 7eb66e3

Browse files
Surface activity-path retrying age on dw system:operator-metrics
Workflow v2 already exposes operator_metrics.activities with the open, pending, running, retrying counts, oldest_retrying_started_at, max_retrying_age_ms, failed_attempts, and max_attempt_count rows that the rollout-safety contract pins. The cli renderer skipped the block entirely, so operators reading dw system:operator-metrics could not see the activity-path coordination signal. Render an Activities section that prints the open/retrying counts on one line and surfaces the oldest retrying age and started-at lines whenever the engine reports them. The block is omitted when the payload lacks activities so older servers stay readable. Pin the schema in schemas/output/operator-metrics.schema.json with the eight contract keys, and pin the rendered output in SystemCommandTest::test_operator_metrics_command_renders_rollout_safety_signals plus the new schema-pin and minimal-payload tests. Tests: vendor/bin/phpunit tests/Commands/SystemCommandTest.php (31/31 green, 159 assertions) vendor/bin/phpunit (533 tests green, 13,756 assertions, 1 skipped)
1 parent 2d9e855 commit 7eb66e3

3 files changed

Lines changed: 103 additions & 1 deletion

File tree

schemas/output/operator-metrics.schema.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,20 @@
109109
},
110110
"additionalProperties": true
111111
},
112+
"activities": {
113+
"type": "object",
114+
"properties": {
115+
"open": { "type": ["integer", "null"] },
116+
"pending": { "type": ["integer", "null"] },
117+
"running": { "type": ["integer", "null"] },
118+
"retrying": { "type": ["integer", "null"] },
119+
"oldest_retrying_started_at": { "type": ["string", "null"] },
120+
"max_retrying_age_ms": { "type": ["integer", "null"] },
121+
"failed_attempts": { "type": ["integer", "null"] },
122+
"max_attempt_count": { "type": ["integer", "null"] }
123+
},
124+
"additionalProperties": true
125+
},
112126
"repair_policy": {
113127
"type": "object",
114128
"properties": {

src/Commands/SystemCommand/OperatorMetricsCommand.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ protected function configure(): void
3838
<info>dw system:operator-metrics --json | jq '.operator_metrics.workers'</info>
3939
4040
The contract guarantees the frozen key inventory under
41-
`operator_metrics.{runs,tasks,backlog,repair,workers,backend,schedules,repair_policy,matching_role}`;
41+
`operator_metrics.{runs,tasks,backlog,repair,workers,backend,schedules,activities,repair_policy,matching_role}`;
4242
consumers MAY add derived keys but MUST NOT rename these. The
4343
`matching_role` block is per-process and reflects only the node
4444
serving the request; read one snapshot per node to see the full
@@ -73,6 +73,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7373
$this->renderBackend($output, $this->sectionArray($metrics, 'backend'));
7474
$this->renderMatchingRole($output, $this->sectionArray($metrics, 'matching_role'));
7575
$this->renderSchedules($output, $this->sectionArray($metrics, 'schedules'));
76+
$this->renderActivities($output, $this->sectionArray($metrics, 'activities'));
7677
$this->renderRepairPolicy($output, $this->sectionArray($metrics, 'repair_policy'));
7778

7879
return Command::SUCCESS;
@@ -348,6 +349,40 @@ private function renderSchedules(OutputInterface $output, array $schedules): voi
348349
$output->writeln('');
349350
}
350351

352+
/**
353+
* @param array<string, mixed> $activities
354+
*/
355+
private function renderActivities(OutputInterface $output, array $activities): void
356+
{
357+
if ($activities === []) {
358+
return;
359+
}
360+
361+
$output->writeln('<info>Activities</info>');
362+
$output->writeln(sprintf(
363+
' Open %d (pending %d, running %d), retrying %d',
364+
(int) ($activities['open'] ?? 0),
365+
(int) ($activities['pending'] ?? 0),
366+
(int) ($activities['running'] ?? 0),
367+
(int) ($activities['retrying'] ?? 0),
368+
));
369+
if (array_key_exists('max_retrying_age_ms', $activities)) {
370+
$output->writeln(sprintf(
371+
' Oldest retrying age: %d ms',
372+
(int) ($activities['max_retrying_age_ms'] ?? 0),
373+
));
374+
}
375+
if (is_string($activities['oldest_retrying_started_at'] ?? null)) {
376+
$output->writeln(sprintf(' Oldest retrying started at: %s', $activities['oldest_retrying_started_at']));
377+
}
378+
$output->writeln(sprintf(
379+
' Failed attempts: %d (max attempts on a single execution: %d)',
380+
(int) ($activities['failed_attempts'] ?? 0),
381+
(int) ($activities['max_attempt_count'] ?? 0),
382+
));
383+
$output->writeln('');
384+
}
385+
351386
/**
352387
* @param array<string, mixed> $policy
353388
*/

tests/Commands/SystemCommandTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,12 @@ public function test_operator_metrics_command_renders_rollout_safety_signals():
557557
self::assertStringContainsString('Active 4, paused 1, missed 1, oldest overdue 5000 ms', $display);
558558
self::assertStringContainsString('Lifetime fires: 128 (3 failures)', $display);
559559

560+
self::assertStringContainsString('Activities', $display);
561+
self::assertStringContainsString('Open 12 (pending 8, running 4), retrying 3', $display);
562+
self::assertStringContainsString('Oldest retrying age: 270000 ms', $display);
563+
self::assertStringContainsString('Oldest retrying started at: 2026-04-24T11:25:30Z', $display);
564+
self::assertStringContainsString('Failed attempts: 7 (max attempts on a single execution: 5)', $display);
565+
560566
self::assertStringContainsString('Redispatch after: 120s', $display);
561567
self::assertStringContainsString('Loop throttle: 10s', $display);
562568
self::assertStringContainsString('Scan limit: 100', $display);
@@ -688,6 +694,43 @@ public function test_operator_metrics_schema_pins_matching_role_keys(): void
688694
self::assertSame(['string', 'null'], $matchingRole['task_dispatch_mode']['type']);
689695
}
690696

697+
public function test_operator_metrics_schema_pins_activities_retrying_age_keys(): void
698+
{
699+
$schema = json_decode(
700+
(string) file_get_contents(__DIR__.'/../../schemas/output/operator-metrics.schema.json'),
701+
true,
702+
flags: JSON_THROW_ON_ERROR,
703+
);
704+
705+
$activities = $schema['properties']['operator_metrics']['properties']['activities']['properties'];
706+
707+
self::assertSame(['integer', 'null'], $activities['open']['type']);
708+
self::assertSame(['integer', 'null'], $activities['pending']['type']);
709+
self::assertSame(['integer', 'null'], $activities['running']['type']);
710+
self::assertSame(['integer', 'null'], $activities['retrying']['type']);
711+
self::assertSame(['string', 'null'], $activities['oldest_retrying_started_at']['type']);
712+
self::assertSame(['integer', 'null'], $activities['max_retrying_age_ms']['type']);
713+
self::assertSame(['integer', 'null'], $activities['failed_attempts']['type']);
714+
self::assertSame(['integer', 'null'], $activities['max_attempt_count']['type']);
715+
}
716+
717+
public function test_operator_metrics_command_omits_activities_block_when_payload_lacks_it(): void
718+
{
719+
$payload = self::operatorMetricsPayload();
720+
unset($payload['operator_metrics']['activities']);
721+
722+
$command = new OperatorMetricsCommand();
723+
$command->setServerClient(new SystemFakeClient($payload));
724+
725+
$tester = new CommandTester($command);
726+
self::assertSame(Command::SUCCESS, $tester->execute([]));
727+
728+
$display = $tester->getDisplay();
729+
730+
self::assertStringNotContainsString('Activities', $display);
731+
self::assertStringNotContainsString('Oldest retrying age', $display);
732+
}
733+
691734
public function test_operator_metrics_command_renders_dedicated_matching_role_shape(): void
692735
{
693736
$payload = self::operatorMetricsPayload();
@@ -817,6 +860,16 @@ private static function operatorMetricsPayload(): array
817860
'fires_total' => 128,
818861
'failures_total' => 3,
819862
],
863+
'activities' => [
864+
'open' => 12,
865+
'pending' => 8,
866+
'running' => 4,
867+
'retrying' => 3,
868+
'oldest_retrying_started_at' => '2026-04-24T11:25:30Z',
869+
'max_retrying_age_ms' => 270000,
870+
'failed_attempts' => 7,
871+
'max_attempt_count' => 5,
872+
],
820873
'repair_policy' => [
821874
'redispatch_after_seconds' => 120,
822875
'loop_throttle_seconds' => 10,

0 commit comments

Comments
 (0)