@@ -41,6 +41,7 @@ import (
4141 "google.golang.org/grpc/resolver"
4242 "google.golang.org/grpc/resolver/manual"
4343 "google.golang.org/grpc/serviceconfig"
44+ gstats "google.golang.org/grpc/stats"
4445)
4546
4647var defaultTestTimeout = 5 * time .Second
@@ -114,7 +115,6 @@ func (recordingLoadBalancerBuilder) Build(cc balancer.ClientConn, bOpts balancer
114115 intHistoHandle .Record (cc .MetricsRecorder (), 3 , "int histo label val" , "int histo optional label val" )
115116 floatHistoHandle .Record (cc .MetricsRecorder (), 4 , "float histo label val" , "float histo optional label val" )
116117 intGaugeHandle .Record (cc .MetricsRecorder (), 5 , "int gauge label val" , "int gauge optional label val" )
117-
118118 return & recordingLoadBalancer {
119119 Balancer : balancer .Get (pickfirst .Name ).Build (cc , bOpts ),
120120 }
@@ -255,3 +255,82 @@ func (s) TestMetricRecorderListPanic(t *testing.T) {
255255
256256 intCountHandle .Record (mrl , 1 , "only one label" )
257257}
258+
259+ // TestMetricsRecorderList_RegisterAsyncReporter verifies that the list implementation
260+ // correctly fans out registration calls to all underlying recorders and
261+ // aggregates the cleanup calls.
262+ func TestMetricsRecorderList_RegisterAsyncReporter (t * testing.T ) {
263+ spy1 := & spyMetricsRecorder {name : "spy1" }
264+ spy2 := & spyMetricsRecorder {name : "spy2" }
265+ spy3 := & spyMetricsRecorder {name : "spy3" }
266+
267+ list := istats .NewMetricsRecorderList ([]gstats.Handler {spy1 , spy2 , spy3 })
268+
269+ desc := & estats.MetricDescriptor {Name : "test_metric" , Description : "test" }
270+ mockMetric := & mockAsyncMetric {d : desc }
271+
272+ dummyReporter := estats .AsyncMetricReporterFunc (func (estats.AsyncMetricsRecorder ) error {
273+ return nil
274+ })
275+ cleanup := list .RegisterAsyncReporter (dummyReporter , mockMetric )
276+
277+ // Check that RegisterAsyncReporter was called exactly once on ALL spies
278+ if spy1 .registerCalledCount != 1 {
279+ t .Errorf ("spy1 register called %d times, want 1" , spy1 .registerCalledCount )
280+ }
281+ if spy2 .registerCalledCount != 1 {
282+ t .Errorf ("spy2 register called %d times, want 1" , spy2 .registerCalledCount )
283+ }
284+ if spy3 .registerCalledCount != 1 {
285+ t .Errorf ("spy3 register called %d times, want 1" , spy3 .registerCalledCount )
286+ }
287+
288+ // Verify that cleanup has NOT been called yet
289+ if spy1 .cleanupCalledCount != 0 {
290+ t .Error ("spy1 cleanup called prematurely" )
291+ }
292+
293+ cleanup ()
294+
295+ // Check that the cleanup function returned by the list actually triggers
296+ // the cleanup logic on ALL underlying spies.
297+ if spy1 .cleanupCalledCount != 1 {
298+ t .Errorf ("spy1 cleanup called %d times, want 1" , spy1 .cleanupCalledCount )
299+ }
300+ if spy2 .cleanupCalledCount != 1 {
301+ t .Errorf ("spy2 cleanup called %d times, want 1" , spy2 .cleanupCalledCount )
302+ }
303+ if spy3 .cleanupCalledCount != 1 {
304+ t .Errorf ("spy3 cleanup called %d times, want 1" , spy3 .cleanupCalledCount )
305+ }
306+ }
307+
308+ // --- Helpers & Spies ---
309+
310+ // mockAsyncMetric implements estats.AsyncMetric
311+ type mockAsyncMetric struct {
312+ estats.AsyncMetric
313+ d * estats.MetricDescriptor
314+ }
315+
316+ func (m * mockAsyncMetric ) Descriptor () * estats.MetricDescriptor {
317+ return m .d
318+ }
319+
320+ // spyMetricsRecorder implements estats.MetricsRecorder
321+ type spyMetricsRecorder struct {
322+ stats.TestMetricsRecorder
323+ name string
324+ registerCalledCount int
325+ cleanupCalledCount int
326+ }
327+
328+ // RegisterAsyncReporter implements the interface and tracks calls.
329+ func (s * spyMetricsRecorder ) RegisterAsyncReporter (estats.AsyncMetricReporter , ... estats.AsyncMetric ) func () {
330+ s .registerCalledCount ++
331+
332+ // Return a cleanup function that tracks if it was called
333+ return func () {
334+ s .cleanupCalledCount ++
335+ }
336+ }
0 commit comments