internal: adds utility for refcounting and cleanup#9189
internal: adds utility for refcounting and cleanup#9189eshitachandwani wants to merge 3 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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
🚀 New features to boost your workflow:
|
|
/gemini review |
There was a problem hiding this comment.
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.
| func NewRefCounted[V any](val V, onZero func()) *RefCounted[V] { | ||
| rc := &RefCounted[V]{ | ||
| val: val, | ||
| onZero: onZero, | ||
| } | ||
| rc.refCount.Store(1) | ||
| return rc | ||
| } |
There was a problem hiding this comment.
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
}There was a problem hiding this comment.
Makes sense. Added
| // 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") |
There was a problem hiding this comment.
Nit: prefix error with package name
| "sync/atomic" | ||
| ) | ||
|
|
||
| // RefCounted manages the lifecycle of a resource using atomic reference |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Right! CHanged. Let me know if it is better ?
| // 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") |
There was a problem hiding this comment.
Nit: this should also be specified in the docstring.
| // 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. |
There was a problem hiding this comment.
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?
| } | ||
|
|
||
| // Decrement decrements the reference count. If it drops to zero, the onZero | ||
| // callback is executed. |
There was a problem hiding this comment.
Nit: Mention that the callback is executed before this method returns.
| } | ||
|
|
||
| var wg sync.WaitGroup | ||
| wg.Add(numGoroutines) |
There was a problem hiding this comment.
Nit: Consider using the newly added wg.Go() instead.
| if err == nil { | ||
| t.Fatalf("NewRefCounted(_, nil) succeeded, want error") | ||
| } | ||
| expectedErr := "onZero callback cannot be nil" |
There was a problem hiding this comment.
Nit: s/expectedErr/wantErr
Also, make it a const.
| t.Fatalf("NewRefCounted(_, nil) succeeded, want error") | ||
| } | ||
| expectedErr := "onZero callback cannot be nil" | ||
| if err.Error() != expectedErr { |
There was a problem hiding this comment.
Combine this check with err == nil
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