aboutsummaryrefslogtreecommitdiff
path: root/fanin
diff options
context:
space:
mode:
authorsangtran <tranquangsang12.7@gmail.com>2026-09-03 23:10:33 +0700
committersangtran <tranquangsang12.7@gmail.com>2026-09-03 23:10:33 +0700
commit02123df8cc15896f97c1580e49d8e2a991e37ddd (patch)
treed0c00bc3f65e5f5493a9dff8034c80640cb12960 /fanin
signatureVerified sangtran <tranquangsang12.7@gmail.com> Key: 8DD79823D235E1BB ultimate trust
downloadquacker-main.tar.gz
quacker-main.zip
Verified Moving all legacy quacker source code from github to cgitHEADmain
Diffstat (limited to 'fanin')
-rw-r--r--fanin/example_test.go35
-rw-r--r--fanin/fanin.go142
-rw-r--r--fanin/fanin_bench_test.go36
-rw-r--r--fanin/fanin_test.go281
4 files changed, 494 insertions, 0 deletions
diff --git a/fanin/example_test.go b/fanin/example_test.go
new file mode 100644
index 0000000..0edae08
--- /dev/null
+++ b/fanin/example_test.go
@@ -0,0 +1,35 @@
1package fanin_test
2
3import (
4 "context"
5 "fmt"
6 "sort"
7
8 "git.sangtran.io.vn/quacker.git/fanin"
9)
10
11func ExampleNewFanIn() {
12 fi, _ := fanin.NewFanIn[int]()
13
14 ch1 := make(chan int, 2)
15 ch1 <- 1
16 ch1 <- 2
17 close(ch1)
18
19 ch2 := make(chan int, 2)
20 ch2 <- 3
21 ch2 <- 4
22 close(ch2)
23
24 fi.Add(ch1)
25 fi.Add(ch2)
26
27 var results []int
28 for v := range fi.Run(context.Background()) {
29 results = append(results, v)
30 }
31
32 sort.Ints(results)
33 fmt.Println(results)
34 // Output: [1 2 3 4]
35}
diff --git a/fanin/fanin.go b/fanin/fanin.go
new file mode 100644
index 0000000..913927f
--- /dev/null
+++ b/fanin/fanin.go
@@ -0,0 +1,142 @@
1// Package fanin merges multiple input channels into a single output channel.
2package fanin
3
4import (
5 "context"
6 "fmt"
7 "sync"
8 "sync/atomic"
9)
10
11// FanInObserver defines callbacks for monitoring fan-in lifecycle events.
12type FanInObserver interface {
13 OnInputAdded()
14 OnInputClosed()
15}
16
17// FanInConfig holds configuration options for creating a FanIn instance.
18type FanInConfig struct {
19 BufferSize int
20 Observer FanInObserver
21}
22
23// Option is a functional option for configuring a FanIn instance.
24type Option func(*FanInConfig)
25
26// FanIn merges multiple input channels of type T into a single output channel.
27type FanIn[T any] struct {
28 cfg *FanInConfig
29 mu sync.Mutex
30 inputs []<-chan T
31 running atomic.Bool
32 done chan struct{}
33}
34
35// NewFanIn creates a new FanIn instance with the provided options.
36func NewFanIn[T any](opts ...Option) (*FanIn[T], error) {
37 cfg := &FanInConfig{}
38
39 for _, opt := range opts {
40 opt(cfg)
41 }
42
43 if cfg.BufferSize < 0 {
44 return nil, fmt.Errorf("fanin: buffer size must not be negative, got %d", cfg.BufferSize)
45 }
46
47 return &FanIn[T]{
48 cfg: cfg,
49 }, nil
50}
51
52// WithBufferSize sets the buffer capacity for the merged output channel.
53func WithBufferSize(size int) Option {
54 return func(f *FanInConfig) {
55 f.BufferSize = size
56 }
57}
58
59// WithObserver attaches a FanInObserver to receive lifecycle event callbacks.
60func WithObserver(observer FanInObserver) Option {
61 return func(f *FanInConfig) {
62 f.Observer = observer
63 }
64}
65
66// Add registers an input channel to be merged into the output.
67// It must be called before Run, and is safe for concurrent use.
68func (f *FanIn[T]) Add(ch <-chan T) {
69 if ch == nil {
70 panic("fanin: cannot add nil channel")
71 }
72
73 if f.running.Load() {
74 panic("fanin: Add() called after Run()")
75 }
76
77 f.mu.Lock()
78 defer f.mu.Unlock()
79
80 f.inputs = append(f.inputs, ch)
81
82 if f.cfg.Observer != nil {
83 safeObserve(f.cfg.Observer.OnInputAdded)
84 }
85}
86
87// Run starts merging the registered input channels into a single output channel.
88func (f *FanIn[T]) Run(ctx context.Context) <-chan T {
89 if !f.running.CompareAndSwap(false, true) {
90 panic("fanin: Run() called multiple times")
91 }
92
93 f.mu.Lock()
94 inputs := append([]<-chan T(nil), f.inputs...)
95 out := make(chan T, f.cfg.BufferSize)
96 f.done = make(chan struct{})
97 f.mu.Unlock()
98
99 var wg sync.WaitGroup
100 for _, ch := range inputs {
101 wg.Go(func() {
102 for {
103 select {
104 case v, ok := <-ch:
105 if !ok {
106 if f.cfg.Observer != nil {
107 safeObserve(f.cfg.Observer.OnInputClosed)
108 }
109 return
110 }
111 select {
112 case out <- v:
113 case <-ctx.Done():
114 return
115 }
116 case <-ctx.Done():
117 return
118 }
119 }
120 })
121 }
122
123 go func() {
124 wg.Wait()
125 close(out)
126 close(f.done)
127 }()
128
129 return out
130}
131
132func safeObserve(fn func()) {
133 defer func() { recover() }()
134 fn()
135}
136
137// Done returns a channel that is closed when all inputs are processed or the context is cancelled.
138func (f *FanIn[T]) Done() <-chan struct{} {
139 f.mu.Lock()
140 defer f.mu.Unlock()
141 return f.done
142}
diff --git a/fanin/fanin_bench_test.go b/fanin/fanin_bench_test.go
new file mode 100644
index 0000000..1141224
--- /dev/null
+++ b/fanin/fanin_bench_test.go
@@ -0,0 +1,36 @@
1package fanin
2
3import (
4 "testing"
5)
6
7func BenchmarkFanIn(b *testing.B) {
8 b.ReportAllocs()
9
10 fi, _ := NewFanIn[int](WithBufferSize(64))
11
12 producers := 4
13 chs := make([]chan int, producers)
14 for i := range chs {
15 chs[i] = make(chan int, 64)
16 fi.Add(chs[i])
17 }
18
19 out := fi.Run(b.Context())
20
21 // Drain output
22 go func() {
23 for range out {
24 }
25 }()
26
27 for b.Loop() {
28 chs[b.N%producers] <- 1
29 }
30
31 for _, ch := range chs {
32 close(ch)
33 }
34
35 <-fi.Done()
36}
diff --git a/fanin/fanin_test.go b/fanin/fanin_test.go
new file mode 100644
index 0000000..bf2a413
--- /dev/null
+++ b/fanin/fanin_test.go
@@ -0,0 +1,281 @@
1package fanin
2
3import (
4 "context"
5 "sync"
6 "testing"
7 "time"
8)
9
10type mockObserver struct {
11 name string
12 t *testing.T
13}
14
15func (o *mockObserver) OnInputAdded() {
16 o.t.Logf("%s: OnInputAdded", o.name)
17}
18
19func (o *mockObserver) OnInputClosed() {
20 o.t.Logf("%s: OnInputClosed", o.name)
21}
22
23func TestNewFanIn(t *testing.T) {
24 t.Parallel()
25
26 obs := new(mockObserver)
27
28 tests := []struct {
29 name string
30 cfg *FanInConfig
31 shouldErr bool
32 }{
33 {
34 name: "valid config",
35 cfg: &FanInConfig{
36 BufferSize: 1,
37 Observer: obs,
38 },
39 shouldErr: false,
40 },
41 {
42 name: "invalid buffer size",
43 cfg: &FanInConfig{
44 BufferSize: -1,
45 Observer: obs,
46 },
47 shouldErr: true,
48 },
49 }
50
51 for _, test := range tests {
52 t.Run(test.name, func(t *testing.T) {
53 t.Parallel()
54 _, err := NewFanIn[int](WithBufferSize(test.cfg.BufferSize), WithObserver(obs))
55
56 if (err != nil) != test.shouldErr {
57 t.Errorf("validation error = %v, want err %v", err, test.shouldErr)
58 }
59 })
60 }
61}
62
63func TestFanIn_Add(t *testing.T) {
64 t.Parallel()
65 fi, _ := NewFanIn[int]()
66
67 inputs := make([]chan int, 10)
68 for i := range inputs {
69 inputs[i] = make(chan int, 1)
70 inputs[i] <- i
71 close(inputs[i])
72 }
73
74 for _, ch := range inputs {
75 fi.Add(ch)
76 }
77
78 count := 0
79 for range fi.Run(t.Context()) {
80 count++
81 }
82
83 if count != 10 {
84 t.Fatalf("expected 10 items, got %d", count)
85 }
86}
87
88func TestFanIn_Run(t *testing.T) {
89 t.Parallel()
90 fi, _ := NewFanIn[int]()
91
92 ch := make(chan int)
93 go func() {
94 ch <- 1
95 ch <- 2
96 ch <- 3
97 close(ch)
98 }()
99
100 fi.Add(ch)
101
102 for v := range fi.Run(t.Context()) {
103 t.Logf("value received %d", v)
104 }
105
106 <-fi.Done()
107}
108
109func TestFanIn_ContextCancellation(t *testing.T) {
110 t.Parallel()
111 fi, _ := NewFanIn[int]()
112 ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond)
113 defer cancel()
114
115 inputs := make([]chan int, 10)
116 for i := range inputs {
117 inputs[i] = make(chan int, 1)
118 inputs[i] <- i
119 close(inputs[i])
120 }
121
122 for _, ch := range inputs {
123 fi.Add(ch)
124 }
125
126 select {
127 case <-fi.Run(ctx):
128 cancel()
129 case <-fi.Done():
130 cancel()
131 }
132}
133
134func TestFanIn_ShouldPanicRunMultiple(t *testing.T) {
135 t.Parallel()
136
137 defer func() {
138 if r := recover(); r == nil {
139 t.Errorf("expected panic for multiple Run calls")
140 }
141 }()
142
143 ctx := t.Context()
144 fi, _ := NewFanIn[int]()
145
146 inputs := make([]chan int, 10)
147 for i := range inputs {
148 inputs[i] = make(chan int, 1)
149 inputs[i] <- i
150 close(inputs[i])
151 }
152
153 for _, ch := range inputs {
154 fi.Add(ch)
155 }
156
157 for v := range fi.Run(ctx) {
158 t.Logf("value received %d", v)
159 }
160
161 fi.Run(ctx)
162}
163
164func TestFanIn_ShouldPanicAddAfterRun(t *testing.T) {
165 t.Parallel()
166
167 defer func() {
168 if r := recover(); r == nil {
169 t.Errorf("expected panic for Add after Run")
170 }
171 }()
172
173 fi, _ := NewFanIn[int]()
174
175 inputs := make([]chan int, 10)
176 for i := range inputs {
177 inputs[i] = make(chan int, 1)
178 inputs[i] <- i
179 close(inputs[i])
180 }
181
182 for _, ch := range inputs {
183 fi.Add(ch)
184 }
185
186 for v := range fi.Run(t.Context()) {
187 t.Logf("value received %d", v)
188 }
189
190 fi.Add(make(chan int))
191}
192
193func TestFanIn_Observer(t *testing.T) {
194 t.Parallel()
195 obs := &mockObserver{t: t}
196
197 fi, _ := NewFanIn[int](WithObserver(obs))
198
199 inputs := make([]chan int, 10)
200 for i := range inputs {
201 inputs[i] = make(chan int, 1)
202 inputs[i] <- i
203 close(inputs[i])
204 }
205
206 for _, ch := range inputs {
207 fi.Add(ch)
208 }
209
210 for v := range fi.Run(t.Context()) {
211 t.Logf("value received %d", v)
212 }
213
214 <-fi.Done()
215}
216
217func TestFanIn_AddConcurrency(t *testing.T) {
218 t.Parallel()
219
220 fi, _ := NewFanIn[int]()
221 var wg sync.WaitGroup
222 numGoroutines := 10
223 channelPerGoroutine := 10
224
225 for range numGoroutines {
226 wg.Go(func() {
227 inputs := make([]chan int, channelPerGoroutine)
228 for i := range inputs {
229 inputs[i] = make(chan int, 1)
230 inputs[i] <- i
231 close(inputs[i])
232 fi.Add(inputs[i])
233 }
234 })
235 }
236 wg.Wait()
237
238 count := 0
239 for range fi.Run(t.Context()) {
240 count++
241 }
242
243 <-fi.Done()
244
245 expectedCount := numGoroutines * channelPerGoroutine
246 if count != expectedCount {
247 t.Errorf("expected %d items, got %d", expectedCount, count)
248 }
249}
250
251func TestFanIn_NilChannel(t *testing.T) {
252 t.Parallel()
253
254 fi, _ := NewFanIn[int]()
255
256 defer func() {
257 if r := recover(); r == nil {
258 t.Error("expected panic when adding nil channel")
259 }
260 }()
261
262 fi.Add(nil)
263}
264
265func TestFanIn_RunWithNoInputs(t *testing.T) {
266 t.Parallel()
267
268 fi, _ := NewFanIn[int]()
269 out := fi.Run(t.Context())
270
271 count := 0
272 for range out {
273 count++
274 }
275
276 if count != 0 {
277 t.Errorf("expected 0 values from empty fan-in, got %d", count)
278 }
279
280 <-fi.Done()
281}