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

     1  /*
     2  © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package parl
     7  
     8  import (
     9  	"github.com/haraldrudell/parl/perrors"
    10  )
    11  
    12  // Close closes the underlying channel without data loss
    13  //   - didClose true: this Close invocation executed channel close
    14  //   - didClose may be false for all invocations if the channel is closed by sendThread
    15  //   - when Close returns, the channel may still be open and have items
    16  //   - Close is thread-safe, non-blocking, error-free and panic-free
    17  //   - underlying channel closes once Send SendMany completes and the channel
    18  //     is empty
    19  func (n *NBChan[T]) Close() (didClose bool) {
    20  
    21  	// atomic performance check if Close already invoked
    22  	if n.isCloseInvoked.IsInvoked() {
    23  		// await winner return
    24  		<-n.isCloseInvoked.Ch()
    25  		return // Close complete return
    26  	}
    27  
    28  	// select Close winner
    29  	var isWinner, isRunningThread, done = n.selectCloseWinner()
    30  	if !isWinner {
    31  		// await winner return
    32  		<-n.isCloseInvoked.Ch()
    33  		return
    34  	}
    35  	defer done.Done()
    36  	// isCloseInvoked.IsInvoked is true so new Send SendMany will return immediately
    37  	//	- unsent count is therefore strictly decreasing by Get and send-thread
    38  
    39  	// handle deferred Close
    40  	if isRunningThread {
    41  		// a thread is running, if it does not exit, it’s deferred close
    42  		//	- await static thread state, one of: send-block exit sends gets alert
    43  		var threadState NBChanTState
    44  		select {
    45  		// thread exit
    46  		case <-n.tcThreadExitAwaitable.Ch():
    47  			// the thread exited
    48  			// if at end of items, executee close immediately
    49  			if n.unsentCount.Load() == 0 {
    50  				threadState = NBChanExit
    51  			}
    52  
    53  			// NBChanSendBlock NBChanAlert NBChanSends NBChanGets
    54  		case threadState = <-n.stateCh():
    55  		}
    56  
    57  		// an always thread in NBChanAlert must be alerted
    58  		//	- isCloseInvoked.IsInvoked prevents further alert wait
    59  		if threadState == NBChanAlert {
    60  			n.tcAlertThread()
    61  		}
    62  
    63  		// deferred close function
    64  		if threadState != NBChanExit {
    65  			return // deferred close
    66  		}
    67  	}
    68  
    69  	// immediate close
    70  	//	- send-thread or Get to consume remaining items
    71  	for n.unsentCount.Load() > 0 {
    72  		<-n.updateDataAvailable()
    73  		n.getsWait.Wait()
    74  	}
    75  
    76  	didClose, _ = n.executeChClose()
    77  	// update datawaitCh
    78  	n.setDataAvailableAfterClose()
    79  
    80  	return
    81  }
    82  
    83  // CloseNow closes without waiting for sends to complete.
    84  //   - CloseNow is thread-safe, panic-free, idempotent, deferrable and is
    85  //     designed to not block for long
    86  //   - CloseNow does not return until the channel is closed and no thread is running
    87  //   - Upon return, errp and err receive any close or panic errors for this [NBChan]
    88  //   - if errp is non-nil, it is updated with error status
    89  func (n *NBChan[T]) CloseNow(errp ...*error) (didClose bool, err error) {
    90  
    91  	// add any occuring errors for this [NBChan]
    92  	defer n.appendErrors(&err, errp...)
    93  
    94  	// select close now winner
    95  	var isWinner, isRunningThread, done, doneClose = n.selectCloseNowWinner()
    96  	if !isWinner {
    97  		<-n.isCloseNow.Ch()
    98  		return // close now loser threads
    99  	}
   100  	// this is CloseNow winner thread
   101  	defer done.Done()
   102  	if doneClose != nil {
   103  		defer doneClose.Done()
   104  	}
   105  
   106  	// wait for any Send SendMany Get to complete
   107  	//	- Get Collect uses underlying channel
   108  	//	- new invocations are canceled by:
   109  	//	- — Send SendMany: isCloseInvoked.IsInvoked
   110  	//	- — Get: isCloseNow.IsInvoked
   111  	n.getsWait.Wait()
   112  	n.sendsWait.Wait()
   113  
   114  	// release thread so it will exit
   115  	//	- gets and sends is zero, so thread is not held up there
   116  	//	- isCloseNow.IsInvoked is true
   117  	if isRunningThread {
   118  		// a thread was running when IsCloseNow activated
   119  		//	- await a state for the thread
   120  		//	- holding inputLock outputLock so no Send SendMany Get
   121  		var threadState NBChanTState
   122  		select {
   123  		// thread exit
   124  		case <-n.tcThreadExitAwaitable.Ch():
   125  			// NBChanSendBlock NBChanAlert
   126  		case threadState = <-n.stateCh():
   127  		}
   128  
   129  		switch threadState {
   130  		// alway thread awaiting alert
   131  		case NBChanAlert:
   132  			// alert and isclosenow will cause thread to exit
   133  			n.tcAlertThread()
   134  			// thread blocked in value send
   135  		case NBChanSendBlock:
   136  			select {
   137  			// discard any data item received from thread
   138  			//	- thread will exit due to isCloseNow
   139  			case <-n.closableChan.Ch():
   140  				// cancel on thread exit
   141  			case <-n.tcThreadExitAwaitable.Ch():
   142  			}
   143  		}
   144  	}
   145  
   146  	// await thread exit
   147  	if isRunningThread {
   148  		<-n.tcThreadExitAwaitable.Ch()
   149  	}
   150  
   151  	// close data ch waiter
   152  	//	- this will release a possible held Close invocation
   153  	n.setDataAvailableAfterClose()
   154  
   155  	// execute close
   156  	didClose, _ = n.executeChClose()
   157  
   158  	// discard pending data
   159  	n.outputLock.Lock()
   160  	defer n.outputLock.Unlock()
   161  	n.inputLock.Lock()
   162  	defer n.inputLock.Unlock()
   163  
   164  	//	- thread has exited
   165  	//	- Send SendMany Gets have ceased
   166  	//	- this thread holds both locks
   167  
   168  	if nT := len(n.inputQueue); nT > 0 {
   169  		n.unsentCount.Add(uint64(-nT))
   170  	}
   171  	n.inputQueue = nil
   172  	n.inputCapacity.Store(0)
   173  	if nT := len(n.outputQueue); nT > 0 {
   174  		n.unsentCount.Add(uint64(-nT))
   175  	}
   176  	n.outputQueue = nil
   177  	n.outputCapacity.Store(0)
   178  
   179  	return
   180  }
   181  
   182  // selectCloseNowWinner ensures CloseNow execution
   183  //   - isWinner true: this thread executes close now
   184  //   - isRunningThread: a thread was running at time of close now/
   185  //     if so, tcThreadExitAwaitable is armed
   186  //   - done: completion Done for winner thread
   187  //   - caller must hold inputLock: to update isCloseNow and isCloseInvoked
   188  //   - caller must hold inputLock and outputLock to prevent subsequent thread launch
   189  func (n *NBChan[T]) selectCloseNowWinner() (
   190  	isWinner,
   191  	isRunningThread bool,
   192  	done, doneClose Done,
   193  ) {
   194  	n.tcThreadLock.Lock() // atomizes CloseNow true with tcRunningThread
   195  	defer n.tcThreadLock.Unlock()
   196  
   197  	// select CloseNow winner
   198  	if isWinner, done = n.isCloseNow.IsWinner(NoOnceWait); !isWinner {
   199  		return // CloseNow was completed by another thread
   200  	}
   201  	// is winning CloseNow thread
   202  
   203  	// CloseNow also signals Close
   204  	_, doneClose = n.isCloseInvoked.IsWinner(NoOnceWait)
   205  
   206  	// thread status at time of CloseNow
   207  	isRunningThread = n.tcRunningThread.Load()
   208  
   209  	return
   210  }
   211  
   212  // selectCloseWinner selects the winner to execuite close possily deferred to thread
   213  //   - executeCloseNow true: is winner thread and close is not deferred
   214  //   - deferred close is setting isCloseInvoked to true while a thread is running
   215  //   - caller must hold inputLock for isCloseInvoked update
   216  func (n *NBChan[T]) selectCloseWinner() (isWinner, isRunningThread bool, done Done) {
   217  	// atomize close win with reading running thread-state
   218  	n.tcThreadLock.Lock() // atomizes Close:true tcRunningThread
   219  	defer n.tcThreadLock.Unlock()
   220  
   221  	var _ OnceCh
   222  
   223  	// select winner
   224  	//	- losers do not wait here to get out of tcThreadLock
   225  	if isWinner, done = n.isCloseInvoked.IsWinner(NoOnceWait); !isWinner {
   226  		return // Close was already invoked return: executeCloseNow: false
   227  	}
   228  	// this thread is close winner
   229  
   230  	isRunningThread = n.tcRunningThread.Load()
   231  
   232  	return // if thread is not running: executeCloseNow: true: execute close now
   233  }
   234  
   235  // executeChClose closes the underlying channel
   236  //   - didClose true: this invocation closed the channel
   237  //   - err possible error, already submitted: unused
   238  //   - idempotent thread-safe
   239  //   - isCloseInvoked.IsInvoked must be true
   240  //   - unsent count must be zero
   241  //   - invoked by:
   242  //   - — CloseNow
   243  //   - — Close if not deferred close
   244  //   - — send thread in deferred close: on exit if Close was invoked prior to thread exit
   245  func (n *NBChan[T]) executeChClose() (didClose bool, err error) {
   246  
   247  	// await Send SendMany ceasing
   248  	//	- new invocations return immediately due to isCloseInvoked.IsInvoked true
   249  	n.sendsWait.Wait()
   250  
   251  	// wait for any Send SendMany Get to complete
   252  	//	- Get Collect uses underlying channel
   253  
   254  	if didClose, err = n.closableChan.Close(); !didClose {
   255  		return // already closed return: noop
   256  	} else if err != nil {
   257  		n.AddError(err) // store possible close error
   258  	}
   259  	// update [NBChan.waitForClose]
   260  	n.waitForClose.Close()
   261  
   262  	return
   263  }
   264  
   265  // appendErrors aggregates any errors for this [NBChan] in any non-nil errp0 or errp
   266  //   - like perrors.AppendErrorDefer but allows for errp to be nil
   267  func (n *NBChan[T]) appendErrors(errp0 *error, errp ...*error) {
   268  
   269  	if errp0 != nil {
   270  		perrors.AppendErrorDefer(errp0, nil, n.GetError)
   271  	}
   272  	// obtain error pointers
   273  	for _, errpx := range errp {
   274  		if errpx == nil {
   275  			continue
   276  		}
   277  		perrors.AppendErrorDefer(errpx, nil, n.GetError)
   278  	}
   279  }