github.com/haraldrudell/parl@v0.4.176/perrors/errorglue/send-nb.go (about)

     1  /*
     2  © 2021–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package errorglue
     7  
     8  import (
     9  	"fmt"
    10  	"os"
    11  	"sync"
    12  	"sync/atomic"
    13  )
    14  
    15  // SendNb implements non-blocking send using a thread and buffer up to size of int
    16  type SendNb struct {
    17  	errCh chan error // may be nil: check using method HasChannel
    18  
    19  	sqLock    sync.Mutex
    20  	sendQueue []error       // inside lock
    21  	hasThread bool          // inside lock
    22  	waitCh    chan struct{} // before go statement
    23  
    24  	isShutdown atomic.Bool // atomic: check using method: IsShutdown
    25  }
    26  
    27  // NewSendNb returns a buffered Send object using errCh
    28  //   - errCh may be nil
    29  func NewSendNb(errCh chan error) (sc *SendNb) {
    30  	return &SendNb{errCh: errCh}
    31  }
    32  
    33  // Send sends an error on the error channel. Non-blocking. Thread-safe.
    34  //   - if err is nil, nothing is done
    35  //   - if SendNb was not initialized with non-zero channel, nothing is done
    36  func (sc *SendNb) Send(err error) {
    37  	if !sc.canSend(err) {
    38  		return // no-data not-initialized is-shutdown return
    39  	}
    40  
    41  	sc.sqLock.Lock()
    42  	defer sc.sqLock.Unlock()
    43  
    44  	if !sc.canSend(err) {
    45  		return // no-data not-initialized is-shutdown return
    46  	}
    47  
    48  	// put err in send queue
    49  	sc.sendQueue = append(sc.sendQueue, err)
    50  	if sc.hasThread {
    51  		return // thread is already reading from queue
    52  	}
    53  
    54  	// launch thread
    55  	sc.hasThread = true
    56  	sc.waitCh = make(chan struct{})
    57  	go sc.sendThread() // send err in new thread
    58  }
    59  
    60  func (sc *SendNb) HasChannel() (hasChannel bool) {
    61  	return sc.errCh != nil
    62  }
    63  
    64  func (sc *SendNb) IsShutdown() (isShutdown bool) {
    65  	return sc.isShutdown.Load()
    66  }
    67  
    68  // Shutdown closes the channel exactly once. Thread-safe
    69  func (sc *SendNb) Shutdown() {
    70  	if sc.isShutdown.Load() {
    71  		return // already shutdown
    72  	}
    73  
    74  	// ensure channel completes send
    75  	if sc.shutdown() {
    76  		select {
    77  		case <-sc.errCh: // release thread from send wait
    78  			<-sc.waitCh // wait for thread to exit
    79  		case <-sc.waitCh: // or wait until thread has exited
    80  		}
    81  	}
    82  
    83  	if sc.errCh != nil {
    84  		close(sc.errCh)
    85  	}
    86  }
    87  
    88  func (sc *SendNb) shutdown() (hasThread bool) {
    89  	sc.sqLock.Lock()
    90  	defer sc.sqLock.Unlock()
    91  
    92  	// block further Send invocation and thread sends
    93  	if sc.isShutdown.Swap(true) {
    94  		return // already shutdown
    95  	}
    96  
    97  	hasThread = sc.hasThread
    98  
    99  	// drop any send queue
   100  	sc.sendQueue = nil
   101  
   102  	return
   103  }
   104  
   105  func (sc *SendNb) canSend(err error) (canSend bool) {
   106  	return err != nil && // nothing to send
   107  		sc.errCh != nil && // not initialized to send errors
   108  		!sc.isShutdown.Load() // is shutdown
   109  }
   110  
   111  func (sc *SendNb) sendThread() {
   112  	defer sc.closeWaitCh()
   113  	defer RecoverThread("send on error channel", func(err error) {
   114  		fmt.Fprintln(os.Stderr, err)
   115  	})
   116  
   117  	var hasValue bool
   118  	var err error
   119  	for {
   120  		if hasValue, err = sc.getErrFromSendQueue(); !hasValue {
   121  			return // queue empty return: exit thread
   122  		}
   123  
   124  		sc.errCh <- err
   125  	}
   126  }
   127  
   128  func (sc *SendNb) getErrFromSendQueue() (hasValue bool, err error) {
   129  	sc.sqLock.Lock()
   130  	defer sc.sqLock.Unlock()
   131  
   132  	if hasValue = len(sc.sendQueue) > 0; !hasValue {
   133  		sc.hasThread = false
   134  		return
   135  	}
   136  
   137  	err = sc.sendQueue[0]
   138  	copy(sc.sendQueue, sc.sendQueue[1:])
   139  	lastIndex := len(sc.sendQueue) - 1
   140  	sc.sendQueue[lastIndex] = nil
   141  	sc.sendQueue = sc.sendQueue[:lastIndex]
   142  	return
   143  }
   144  
   145  func (sc *SendNb) closeWaitCh() {
   146  	sc.sqLock.Lock()
   147  	defer sc.sqLock.Unlock()
   148  
   149  	close(sc.waitCh)
   150  	sc.hasThread = false
   151  }