go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/sync/dispatcher/channel.go (about)

     1  // Copyright 2019 The LUCI Authors.
     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 dispatcher
    16  
    17  import (
    18  	"context"
    19  
    20  	"go.chromium.org/luci/common/clock"
    21  	"go.chromium.org/luci/common/errors"
    22  	"go.chromium.org/luci/common/sync/dispatcher/buffer"
    23  )
    24  
    25  // Channel holds a chan which you can push individual work items to.
    26  type Channel struct {
    27  	// C is an unbuffered channel which you can push single work items into.
    28  	//
    29  	// Close this to shut down the Channel.
    30  	C chan<- any
    31  
    32  	// DrainC will unblock when this Channel is closed/canceled and fully drained.
    33  	DrainC <-chan struct{}
    34  }
    35  
    36  // Close is a convenience function which closes C (and swallows panic if
    37  // already closed).
    38  func (c Channel) Close() {
    39  	defer func() { recover() }()
    40  	close(c.C)
    41  }
    42  
    43  // CloseAndDrain is a convenience function which closes C (and swallows panic if
    44  // already closed) and then blocks on DrainC/ctx.Done().
    45  func (c Channel) CloseAndDrain(ctx context.Context) {
    46  	c.Close()
    47  	select {
    48  	case <-ctx.Done():
    49  	case <-c.DrainC:
    50  	}
    51  }
    52  
    53  // IsDrained returns true iff the Channel is closed and drained.
    54  func (c Channel) IsDrained() bool {
    55  	select {
    56  	case <-c.DrainC:
    57  		return true
    58  	default:
    59  		return false
    60  	}
    61  }
    62  
    63  // SendFn is the function which does the work to actually transmit the Batch to
    64  // the next stage of your processing pipeline (e.g. do an RPC to a remote
    65  // service).
    66  //
    67  // The function may manipulate the Batch however it wants (see Batch).
    68  //
    69  // In particular, shrinking the size of Batch.Data for confirmed-sent items
    70  // will allow the dispatcher to reduce its buffer count when SendFn returns,
    71  // even if SendFn returns an error. Removing items from the Batch will not
    72  // cause the remaining items to be coalesced into a different Batch.
    73  //
    74  // The SendFn MUST be bound to this Channel's Context; if the Channel's Context
    75  // is Cancel'd, SendFn MUST terminate, or the Channel's DrainC will be blocked.
    76  // We don't pass it as part of SendFn's signature in case SendFn needs to be
    77  // bound to a derived Context.
    78  //
    79  // Non-nil errors returned by this function will be handled by ErrorFn.
    80  type SendFn func(data *buffer.Batch) error
    81  
    82  // NewChannel produces a new Channel ready to listen and send work items.
    83  //
    84  // Args:
    85  //   - `ctx` will be used for cancellation and logging. When the `ctx` is
    86  //     canceled, the Channel will:
    87  //   - drop all incoming data on Channel.C; All new data will be dropped
    88  //     (calling DropFn).
    89  //   - drop all existing unleased batches (calling DropFn)
    90  //   - ignore all errors from SendFn (i.e. even if ErrorFn returns
    91  //     'retry=true', the batch will be dropped anyway)
    92  //     If you want to gracefully drain the Channel, you must close the channel
    93  //     and wait for DrainC before canceling the context.
    94  //   - `send` is required, and defines the function to use to process Batches
    95  //     of data. This function MUST respect `ctx.Done`, or the Channel cannot
    96  //     drain properly.
    97  //   - `opts` is optional (see Options for the defaults).
    98  //
    99  // The Channel MUST be Close()'d when you're done with it, or the Channel will
   100  // not terminate. This applies even if you cancel it via ctx. The caller is
   101  // responsible for this (as opposed to having Channel implement this internally)
   102  // because there is no generally-safe way in Go to close a channel without
   103  // coordinating that event with all senders on that channel. Because the caller
   104  // of NewChannel is effectively the sender (owner) of Channel.C, they must
   105  // coordinate closure of this channel with all their use of sends to this
   106  // channel.
   107  func NewChannel(ctx context.Context, opts *Options, send SendFn) (Channel, error) {
   108  	if send == nil {
   109  		return Channel{}, errors.New("send is required: got nil")
   110  	}
   111  
   112  	var optsCopy Options
   113  	if opts != nil {
   114  		optsCopy = *opts
   115  	}
   116  
   117  	buf, err := buffer.NewBuffer(&optsCopy.Buffer)
   118  	if err != nil {
   119  		return Channel{}, errors.Annotate(err, "allocating Buffer").Err()
   120  	}
   121  
   122  	if err = optsCopy.normalize(ctx); err != nil {
   123  		return Channel{}, errors.Annotate(err, "normalizing dispatcher.Options").Err()
   124  	}
   125  
   126  	itemCh := make(chan any)
   127  	drainCh := make(chan struct{})
   128  
   129  	cstate := coordinatorState{
   130  		opts:    optsCopy,
   131  		buf:     buf,
   132  		itemCh:  itemCh,
   133  		drainCh: drainCh,
   134  
   135  		resultCh: make(chan workerResult),
   136  
   137  		timer: clock.NewTimer(clock.Tag(ctx, "coordinator")),
   138  	}
   139  
   140  	go cstate.run(ctx, send)
   141  	return Channel{C: itemCh, DrainC: drainCh}, nil
   142  }