github.com/bytedance/gopkg@v0.0.0-20240514070511-01b2cbcf35e1/lang/channel/channel.go (about) 1 // Copyright 2023 ByteDance Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package channel 16 17 import ( 18 "container/list" 19 "runtime" 20 "sync" 21 "sync/atomic" 22 "time" 23 ) 24 25 const ( 26 defaultThrottleWindow = time.Millisecond * 100 27 defaultMinSize = 1 28 ) 29 30 type item struct { 31 value interface{} 32 deadline time.Time 33 } 34 35 // IsExpired check is item exceed deadline, zero means non-expired 36 func (i item) IsExpired() bool { 37 if i.deadline.IsZero() { 38 return false 39 } 40 return time.Now().After(i.deadline) 41 } 42 43 // Option define channel Option 44 type Option func(c *channel) 45 46 // Throttle define channel Throttle function 47 type Throttle func(c Channel) bool 48 49 // WithSize define the size of channel. If channel is full, it will block. 50 // It conflicts with WithNonBlock option. 51 func WithSize(size int) Option { 52 return func(c *channel) { 53 // with non block mode, no need to change size 54 if size >= defaultMinSize && !c.nonblock { 55 c.size = size 56 } 57 } 58 } 59 60 // WithNonBlock will set channel to non-blocking Mode. 61 // The input channel will not block for any cases. 62 func WithNonBlock() Option { 63 return func(c *channel) { 64 c.nonblock = true 65 } 66 } 67 68 // WithTimeout sets the expiration time of each channel item. 69 // If the item not consumed in timeout duration, it will be aborted. 70 func WithTimeout(timeout time.Duration) Option { 71 return func(c *channel) { 72 c.timeout = timeout 73 } 74 } 75 76 // WithTimeoutCallback sets callback function when item hit timeout. 77 func WithTimeoutCallback(timeoutCallback func(interface{})) Option { 78 return func(c *channel) { 79 c.timeoutCallback = timeoutCallback 80 } 81 } 82 83 // WithThrottle sets both producerThrottle and consumerThrottle 84 // If producerThrottle throttled, it input channel will be blocked(if using blocking mode). 85 // If consumerThrottle throttled, it output channel will be blocked. 86 func WithThrottle(producerThrottle, consumerThrottle Throttle) Option { 87 return func(c *channel) { 88 if c.producerThrottle == nil { 89 c.producerThrottle = producerThrottle 90 } else { 91 prevChecker := c.producerThrottle 92 c.producerThrottle = func(c Channel) bool { 93 return prevChecker(c) && producerThrottle(c) 94 } 95 } 96 if c.consumerThrottle == nil { 97 c.consumerThrottle = consumerThrottle 98 } else { 99 prevChecker := c.consumerThrottle 100 c.consumerThrottle = func(c Channel) bool { 101 return prevChecker(c) && consumerThrottle(c) 102 } 103 } 104 } 105 } 106 107 // WithThrottleWindow sets the interval time for throttle function checking. 108 func WithThrottleWindow(window time.Duration) Option { 109 return func(c *channel) { 110 c.throttleWindow = window 111 } 112 } 113 114 // WithRateThrottle is a helper function to control producer and consumer process rate. 115 // produceRate and consumeRate mean how many item could be processed in one second, aka TPS. 116 func WithRateThrottle(produceRate, consumeRate int) Option { 117 // throttle function will be called sequentially 118 producedMax := uint64(produceRate) 119 consumedMax := uint64(consumeRate) 120 var producedBegin, consumedBegin uint64 121 var producedTS, consumedTS int64 122 return WithThrottle(func(c Channel) bool { 123 ts := time.Now().Unix() // in second 124 produced, _ := c.Stats() 125 if producedTS != ts { 126 // move to a new second, so store the current process as beginning value 127 producedBegin = produced 128 producedTS = ts 129 return false 130 } 131 // get the value of beginning 132 producedDiff := produced - producedBegin 133 return producedMax > 0 && producedMax < producedDiff 134 }, func(c Channel) bool { 135 ts := time.Now().Unix() // in second 136 _, consumed := c.Stats() 137 if consumedTS != ts { 138 // move to a new second, so store the current process as beginning value 139 consumedBegin = consumed 140 consumedTS = ts 141 return false 142 } 143 // get the value of beginning 144 consumedDiff := consumed - consumedBegin 145 return consumedMax > 0 && consumedMax < consumedDiff 146 }) 147 } 148 149 var ( 150 _ Channel = (*channel)(nil) 151 ) 152 153 // Channel is a safe and feature-rich alternative for Go chan struct 154 type Channel interface { 155 // Input send value to Output channel. If channel is closed, do nothing and will not panic. 156 Input(v interface{}) 157 // Output return a read-only native chan for consumer. 158 Output() <-chan interface{} 159 // Len return the count of un-consumed items. 160 Len() int 161 // Stats return the produced and consumed count. 162 Stats() (produced uint64, consumed uint64) 163 // Close closed the output chan. If channel is not closed explicitly, it will be closed when it's finalized. 164 Close() 165 } 166 167 // channelWrapper use to detect user never hold the reference of Channel object, and runtime will help to close channel implicitly. 168 type channelWrapper struct { 169 Channel 170 } 171 172 // channel implements a safe and feature-rich channel struct for the real world. 173 type channel struct { 174 size int 175 state int32 176 consumer chan interface{} 177 nonblock bool // non blocking mode 178 timeout time.Duration 179 timeoutCallback func(interface{}) 180 producerThrottle Throttle 181 consumerThrottle Throttle 182 throttleWindow time.Duration 183 // statistics 184 produced uint64 // item already been insert into buffer 185 consumed uint64 // item already been sent into Output chan 186 // buffer 187 buffer *list.List // TODO: use high perf queue to reduce GC here 188 bufferCond *sync.Cond 189 bufferLock sync.Mutex 190 } 191 192 // New create a new channel. 193 func New(opts ...Option) Channel { 194 c := new(channel) 195 c.size = defaultMinSize 196 c.throttleWindow = defaultThrottleWindow 197 c.bufferCond = sync.NewCond(&c.bufferLock) 198 for _, opt := range opts { 199 opt(c) 200 } 201 c.consumer = make(chan interface{}) 202 c.buffer = list.New() 203 go c.consume() 204 205 // register finalizer for wrapper of channel 206 cw := &channelWrapper{c} 207 runtime.SetFinalizer(cw, func(obj *channelWrapper) { 208 // it's ok to call Close again if user already closed the channel 209 obj.Close() 210 }) 211 return cw 212 } 213 214 // Close will close the producer and consumer goroutines gracefully 215 func (c *channel) Close() { 216 if !atomic.CompareAndSwapInt32(&c.state, 0, -1) { 217 return 218 } 219 // Close function only notify Input/consume goroutine to close gracefully 220 c.bufferCond.Broadcast() 221 } 222 223 func (c *channel) isClosed() bool { 224 return atomic.LoadInt32(&c.state) < 0 225 } 226 227 func (c *channel) Input(v interface{}) { 228 if c.isClosed() { 229 return 230 } 231 232 // prepare item 233 it := item{value: v} 234 if c.timeout > 0 { 235 it.deadline = time.Now().Add(c.timeout) 236 } 237 238 // only check throttle function in blocking mode 239 if !c.nonblock { 240 if c.throttling(c.producerThrottle) { 241 // closed 242 return 243 } 244 } 245 246 // enqueue buffer 247 c.bufferLock.Lock() 248 if !c.nonblock { 249 // only check length with blocking mode 250 for c.buffer.Len() >= c.size { 251 // wait for consuming 252 c.bufferCond.Wait() 253 if c.isClosed() { 254 // blocking send a closed channel should return directly 255 return 256 } 257 } 258 } 259 c.enqueueBuffer(it) 260 atomic.AddUint64(&c.produced, 1) 261 c.bufferLock.Unlock() 262 c.bufferCond.Signal() // use Signal because only 1 goroutine wait for cond 263 } 264 265 func (c *channel) Output() <-chan interface{} { 266 return c.consumer 267 } 268 269 func (c *channel) Len() int { 270 produced, consumed := c.Stats() 271 l := produced - consumed 272 return int(l) 273 } 274 275 func (c *channel) Stats() (uint64, uint64) { 276 produced, consumed := atomic.LoadUint64(&c.produced), atomic.LoadUint64(&c.consumed) 277 return produced, consumed 278 } 279 280 // consume used to process input buffer 281 func (c *channel) consume() { 282 for { 283 // check throttle 284 if c.throttling(c.consumerThrottle) { 285 // closed 286 return 287 } 288 289 // dequeue buffer 290 c.bufferLock.Lock() 291 for c.buffer.Len() == 0 { 292 if c.isClosed() { 293 close(c.consumer) // close consumer 294 atomic.StoreInt32(&c.state, -2) // -2 means closed totally 295 c.bufferLock.Unlock() 296 return 297 } 298 c.bufferCond.Wait() 299 } 300 it, ok := c.dequeueBuffer() 301 c.bufferLock.Unlock() 302 c.bufferCond.Broadcast() // use Broadcast because there will be more than 1 goroutines wait for cond 303 if !ok { 304 // in fact, this case will never happen 305 continue 306 } 307 308 // check expired 309 if it.IsExpired() { 310 if c.timeoutCallback != nil { 311 c.timeoutCallback(it.value) 312 } 313 atomic.AddUint64(&c.consumed, 1) 314 continue 315 } 316 // consuming, if block here means consumer is busy 317 c.consumer <- it.value 318 atomic.AddUint64(&c.consumed, 1) 319 } 320 } 321 322 func (c *channel) throttling(throttle Throttle) (closed bool) { 323 if throttle == nil { 324 return 325 } 326 throttled := throttle(c) 327 if !throttled { 328 return 329 } 330 ticker := time.NewTicker(c.throttleWindow) 331 defer ticker.Stop() 332 333 closed = c.isClosed() 334 for throttled && !closed { 335 <-ticker.C 336 throttled, closed = throttle(c), c.isClosed() 337 } 338 return closed 339 } 340 341 func (c *channel) enqueueBuffer(it item) { 342 c.buffer.PushBack(it) 343 } 344 345 func (c *channel) dequeueBuffer() (it item, ok bool) { 346 bi := c.buffer.Front() 347 if bi == nil { 348 return it, false 349 } 350 c.buffer.Remove(bi) 351 352 it = bi.Value.(item) 353 return it, true 354 }