github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/state/coalescer.go (about)

     1  package state
     2  
     3  import (
     4  	"context"
     5  	"time"
     6  )
     7  
     8  // Coalescer performs coalesced signaling, combining multiple signals that occur
     9  // within a specified time window. A Coalescer is safe for concurrent usage. It
    10  // maintains a background Goroutine that must be terminated using Terminate.
    11  type Coalescer struct {
    12  	// strobes is used to transmit strobes to the run loop.
    13  	strobes chan struct{}
    14  	// signals is the channel on which signals are delivered.
    15  	signals chan struct{}
    16  	// cancel signals termination to the run loop.
    17  	cancel context.CancelFunc
    18  	// done is closed to indicate that the run loop has exited.
    19  	done chan struct{}
    20  }
    21  
    22  // NewCoalescer creates a new coalescer that will group signals that occur
    23  // within the specified time window of each other. If window is negative, it
    24  // will be treated as zero.
    25  func NewCoalescer(window time.Duration) *Coalescer {
    26  	// If the specified window is negative, then treat it as zero.
    27  	if window < 0 {
    28  		window = 0
    29  	}
    30  
    31  	// Create a cancellable context to regulate the run loop.
    32  	ctx, cancel := context.WithCancel(context.Background())
    33  
    34  	// Create the coalescer.
    35  	coalescer := &Coalescer{
    36  		strobes: make(chan struct{}),
    37  		signals: make(chan struct{}, 1),
    38  		cancel:  cancel,
    39  		done:    make(chan struct{}),
    40  	}
    41  
    42  	// Start the coalescer's run loop.
    43  	go coalescer.run(ctx, window)
    44  
    45  	// Done.
    46  	return coalescer
    47  }
    48  
    49  // run implements the signal processing run loop for Coalescer.
    50  func (c *Coalescer) run(ctx context.Context, window time.Duration) {
    51  	// Create the (initially stopped) coalescing timer.
    52  	timer := time.NewTimer(0)
    53  	if !timer.Stop() {
    54  		<-timer.C
    55  	}
    56  
    57  	// Loop and process events until cancelled.
    58  	for {
    59  		select {
    60  		case <-ctx.Done():
    61  			timer.Stop()
    62  			close(c.done)
    63  			return
    64  		case <-c.strobes:
    65  			timer.Stop()
    66  			select {
    67  			case <-timer.C:
    68  			default:
    69  			}
    70  			timer.Reset(window)
    71  		case <-timer.C:
    72  			select {
    73  			case c.signals <- struct{}{}:
    74  			default:
    75  			}
    76  		}
    77  	}
    78  }
    79  
    80  // Strobe enqueues a signal to be sent after the coalescing window. If a
    81  // subsequent call to Strobe is made within the coalescing window, then it will
    82  // reset the coalescing timer and an event will only be sent after Strobe hasn't
    83  // been called for the coalescing window period.
    84  func (c *Coalescer) Strobe() {
    85  	select {
    86  	case c.strobes <- struct{}{}:
    87  	case <-c.done:
    88  	}
    89  }
    90  
    91  // Signals returns the signal notification channel. This channel is buffered
    92  // with a capacity of 1, so no signaling will ever be lost if it's not actively
    93  // polled. The resulting channel is never closed.
    94  func (c *Coalescer) Signals() <-chan struct{} {
    95  	return c.signals
    96  }
    97  
    98  // Terminate shuts down the coalescer's internal run loop and waits for it to
    99  // terminate. It's safe to continue invoking other methods after invoking
   100  // Terminate (including Terminate, which is idempotent), though Strobe will have
   101  // no effect and only previously buffered events will be delivered on the
   102  // channel returned by Signals.
   103  func (c *Coalescer) Terminate() {
   104  	c.cancel()
   105  	<-c.done
   106  }