github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/nomad/stream/event_buffer.go (about) 1 package stream 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync/atomic" 8 "time" 9 10 "github.com/hashicorp/nomad/nomad/structs" 11 ) 12 13 // eventBuffer is a single-writer, multiple-reader, fixed length concurrent 14 // buffer of events that have been published. The buffer is 15 // the head and tail of an atomically updated single-linked list. Atomic 16 // accesses are usually to be suspected as premature optimization but this 17 // specific design has several important features that significantly simplify a 18 // lot of our PubSub machinery. 19 // 20 // eventBuffer is an adaptation of conuls agent/stream/event eventBuffer but 21 // has been updated to be a max length buffer to work for Nomad's usecase. 22 // 23 // The eventBuffer only tracks the most recent set of published events, 24 // up to the max configured size, older events are dropped from the buffer 25 // but will only be garbage collected once the slowest reader drops the item. 26 // Consumers are notified of new events by closing a channel on the previous head 27 // allowing efficient broadcast to many watchers without having to run multiple 28 // goroutines or deliver to O(N) separate channels. 29 // 30 // Because eventBuffer is a linked list with atomically updated pointers, readers don't 31 // have to take a lock and can consume at their own pace. Slow readers will eventually 32 // be forced to reconnect to the lastest head by being notified via a bufferItem's droppedCh. 33 // 34 // A new buffer is constructed with a sentinel "empty" bufferItem that has a nil 35 // Events array. This enables subscribers to start watching for the next update 36 // immediately. 37 // 38 // The zero value eventBuffer is _not_ usable, as it has not been 39 // initialized with an empty bufferItem so can not be used to wait for the first 40 // published event. Call newEventBuffer to construct a new buffer. 41 // 42 // Calls to Append or purne that mutate the head must be externally 43 // synchronized. This allows systems that already serialize writes to append 44 // without lock overhead. 45 type eventBuffer struct { 46 size *int64 47 48 head atomic.Value 49 tail atomic.Value 50 51 maxSize int64 52 } 53 54 // newEventBuffer creates an eventBuffer ready for use. 55 func newEventBuffer(size int64) *eventBuffer { 56 zero := int64(0) 57 b := &eventBuffer{ 58 maxSize: size, 59 size: &zero, 60 } 61 62 item := newBufferItem(&structs.Events{Index: 0, Events: nil}) 63 64 b.head.Store(item) 65 b.tail.Store(item) 66 67 return b 68 } 69 70 // Append a set of events from one raft operation to the buffer and notify 71 // watchers. After calling append, the caller must not make any further 72 // mutations to the events as they may have been exposed to subscribers in other 73 // goroutines. Append only supports a single concurrent caller and must be 74 // externally synchronized with other Append calls. 75 func (b *eventBuffer) Append(events *structs.Events) { 76 b.appendItem(newBufferItem(events)) 77 } 78 79 func (b *eventBuffer) appendItem(item *bufferItem) { 80 // Store the next item to the old tail 81 oldTail := b.Tail() 82 oldTail.link.next.Store(item) 83 84 // Update the tail to the new item 85 b.tail.Store(item) 86 87 // Increment the buffer size 88 atomic.AddInt64(b.size, 1) 89 90 // Advance Head until we are under allowable size 91 for atomic.LoadInt64(b.size) > b.maxSize { 92 b.advanceHead() 93 } 94 95 // notify waiters next event is available 96 close(oldTail.link.nextCh) 97 } 98 99 func newSentinelItem() *bufferItem { 100 return newBufferItem(&structs.Events{}) 101 } 102 103 // advanceHead drops the current Head buffer item and notifies readers 104 // that the item should be discarded by closing droppedCh. 105 // Slow readers will prevent the old head from being GC'd until they 106 // discard it. 107 func (b *eventBuffer) advanceHead() { 108 old := b.Head() 109 110 next := old.link.next.Load() 111 // if the next item is nil replace it with a sentinel value 112 if next == nil { 113 next = newSentinelItem() 114 } 115 116 // notify readers that old is being dropped 117 close(old.link.droppedCh) 118 119 // store the next value to head 120 b.head.Store(next) 121 122 // If the old head is equal to the tail 123 // update the tail value as well 124 if old == b.Tail() { 125 b.tail.Store(next) 126 } 127 128 // In the case of there being a sentinel item or advanceHead being called 129 // on a sentinel item, only decrement if there are more than sentinel 130 // values 131 if atomic.LoadInt64(b.size) > 0 { 132 // update the amount of events we have in the buffer 133 atomic.AddInt64(b.size, -1) 134 } 135 } 136 137 // Head returns the current head of the buffer. It will always exist but it may 138 // be a "sentinel" empty item with a nil Events slice to allow consumers to 139 // watch for the next update. Consumers should always check for empty Events and 140 // treat them as no-ops. Will panic if eventBuffer was not initialized correctly 141 // with NewEventBuffer 142 func (b *eventBuffer) Head() *bufferItem { 143 return b.head.Load().(*bufferItem) 144 } 145 146 // Tail returns the current tail of the buffer. It will always exist but it may 147 // be a "sentinel" empty item with a Nil Events slice to allow consumers to 148 // watch for the next update. Consumers should always check for empty Events and 149 // treat them as no-ops. Will panic if eventBuffer was not initialized correctly 150 // with NewEventBuffer 151 func (b *eventBuffer) Tail() *bufferItem { 152 return b.tail.Load().(*bufferItem) 153 } 154 155 // StarStartAtClosest returns the closest bufferItem to a requested starting 156 // index as well as the offset between the requested index and returned one. 157 func (b *eventBuffer) StartAtClosest(index uint64) (*bufferItem, int) { 158 item := b.Head() 159 if index < item.Events.Index { 160 return item, int(item.Events.Index) - int(index) 161 } 162 if item.Events.Index == index { 163 return item, 0 164 } 165 166 for { 167 prev := item 168 item = item.NextNoBlock() 169 if item == nil { 170 return prev, int(index) - int(prev.Events.Index) 171 } 172 if index < item.Events.Index { 173 return item, int(item.Events.Index) - int(index) 174 } 175 if index == item.Events.Index { 176 return item, 0 177 } 178 } 179 } 180 181 // Len returns the current length of the buffer 182 func (b *eventBuffer) Len() int { 183 return int(atomic.LoadInt64(b.size)) 184 } 185 186 // bufferItem represents a set of events published by a single raft operation. 187 // The first item returned by a newly constructed buffer will have nil Events. 188 // It is a sentinel value which is used to wait on the next events via Next. 189 // 190 // To iterate to the next event, a Next method may be called which may block if 191 // there is no next element yet. 192 // 193 // Holding a pointer to the item keeps all the events published since in memory 194 // so it's important that subscribers don't hold pointers to buffer items after 195 // they have been delivered except where it's intentional to maintain a cache or 196 // trailing store of events for performance reasons. 197 // 198 // Subscribers must not mutate the bufferItem or the Events or Encoded payloads 199 // inside as these are shared between all readers. 200 type bufferItem struct { 201 // Events is the set of events published at one raft index. This may be nil as 202 // a sentinel value to allow watching for the first event in a buffer. Callers 203 // should check and skip nil Events at any point in the buffer. It will also 204 // be nil if the producer appends an Error event because they can't complete 205 // the request to populate the buffer. Err will be non-nil in this case. 206 Events *structs.Events 207 208 // Err is non-nil if the producer can't complete their task and terminates the 209 // buffer. Subscribers should return the error to clients and cease attempting 210 // to read from the buffer. 211 Err error 212 213 // link holds the next pointer and channel. This extra bit of indirection 214 // allows us to splice buffers together at arbitrary points without including 215 // events in one buffer just for the side-effect of watching for the next set. 216 // The link may not be mutated once the event is appended to a buffer. 217 link *bufferLink 218 219 createdAt time.Time 220 } 221 222 type bufferLink struct { 223 // next is an atomically updated pointer to the next event in the buffer. It 224 // is written exactly once by the single published and will always be set if 225 // ch is closed. 226 next atomic.Value 227 228 // nextCh is closed when the next event is published. It should never be mutated 229 // (e.g. set to nil) as that is racey, but is closed once when the next event 230 // is published. the next pointer will have been set by the time this is 231 // closed. 232 nextCh chan struct{} 233 234 // droppedCh is closed when the event is dropped from the buffer due to 235 // sizing constraints. 236 droppedCh chan struct{} 237 } 238 239 // newBufferItem returns a blank buffer item with a link and chan ready to have 240 // the fields set and be appended to a buffer. 241 func newBufferItem(events *structs.Events) *bufferItem { 242 return &bufferItem{ 243 link: &bufferLink{ 244 nextCh: make(chan struct{}), 245 droppedCh: make(chan struct{}), 246 }, 247 Events: events, 248 createdAt: time.Now(), 249 } 250 } 251 252 // Next return the next buffer item in the buffer. It may block until ctx is 253 // cancelled or until the next item is published. 254 func (i *bufferItem) Next(ctx context.Context, forceClose <-chan struct{}) (*bufferItem, error) { 255 // See if there is already a next value, block if so. Note we don't rely on 256 // state change (chan nil) as that's not threadsafe but detecting close is. 257 select { 258 case <-ctx.Done(): 259 return nil, ctx.Err() 260 case <-forceClose: 261 return nil, fmt.Errorf("subscription closed") 262 case <-i.link.nextCh: 263 } 264 265 // Check if the reader is too slow and the event buffer as discarded the event 266 // This must happen after the above select to prevent a random selection 267 // between linkCh and droppedCh 268 select { 269 case <-i.link.droppedCh: 270 return nil, fmt.Errorf("event dropped from buffer") 271 default: 272 } 273 274 // If channel closed, there must be a next item to read 275 nextRaw := i.link.next.Load() 276 if nextRaw == nil { 277 // shouldn't be possible 278 return nil, errors.New("invalid next item") 279 } 280 next := nextRaw.(*bufferItem) 281 if next.Err != nil { 282 return nil, next.Err 283 } 284 return next, nil 285 } 286 287 // NextNoBlock returns the next item in the buffer without blocking. If it 288 // reaches the most recent item it will return nil. 289 func (i *bufferItem) NextNoBlock() *bufferItem { 290 nextRaw := i.link.next.Load() 291 if nextRaw == nil { 292 return nil 293 } 294 return nextRaw.(*bufferItem) 295 }