Skip to content

Commit

Permalink
internal/memoize: record the caller of Destroy
Browse files Browse the repository at this point in the history
The hypothesis for golang/go#48774 is that the generation is being destroyed by
a call to (*View).shutdown. This change adds a bit of logging to
confirm that hypothesis.

For golang/go#48774

Change-Id: I34be2e16a0dcab4cea7e9b704b56f4cf0abb0c71
Reviewed-on: https://go-review.googlesource.com/c/tools/+/367674
Trust: Bryan C. Mills <[email protected]>
Run-TryBot: Bryan C. Mills <[email protected]>
gopls-CI: kokoro <[email protected]>
TryBot-Result: Go Bot <[email protected]>
Reviewed-by: Robert Findley <[email protected]>
  • Loading branch information
Bryan C. Mills committed Nov 30, 2021
1 parent 6e52f51 commit 2c9b078
Show file tree
Hide file tree
Showing 3 changed files with 20 additions and 12 deletions.
4 changes: 2 additions & 2 deletions internal/lsp/cache/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ func (v *View) shutdown(ctx context.Context) {
}
v.mu.Unlock()
v.snapshotMu.Lock()
go v.snapshot.generation.Destroy()
go v.snapshot.generation.Destroy("View.shutdown")
v.snapshotMu.Unlock()
v.importsState.destroy()
}
Expand Down Expand Up @@ -694,7 +694,7 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]*file
event.Error(ctx, "copying workspace dir", err)
}
}
go oldSnapshot.generation.Destroy()
go oldSnapshot.generation.Destroy("View.invalidateContent")

return v.snapshot, v.snapshot.generation.Acquire(ctx)
}
Expand Down
20 changes: 14 additions & 6 deletions internal/memoize/memoize.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,25 @@ type Generation struct {
destroyed uint32
store *Store
name string
// destroyedBy describes the caller that togged destroyed from 0 to 1.
destroyedBy string
// wg tracks the reference count of this generation.
wg sync.WaitGroup
}

// Destroy waits for all operations referencing g to complete, then removes
// all references to g from cache entries. Cache entries that no longer
// reference any non-destroyed generation are removed. Destroy must be called
// exactly once for each generation.
func (g *Generation) Destroy() {
// exactly once for each generation, and destroyedBy describes the caller.
func (g *Generation) Destroy(destroyedBy string) {
g.wg.Wait()
atomic.StoreUint32(&g.destroyed, 1)

prevDestroyedBy := g.destroyedBy
g.destroyedBy = destroyedBy
if ok := atomic.CompareAndSwapUint32(&g.destroyed, 0, 1); !ok {
panic("Destroy on generation " + g.name + " already destroyed by " + prevDestroyedBy)
}

g.store.mu.Lock()
defer g.store.mu.Unlock()
for k, e := range g.store.handles {
Expand Down Expand Up @@ -100,7 +108,7 @@ func (g *Generation) Acquire(ctx context.Context) func() {
return func() {}
}
if destroyed != 0 {
panic("acquire on destroyed generation " + g.name)
panic("acquire on generation " + g.name + " destroyed by " + g.destroyedBy)
}
g.wg.Add(1)
return g.wg.Done
Expand Down Expand Up @@ -175,7 +183,7 @@ func (g *Generation) Bind(key interface{}, function Function, cleanup func(inter
panic("the function passed to bind must not be nil")
}
if atomic.LoadUint32(&g.destroyed) != 0 {
panic("operation on destroyed generation " + g.name)
panic("operation on generation " + g.name + " destroyed by " + g.destroyedBy)
}
g.store.mu.Lock()
defer g.store.mu.Unlock()
Expand Down Expand Up @@ -233,7 +241,7 @@ func (s *Store) DebugOnlyIterate(f func(k, v interface{})) {
func (g *Generation) Inherit(hs ...*Handle) {
for _, h := range hs {
if atomic.LoadUint32(&g.destroyed) != 0 {
panic("inherit on destroyed generation " + g.name)
panic("inherit on generation " + g.name + " destroyed by " + g.destroyedBy)
}

h.mu.Lock()
Expand Down
8 changes: 4 additions & 4 deletions internal/memoize/memoize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ func TestGenerations(t *testing.T) {
expectGet(t, h2, g2, "res")

// With g1 destroyed, g2 should still work.
g1.Destroy()
g1.Destroy("TestGenerations")
expectGet(t, h2, g2, "res")

// With all generations destroyed, key should be re-evaluated.
g2.Destroy()
g2.Destroy("TestGenerations")
g3 := s.Generation("g3")
h3 := g3.Bind("key", func(context.Context, memoize.Arg) interface{} { return "new res" }, nil)
expectGet(t, h3, g3, "new res")
Expand All @@ -89,15 +89,15 @@ func TestCleanup(t *testing.T) {
g2 := s.Generation("g2")
g2.Inherit(h1, h2)

g1.Destroy()
g1.Destroy("TestCleanup")
expectGet(t, h1, g2, &v1)
expectGet(t, h2, g2, &v2)
for k, v := range map[string]*bool{"key1": &v1, "key2": &v2} {
if got, want := *v, false; got != want {
t.Errorf("after destroying g1, bound value %q is cleaned up", k)
}
}
g2.Destroy()
g2.Destroy("TestCleanup")
if got, want := v1, false; got != want {
t.Error("after destroying g2, v1 is cleaned up")
}
Expand Down

0 comments on commit 2c9b078

Please sign in to comment.