Skip to content

internal: adds utility for refcounting and cleanup#9189

Open
eshitachandwani wants to merge 3 commits into
grpc:masterfrom
eshitachandwani:refcount_utility
Open

internal: adds utility for refcounting and cleanup#9189
eshitachandwani wants to merge 3 commits into
grpc:masterfrom
eshitachandwani:refcount_utility

Conversation

@eshitachandwani

Copy link
Copy Markdown
Member

This PR adds the utility for refcounting objects and cleanups when the references go to zero.

For now it will be used for channel retention in A93 :Ext proc.

#ext-proc-a93

RELEASE NOTES: None

@eshitachandwani eshitachandwani added this to the 1.83 Release milestone Jun 18, 2026
@eshitachandwani eshitachandwani added Type: Feature New features or improvements in behavior Area: xDS Includes everything xDS related, including LB policies used with xDS. labels Jun 18, 2026
@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.25%. Comparing base (5c7f936) to head (ff1f329).
⚠️ Report is 11 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9189      +/-   ##
==========================================
+ Coverage   83.09%   83.25%   +0.16%     
==========================================
  Files         419      421       +2     
  Lines       33858    34050     +192     
==========================================
+ Hits        28134    28350     +216     
+ Misses       4291     4269      -22     
+ Partials     1433     1431       -2     
Files with missing lines Coverage Δ
internal/grpcsync/refcounted.go 100.00% <100.00%> (ø)

... and 39 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mbissa

mbissa commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new generic RefCounted utility in internal/grpcsync/refcounted.go to manage resource lifecycles via atomic reference counting, accompanied by comprehensive unit tests. The review feedback focuses on enhancing safety and robustness by suggesting defensive programming checks: panicking if Increment is called on a dead resource, panicking if Decrement drops the reference count below zero, and validating that the onZero callback is not nil during initialization.

Comment thread internal/grpcsync/refcounted.go
Comment thread internal/grpcsync/refcounted.go
Comment thread internal/grpcsync/refcounted.go Outdated
Comment on lines +36 to +43
func NewRefCounted[V any](val V, onZero func()) *RefCounted[V] {
rc := &RefCounted[V]{
val: val,
onZero: onZero,
}
rc.refCount.Store(1)
return rc
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent hard-to-debug runtime panics later during asynchronous decrements, it is highly recommended to validate that the onZero callback is not nil when creating a new RefCounted instance. This ensures that any configuration errors are caught synchronously at the creation site.

func NewRefCounted[V any](val V, onZero func()) *RefCounted[V] {
	if onZero == nil {
		panic("grpcsync: onZero callback cannot be nil")
	}
	rc := &RefCounted[V]{
		val:    val,
		onZero: onZero,
	}
	rc.refCount.Store(1)
	return rc
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Added

@easwars easwars removed their assignment Jun 25, 2026
Comment thread internal/grpcsync/refcounted.go Outdated
// drops to zero.
func NewRefCounted[V any](val V, onZero func()) (*RefCounted[V], error) {
if onZero == nil {
return nil, fmt.Errorf("onZero callback cannot be nil")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: prefix error with package name

"sync/atomic"
)

// RefCounted manages the lifecycle of a resource using atomic reference

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Does the term "resource" apply here? This should be a generic utility that can be used to share any value among multiple users and ensure that the value is kept alive until there is at least one user.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not aware if 'resource' has another specific meaning that I am missing , but I felt it was helpful here to distinguish it from a regular 'value'. Since Go's GC handles standard values automatically, 'resource' helps signal that this item needs explicit lifecycle management and teardown (via the onZero callback). What do you think?

// 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], error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Should this docstring carry any recommendation about the type of V that makes sense here. For example, if someone stores something by value instead of by reference, it probably doesn't make much sense, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! CHanged. Let me know if it is better ?

Comment thread internal/grpcsync/refcounted.go Outdated
// drops to zero.
func NewRefCounted[V any](val V, onZero func()) (*RefCounted[V], error) {
if onZero == nil {
return nil, fmt.Errorf("onZero callback cannot be nil")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this should also be specified in the docstring.

Comment thread internal/grpcsync/refcounted.go Outdated
Comment on lines +58 to +61
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a load of implementation detail. Should this be part of the docstring or can we move it inside the method and have something more palatable to users of this package?

Comment thread internal/grpcsync/refcounted.go Outdated
}

// Decrement decrements the reference count. If it drops to zero, the onZero
// callback is executed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Mention that the callback is executed before this method returns.

Comment thread internal/grpcsync/refcounted_test.go Outdated
}

var wg sync.WaitGroup
wg.Add(numGoroutines)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Consider using the newly added wg.Go() instead.

Comment thread internal/grpcsync/refcounted_test.go Outdated
if err == nil {
t.Fatalf("NewRefCounted(_, nil) succeeded, want error")
}
expectedErr := "onZero callback cannot be nil"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: s/expectedErr/wantErr

Also, make it a const.

Comment thread internal/grpcsync/refcounted_test.go Outdated
t.Fatalf("NewRefCounted(_, nil) succeeded, want error")
}
expectedErr := "onZero callback cannot be nil"
if err.Error() != expectedErr {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combine this check with err == nil

@easwars easwars assigned eshitachandwani and unassigned mbissa Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: xDS Includes everything xDS related, including LB policies used with xDS. Type: Feature New features or improvements in behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants