-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add asynchronous ACK handling to S3 and SQS inputs #40699
Changes from 25 commits
e9ccebe
b17f7b5
d023027
aedb5a2
ed4953a
12a39b4
7e70ea6
ff4f57c
2d18b28
6bc5e42
b4cc30e
d4ecc1f
64a96d4
7a20250
977e4c5
5282671
1548ac2
2358241
8c447bc
2deefa7
8cde326
c2131e1
1a07561
c99b94d
da1c5ed
70821fa
5977072
6fe1183
ba6ecfe
f1732f8
83ba548
577759c
7ba1374
cf90022
a743c08
48025b5
10ecfd3
7a38f55
84cb422
b088ca7
bbbd1f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// 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 fifo | ||
|
||
// FIFO is a minimal first-in first-out queue based on a singly linked list. | ||
type FIFO[T any] struct { | ||
first *node[T] | ||
last *node[T] | ||
} | ||
|
||
type node[T any] struct { | ||
next *node[T] | ||
value T | ||
} | ||
|
||
func (f *FIFO[T]) Add(value T) { | ||
newNode := &node[T]{value: value} | ||
if f.first == nil { | ||
f.first = newNode | ||
} else { | ||
f.last.next = newNode | ||
} | ||
f.last = newNode | ||
} | ||
|
||
func (f *FIFO[T]) Empty() bool { | ||
return f.first == nil | ||
} | ||
|
||
// Return the first value (if present) without removing it from the queue. | ||
// If the queue is empty a default value is returned, to detect this case | ||
// use f.Empty(). | ||
func (f *FIFO[T]) First() T { | ||
if f.first == nil { | ||
var none T | ||
return none | ||
} | ||
return f.first.value | ||
} | ||
|
||
// Return the first value (if present) and remove it from the queue. | ||
// If the queue is empty a default value is returned, to detect this case | ||
// use f.Empty(). | ||
func (f *FIFO[T]) ConsumeFirst() T { | ||
result := f.First() | ||
f.Remove() | ||
return result | ||
} | ||
|
||
// Remove the first entry in the queue. Does nothing if the FIFO is empty. | ||
func (f *FIFO[T]) Remove() { | ||
if f.first != nil { | ||
f.first = f.first.next | ||
if f.first == nil { | ||
f.last = nil | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
// or more contributor license agreements. Licensed under the Elastic License; | ||
// you may not use this file except in compliance with the Elastic License. | ||
|
||
package awss3 | ||
|
||
import ( | ||
"github.com/elastic/beats/v7/libbeat/beat" | ||
"github.com/elastic/beats/v7/libbeat/common/acker" | ||
"github.com/elastic/beats/v7/libbeat/common/fifo" | ||
) | ||
|
||
type awsACKHandler struct { | ||
pending fifo.FIFO[pendingACK] | ||
ackedCount int | ||
|
||
pendingChan chan pendingACK | ||
ackChan chan int | ||
} | ||
|
||
type pendingACK struct { | ||
eventCount int | ||
ackCallback func() | ||
} | ||
|
||
func newAWSACKHandler() *awsACKHandler { | ||
handler := &awsACKHandler{ | ||
pendingChan: make(chan pendingACK, 10), | ||
ackChan: make(chan int, 10), | ||
Comment on lines
+46
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At a minimum I think we need a comment on why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, documenting why we picked up 10, and if they need to stay in sync would be useful. Do we ever need to make this configurable? |
||
} | ||
go handler.run() | ||
return handler | ||
} | ||
|
||
func (ah *awsACKHandler) Add(eventCount int, ackCallback func()) { | ||
ah.pendingChan <- pendingACK{ | ||
eventCount: eventCount, | ||
ackCallback: ackCallback, | ||
} | ||
} | ||
|
||
// Called when a worker is closing, to indicate to the ack handler that it | ||
// should shut down as soon as the current pending list is acknowledged. | ||
func (ah *awsACKHandler) Close() { | ||
close(ah.pendingChan) | ||
} | ||
|
||
func (ah *awsACKHandler) pipelineEventListener() beat.EventListener { | ||
return acker.TrackingCounter(func(_ int, total int) { | ||
// Notify the ack handler goroutine | ||
ah.ackChan <- total | ||
}) | ||
} | ||
|
||
// Listener that handles both incoming metadata and ACK | ||
// confirmations. | ||
func (ah *awsACKHandler) run() { | ||
for { | ||
select { | ||
case result, ok := <-ah.pendingChan: | ||
if ok { | ||
ah.pending.Add(result) | ||
} else { | ||
// Channel is closed, reset so we don't receive any more values | ||
ah.pendingChan = nil | ||
} | ||
case count := <-ah.ackChan: | ||
ah.ackedCount += count | ||
} | ||
|
||
// Finalize any objects that are now completed | ||
for !ah.pending.Empty() && ah.ackedCount >= ah.pending.First().eventCount { | ||
result := ah.pending.ConsumeFirst() | ||
ah.ackedCount -= result.eventCount | ||
// Run finalization asynchronously so we don't block the SQS worker | ||
// or the queue by ignoring the ack handler's input channels. Ordering | ||
// is no longer important at this point. | ||
go result.ackCallback() | ||
cmacknz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
// If the input is closed and all acks are completed, we're done | ||
if ah.pending.Empty() && ah.pendingChan == nil { | ||
return | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a test file as well? Example test would suffice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to use something like https://github.com/zyedidia/generic/tree/v1.2.1/queue ?