inBackground ran the take-ins of done keys (now in #463's slots), the start's look, the given values' replacement and the announced reads beside serve without serve waiting for them: a take-in could outlive its controller as the loops did. It now takes the context and is counted in its serving group as well, and a serve that is stopping starts none.
61 lines
1.9 KiB
Go
61 lines
1.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
)
|
|
|
|
// **Every goroutine the serving controller starts ends before serve returns** (issue 1945). A loop or a watch started
|
|
// beside the serving runs until serve's context ends, and serve's stop waits for it before it closes the stores, the
|
|
// bus and the condition keeper those loops read: a loop that outlived its serve met the next serve's globals, its
|
|
// closed stores and its bus, and in the test process that is the next test (a race on conditionsFrom, a package that
|
|
// passed every test and still exited failing). Where it is checked: the tests that run serve whole, under -race.
|
|
|
|
// servingGroup counts the goroutines one serve started. Once its stop began it starts nothing more: what would start is
|
|
// work for a controller that is stopping.
|
|
type servingGroup struct {
|
|
mu sync.Mutex
|
|
stopped bool
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
type servingGroupKey struct{}
|
|
|
|
// withServingGroup is ctx carrying a new group, which goServing under it counts.
|
|
func withServingGroup(ctx context.Context) (context.Context, *servingGroup) {
|
|
g := &servingGroup{}
|
|
return context.WithValue(ctx, servingGroupKey{}, g), g
|
|
}
|
|
|
|
// goServing runs work beside the caller, counted in the group ctx carries, so serve's stop waits for it. Under a group
|
|
// whose stop began it runs nothing; under a context with no group (a part run alone, by a test or a verb) it is a plain
|
|
// goroutine, as before. It says whether it started work.
|
|
func goServing(ctx context.Context, work func()) bool {
|
|
g, _ := ctx.Value(servingGroupKey{}).(*servingGroup)
|
|
if g == nil {
|
|
go work()
|
|
return true
|
|
}
|
|
g.mu.Lock()
|
|
if g.stopped {
|
|
g.mu.Unlock()
|
|
return false
|
|
}
|
|
g.wg.Add(1)
|
|
g.mu.Unlock()
|
|
go func() {
|
|
defer g.wg.Done()
|
|
work()
|
|
}()
|
|
return true
|
|
}
|
|
|
|
// stop starts nothing more and waits for what was started; the caller ends their context first. A second stop waits
|
|
// for nothing.
|
|
func (g *servingGroup) stop() {
|
|
g.mu.Lock()
|
|
g.stopped = true
|
|
g.mu.Unlock()
|
|
g.wg.Wait()
|
|
}
|