Skip to content

Commit d6e6857

Browse files
Surface matching-role shape on dw system:operator-metrics
Declares operator_metrics.matching_role in the CLI output schema and renders queue_wake_enabled, shape, and task_dispatch_mode on a dedicated "Matching-role (this node)" section with a graceful fallback when the server does not emit the block. The block is per-process and the renderer labels the scope so operators in mixed-shape fleets understand one snapshot reflects one node.
1 parent f475757 commit d6e6857

3 files changed

Lines changed: 91 additions & 2 deletions

File tree

schemas/output/operator-metrics.schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@
118118
"failure_backoff_max_seconds": { "type": ["integer", "null"] }
119119
},
120120
"additionalProperties": true
121+
},
122+
"matching_role": {
123+
"type": "object",
124+
"properties": {
125+
"queue_wake_enabled": { "type": ["boolean", "null"] },
126+
"shape": { "type": ["string", "null"] },
127+
"task_dispatch_mode": { "type": ["string", "null"] }
128+
},
129+
"additionalProperties": true
121130
}
122131
},
123132
"additionalProperties": true

src/Commands/SystemCommand/OperatorMetricsCommand.php

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ 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}`;
42-
consumers MAY add derived keys but MUST NOT rename these.
41+
`operator_metrics.{runs,tasks,backlog,repair,workers,backend,schedules,repair_policy,matching_role}`;
42+
consumers MAY add derived keys but MUST NOT rename these. The
43+
`matching_role` block is per-process and reflects only the node
44+
serving the request; read one snapshot per node to see the full
45+
deployment shape.
4346
HELP)
4447
->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON');
4548
}
@@ -68,6 +71,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6871
$this->renderRepair($output, $this->sectionArray($metrics, 'repair'));
6972
$this->renderWorkers($output, $this->sectionArray($metrics, 'workers'));
7073
$this->renderBackend($output, $this->sectionArray($metrics, 'backend'));
74+
$this->renderMatchingRole($output, $this->sectionArray($metrics, 'matching_role'));
7175
$this->renderSchedules($output, $this->sectionArray($metrics, 'schedules'));
7276
$this->renderRepairPolicy($output, $this->sectionArray($metrics, 'repair_policy'));
7377

@@ -287,6 +291,35 @@ private function renderBackend(OutputInterface $output, array $backend): void
287291
$output->writeln('');
288292
}
289293

294+
/**
295+
* @param array<string, mixed> $matchingRole
296+
*/
297+
private function renderMatchingRole(OutputInterface $output, array $matchingRole): void
298+
{
299+
if ($matchingRole === []) {
300+
return;
301+
}
302+
303+
$output->writeln('<info>Matching-role (this node)</info>');
304+
305+
if (array_key_exists('queue_wake_enabled', $matchingRole)) {
306+
$output->writeln(sprintf(
307+
' Queue wake enabled: %s',
308+
((bool) $matchingRole['queue_wake_enabled']) ? 'yes' : 'no',
309+
));
310+
}
311+
312+
if (is_string($matchingRole['shape'] ?? null) && $matchingRole['shape'] !== '') {
313+
$output->writeln(sprintf(' Shape: %s', $matchingRole['shape']));
314+
}
315+
316+
if (is_string($matchingRole['task_dispatch_mode'] ?? null) && $matchingRole['task_dispatch_mode'] !== '') {
317+
$output->writeln(sprintf(' Task dispatch mode: %s', $matchingRole['task_dispatch_mode']));
318+
}
319+
320+
$output->writeln('');
321+
}
322+
290323
/**
291324
* @param array<string, mixed> $schedules
292325
*/

tests/Commands/SystemCommandTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,11 @@ public function test_operator_metrics_command_renders_rollout_safety_signals():
549549
self::assertStringContainsString('Database: mysql/mysql', $display);
550550
self::assertStringContainsString('Cache: redis/redis', $display);
551551

552+
self::assertStringContainsString('Matching-role (this node)', $display);
553+
self::assertStringContainsString('Queue wake enabled: yes', $display);
554+
self::assertStringContainsString('Shape: in_worker', $display);
555+
self::assertStringContainsString('Task dispatch mode: queue', $display);
556+
552557
self::assertStringContainsString('Active 4, paused 1, missed 1, oldest overdue 5000 ms', $display);
553558
self::assertStringContainsString('Lifetime fires: 128 (3 failures)', $display);
554559

@@ -668,6 +673,43 @@ public function test_operator_metrics_schema_pins_run_wait_age_keys(): void
668673
self::assertSame(['integer', 'null'], $runs['max_wait_age_ms']['type']);
669674
}
670675

676+
public function test_operator_metrics_schema_pins_matching_role_keys(): void
677+
{
678+
$schema = json_decode(
679+
(string) file_get_contents(__DIR__.'/../../schemas/output/operator-metrics.schema.json'),
680+
true,
681+
flags: JSON_THROW_ON_ERROR,
682+
);
683+
684+
$matchingRole = $schema['properties']['operator_metrics']['properties']['matching_role']['properties'];
685+
686+
self::assertSame(['boolean', 'null'], $matchingRole['queue_wake_enabled']['type']);
687+
self::assertSame(['string', 'null'], $matchingRole['shape']['type']);
688+
self::assertSame(['string', 'null'], $matchingRole['task_dispatch_mode']['type']);
689+
}
690+
691+
public function test_operator_metrics_command_renders_dedicated_matching_role_shape(): void
692+
{
693+
$payload = self::operatorMetricsPayload();
694+
$payload['operator_metrics']['matching_role'] = [
695+
'queue_wake_enabled' => false,
696+
'shape' => 'dedicated',
697+
'task_dispatch_mode' => 'poll',
698+
];
699+
700+
$command = new OperatorMetricsCommand();
701+
$command->setServerClient(new SystemFakeClient($payload));
702+
703+
$tester = new CommandTester($command);
704+
self::assertSame(Command::SUCCESS, $tester->execute([]));
705+
706+
$display = $tester->getDisplay();
707+
708+
self::assertStringContainsString('Queue wake enabled: no', $display);
709+
self::assertStringContainsString('Shape: dedicated', $display);
710+
self::assertStringContainsString('Task dispatch mode: poll', $display);
711+
}
712+
671713
public function test_operator_metrics_command_tolerates_minimal_payload(): void
672714
{
673715
$command = new OperatorMetricsCommand();
@@ -781,6 +823,11 @@ private static function operatorMetricsPayload(): array
781823
'scan_limit' => 100,
782824
'failure_backoff_max_seconds' => 300,
783825
],
826+
'matching_role' => [
827+
'queue_wake_enabled' => true,
828+
'shape' => 'in_worker',
829+
'task_dispatch_mode' => 'queue',
830+
],
784831
],
785832
];
786833
}

0 commit comments

Comments
 (0)