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  }