From 0d8ab760df39aec7b3d9c28838a59c6902077b93 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 02:13:28 +0000 Subject: [PATCH 1/3] test: expose SQLite latency histogram bound coercion --- tests/Feature/SystemPrometheusMetricsTest.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/Feature/SystemPrometheusMetricsTest.php b/tests/Feature/SystemPrometheusMetricsTest.php index 626b17f8c..fce648a53 100644 --- a/tests/Feature/SystemPrometheusMetricsTest.php +++ b/tests/Feature/SystemPrometheusMetricsTest.php @@ -33,6 +33,43 @@ protected function tearDown(): void parent::tearDown(); } + public function test_latency_buckets_exclude_durations_above_the_bound(): void + { + $run = $this->workflowRun( + workflowId: 'histogram-run', + workflowType: 'HistogramWorkflow', + queue: 'histogram', + status: 'completed', + statusBucket: 'completed', + durationMs: 2000, + ); + $this->activity($run, 'HistogramActivity', 'histogram', 'completed', 1500); + + $response = $this->getJson( + '/api/system/prometheus-metrics', + $this->controlPlaneHeadersWithWorkerProtocol(), + ); + + $response->assertOk() + ->assertJsonPath('series.workflows.0.latency_seconds.count', 1) + ->assertJsonPath('series.activities.0.latency_seconds.count', 1); + + $expected = [ + '0.1' => 0, + '0.5' => 0, + '1' => 0, + '2.5' => 1, + '5' => 1, + '10' => 1, + '30' => 1, + '60' => 1, + '300' => 1, + '900' => 1, + ]; + $this->assertSame($expected, $response->json('series.workflows.0.latency_seconds.buckets')); + $this->assertSame($expected, $response->json('series.activities.0.latency_seconds.buckets')); + } + public function test_prometheus_metrics_summary_is_granular_by_queue_workflow_and_activity_type(): void { $runA = $this->workflowRun( From 050688c227cc3d193b5b3c8402789361a144f105 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 02:16:10 +0000 Subject: [PATCH 2/3] fix: compare SQLite latency bounds as numbers --- app/Services/PrometheusMetricsSummary.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Services/PrometheusMetricsSummary.php b/app/Services/PrometheusMetricsSummary.php index 1bd36d124..f96802831 100644 --- a/app/Services/PrometheusMetricsSummary.php +++ b/app/Services/PrometheusMetricsSummary.php @@ -400,9 +400,12 @@ private function workerRows(string $namespace, CarbonInterface $now, array $queu private function selectLatencyBuckets($query, string $durationExpr, string $durationAvailableSql): void { + // SQLite duration expressions have no affinity, while PDO binds floats as text. + $bound = $query->getConnection()->getDriverName() === 'sqlite' ? 'CAST(? AS REAL)' : '?'; + foreach (self::LATENCY_BUCKETS_SECONDS as $index => $bucket) { $query->selectRaw( - "SUM(CASE WHEN {$durationAvailableSql} AND {$durationExpr} <= ? THEN 1 ELSE 0 END) as bucket_{$index}", + "SUM(CASE WHEN {$durationAvailableSql} AND {$durationExpr} <= {$bound} THEN 1 ELSE 0 END) as bucket_{$index}", [$bucket], ); } From d7cc197e523f6bae7f4946aff620b9915c064a04 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 23 Oct 2026 02:42:23 +0000 Subject: [PATCH 3/3] test: retain fractional latency in metrics fixtures --- tests/Feature/SystemPrometheusMetricsTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Feature/SystemPrometheusMetricsTest.php b/tests/Feature/SystemPrometheusMetricsTest.php index fce648a53..239be98e6 100644 --- a/tests/Feature/SystemPrometheusMetricsTest.php +++ b/tests/Feature/SystemPrometheusMetricsTest.php @@ -591,6 +591,8 @@ private function workflowRun( private function activity(string $runId, string $activityType, string $queue, string $status, int $durationMs): void { $startedAt = now()->subSeconds(10); + // Query-builder DateTime bindings would discard the fixture's fractional second. + $closedAt = $startedAt->copy()->addMilliseconds($durationMs)->format('Y-m-d H:i:s.u'); DB::table('activity_executions')->insert([ 'id' => (string) Str::ulid(), @@ -601,9 +603,9 @@ private function activity(string $runId, string $activityType, string $queue, st 'status' => $status, 'queue' => $queue, 'started_at' => $startedAt, - 'closed_at' => $startedAt->copy()->addMilliseconds($durationMs), + 'closed_at' => $closedAt, 'created_at' => $startedAt, - 'updated_at' => $startedAt->copy()->addMilliseconds($durationMs), + 'updated_at' => $closedAt, ]); }