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 }