diff options
| author | sangtran <tranquangsang12.7@gmail.com> | 2026-09-03 23:10:33 +0700 |
|---|---|---|
| committer | sangtran <tranquangsang12.7@gmail.com> | 2026-09-03 23:10:33 +0700 |
| commit | 02123df8cc15896f97c1580e49d8e2a991e37ddd (patch) | |
| tree | d0c00bc3f65e5f5493a9dff8034c80640cb12960 /fanout/fanout.go | |
| signature | Verified sangtran <tranquangsang12.7@gmail.com> Key: 8DD79823D235E1BB ultimate trust | |
| download | quacker-02123df8cc15896f97c1580e49d8e2a991e37ddd.tar.gz quacker-02123df8cc15896f97c1580e49d8e2a991e37ddd.zip | |
Diffstat (limited to 'fanout/fanout.go')
| -rw-r--r-- | fanout/fanout.go | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/fanout/fanout.go b/fanout/fanout.go new file mode 100644 index 0000000..1cd2043 --- /dev/null +++ b/fanout/fanout.go | |||
| @@ -0,0 +1,203 @@ | |||
| 1 | // Package fanout distributes values from a single input channel to multiple output channels. | ||
| 2 | package fanout | ||
| 3 | |||
| 4 | import ( | ||
| 5 | "context" | ||
| 6 | "errors" | ||
| 7 | "fmt" | ||
| 8 | "sync" | ||
| 9 | "sync/atomic" | ||
| 10 | ) | ||
| 11 | |||
| 12 | // FanOutObserver defines callbacks for monitoring fan-out lifecycle events. | ||
| 13 | type FanOutObserver interface { | ||
| 14 | OnDistribute() | ||
| 15 | OnOutputClosed() | ||
| 16 | } | ||
| 17 | |||
| 18 | // FanOutStrategy defines how values are distributed across output channels. | ||
| 19 | type FanOutStrategy int | ||
| 20 | |||
| 21 | const ( | ||
| 22 | // RoundRobin distributes values evenly across output channels in rotation. | ||
| 23 | RoundRobin FanOutStrategy = iota | ||
| 24 | // Broadcast sends each value to all output channels. | ||
| 25 | Broadcast | ||
| 26 | ) | ||
| 27 | |||
| 28 | // ErrInvalidStrategy is returned when an unknown FanOutStrategy is provided. | ||
| 29 | var ErrInvalidStrategy = errors.New("fanout: unknown distribution strategy") | ||
| 30 | |||
| 31 | // FanOutConfig holds configuration options for creating a FanOut instance. | ||
| 32 | type FanOutConfig struct { | ||
| 33 | WorkerCount int | ||
| 34 | BufferSize int | ||
| 35 | Strategy FanOutStrategy | ||
| 36 | Observer FanOutObserver | ||
| 37 | } | ||
| 38 | |||
| 39 | // Option is a functional option for configuring a FanOut instance. | ||
| 40 | type Option func(*FanOutConfig) | ||
| 41 | |||
| 42 | // FanOut distributes values from a single input channel to multiple output channels. | ||
| 43 | type FanOut[T any] struct { | ||
| 44 | cfg *FanOutConfig | ||
| 45 | outputs []chan T | ||
| 46 | running atomic.Bool | ||
| 47 | cachedOutputs []<-chan T | ||
| 48 | once sync.Once | ||
| 49 | } | ||
| 50 | |||
| 51 | // NewFanOut creates a new FanOut instance with the provided options. | ||
| 52 | func NewFanOut[T any](opts ...Option) (*FanOut[T], error) { | ||
| 53 | cfg := &FanOutConfig{ | ||
| 54 | WorkerCount: 1, | ||
| 55 | BufferSize: 0, | ||
| 56 | Observer: nil, | ||
| 57 | Strategy: RoundRobin, | ||
| 58 | } | ||
| 59 | |||
| 60 | for _, opt := range opts { | ||
| 61 | opt(cfg) | ||
| 62 | } | ||
| 63 | |||
| 64 | if cfg.WorkerCount < 1 { | ||
| 65 | return nil, fmt.Errorf("fanout: worker count must be at least 1, got %d", cfg.WorkerCount) | ||
| 66 | } | ||
| 67 | |||
| 68 | if cfg.BufferSize < 0 { | ||
| 69 | return nil, fmt.Errorf("fanout: buffer size must not be negative, got %d", cfg.BufferSize) | ||
| 70 | } | ||
| 71 | |||
| 72 | if cfg.Strategy != RoundRobin && cfg.Strategy != Broadcast { | ||
| 73 | return nil, ErrInvalidStrategy | ||
| 74 | } | ||
| 75 | |||
| 76 | outputs := make([]chan T, cfg.WorkerCount) | ||
| 77 | for i := range outputs { | ||
| 78 | outputs[i] = make(chan T, cfg.BufferSize) | ||
| 79 | } | ||
| 80 | |||
| 81 | return &FanOut[T]{ | ||
| 82 | cfg: cfg, | ||
| 83 | outputs: outputs, | ||
| 84 | }, nil | ||
| 85 | } | ||
| 86 | |||
| 87 | // Run starts distributing values from the input channel to all output channels. | ||
| 88 | func (f *FanOut[T]) Run(ctx context.Context, input <-chan T) { | ||
| 89 | if input == nil { | ||
| 90 | panic("fanout: input channel must not be nil") | ||
| 91 | } | ||
| 92 | |||
| 93 | if !f.running.CompareAndSwap(false, true) { | ||
| 94 | panic("fanout: Run() called multiple times") | ||
| 95 | } | ||
| 96 | |||
| 97 | go func() { | ||
| 98 | idx := 0 | ||
| 99 | defer f.running.Store(false) | ||
| 100 | defer f.closeAllOutput() | ||
| 101 | for { | ||
| 102 | select { | ||
| 103 | case value, ok := <-input: | ||
| 104 | if !ok { | ||
| 105 | return | ||
| 106 | } | ||
| 107 | switch f.cfg.Strategy { | ||
| 108 | case RoundRobin: | ||
| 109 | f.roundRobin(ctx, value, &idx) | ||
| 110 | case Broadcast: | ||
| 111 | f.broadcast(ctx, value) | ||
| 112 | } | ||
| 113 | case <-ctx.Done(): | ||
| 114 | return | ||
| 115 | } | ||
| 116 | } | ||
| 117 | }() | ||
| 118 | } | ||
| 119 | |||
| 120 | // Outputs returns receive-only channel references for the distributed streams. | ||
| 121 | func (f *FanOut[T]) Outputs() []<-chan T { | ||
| 122 | f.once.Do(func() { | ||
| 123 | f.cachedOutputs = make([]<-chan T, len(f.outputs)) | ||
| 124 | for i, ch := range f.outputs { | ||
| 125 | f.cachedOutputs[i] = ch | ||
| 126 | } | ||
| 127 | }) | ||
| 128 | |||
| 129 | return f.cachedOutputs | ||
| 130 | } | ||
| 131 | |||
| 132 | func safeObserve(fn func()) { | ||
| 133 | defer func() { recover() }() | ||
| 134 | fn() | ||
| 135 | } | ||
| 136 | |||
| 137 | func (f *FanOut[T]) roundRobin(ctx context.Context, value T, index *int) { | ||
| 138 | target := *index % f.cfg.WorkerCount | ||
| 139 | select { | ||
| 140 | case f.outputs[target] <- value: | ||
| 141 | *index = (*index + 1) % f.cfg.WorkerCount | ||
| 142 | if f.cfg.Observer != nil { | ||
| 143 | safeObserve(f.cfg.Observer.OnDistribute) | ||
| 144 | } | ||
| 145 | case <-ctx.Done(): | ||
| 146 | return | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | func (f *FanOut[T]) broadcast(ctx context.Context, value T) { | ||
| 151 | var wg sync.WaitGroup | ||
| 152 | for _, ch := range f.outputs { | ||
| 153 | wg.Go(func() { | ||
| 154 | select { | ||
| 155 | case ch <- value: | ||
| 156 | if f.cfg.Observer != nil { | ||
| 157 | safeObserve(f.cfg.Observer.OnDistribute) | ||
| 158 | } | ||
| 159 | case <-ctx.Done(): | ||
| 160 | return | ||
| 161 | } | ||
| 162 | }) | ||
| 163 | } | ||
| 164 | wg.Wait() | ||
| 165 | } | ||
| 166 | |||
| 167 | func (f *FanOut[T]) closeAllOutput() { | ||
| 168 | for _, ch := range f.outputs { | ||
| 169 | close(ch) | ||
| 170 | } | ||
| 171 | |||
| 172 | if f.cfg.Observer != nil { | ||
| 173 | safeObserve(f.cfg.Observer.OnOutputClosed) | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | // WithWorkerCount sets the number of output channels for the fan-out. | ||
| 178 | func WithWorkerCount(n int) Option { | ||
| 179 | return func(cfg *FanOutConfig) { | ||
| 180 | cfg.WorkerCount = n | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | // WithBufferSize sets the buffer capacity for each output channel. | ||
| 185 | func WithBufferSize(size int) Option { | ||
| 186 | return func(cfg *FanOutConfig) { | ||
| 187 | cfg.BufferSize = size | ||
| 188 | } | ||
| 189 | } | ||
| 190 | |||
| 191 | // WithStrategy sets the distribution strategy for the fan-out. | ||
| 192 | func WithStrategy(strategy FanOutStrategy) Option { | ||
| 193 | return func(cfg *FanOutConfig) { | ||
| 194 | cfg.Strategy = strategy | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | // WithObserver attaches a FanOutObserver to receive lifecycle event callbacks. | ||
| 199 | func WithObserver(observer FanOutObserver) Option { | ||
| 200 | return func(cfg *FanOutConfig) { | ||
| 201 | cfg.Observer = observer | ||
| 202 | } | ||
| 203 | } | ||
