// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package pipeline import ( "sync" "time" "github.com/elastic/beats/libbeat/beat" "github.com/elastic/beats/libbeat/common/atomic" ) // acker is used to account for published and non-published events to be ACKed // to the beats client. // All pipeline and client ACK handling support is provided by acker instances. type acker interface { close() addEvent(event beat.Event, published bool) bool ackEvents(int) } // emptyACK ignores any ACK signals and events. type emptyACK struct{} var nilACKer acker = (*emptyACK)(nil) func (*emptyACK) close() {} func (*emptyACK) addEvent(_ beat.Event, _ bool) bool { return true } func (*emptyACK) ackEvents(_ int) {} type ackerFn struct { Close func() AddEvent func(beat.Event, bool) bool AckEvents func(int) } func (a *ackerFn) close() { a.Close() } func (a *ackerFn) addEvent(e beat.Event, b bool) bool { return a.AddEvent(e, b) } func (a *ackerFn) ackEvents(n int) { a.AckEvents(n) } // countACK is used when broker ACK events can be simply forwarded to the // producers ACKCount callback. // The countACK is only applicable if no processors are configured. // ACKs for closed clients will be ignored. type countACK struct { pipeline *Pipeline fn func(total, acked int) } func newCountACK(fn func(total, acked int)) *countACK { a := &countACK{fn: fn} return a } func (a *countACK) close() {} func (a *countACK) addEvent(_ beat.Event, _ bool) bool { return true } func (a *countACK) ackEvents(n int) { if a.pipeline.ackActive.Load() { a.fn(n, n) } } // gapCountACK returns event ACKs to the producer, taking account for dropped events. // Events being dropped by processors will always be ACKed with the last batch ACKed // by the broker. This way clients waiting for ACKs can expect all processed // events being always ACKed. type gapCountACK struct { pipeline *Pipeline fn func(total int, acked int) done chan struct{} drop chan struct{} acks chan int events atomic.Uint32 lst gapList } type gapList struct { sync.Mutex head, tail *gapInfo } type gapInfo struct { sync.Mutex next *gapInfo send, dropped int } func newGapCountACK(pipeline *Pipeline, fn func(total, acked int)) *gapCountACK { a := &gapCountACK{} a.init(pipeline, fn) return a } func (a *gapCountACK) init(pipeline *Pipeline, fn func(int, int)) { *a = gapCountACK{ pipeline: pipeline, fn: fn, done: make(chan struct{}), drop: make(chan struct{}), acks: make(chan int, 1), } init := &gapInfo{} a.lst.head = init a.lst.tail = init go a.ackLoop() } func (a *gapCountACK) ackLoop() { // close channels, as no more events should be ACKed: // - once pipeline is closed // - all events of the closed client have been acked/processed by the pipeline acks, drop := a.acks, a.drop closing := false for { select { case <-a.done: closing = true a.done = nil case <-a.pipeline.ackDone: return case n := <-acks: empty := a.handleACK(n) if empty && closing && a.events.Load() == 0 { // stop worker, iff all events accounted for have been ACKed return } case <-drop: // TODO: accumulate multiple drop events + flush count with timer a.fn(1, 0) } } } func (a *gapCountACK) handleACK(n int) bool { // collect items and compute total count from gapList var ( total = 0 acked = n emptyLst bool ) for n > 0 { if emptyLst { panic("too many events acked") } a.lst.Lock() current := a.lst.head current.Lock() if n >= current.send { nxt := current.next emptyLst = nxt == nil if !emptyLst { // advance list all event in current entry have been send and list as // more then 1 gapInfo entry. If only 1 entry is present, list item will be // reset and reused a.lst.head = nxt } } // hand over lock list-entry, so ACK handler and producer can operate // on potentially different list ends a.lst.Unlock() if n < current.send { current.send -= n total += n n = 0 } else { total += current.send + current.dropped n -= current.send current.dropped = 0 current.send = 0 } current.Unlock() } a.events.Sub(uint32(total)) a.fn(total, acked) return emptyLst } func (a *gapCountACK) close() { // close client only, pipeline itself still can handle pending ACKs close(a.done) } func (a *gapCountACK) addEvent(_ beat.Event, published bool) bool { // if gapList is empty and event is being dropped, forward drop event to ack // loop worker: a.events.Inc() if !published { a.addDropEvent() } else { a.addPublishedEvent() } return true } func (a *gapCountACK) addDropEvent() { a.lst.Lock() current := a.lst.tail current.Lock() if current.send == 0 && current.next == nil { // send can only be 0 if no no events/gaps present yet if a.lst.head != a.lst.tail { panic("gap list expected to be empty") } current.Unlock() a.lst.Unlock() a.drop <- struct{}{} } else { a.lst.Unlock() current.dropped++ current.Unlock() } } func (a *gapCountACK) addPublishedEvent() { // event is publisher -> add a new gap list entry if gap is present in current // gapInfo a.lst.Lock() current := a.lst.tail current.Lock() if current.dropped > 0 { tmp := &gapInfo{} a.lst.tail.next = tmp a.lst.tail = tmp current.Unlock() tmp.Lock() current = tmp } a.lst.Unlock() current.send++ current.Unlock() } func (a *gapCountACK) ackEvents(n int) { select { case <-a.pipeline.ackDone: // pipeline is closing down -> ignore event a.acks = nil case a.acks <- n: // send ack event to worker } } // boundGapCountACK guards a gapCountACK instance by bounding the maximum number of // active events. // As beats might accumulate state while waiting for ACK, the boundGapCountACK blocks // if too many events have been filtered out by processors. type boundGapCountACK struct { active bool fn func(total, acked int) acker gapCountACK sema *sema } func newBoundGapCountACK( pipeline *Pipeline, sema *sema, fn func(total, acked int), ) *boundGapCountACK { a := &boundGapCountACK{active: true, sema: sema, fn: fn} a.acker.init(pipeline, a.onACK) return a } func (a *boundGapCountACK) close() { a.acker.close() } func (a *boundGapCountACK) addEvent(event beat.Event, published bool) bool { a.sema.inc() return a.acker.addEvent(event, published) } func (a *boundGapCountACK) ackEvents(n int) { a.acker.ackEvents(n) } func (a *boundGapCountACK) onACK(total, acked int) { a.sema.release(total) a.fn(total, acked) } // eventDataACK reports all dropped and ACKed events private fields. // An instance of eventDataACK requires a counting ACKer (boundGapCountACK or countACK), // for accounting for potentially dropped events. type eventDataACK struct { mutex sync.Mutex acker acker pipeline *Pipeline // TODO: replace with more efficient dynamic sized ring-buffer? data []interface{} fn func(data []interface{}, acked int) } func newEventACK( pipeline *Pipeline, canDrop bool, sema *sema, fn func([]interface{}, int), ) *eventDataACK { a := &eventDataACK{pipeline: pipeline, fn: fn} a.acker = makeCountACK(pipeline, canDrop, sema, a.onACK) return a } func makeCountACK(pipeline *Pipeline, canDrop bool, sema *sema, fn func(int, int)) acker { if canDrop { return newBoundGapCountACK(pipeline, sema, fn) } return newCountACK(fn) } func (a *eventDataACK) close() { a.acker.close() } func (a *eventDataACK) addEvent(event beat.Event, published bool) bool { a.mutex.Lock() active := a.pipeline.ackActive.Load() if active { a.data = append(a.data, event.Private) } a.mutex.Unlock() if active { return a.acker.addEvent(event, published) } return false } func (a *eventDataACK) ackEvents(n int) { a.acker.ackEvents(n) } func (a *eventDataACK) onACK(total, acked int) { n := total a.mutex.Lock() data := a.data[:n] a.data = a.data[n:] a.mutex.Unlock() if len(data) > 0 && a.pipeline.ackActive.Load() { a.fn(data, acked) } } // waitACK keeps track of events being produced and ACKs for events. // On close waitACK will wait for pending events to be ACKed by the broker. // The acker continues the closing operation if all events have been published // or the maximum configured sleep time has been reached. type waitACK struct { acker acker signal chan struct{} waitClose time.Duration active atomic.Bool // number of active events events atomic.Uint64 } func newWaitACK(acker acker, timeout time.Duration) *waitACK { return &waitACK{ acker: acker, signal: make(chan struct{}, 1), waitClose: timeout, active: atomic.MakeBool(true), } } func (a *waitACK) close() { // TODO: wait for events a.active.Store(false) if a.events.Load() > 0 { select { case <-a.signal: case <-time.After(a.waitClose): } } // close the underlying acker upon exit a.acker.close() } func (a *waitACK) addEvent(event beat.Event, published bool) bool { if published { a.events.Inc() } return a.acker.addEvent(event, published) } func (a *waitACK) ackEvents(n int) { // return ACK signal to upper layers a.acker.ackEvents(n) a.releaseEvents(n) } func (a *waitACK) releaseEvents(n int) { value := a.events.Sub(uint64(n)) if value != 0 { return } // send done signal, if close is waiting if !a.active.Load() { a.signal <- struct{}{} } }