Skip to content

Commit 2bdc6d1

Browse files
committed
feat(orchestrator): Record batch size, dependency count, and speculated paths per batch as histograms
1 parent 8f67e53 commit 2bdc6d1

7 files changed

Lines changed: 47 additions & 3 deletions

File tree

‎platform/metrics/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,5 @@ There is no default bucket set. The package exports five common sets:
9191
| `LandLatencyBuckets` | ~1m – 24h | End-to-end request lifecycles, from accepting a land request to its terminal outcome |
9292

9393
Pass one of these sets or a custom `tally.DurationBuckets` to `Begin` or `NamedHistogram`.
94+
95+
For counts recorded with `RecordValue`, such as how many items a unit of work carries or depends on, use `CountBuckets` (0 – 144, Fibonacci-spaced, with zero in its own bucket).

‎platform/metrics/metrics.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ var (
181181
}
182182
)
183183

184+
// CountBuckets suits small cardinalities recorded with RecordValue, such as how
185+
// many items a unit of work carries or depends on. Zero has its own bucket
186+
// because "none" is a meaningful answer, and the Fibonacci spacing keeps
187+
// resolution where counts usually fall while still separating outliers.
188+
var CountBuckets = tally.ValueBuckets{0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144}
189+
184190
// Op tracks the lifecycle of a named operation. It captures the start time on
185191
// creation, emits a {name}.start counter, and records the duration and result
186192
// on a {name}.finish histogram when Complete is called.

‎platform/metrics/metrics_test.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ func TestLatencyBuckets_Sorted(t *testing.T) {
233233
}
234234
}
235235

236+
func TestCountBuckets_Sorted(t *testing.T) {
237+
for i := 1; i < len(CountBuckets); i++ {
238+
assert.Greater(t, CountBuckets[i], CountBuckets[i-1],
239+
"CountBuckets[%d] (%v) must be greater than CountBuckets[%d] (%v)", i, CountBuckets[i], i-1, CountBuckets[i-1])
240+
}
241+
}
242+
236243
// counterKeys extracts map keys for error messages.
237244
func counterKeys(m map[string]tally.CounterSnapshot) []string {
238245
keys := make([]string, 0, len(m))

‎submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er
213213
metrics.NamedCounter(c.metricsScope, opName, "batch_store_errors", 1, metrics.TagsFromContext(ctx)...)
214214
return fmt.Errorf("failed to mark batch %s created: %w", batch.ID, err)
215215
}
216+
// Recorded on the Creating→Created transition, which happens once per batch; abandoned batches never reach it.
217+
metrics.NamedHistogram(c.metricsScope, opName, "batch_size", metrics.CountBuckets, metrics.TagsFromContext(ctx)...).
218+
RecordValue(float64(len(batch.Contains)))
219+
metrics.NamedHistogram(c.metricsScope, opName, "batch_dependencies", metrics.CountBuckets, metrics.TagsFromContext(ctx)...).
220+
RecordValue(float64(len(batch.Dependencies)))
216221
c.logger.Infow("batch created",
217222
"batch_id", batch.ID,
218223
"queue", batch.Queue,

‎submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis_test.go‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ func storageFactoryFor(ctrl *gomock.Controller, store orchstorage.Storage) *orch
113113
// conflicts.
114114
func newTestController(t *testing.T, ctrl *gomock.Controller, store orchstorage.Storage, analyzer conflict.Analyzer, publisher *queuemock.MockPublisher) *Controller {
115115
t.Helper()
116+
return newTestControllerWithScope(t, ctrl, store, analyzer, publisher, tally.NoopScope)
117+
}
118+
119+
func newTestControllerWithScope(t *testing.T, ctrl *gomock.Controller, store orchstorage.Storage, analyzer conflict.Analyzer, publisher *queuemock.MockPublisher, scope tally.Scope) *Controller {
120+
t.Helper()
116121

117122
if analyzer == nil {
118123
analyzer = all.New(analyzerCfg)
@@ -132,7 +137,7 @@ func newTestController(t *testing.T, ctrl *gomock.Controller, store orchstorage.
132137
})
133138
require.NoError(t, err)
134139

135-
return NewController(zaptest.NewLogger(t).Sugar(), tally.NoopScope, storageFactoryFor(ctrl, store),
140+
return NewController(zaptest.NewLogger(t).Sugar(), scope, storageFactoryFor(ctrl, store),
136141
analyzerFactory, registry, topickey.TopicKeyDependencyAnalysis, "orchestrator-dependency-analysis")
137142
}
138143

@@ -248,8 +253,17 @@ func TestController_Process_AnalyzesAndTransitionsToCreated(t *testing.T) {
248253
store.EXPECT().GetRequestStore().Return(requestStoreFor(ctrl, liveRequest())).AnyTimes()
249254
store.EXPECT().GetRequestBatchStore().Return(noPriorEnrollment(ctrl)).AnyTimes()
250255

251-
controller := newTestController(t, ctrl, store, nil, nil)
256+
scope := tally.NewTestScope("", nil)
257+
controller := newTestControllerWithScope(t, ctrl, store, nil, nil, scope)
252258
require.NoError(t, controller.Process(context.Background(), newDelivery(t, ctrl, batch.ID, testQueue)))
259+
260+
histograms := scope.Snapshot().Histograms()
261+
size, ok := histograms["dependency_analysis_controller.process.batch_size+"]
262+
require.True(t, ok)
263+
assert.Equal(t, int64(1), size.Values()[1])
264+
dependencies, ok := histograms["dependency_analysis_controller.process.batch_dependencies+"]
265+
require.True(t, ok)
266+
assert.Equal(t, int64(1), dependencies.Values()[2])
253267
}
254268

255269
// The analyzer may report one in-flight batch several times when more than one
@@ -325,8 +339,10 @@ func TestController_Process_RedeliveryAfterTransitionOnlyRepublishes(t *testing.
325339
publisher.EXPECT().Publish(gomock.Any(), "log", gomock.Any()).Return(nil).AnyTimes()
326340
publisher.EXPECT().Publish(gomock.Any(), "speculate", gomock.Any()).Return(nil)
327341

328-
controller := newTestController(t, ctrl, store, nil, publisher)
342+
scope := tally.NewTestScope("", nil)
343+
controller := newTestControllerWithScope(t, ctrl, store, nil, publisher, scope)
329344
require.NoError(t, controller.Process(context.Background(), newDelivery(t, ctrl, batch.ID, testQueue)))
345+
assert.Empty(t, scope.Snapshot().Histograms(), "a batch already created must not be measured again")
330346
}
331347

332348
// A failure part-way through the index loop leaves the batch in Creating, so

‎submitqueue/orchestrator/controller/speculate/finalize.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,10 @@ func (c *Controller) commitOutcome(ctx context.Context, snap *snapshot, batch en
275275
if err != nil {
276276
return landed, c.attributed(ctx, err, entity.BatchSubject(batch.ID))
277277
}
278+
if landed {
279+
metrics.NamedHistogram(c.metricsScope, opName, "paths", metrics.CountBuckets, metrics.TagsFromContext(ctx)...).
280+
RecordValue(float64(len(snap.pathSets[batch.ID].Paths)))
281+
}
278282
return landed, nil
279283
}
280284

‎submitqueue/orchestrator/controller/speculate/run_test.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1588,6 +1588,10 @@ func TestRun_BypassesUnsettledDependenciesWithFullCoverage(t *testing.T) {
15881588
counter, ok := h.metrics.Snapshot().Counters()["test.speculate_controller.process.bypass+"]
15891589
require.True(t, ok)
15901590
assert.EqualValues(t, 1, counter.Value())
1591+
1592+
paths, ok := h.metrics.Snapshot().Histograms()["test.speculate_controller.process.paths+"]
1593+
require.True(t, ok, "a finalized head records how many paths it speculated")
1594+
assert.Equal(t, int64(1), paths.Values()[5])
15911595
}
15921596

15931597
// The land stage publishes landing as its first act on the dispatch. Both

0 commit comments

Comments
 (0)