Skip to content

Commit 98bfcd1

Browse files
committed
internal/memoize: fix race in Store.Promise
When releasing a promise, there was a theoretical race whereby a promise's refcount could be incremented before Store.promisesMu was acquired and the promise deleted. We could fix this by double-checking after acquiring Store.promisesMu, but it seemed simpler to just guard Promise.refcount with Store.promisesMu, and skip using atomics. We already lock promisesMu when acquiring the promise, and so locking when releasing should not significantly affect our level of contention. Additionally, make it a panic to call the returned release function more than once, and document this behavior. Change-Id: I1135b558b1f13f2b063dcaad129a432c22da0b28 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419504 Reviewed-by: Bryan Mills <[email protected]> Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent e02e98a commit 98bfcd1

File tree

2 files changed

+44
-13
lines changed

2 files changed

+44
-13
lines changed

internal/memoize/memoize.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ type RefCounted interface {
6565
type Promise struct {
6666
debug string // for observability
6767

68+
// refcount is the reference count in the containing Store, used by
69+
// Store.Promise. It is guarded by Store.promisesMu on the containing Store.
70+
refcount int32
71+
6872
mu sync.Mutex
6973

7074
// A Promise starts out IDLE, waiting for something to demand
@@ -91,8 +95,6 @@ type Promise struct {
9195
function Function
9296
// value is set in completed state.
9397
value interface{}
94-
95-
refcount int32 // accessed using atomic load/store
9698
}
9799

98100
// NewPromise returns a promise for the future result of calling the
@@ -267,11 +269,13 @@ func NewStore(policy EvictionPolicy) *Store {
267269
// Promise returns a reference-counted promise for the future result of
268270
// calling the specified function.
269271
//
270-
// Calls to Promise with the same key return the same promise,
271-
// incrementing its reference count. The caller must call the
272-
// returned function to decrement the promise's reference count when
273-
// it is no longer needed. Once the last reference has been released,
274-
// the promise is removed from the store.
272+
// Calls to Promise with the same key return the same promise, incrementing its
273+
// reference count. The caller must call the returned function to decrement
274+
// the promise's reference count when it is no longer needed. The returned
275+
// function must not be called more than once.
276+
//
277+
// Once the last reference has been released, the promise is removed from the
278+
// store.
275279
func (store *Store) Promise(key interface{}, function Function) (*Promise, func()) {
276280
store.promisesMu.Lock()
277281
p, ok := store.promises[key]
@@ -282,18 +286,24 @@ func (store *Store) Promise(key interface{}, function Function) (*Promise, func(
282286
}
283287
store.promises[key] = p
284288
}
285-
atomic.AddInt32(&p.refcount, 1)
289+
p.refcount++
286290
store.promisesMu.Unlock()
287291

292+
var released int32
288293
release := func() {
289-
// TODO(rfindley): this looks racy: it's possible that the refcount is
290-
// incremented before we grab the lock.
291-
if atomic.AddInt32(&p.refcount, -1) == 0 && store.evictionPolicy != NeverEvict {
292-
store.promisesMu.Lock()
294+
if !atomic.CompareAndSwapInt32(&released, 0, 1) {
295+
panic("release called more than once")
296+
}
297+
store.promisesMu.Lock()
298+
299+
p.refcount--
300+
if p.refcount == 0 && store.evictionPolicy != NeverEvict {
301+
// Inv: if p.refcount > 0, then store.promises[key] == p.
293302
delete(store.promises, key)
294-
store.promisesMu.Unlock()
295303
}
304+
store.promisesMu.Unlock()
296305
}
306+
297307
return p, release
298308
}
299309

internal/memoize/memoize_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,24 @@ func TestPromiseDestroyedWhileRunning(t *testing.T) {
143143
t.Errorf("Get() = %v, want %v", got, v)
144144
}
145145
}
146+
147+
func TestDoubleReleasePanics(t *testing.T) {
148+
var store memoize.Store
149+
_, release := store.Promise("key", func(ctx context.Context, _ interface{}) interface{} { return 0 })
150+
151+
panicked := false
152+
153+
func() {
154+
defer func() {
155+
if recover() != nil {
156+
panicked = true
157+
}
158+
}()
159+
release()
160+
release()
161+
}()
162+
163+
if !panicked {
164+
t.Errorf("calling release() twice did not panic")
165+
}
166+
}

0 commit comments

Comments
 (0)