-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathworkerpool.go
More file actions
107 lines (98 loc) · 2.87 KB
/
Copy pathworkerpool.go
File metadata and controls
107 lines (98 loc) · 2.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package wshub
import (
"context"
"sync"
"sync/atomic"
)
// broadcastTask is a unit of work dispatched to the worker pool.
// Each task represents a batch of clients that need to receive a sendItem.
type broadcastTask struct {
hub *Hub
clients []*Client
item sendItem
ctx context.Context // nil for non-context sends
result *contextResult // nil for non-context sends
wg *sync.WaitGroup // caller's WaitGroup — Done() called after batch completes
}
// contextResult collects the first context error across concurrent batches.
type contextResult struct {
mu sync.Mutex
err error
}
// workerPool is a fixed set of long-lived goroutines that process broadcast
// tasks. It replaces the per-broadcast goroutine spawning in parallelSend,
// eliminating goroutine creation/teardown overhead on every broadcast call.
type workerPool struct {
tasks chan broadcastTask
wg sync.WaitGroup // tracks worker goroutines for clean shutdown
stopped atomic.Bool
closeOnce sync.Once
}
// newWorkerPool creates and starts a pool of numWorkers goroutines.
// Workers pull tasks from a shared channel and process them until the
// channel is closed via shutdown().
func newWorkerPool(numWorkers int) *workerPool {
p := &workerPool{
tasks: make(chan broadcastTask, numWorkers*4),
}
p.wg.Add(numWorkers)
for range numWorkers {
go p.runWorker()
}
return p
}
// runWorker is the main loop for each worker goroutine.
// It processes tasks until the tasks channel is closed.
func (p *workerPool) runWorker() {
defer p.wg.Done()
for task := range p.tasks {
task.execute()
}
}
// shutdown closes the tasks channel and waits for all workers to drain
// remaining tasks and exit. Safe to call multiple times.
func (p *workerPool) shutdown() {
p.stopped.Store(true)
p.closeOnce.Do(func() {
close(p.tasks)
})
p.wg.Wait()
}
// submit sends a task to the pool. Returns false if the pool is shut down,
// allowing callers to fall back to inline execution.
func (p *workerPool) submit(task broadcastTask) (ok bool) {
if p.stopped.Load() {
return false
}
// Guard against the TOCTOU window between the stopped check and the
// channel send — shutdown could close the channel in between.
defer func() {
if recover() != nil {
ok = false
}
}()
p.tasks <- task
return true
}
// execute processes a single broadcast task — sending the item to every
// client in the batch. It supports two paths: a fast path without context
// checking, and a context-aware path that stops early on cancellation.
func (t broadcastTask) execute() {
defer t.wg.Done()
if t.ctx != nil {
for _, client := range t.clients {
if !t.hub.trySendWithContext(t.ctx, client, t.item) {
t.result.mu.Lock()
if t.result.err == nil {
t.result.err = t.ctx.Err()
}
t.result.mu.Unlock()
return
}
}
} else {
for _, client := range t.clients {
t.hub.trySend(client, t.item)
}
}
}