Files
temporal/common/goro/keyed_set.go
David Reiss c7bd13ad48 Add goro.KeyedSet (#8911)
## What changed?
Add a utility for managing a set of goroutines.

## Why?
Planning to use this in upcoming changes to simplify some code.

## How did you test it?
- [x] built
- [x] run locally and tested manually
- [ ] covered by existing tests
- [x] added new unit test(s)
- [ ] added new functional test(s)
2025-12-30 12:51:49 -08:00

54 lines
1.7 KiB
Go

package goro
import (
"context"
"sync"
)
// KeyedSet maintains a set of goroutines addressed by keys, creating new ones as necessary and
// canceling obsolete ones. A KeyedSet may not be copied.
type KeyedSet[K comparable] struct {
baseCtx context.Context
lock sync.Mutex
cancels map[K]context.CancelFunc
}
// NewKeyedSet returns a new KeyedSet where all goroutines inherit a context from baseCtx.
func NewKeyedSet[K comparable](baseCtx context.Context) *KeyedSet[K] {
return &KeyedSet[K]{
baseCtx: baseCtx,
cancels: make(map[K]context.CancelFunc),
}
}
// Sync cancels/starts goroutines as necessary so that the running set matches the set of keys
// in target. To start a new goroutine, it does "go f(ctx, key)".
// You can use Sync(nil, nil) to cancel all running goroutines.
//
// Note that even if f returns without its context being canceled, it will still be considered
// "running" for future calls to Sync. In general f should not return _until_ its context is
// canceled. If KeyedSet were to remove f from the running set when it returned, then we could
// have a race where f decides to return, a concurrent call to Sync includes f's key, then f
// returns and is removed, but the caller of f thinks it's now active. In other words, there
// should be one source of truth for what should be running.
func (s *KeyedSet[K]) Sync(target map[K]struct{}, f func(context.Context, K)) {
s.lock.Lock()
defer s.lock.Unlock()
for key, cancel := range s.cancels {
if _, ok := target[key]; !ok {
cancel()
delete(s.cancels, key)
}
}
for key := range target {
if _, ok := s.cancels[key]; ok {
continue
}
ctx, cancel := context.WithCancel(s.baseCtx)
s.cancels[key] = cancel
go f(ctx, key)
}
}