| /* | |
| * | |
| * 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() | |
| } | |
| } |