Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
110 lines
3.5 KiB
Go
110 lines
3.5 KiB
Go
/*
|
|
*
|
|
* 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()
|
|
}
|
|
}
|