blob: bca9b83754dda76b5236cdfbee248c62a7374bdf [file]
/*
*
* 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"
"google.golang.org/grpc/grpclog"
)
var logger = grpclog.Component("grpcsync")
// RefCounted is a reference counted wrapper of type T. It tracks the number of
// active references and runs a cleanup when the last reference is released.
type RefCounted[T any] struct {
val T
refCount atomic.Int32
onZero func()
}
// NewRefCounted creates a new RefCounted instance wrapping the given value with
// initial refcount of one.
//
// The value should typically be a pointer, interface, or handle rather than a
// plain value type (such as a struct or primitive value).
//
// The provided onZero callback must not be nil, and is executed exactly once
// when the reference count drops to zero. Panics if onZero is nil.
//
// WARNING: onZero runs synchronously inside Decrement; it must not acquire
// locks held by Decrement callers.
func NewRefCounted[T any](val T, onZero func()) *RefCounted[T] {
if onZero == nil {
panic("grpcsync: onZero callback cannot be nil")
}
rc := &RefCounted[T]{
val: val,
onZero: onZero,
}
rc.refCount.Store(1)
return rc
}
// Value returns the encapsulated resource.
func (rc *RefCounted[T]) Value() T {
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 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[T]) 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 {
count := rc.refCount.Load()
if count <= 0 {
return false // Already dead or dying
}
if rc.refCount.CompareAndSwap(count, count+1) {
return true
}
}
}
// Increment increments the reference count.
//
// 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[T]) Increment() {
if rc.refCount.Add(1) <= 1 {
logger.Errorf("Resource already closed or dead")
}
}
// Decrement decrements the reference count. If it drops to zero, the onZero
// callback is executed synchronously before this method returns.
func (rc *RefCounted[T]) Decrement() {
if v := rc.refCount.Add(-1); v < 0 {
logger.Errorf("Refcount cannot be negative")
} else if v == 0 {
rc.onZero()
}
}