// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package stream import ( "context" "errors" "fmt" "sync/atomic" "time" "github.com/hashicorp/nomad/nomad/structs" ) // eventBuffer is a single-writer, multiple-reader, fixed length concurrent // buffer of events that have been published. The buffer is // the head and tail of an atomically updated single-linked list. Atomic // accesses are usually to be suspected as premature optimization but this // specific design has several important features that significantly simplify a // lot of our PubSub machinery. // // eventBuffer is an adaptation of conuls agent/stream/event eventBuffer but // has been updated to be a max length buffer to work for Nomad's usecase. // // The eventBuffer only tracks the most recent set of published events, // up to the max configured size, older events are dropped from the buffer // but will only be garbage collected once the slowest reader drops the item. // Consumers are notified of new events by closing a channel on the previous head // allowing efficient broadcast to many watchers without having to run multiple // goroutines or deliver to O(N) separate channels. // // Because eventBuffer is a linked list with atomically updated pointers, readers don't // have to take a lock and can consume at their own pace. Slow readers will eventually // be forced to reconnect to the lastest head by being notified via a bufferItem's droppedCh. // // A new buffer is constructed with a sentinel "empty" bufferItem that has a nil // Events array. This enables subscribers to start watching for the next update // immediately. // // The zero value eventBuffer is _not_ usable, as it has not been // initialized with an empty bufferItem so can not be used to wait for the first // published event. Call newEventBuffer to construct a new buffer. // // Calls to Append or purne that mutate the head must be externally // synchronized. This allows systems that already serialize writes to append // without lock overhead. type eventBuffer struct { size *int64 head atomic.Value tail atomic.Value maxSize int64 } // newEventBuffer creates an eventBuffer ready for use. func newEventBuffer(size int64) *eventBuffer { zero := int64(0) b := &eventBuffer{ maxSize: size, size: &zero, } item := newBufferItem(&structs.Events{Index: 0, Events: nil}) b.head.Store(item) b.tail.Store(item) return b } // Append a set of events from one raft operation to the buffer and notify // watchers. After calling append, the caller must not make any further // mutations to the events as they may have been exposed to subscribers in other // goroutines. Append only supports a single concurrent caller and must be // externally synchronized with other Append calls. func (b *eventBuffer) Append(events *structs.Events) { b.appendItem(newBufferItem(events)) } func (b *eventBuffer) appendItem(item *bufferItem) { // Store the next item to the old tail oldTail := b.Tail() oldTail.link.next.Store(item) // Update the tail to the new item b.tail.Store(item) // Increment the buffer size atomic.AddInt64(b.size, 1) // Advance Head until we are under allowable size for atomic.LoadInt64(b.size) > b.maxSize { b.advanceHead() } // notify waiters next event is available close(oldTail.link.nextCh) } func newSentinelItem() *bufferItem { return newBufferItem(&structs.Events{}) } // advanceHead drops the current Head buffer item and notifies readers // that the item should be discarded by closing droppedCh. // Slow readers will prevent the old head from being GC'd until they // discard it. func (b *eventBuffer) advanceHead() { old := b.Head() next := old.link.next.Load() // if the next item is nil replace it with a sentinel value if next == nil { next = newSentinelItem() } // notify readers that old is being dropped close(old.link.droppedCh) // store the next value to head b.head.Store(next) // If the old head is equal to the tail // update the tail value as well if old == b.Tail() { b.tail.Store(next) } // In the case of there being a sentinel item or advanceHead being called // on a sentinel item, only decrement if there are more than sentinel // values if atomic.LoadInt64(b.size) > 0 { // update the amount of events we have in the buffer atomic.AddInt64(b.size, -1) } } // Head returns the current head of the buffer. It will always exist but it may // be a "sentinel" empty item with a nil Events slice to allow consumers to // watch for the next update. Consumers should always check for empty Events and // treat them as no-ops. Will panic if eventBuffer was not initialized correctly // with NewEventBuffer func (b *eventBuffer) Head() *bufferItem { return b.head.Load().(*bufferItem) } // Tail returns the current tail of the buffer. It will always exist but it may // be a "sentinel" empty item with a Nil Events slice to allow consumers to // watch for the next update. Consumers should always check for empty Events and // treat them as no-ops. Will panic if eventBuffer was not initialized correctly // with NewEventBuffer func (b *eventBuffer) Tail() *bufferItem { return b.tail.Load().(*bufferItem) } // StarStartAtClosest returns the closest bufferItem to a requested starting // index as well as the offset between the requested index and returned one. func (b *eventBuffer) StartAtClosest(index uint64) (*bufferItem, int) { item := b.Head() if index < item.Events.Index { return item, int(item.Events.Index) - int(index) } if item.Events.Index == index { return item, 0 } for { prev := item item = item.NextNoBlock() if item == nil { return prev, int(index) - int(prev.Events.Index) } if index < item.Events.Index { return item, int(item.Events.Index) - int(index) } if index == item.Events.Index { return item, 0 } } } // Len returns the current length of the buffer func (b *eventBuffer) Len() int { return int(atomic.LoadInt64(b.size)) } // bufferItem represents a set of events published by a single raft operation. // The first item returned by a newly constructed buffer will have nil Events. // It is a sentinel value which is used to wait on the next events via Next. // // To iterate to the next event, a Next method may be called which may block if // there is no next element yet. // // Holding a pointer to the item keeps all the events published since in memory // so it's important that subscribers don't hold pointers to buffer items after // they have been delivered except where it's intentional to maintain a cache or // trailing store of events for performance reasons. // // Subscribers must not mutate the bufferItem or the Events or Encoded payloads // inside as these are shared between all readers. type bufferItem struct { // Events is the set of events published at one raft index. This may be nil as // a sentinel value to allow watching for the first event in a buffer. Callers // should check and skip nil Events at any point in the buffer. It will also // be nil if the producer appends an Error event because they can't complete // the request to populate the buffer. Err will be non-nil in this case. Events *structs.Events // Err is non-nil if the producer can't complete their task and terminates the // buffer. Subscribers should return the error to clients and cease attempting // to read from the buffer. Err error // link holds the next pointer and channel. This extra bit of indirection // allows us to splice buffers together at arbitrary points without including // events in one buffer just for the side-effect of watching for the next set. // The link may not be mutated once the event is appended to a buffer. link *bufferLink createdAt time.Time } type bufferLink struct { // next is an atomically updated pointer to the next event in the buffer. It // is written exactly once by the single published and will always be set if // ch is closed. next atomic.Value // nextCh is closed when the next event is published. It should never be mutated // (e.g. set to nil) as that is racey, but is closed once when the next event // is published. the next pointer will have been set by the time this is // closed. nextCh chan struct{} // droppedCh is closed when the event is dropped from the buffer due to // sizing constraints. droppedCh chan struct{} } // newBufferItem returns a blank buffer item with a link and chan ready to have // the fields set and be appended to a buffer. func newBufferItem(events *structs.Events) *bufferItem { return &bufferItem{ link: &bufferLink{ nextCh: make(chan struct{}), droppedCh: make(chan struct{}), }, Events: events, createdAt: time.Now(), } } // Next return the next buffer item in the buffer. It may block until ctx is // cancelled or until the next item is published. func (i *bufferItem) Next(ctx context.Context, forceClose <-chan struct{}) (*bufferItem, error) { // See if there is already a next value, block if so. Note we don't rely on // state change (chan nil) as that's not threadsafe but detecting close is. select { case <-ctx.Done(): return nil, ctx.Err() case <-forceClose: return nil, fmt.Errorf("subscription closed") case <-i.link.nextCh: } // Check if the reader is too slow and the event buffer as discarded the event // This must happen after the above select to prevent a random selection // between linkCh and droppedCh select { case <-i.link.droppedCh: return nil, fmt.Errorf("event dropped from buffer") default: } // If channel closed, there must be a next item to read nextRaw := i.link.next.Load() if nextRaw == nil { // shouldn't be possible return nil, errors.New("invalid next item") } next := nextRaw.(*bufferItem) if next.Err != nil { return nil, next.Err } return next, nil } // NextNoBlock returns the next item in the buffer without blocking. If it // reaches the most recent item it will return nil. func (i *bufferItem) NextNoBlock() *bufferItem { nextRaw := i.link.next.Load() if nextRaw == nil { return nil } return nextRaw.(*bufferItem) }