From ecece78aa0e3146dcf886169c4b5748797203b8c Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Thu, 18 Jun 2026 12:59:46 +0530 Subject: [PATCH 1/5] refcounting utility --- internal/grpcsync/refcounted.go | 84 ++++++++++++++++ internal/grpcsync/refcounted_test.go | 137 +++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 internal/grpcsync/refcounted.go create mode 100644 internal/grpcsync/refcounted_test.go diff --git a/internal/grpcsync/refcounted.go b/internal/grpcsync/refcounted.go new file mode 100644 index 000000000000..750825dcc0f4 --- /dev/null +++ b/internal/grpcsync/refcounted.go @@ -0,0 +1,84 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package grpcsync + +import ( + "sync/atomic" +) + +// RefCounted manages the lifecycle of a resource using atomic reference +// counting. +type RefCounted[V any] struct { + val V + refCount atomic.Int32 + onZero func() +} + +// NewRefCounted creates a new RefCounted instance wrapping the given value. The +// provided onZero callback is executed exactly once when the reference count +// drops to zero. +func NewRefCounted[V any](val V, onZero func()) *RefCounted[V] { + rc := &RefCounted[V]{ + val: val, + onZero: onZero, + } + rc.refCount.Store(1) + return rc +} + +// Value returns the encapsulated resource. +func (rc *RefCounted[V]) Value() V { + return rc.val +} + +// TryIncrement attempts to increment the reference count, returning true if +// successful. It returns false if the count has already reached 0, indicating +// the resource has been cleaned up and is no longer usable. +// +// This method guarantees safe concurrent access by utilizing a CompareAndSwap +// loop. This prevents race conditions where a concurrent decrement could drop +// the count to zero between the read and the increment operation, which would +// otherwise inadvertently resurrect a closed resource. +func (rc *RefCounted[V]) TryIncrement() bool { + for { + val := rc.refCount.Load() + if val <= 0 { + return false // Already dead or dying + } + if rc.refCount.CompareAndSwap(val, val+1) { + return true + } + } +} + +// Increment increments the reference count. +// +// The caller must already hold an active reference, ensuring the resource is +// not dead. If the resource might already be dead, use TryIncrement instead. +func (rc *RefCounted[V]) Increment() { + rc.refCount.Add(1) +} + +// Decrement decrements the reference count. If it drops to zero, the onZero +// callback is executed. +func (rc *RefCounted[V]) Decrement() { + if v := rc.refCount.Add(-1); v == 0 { + rc.onZero() + } +} diff --git a/internal/grpcsync/refcounted_test.go b/internal/grpcsync/refcounted_test.go new file mode 100644 index 000000000000..3dab9ca58c1d --- /dev/null +++ b/internal/grpcsync/refcounted_test.go @@ -0,0 +1,137 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package grpcsync + +import ( + "sync" + "sync/atomic" + "testing" +) + +// TestRefCounted_Basic tests the scenario where a RefCounted instance is +// created and its reference count is decremented. It verifies that the +// encapsulated value is correctly retrieved and that the onZero callback +// is invoked when the reference count drops to zero. +func (s) TestRefCounted(t *testing.T) { + val := "test-value" + var onZeroCalled atomic.Bool + + rc := NewRefCounted(val, func() { + onZeroCalled.Store(true) + }) + + if got := rc.Value(); got != val { + t.Fatalf("Value() = %v, want %v", got, val) + } + + if got := onZeroCalled.Load(); got { + t.Fatalf("Before decrement, onZeroCalled = %v, want false", got) + } + + rc.Decrement() + + if got := onZeroCalled.Load(); !got { + t.Fatalf("After decrementing count to zero, onZeroCalled = %v, want true", got) + } +} + +// TestRefCounted_IncrementDecrement tests the scenario where the reference +// count of a resource is explicitly incremented and decremented multiple +// times. It verifies that the onZero callback is only executed when the count +// drops to zero, and not beforehand. +func (s) TestRefCounted_IncrementDecrement(t *testing.T) { + val := 42 + var onZeroCount atomic.Int32 + + rc := NewRefCounted(val, func() { + onZeroCount.Add(1) + }) + + rc.Increment() + rc.Decrement() + + if got := onZeroCount.Load(); got != 0 { + t.Fatal("OnZero called unexpectedly after decrementing refcount from 2 to 1") + } + + rc.Decrement() + + if got := onZeroCount.Load(); got != 1 { + t.Fatalf("After decrementing count from 1 to 0, onZero called = %v times, want 1", got) + } +} + +// TestRefCounted_TryIncrement tests the scenario where TryIncrement is called +// on both an active and a dead resource. It verifies that TryIncrement +// successfully increments active resources but fails on resources whose +// reference count has already dropped to zero. +func (s) TestRefCounted_TryIncrement(t *testing.T) { + var onZeroCount atomic.Int32 + rc := NewRefCounted("val", func() { + onZeroCount.Add(1) + }) + + if got := rc.TryIncrement(); !got { + t.Fatalf("TryIncrement() on active resource = %v, want true", got) + } + + rc.Decrement() + rc.Decrement() + + if got := onZeroCount.Load(); got != 1 { + t.Fatalf("After decrementing count to zero, onZero called = %v times, want 1", got) + } + + if got := rc.TryIncrement(); got { + t.Fatalf("TryIncrement() on dead resource = %v, want false", got) + } +} + +// TestRefCounted_Concurrent tests the scenario where multiple goroutines +// concurrently increment and decrement the reference count. It verifies that +// the reference counting is thread-safe and the onZero callback is invoked +// exactly once. +func (s) TestRefCounted_Concurrent(t *testing.T) { + const numGoroutines = 10 + var onZeroCount atomic.Int32 + + rc := NewRefCounted("concurrent-val", func() { + onZeroCount.Add(1) + }) + + var wg sync.WaitGroup + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func() { + defer wg.Done() + if rc.TryIncrement() { + rc.Decrement() + } + }() + } + + rc.Decrement() + + wg.Wait() + + if got := onZeroCount.Load(); got != 1 { + t.Fatalf("After concurrent increments/decrements and final decrement, onZeroCount = %v, want 1", got) + } +} From a55e379d4199abfad051227b00394e163b16c44f Mon Sep 17 00:00:00 2001 From: Eshita Chandwani Date: Mon, 22 Jun 2026 11:42:37 +0530 Subject: [PATCH 2/5] add nil check for onZero callback --- internal/grpcsync/refcounted.go | 8 +++++-- internal/grpcsync/refcounted_test.go | 33 ++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/internal/grpcsync/refcounted.go b/internal/grpcsync/refcounted.go index 750825dcc0f4..368cbfd37ca5 100644 --- a/internal/grpcsync/refcounted.go +++ b/internal/grpcsync/refcounted.go @@ -19,6 +19,7 @@ package grpcsync import ( + "fmt" "sync/atomic" ) @@ -33,13 +34,16 @@ type RefCounted[V any] struct { // NewRefCounted creates a new RefCounted instance wrapping the given value. The // provided onZero callback is executed exactly once when the reference count // drops to zero. -func NewRefCounted[V any](val V, onZero func()) *RefCounted[V] { +func NewRefCounted[V any](val V, onZero func()) (*RefCounted[V], error) { + if onZero == nil { + return nil, fmt.Errorf("onZero callback cannot be nil") + } rc := &RefCounted[V]{ val: val, onZero: onZero, } rc.refCount.Store(1) - return rc + return rc, nil } // Value returns the encapsulated resource. diff --git a/internal/grpcsync/refcounted_test.go b/internal/grpcsync/refcounted_test.go index 3dab9ca58c1d..9064b20f035a 100644 --- a/internal/grpcsync/refcounted_test.go +++ b/internal/grpcsync/refcounted_test.go @@ -32,9 +32,12 @@ func (s) TestRefCounted(t *testing.T) { val := "test-value" var onZeroCalled atomic.Bool - rc := NewRefCounted(val, func() { + rc, err := NewRefCounted(val, func() { onZeroCalled.Store(true) }) + if err != nil { + t.Fatalf("NewRefCounted() failed: %v", err) + } if got := rc.Value(); got != val { t.Fatalf("Value() = %v, want %v", got, val) @@ -59,9 +62,12 @@ func (s) TestRefCounted_IncrementDecrement(t *testing.T) { val := 42 var onZeroCount atomic.Int32 - rc := NewRefCounted(val, func() { + rc, err := NewRefCounted(val, func() { onZeroCount.Add(1) }) + if err != nil { + t.Fatalf("NewRefCounted() failed: %v", err) + } rc.Increment() rc.Decrement() @@ -83,9 +89,12 @@ func (s) TestRefCounted_IncrementDecrement(t *testing.T) { // reference count has already dropped to zero. func (s) TestRefCounted_TryIncrement(t *testing.T) { var onZeroCount atomic.Int32 - rc := NewRefCounted("val", func() { + rc, err := NewRefCounted("val", func() { onZeroCount.Add(1) }) + if err != nil { + t.Fatalf("NewRefCounted() failed: %v", err) + } if got := rc.TryIncrement(); !got { t.Fatalf("TryIncrement() on active resource = %v, want true", got) @@ -111,9 +120,12 @@ func (s) TestRefCounted_Concurrent(t *testing.T) { const numGoroutines = 10 var onZeroCount atomic.Int32 - rc := NewRefCounted("concurrent-val", func() { + rc, err := NewRefCounted("concurrent-val", func() { onZeroCount.Add(1) }) + if err != nil { + t.Fatalf("NewRefCounted() failed: %v", err) + } var wg sync.WaitGroup wg.Add(numGoroutines) @@ -135,3 +147,16 @@ func (s) TestRefCounted_Concurrent(t *testing.T) { t.Fatalf("After concurrent increments/decrements and final decrement, onZeroCount = %v, want 1", got) } } + +// TestNilOnZero tests that NewRefCounted returns an error if the provided +// onZero callback is nil. +func (s) TestNilOnZero(t *testing.T) { + _, err := NewRefCounted("val", nil) + if err == nil { + t.Fatalf("NewRefCounted(_, nil) succeeded, want error") + } + expectedErr := "onZero callback cannot be nil" + if err.Error() != expectedErr { + t.Fatalf("NewRefCounted(_, nil) returned error: %v, want: %v", err, expectedErr) + } +} From ff1f329a0ac0119851328556d8a1ff56af22de3f Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Wed, 8 Jul 2026 20:34:06 +0530 Subject: [PATCH 3/5] update comments --- internal/grpcsync/refcounted.go | 24 +++++++++++++----------- internal/grpcsync/refcounted_test.go | 17 +++++------------ 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/internal/grpcsync/refcounted.go b/internal/grpcsync/refcounted.go index 368cbfd37ca5..eee75f1592aa 100644 --- a/internal/grpcsync/refcounted.go +++ b/internal/grpcsync/refcounted.go @@ -31,12 +31,15 @@ type RefCounted[V any] struct { onZero func() } -// NewRefCounted creates a new RefCounted instance wrapping the given value. The -// provided onZero callback is executed exactly once when the reference count -// drops to zero. +// NewRefCounted creates a new RefCounted instance wrapping the given value with +// initial refcount of one. The provided onZero callback must not be nil, and is +// executed exactly once when the reference count drops to zero. +// +// The value should typically be a pointer, interface, or handle rather than a +// plain value type (such as a struct or primitive value). func NewRefCounted[V any](val V, onZero func()) (*RefCounted[V], error) { if onZero == nil { - return nil, fmt.Errorf("onZero callback cannot be nil") + return nil, fmt.Errorf("grpcsync: onZero callback cannot be nil") } rc := &RefCounted[V]{ val: val, @@ -53,13 +56,12 @@ func (rc *RefCounted[V]) Value() V { // TryIncrement attempts to increment the reference count, returning true if // successful. It returns false if the count has already reached 0, indicating -// the resource has been cleaned up and is no longer usable. -// -// This method guarantees safe concurrent access by utilizing a CompareAndSwap -// loop. This prevents race conditions where a concurrent decrement could drop -// the count to zero between the read and the increment operation, which would -// otherwise inadvertently resurrect a closed resource. +// the resource has been cleaned up and cannot be resurrected. func (rc *RefCounted[V]) TryIncrement() bool { + // Utilize a CompareAndSwap loop to prevent race conditions where a + // concurrent decrement could drop the count to zero between the read and + // the increment operation, which would otherwise inadvertently resurrect a + // closed resource. for { val := rc.refCount.Load() if val <= 0 { @@ -80,7 +82,7 @@ func (rc *RefCounted[V]) Increment() { } // Decrement decrements the reference count. If it drops to zero, the onZero -// callback is executed. +// callback is executed synchronously before this method returns. func (rc *RefCounted[V]) Decrement() { if v := rc.refCount.Add(-1); v == 0 { rc.onZero() diff --git a/internal/grpcsync/refcounted_test.go b/internal/grpcsync/refcounted_test.go index 9064b20f035a..7f9883d23ce7 100644 --- a/internal/grpcsync/refcounted_test.go +++ b/internal/grpcsync/refcounted_test.go @@ -128,15 +128,12 @@ func (s) TestRefCounted_Concurrent(t *testing.T) { } var wg sync.WaitGroup - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() + wg.Go(func() { if rc.TryIncrement() { rc.Decrement() } - }() + }) } rc.Decrement() @@ -151,12 +148,8 @@ func (s) TestRefCounted_Concurrent(t *testing.T) { // TestNilOnZero tests that NewRefCounted returns an error if the provided // onZero callback is nil. func (s) TestNilOnZero(t *testing.T) { - _, err := NewRefCounted("val", nil) - if err == nil { - t.Fatalf("NewRefCounted(_, nil) succeeded, want error") - } - expectedErr := "onZero callback cannot be nil" - if err.Error() != expectedErr { - t.Fatalf("NewRefCounted(_, nil) returned error: %v, want: %v", err, expectedErr) + const wantErr = "grpcsync: onZero callback cannot be nil" + if _, err := NewRefCounted("val", nil); err == nil || err.Error() != wantErr { + t.Fatalf("NewRefCounted(_, nil) returned error: %v, want: %q", err, wantErr) } } From ce01498f6d045b0cafd7f38de12254bd84141196 Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 10 Jul 2026 16:41:58 +0530 Subject: [PATCH 4/5] comments clarified, error added --- internal/grpcsync/refcounted.go | 46 +++++++++++++++++++--------- internal/grpcsync/refcounted_test.go | 20 +++++++++--- 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/internal/grpcsync/refcounted.go b/internal/grpcsync/refcounted.go index eee75f1592aa..beab497f3759 100644 --- a/internal/grpcsync/refcounted.go +++ b/internal/grpcsync/refcounted.go @@ -23,8 +23,8 @@ import ( "sync/atomic" ) -// RefCounted manages the lifecycle of a resource using atomic reference -// counting. +// RefCounted tracks how many consumers hold a value of type V and runs a +// cleanup when the last reference is released type RefCounted[V any] struct { val V refCount atomic.Int32 @@ -37,6 +37,9 @@ type RefCounted[V any] struct { // // The value should typically be a pointer, interface, or handle rather than a // plain value type (such as a struct or primitive value). +// +// WARNING: onZero runs synchronously inside Decrement; it must not acquire +// locks held by Decrement callers or invert lock ordering. func NewRefCounted[V any](val V, onZero func()) (*RefCounted[V], error) { if onZero == nil { return nil, fmt.Errorf("grpcsync: onZero callback cannot be nil") @@ -57,17 +60,22 @@ func (rc *RefCounted[V]) Value() V { // TryIncrement attempts to increment the reference count, returning true if // successful. It returns false if the count has already reached 0, indicating // the resource has been cleaned up and cannot be resurrected. +// +// WARNING: Avoid calling TryIncrement on hot paths when an active reference is +// already guaranteed by the caller; use Increment instead to bypass the +// CompareAndSwap loop overhead. TryIncrement should be reserved for speculative +// lookups (such as fetching from a cache or map) where the resource might be +// dead. func (rc *RefCounted[V]) TryIncrement() bool { - // Utilize a CompareAndSwap loop to prevent race conditions where a - // concurrent decrement could drop the count to zero between the read and - // the increment operation, which would otherwise inadvertently resurrect a - // closed resource. + // Utilize a CompareAndSwap loop to prevent race conditions where a concurrent + // decrement could drop the count to zero between the read and the increment + // operation, which would otherwise inadvertently resurrect a closed resource. for { - val := rc.refCount.Load() - if val <= 0 { + count := rc.refCount.Load() + if count == 0 { return false // Already dead or dying } - if rc.refCount.CompareAndSwap(val, val+1) { + if rc.refCount.CompareAndSwap(count, count+1) { return true } } @@ -75,16 +83,24 @@ func (rc *RefCounted[V]) TryIncrement() bool { // Increment increments the reference count. // -// The caller must already hold an active reference, ensuring the resource is -// not dead. If the resource might already be dead, use TryIncrement instead. -func (rc *RefCounted[V]) Increment() { - rc.refCount.Add(1) +// WARNING: Call Increment only when there is a guarantee that an active +// reference is already present, ensuring the resource is not dead. If there is +// a possibility that the resource is dead or its reference count might have +// reached zero, call TryIncrement instead. +func (rc *RefCounted[V]) Increment() error { + if rc.refCount.Add(1) <= 1 { + return fmt.Errorf("grpcsync: resource already closed or dead") + } + return nil } // Decrement decrements the reference count. If it drops to zero, the onZero // callback is executed synchronously before this method returns. -func (rc *RefCounted[V]) Decrement() { - if v := rc.refCount.Add(-1); v == 0 { +func (rc *RefCounted[V]) Decrement() error { + if v := rc.refCount.Add(-1); v < 0 { + return fmt.Errorf("grpcsync: refcount cannot be negative") + } else if v == 0 { rc.onZero() } + return nil } diff --git a/internal/grpcsync/refcounted_test.go b/internal/grpcsync/refcounted_test.go index 7f9883d23ce7..def06162e588 100644 --- a/internal/grpcsync/refcounted_test.go +++ b/internal/grpcsync/refcounted_test.go @@ -115,7 +115,8 @@ func (s) TestRefCounted_TryIncrement(t *testing.T) { // TestRefCounted_Concurrent tests the scenario where multiple goroutines // concurrently increment and decrement the reference count. It verifies that // the reference counting is thread-safe and the onZero callback is invoked -// exactly once. +// exactly once. It also verifies that once onZero has been called, subsequent +// increament/decrement return error. func (s) TestRefCounted_Concurrent(t *testing.T) { const numGoroutines = 10 var onZeroCount atomic.Int32 @@ -135,14 +136,25 @@ func (s) TestRefCounted_Concurrent(t *testing.T) { } }) } - rc.Decrement() - wg.Wait() - if got := onZeroCount.Load(); got != 1 { t.Fatalf("After concurrent increments/decrements and final decrement, onZeroCount = %v, want 1", got) } + + wantErr := "grpcsync: refcount cannot be negative" + if err := rc.Decrement(); err == nil || err.Error() != wantErr { + t.Fatalf("Decrement() on dead resource = %v, want %q", err, wantErr) + } + + wantErr = "grpcsync: resource already closed or dead" + if err := rc.Increment(); err == nil || err.Error() != wantErr { + t.Fatalf("Increment() on dead resource = %v, want %q", err, wantErr) + } + + if got := rc.TryIncrement(); got { + t.Fatalf("TryIncrement() on dead resource = %v, want false", got) + } } // TestNilOnZero tests that NewRefCounted returns an error if the provided From 6551da22547b6c8bef8d16a81a1e3b9886f7545d Mon Sep 17 00:00:00 2001 From: eshitachandwani Date: Fri, 10 Jul 2026 16:59:41 +0530 Subject: [PATCH 5/5] correct try increment check --- internal/grpcsync/refcounted.go | 2 +- internal/grpcsync/refcounted_test.go | 38 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/internal/grpcsync/refcounted.go b/internal/grpcsync/refcounted.go index beab497f3759..2affcac7a8ae 100644 --- a/internal/grpcsync/refcounted.go +++ b/internal/grpcsync/refcounted.go @@ -72,7 +72,7 @@ func (rc *RefCounted[V]) TryIncrement() bool { // operation, which would otherwise inadvertently resurrect a closed resource. for { count := rc.refCount.Load() - if count == 0 { + if count <= 0 { return false // Already dead or dying } if rc.refCount.CompareAndSwap(count, count+1) { diff --git a/internal/grpcsync/refcounted_test.go b/internal/grpcsync/refcounted_test.go index def06162e588..0c9c92b73af5 100644 --- a/internal/grpcsync/refcounted_test.go +++ b/internal/grpcsync/refcounted_test.go @@ -24,10 +24,10 @@ import ( "testing" ) -// TestRefCounted_Basic tests the scenario where a RefCounted instance is -// created and its reference count is decremented. It verifies that the -// encapsulated value is correctly retrieved and that the onZero callback -// is invoked when the reference count drops to zero. +// Test verifies the scenario where a RefCounted instance is created and its +// reference count is decremented. It verifies that the encapsulated value is +// correctly retrieved and that the onZero callback is invoked when the +// reference count drops to zero. func (s) TestRefCounted(t *testing.T) { val := "test-value" var onZeroCalled atomic.Bool @@ -54,10 +54,10 @@ func (s) TestRefCounted(t *testing.T) { } } -// TestRefCounted_IncrementDecrement tests the scenario where the reference -// count of a resource is explicitly incremented and decremented multiple -// times. It verifies that the onZero callback is only executed when the count -// drops to zero, and not beforehand. +// Test verifies the scenario where the reference count of a resource is +// explicitly incremented and decremented multiple times. It verifies that the +// onZero callback is only executed when the count drops to zero, and not +// beforehand. func (s) TestRefCounted_IncrementDecrement(t *testing.T) { val := 42 var onZeroCount atomic.Int32 @@ -83,10 +83,10 @@ func (s) TestRefCounted_IncrementDecrement(t *testing.T) { } } -// TestRefCounted_TryIncrement tests the scenario where TryIncrement is called -// on both an active and a dead resource. It verifies that TryIncrement -// successfully increments active resources but fails on resources whose -// reference count has already dropped to zero. +// Test verifies the scenario where TryIncrement is called on both an active and +// a dead resource. It verifies that TryIncrement successfully increments active +// resources but fails on resources whose reference count has already dropped to +// zero. func (s) TestRefCounted_TryIncrement(t *testing.T) { var onZeroCount atomic.Int32 rc, err := NewRefCounted("val", func() { @@ -112,11 +112,11 @@ func (s) TestRefCounted_TryIncrement(t *testing.T) { } } -// TestRefCounted_Concurrent tests the scenario where multiple goroutines -// concurrently increment and decrement the reference count. It verifies that -// the reference counting is thread-safe and the onZero callback is invoked -// exactly once. It also verifies that once onZero has been called, subsequent -// increament/decrement return error. +// Test verifies the scenario where multiple goroutines concurrently increment +// and decrement the reference count. It verifies that the reference counting is +// thread-safe and the onZero callback is invoked exactly once. It also verifies +// that once onZero has been called, subsequent increament/decrement return +// error. func (s) TestRefCounted_Concurrent(t *testing.T) { const numGoroutines = 10 var onZeroCount atomic.Int32 @@ -157,8 +157,8 @@ func (s) TestRefCounted_Concurrent(t *testing.T) { } } -// TestNilOnZero tests that NewRefCounted returns an error if the provided -// onZero callback is nil. +// Test verifies that NewRefCounted returns an error if the provided onZero +// callback is nil. func (s) TestNilOnZero(t *testing.T) { const wantErr = "grpcsync: onZero callback cannot be nil" if _, err := NewRefCounted("val", nil); err == nil || err.Error() != wantErr {