github.com/haraldrudell/parl@v0.4.176/nb-rare-chan.go (about)

     1  /*
     2  © 2024–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package parl
     7  
     8  import (
     9  	"sync"
    10  	"sync/atomic"
    11  
    12  	"github.com/haraldrudell/parl/perrors"
    13  )
    14  
    15  // NBRareChan is a simplified [NBChan] using on-demand thread
    16  //   - NBRareChan is a channel with unbound buffer
    17  //     like an unbuffered channel reading from a thread-safe slice
    18  //   - [NBRareChan.Send] provides value send that is non-blocking-send, thread-safe, panic, deadlock and error-free
    19  //   - [NBRareChan.Ch] provides real-time value stream or
    20  //   - [NBRareChan.Close] provides all buffered values
    21  //   - [NBRareChan.StopSend] blocks further Send allowing for graceful shutdown
    22  //   - [NBRareChan.IsClose] returns whether the underlying channel is closed
    23  //   - [NBRareChan.PanicCh] provides real-time notice of thread panic, should not happen
    24  //   - NBRareChan is initialization-free and thread-safe with
    25  //     thread-safe panic-free idempotent observable deferrable Close
    26  //   - used as an error sink, NBRareChan[error] prevents error propagation from affecting the thread
    27  //   - ignoring thread panics and Close errp is reasonably safe simplification
    28  //   - intended for infrequent use such as an error sink
    29  //   - benefits over plain channel:
    30  //   - — [NBRareChan.Send] is non-blocking-send panic-free non-dead-locking
    31  //   - — initialization-free
    32  //   - — unbound buffer
    33  //   - — any thread can close the channel as opposed to only the sending thread
    34  //   - — thread-synchronizing unbuffered channel-send mechanic as opposed to a buffered channel
    35  //   - — graceful shutdown like a buffered channel
    36  //   - — [NBRareChan.Close] is any-thread data-race-free thread-safe panic-free idempotent observable deferrable
    37  //   - drawbacks compared to [NBChan]:
    38  //   - — on-demand thread may lead to high cpu if used frequently like every second.
    39  //     NBChan offers no-thread or always-thread operation
    40  //   - — buffering a large number of items leads to a temporary memory leak in queue
    41  //   - — there is no contention-separation between Send and reading Ch
    42  //   - — no multiple-item operations like SendMany or Get
    43  //   - — less observable and configurable
    44  //   - see also:
    45  //   - — [NBChan] fully-featured unbound channel
    46  //   - — [AwaitableSlice] unbound awaitable queue
    47  //
    48  // Deprecated: NBRareChan is replaced by [github.com/haraldrudell/parl.AwaitableSlice] for performance and
    49  // efficiency reasons. [github.com/haraldrudell/parl.ErrSlice] is an error container implementation
    50  type NBRareChan[T any] struct {
    51  	// underlying channel
    52  	//	- closed by first Close invocation
    53  	closableChan ClosableChan[T]
    54  	// threadWait makes all created send threads awaitable
    55  	threadWait sync.WaitGroup
    56  	// queueLock ensures thread-safety of queue
    57  	//	- also ensures sequenced access to isThread isStopSend
    58  	queueLock sync.Mutex
    59  	// didSend idicates that Send did create a send-thread
    60  	// whose value may need to be collected on Close
    61  	//	- behind queueLock
    62  	didSend bool
    63  	// queue is a slice-away slice of unsent data
    64  	//	- Send appends to queue
    65  	//	- behind queueLock
    66  	queue []T
    67  	// threadReadingValues indicates that a send thread is
    68  	// currently reading values from queue
    69  	//	- accessed behind queueLock
    70  	threadReadingValues CyclicAwaitable
    71  	// accessed behind queueLock
    72  	isStopSend atomic.Bool
    73  	// returned by StopSend await empty channel
    74  	isEmpty Awaitable
    75  	// sendThread panics, should be none
    76  	errs atomic.Pointer[error]
    77  	// isPanic indicates that a send thread suffered a panic
    78  	//	- triggers PanicCh awaitable
    79  	isPanic Awaitable
    80  	// ensures close executed once
    81  	closeOnce OnceCh
    82  }
    83  
    84  // Ch obtains the underlying channel for channel receive operations
    85  func (n *NBRareChan[T]) Ch() (ch <-chan T) { return n.closableChan.Ch() }
    86  
    87  // Send sends a single value on the channel
    88  //   - non-blocking-send, thread-safe, deadlock-free, panic-free and error-free
    89  //   - if Close or StopSend was invoked, value is discarded
    90  func (n *NBRareChan[T]) Send(value T) {
    91  	n.queueLock.Lock()
    92  	defer n.queueLock.Unlock()
    93  
    94  	// ignore values after Close or StopSend
    95  	if n.closableChan.IsClosed() || n.isStopSend.Load() {
    96  		return
    97  	}
    98  
    99  	// possibly create thread with value
   100  	var createThread bool
   101  	if createThread = !n.didSend; createThread {
   102  		n.didSend = true
   103  	} else {
   104  		createThread = n.threadReadingValues.IsClosed()
   105  	}
   106  	if createThread {
   107  		n.threadReadingValues.Open()
   108  		n.threadWait.Add(1)
   109  		go n.sendThread(value)
   110  		return
   111  	}
   112  
   113  	// append value to buffer
   114  	n.queue = append(n.queue, value)
   115  }
   116  
   117  // StopSend ignores further Send allowing for the channel to be drained
   118  //   - emptyAwaitable triggers once the channel is empty
   119  func (n *NBRareChan[T]) StopSend() (emptyAwaitable AwaitableCh) {
   120  	n.queueLock.Lock()
   121  	defer n.queueLock.Unlock()
   122  
   123  	n.isStopSend.CompareAndSwap(false, true)
   124  	emptyAwaitable = n.isEmpty.Ch()
   125  	if len(n.queue) == 0 && n.threadReadingValues.IsClosed() {
   126  		n.isEmpty.Close()
   127  	}
   128  	return
   129  }
   130  
   131  // PanicCh is real-time awaitable for panic in sendThread
   132  //   - this should not happen
   133  func (n *NBRareChan[T]) PanicCh() (emptyAwaitable AwaitableCh) { return n.isPanic.Ch() }
   134  
   135  // IsClose returns true if underlying channel is closed
   136  func (n *NBRareChan[T]) IsClose() (isClose bool) { return n.closableChan.IsClosed() }
   137  
   138  // Close immediately closes the channel returning any unsent values
   139  //   - values: possible values that were in channel, may be nil
   140  //   - errp: receives any panics from thread. Should be none. may be nil
   141  //   - upon return, resources are released and further Send ineffective
   142  func (n *NBRareChan[T]) Close(values *[]T, errp *error) {
   143  
   144  	// ensure once execution
   145  	if isWinner, done := n.closeOnce.IsWinner(); !isWinner {
   146  		return // loser thread has already awaited done
   147  	} else {
   148  		defer done.Done()
   149  	}
   150  
   151  	// collect queue and stop further Send
   152  	var queue = n.close()
   153  
   154  	// collect possible value from thread and shut it down
   155  	if n.didSend {
   156  		select {
   157  		case value := <-n.closableChan.Ch():
   158  			queue = append([]T{value}, queue...)
   159  		case <-n.threadReadingValues.Ch():
   160  			n.isEmpty.Close()
   161  		}
   162  		// wait for all created threads to exit
   163  		n.threadWait.Wait()
   164  		if errp != nil {
   165  			if ep := n.errs.Load(); ep != nil {
   166  				*errp = perrors.AppendError(*errp, *ep)
   167  			}
   168  		}
   169  	}
   170  
   171  	// close underlying channel
   172  	n.closableChan.Close()
   173  
   174  	// return values
   175  	if values != nil && len(queue) > 0 {
   176  		*values = queue
   177  	}
   178  }
   179  
   180  // collect queue and stop further Send
   181  func (n *NBRareChan[T]) close() (values []T) {
   182  	n.queueLock.Lock()
   183  	defer n.queueLock.Unlock()
   184  
   185  	if values = n.queue; values != nil {
   186  		n.queue = nil
   187  	}
   188  	n.isStopSend.Store(true)
   189  
   190  	return
   191  }
   192  
   193  // sendThread carries out send operations on the channel
   194  func (n *NBRareChan[T]) sendThread(value T) {
   195  	defer n.threadWait.Done()
   196  	defer Recover(func() DA { return A() }, nil, n.sendThreadPanic)
   197  
   198  	var ch = n.closableChan.Ch()
   199  	for {
   200  		ch <- value
   201  		var hasValue bool
   202  		if value, hasValue = n.sendThreadNextValue(); !hasValue {
   203  			return
   204  		}
   205  	}
   206  }
   207  
   208  // sendThreadNextValue obtains the next value to send for thread if any
   209  func (n *NBRareChan[T]) sendThreadNextValue() (value T, hasValue bool) {
   210  	n.queueLock.Lock()
   211  	defer n.queueLock.Unlock()
   212  
   213  	if hasValue = len(n.queue) > 0; hasValue {
   214  		value = n.queue[0]
   215  		n.queue = n.queue[1:]
   216  		return
   217  	}
   218  	// channel detected empty
   219  
   220  	//	- notify that sendThread is no longer awaiting values
   221  	n.threadReadingValues.Close()
   222  
   223  	// if StopSend was invoked, notify its awaitable
   224  	if n.isStopSend.Load() {
   225  		n.isEmpty.Close()
   226  	}
   227  
   228  	return
   229  }
   230  
   231  // sendThreadPanic aggregates thread panics
   232  func (n *NBRareChan[T]) sendThreadPanic(err error) {
   233  
   234  	// provide panic error to errs
   235  	for {
   236  		var errp0 = n.errs.Load()
   237  		if errp0 == nil && n.errs.CompareAndSwap(nil, &err) {
   238  			break // wrote new error
   239  		}
   240  		var err2 = perrors.AppendError(*errp0, err)
   241  		if n.errs.CompareAndSwap(errp0, &err2) {
   242  			break // appended error
   243  		}
   244  	}
   245  
   246  	// notify awaitable that a panic occured
   247  	n.isPanic.Close()
   248  	n.queueLock.Lock()
   249  	defer n.queueLock.Unlock()
   250  
   251  	n.threadReadingValues.Close()
   252  }