Skip to content

Commit 46dcea5

Browse files
committed
feat: Implement max pending queue size to limit spooler size and remove receive count restriction on streams
1 parent 1cc837a commit 46dcea5

21 files changed

Lines changed: 213 additions & 62 deletions

File tree

docs/log-carver/Configuration.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
- [`enabled` (receiver)](#enabled-receiver)
6262
- [`listen`](#listen)
6363
- [`max pending payloads` (receiver)](#max-pending-payloads-receiver)
64+
- [`max queue size` (receiver)](#max-queue-size-receiver)
6465
- [`max tls version` (receiver)](#max-tls-version-receiver)
6566
- [`min tls version` (receiver)](#min-tls-version-receiver)
6667
- [`name` (receiver)](#name-receiver)
@@ -728,7 +729,10 @@ required)
728729
### `max pending payloads` (receiver)
729730
730731
Number. Optional. Default: 10
731-
Since **alpha** (not yet released)
732+
Since 2.7.0
733+
734+
Only applicable to protocol-based transports such as "tls" and "tcp" that
735+
support acknowledgements.
732736
733737
The maximum number of spools that can be in process from a connection at any
734738
one time. Each spool will be kept in memory until it is fully processed and
@@ -742,6 +746,27 @@ to retry.
742746
*You should only change this value if you changed the equivilant value on a
743747
Log Courier client.*
744748
749+
### `max queue size` (receiver)
750+
751+
Number. Optional. Default: 134217728 (128 MiB)
752+
Since 2.13.0
753+
754+
Maximum number of bytes that can be received and queued from clients at any
755+
one moment in time.
756+
757+
If too many events are being received than can be processed then this queue
758+
can build in size. When this queue is full, when data is received from a
759+
connection that cannot be added to the queue, the data is discarded and the
760+
connection closed.
761+
762+
Warnings will be logged when this happened no more frequently than 1 per
763+
minute to note that events are discarded.
764+
765+
For protocol-based transports that support acknowledgement, no data loss
766+
occurs as the client will know to resubmit the data again on a retried
767+
connection attempt which in the Log Courier case will backoff longer on
768+
each connection attempt to allow Log Carver to catchup.
769+
745770
### `max tls version` (receiver)
746771
747772
String. Optional. Default: ""

lc-lib/admin/api/data.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,41 @@ func (n Number) HumanReadable(string) ([]byte, error) {
4444
return []byte(strconv.FormatInt(int64(n), 10)), nil
4545
}
4646

47+
// Number represents an integer number in the API
48+
type Bytes int64
49+
50+
// HumanReadable returns the Bytes as a string with a human readable suffix such as KB, MB, GB, TB
51+
func (n Bytes) HumanReadable(string) ([]byte, error) {
52+
var suffix string
53+
var size float64
54+
55+
switch {
56+
case n < 1024:
57+
suffix = " B"
58+
size = float64(n)
59+
case n < 1024*1024:
60+
suffix = " KiB"
61+
size = float64(n) / 1024
62+
case n < 1024*1024*1024:
63+
suffix = " MiB"
64+
size = float64(n) / 1024 / 1024
65+
case n < 1024*1024*1024*1024:
66+
suffix = " GiB"
67+
size = float64(n) / 1024 / 1024 / 1024
68+
default:
69+
suffix = " TiB"
70+
size = float64(n) / 1024 / 1024 / 1024 / 1024
71+
}
72+
73+
return []byte(strconv.FormatFloat(size, 'g', 2, 64) + suffix), nil
74+
}
75+
4776
// Float represents a floating point number in the API
4877
type Float float64
4978

5079
// HumanReadable returns the Float as a string
5180
func (f Float) HumanReadable(string) ([]byte, error) {
52-
return []byte(strconv.FormatFloat(float64(f), 'g', -1, 64)), nil
81+
return []byte(strconv.FormatFloat(float64(f), 'g', 2, 64)), nil
5382
}
5483

5584
// String represents a string in the API

lc-lib/harvester/linereader.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package harvester
1919
import (
2020
"bytes"
2121
"io"
22+
23+
"github.com/driskell/log-courier/lc-lib/transports/tcp"
2224
)
2325

2426
// LineReader is a read interface that tails and returns lines
@@ -164,7 +166,21 @@ func (lr *LineReader) fill() error {
164166
return lr.err
165167
}
166168

167-
n, err := lr.rd.Read(lr.buf[lr.end:])
169+
// Loop until we receive data or an error occurs
170+
// Avoids the outer loop in ReadItem which would otherwise do unnecessary checks
171+
var n int
172+
var err error
173+
for {
174+
n, err = lr.rd.Read(lr.buf[lr.end:])
175+
if err == tcp.ErrIOWouldBlock {
176+
// Ignore incomplete reads - we will try again
177+
err = nil
178+
}
179+
if n > 0 || err != nil {
180+
break
181+
}
182+
}
183+
168184
lr.end += n
169185
if err != nil {
170186
// Remember last error - so we can continue processing current buffer and once

lc-lib/publisher/endpoint/endpoint.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,9 @@ func (e *Endpoint) queuePayload(payload *payload.Payload) error {
171171
}
172172

173173
if payload.Resending {
174-
log.Debugf("[E %s] Resending payload %x with %d events", e.Server(), payload.Nonce, payload.Size())
174+
log.Debugf("[E %s] Resending payload %x with %d events", e.Server(), payload.Nonce, payload.Len())
175175
} else {
176-
log.Debugf("[E %s] Sending payload %x with %d events", e.Server(), payload.Nonce, payload.Size())
176+
log.Debugf("[E %s] Sending payload %x with %d events", e.Server(), payload.Nonce, payload.Len())
177177
}
178178

179179
if err := e.transport.SendEvents(payload.Nonce, payload.Events()); err != nil {
@@ -232,7 +232,7 @@ func (e *Endpoint) ReduceLatency() {
232232
func (e *Endpoint) updateEstDelTime() {
233233
e.estDelTime = time.Now()
234234
for _, payload := range e.pendingPayloads {
235-
e.estDelTime = e.estDelTime.Add(time.Duration(e.averageLatency) * time.Duration(payload.Size()))
235+
e.estDelTime = e.estDelTime.Add(time.Duration(e.averageLatency) * time.Duration(payload.Len()))
236236
}
237237
}
238238

@@ -274,7 +274,7 @@ func (e *Endpoint) processAck(ack transports.AckEvent, onAck func(*Endpoint, *pa
274274
1,
275275
5,
276276
e.averageLatency,
277-
float64(time.Since(e.transmissionStart))/float64(payload.Size()),
277+
float64(time.Since(e.transmissionStart))/float64(payload.Len()),
278278
)
279279

280280
e.updateEstDelTime()

lc-lib/publisher/endpoint/sink_endpoint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (s *Sink) QueuePayload(payload *payload.Payload) (*Endpoint, error) {
4444
return nil, nil
4545
}
4646

47-
events := time.Duration(payload.Size())
47+
events := time.Duration(payload.Len())
4848
bestEndpoint := entry.Value.(*Endpoint)
4949
bestEDT := bestEndpoint.EstDelTime().Add(bestEndpoint.AverageLatency() * events)
5050

lc-lib/publisher/payload/payload.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ func (pp *Payload) Init() {
5757
pp.ResendElement.Value = pp
5858
}
5959

60-
// Size returns the original size of this payload
61-
func (pp *Payload) Size() int {
60+
// Len returns the original size of this payload
61+
func (pp *Payload) Len() int {
6262
return pp.sequenceLen
6363
}
6464

lc-lib/receiver/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ func (a *apiStatus) Update() error {
3131
// Update the values and pass through to node
3232
a.r.connectionLock.RLock()
3333
a.SetEntry("activeConnections", api.Number(len(a.r.connectionStatus)))
34+
a.SetEntry("queuePayloads", api.Number(len(a.r.spool)))
35+
a.SetEntry("queueSize", api.Number(a.r.spoolSize))
3436
a.r.connectionLock.RUnlock()
3537

3638
return nil

lc-lib/receiver/pool.go

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ const (
3838
poolContextEventPosition poolContext = "eventpos"
3939
)
4040

41+
type spoolEntry struct {
42+
events []*event.Event
43+
size int
44+
}
45+
4146
// Pool manages a list of receivers
4247
type Pool struct {
4348
// Pipeline
@@ -53,6 +58,8 @@ type Pool struct {
5358
scheduler *scheduler.Scheduler
5459
connectionLock sync.RWMutex
5560
connectionStatus map[interface{}]*poolConnectionStatus
61+
spool []*spoolEntry
62+
spoolSize int64
5663

5764
apiConfig *admin.Config
5865
apiConnections api.Array
@@ -104,16 +111,15 @@ func (r *Pool) Init(cfg *config.Config) error {
104111

105112
// Run starts listening
106113
func (r *Pool) Run() {
107-
var spool [][]*event.Event
108114
var spoolChan chan<- []*event.Event
109115
eventChan := r.eventChan
110116
shutdownChan := r.shutdownChan
111117

112118
ReceiverLoop:
113119
for {
114-
var nextSpool []*event.Event = nil
115-
if len(spool) != 0 {
116-
nextSpool = spool[0]
120+
var nextSpool *spoolEntry = nil
121+
if len(r.spool) != 0 {
122+
nextSpool = r.spool[0]
117123
}
118124

119125
select {
@@ -182,30 +188,52 @@ ReceiverLoop:
182188
// We replace because a reconnect on the same port could occur before we get around to handling the disconnection, and we're keyed by port
183189
r.apiConnections.ReplaceEntry(eventImpl.Remote(), connectionStatus)
184190
case transports.EventsEvent:
191+
size := calcSize(eventImpl)
185192
r.connectionLock.Lock()
186193
connection := eventImpl.Context().Value(transports.ContextConnection)
187194
receiver := eventImpl.Context().Value(transports.ContextReceiver).(transports.Receiver)
188195
connectionStatus := r.connectionStatus[connection]
189-
// Schedule partial ack if this is first set of events
190-
if len(connectionStatus.progress) == 0 {
191-
r.scheduler.Set(connection, 5*time.Second)
196+
if r.spoolSize+int64(size) > r.receivers[receiver].config.MaxQueueSize {
197+
receiver.ShutdownConnectionRead(eventImpl.Context(), fmt.Errorf("max queue size exceeded"))
198+
r.connectionLock.Unlock()
199+
break
192200
}
193-
connectionStatus.progress = append(connectionStatus.progress, &poolEventProgress{event: eventImpl, sequence: 0})
201+
var acker event.Acknowledger
202+
if receiver.SupportsAck() {
203+
if len(r.connectionStatus[connection].progress)+1 > int(r.receivers[receiver].config.MaxPendingPayloads) {
204+
receiver.ShutdownConnectionRead(eventImpl.Context(), fmt.Errorf("max pending payloads exceeded"))
205+
r.connectionLock.Unlock()
206+
break
207+
}
208+
// Schedule partial ack if this is first set of events
209+
if len(connectionStatus.progress) == 0 {
210+
r.scheduler.Set(connection, 5*time.Second)
211+
}
212+
connectionStatus.progress = append(connectionStatus.progress, &poolEventProgress{event: eventImpl, sequence: 0})
213+
acker = r
214+
} else {
215+
// Reset idle timeout
216+
r.startIdleTimeout(eventImpl.Context(), receiver, connection)
217+
}
218+
connectionStatus.bytes += eventImpl.Size()
194219
r.connectionLock.Unlock()
195220
// Build the events with our acknowledger and submit the bundle
196221
var events = make([]*event.Event, len(eventImpl.Events()))
222+
var ctx context.Context
197223
for idx, item := range eventImpl.Events() {
198-
ctx := context.WithValue(eventImpl.Context(), poolContextEventPosition, &poolEventPosition{nonce: eventImpl.Nonce(), sequence: uint32(idx + 1)})
199-
item := event.NewEvent(ctx, r, item)
224+
if acker == nil {
225+
ctx = eventImpl.Context()
226+
} else {
227+
ctx = context.WithValue(eventImpl.Context(), poolContextEventPosition, &poolEventPosition{nonce: eventImpl.Nonce(), sequence: uint32(idx + 1)})
228+
}
229+
item := event.NewEvent(ctx, acker, item)
200230
item.MustResolve("@metadata[receiver]", connectionStatus.metadataReceiver)
201231
events[idx] = item
202232
}
203-
spool = append(spool, events)
233+
spoolEntry := &spoolEntry{events, size}
234+
r.spool = append(r.spool, spoolEntry)
235+
r.spoolSize += int64(spoolEntry.size)
204236
spoolChan = r.output
205-
// Stop reading events if this client breached our limit
206-
if len(r.connectionStatus[connection].progress) > int(r.receivers[receiver].config.MaxPendingPayloads) {
207-
receiver.ShutdownConnectionRead(eventImpl.Context(), fmt.Errorf("max pending payloads exceeded"))
208-
}
209237
case *transports.EndEvent:
210238
// Connection EOF
211239
r.connectionLock.Lock()
@@ -263,10 +291,11 @@ ReceiverLoop:
263291
r.startIdleTimeout(eventImpl.Context(), receiver, connection)
264292
}
265293
}
266-
case spoolChan <- nextSpool:
267-
copy(spool, spool[1:])
268-
spool = spool[:len(spool)-1]
269-
if len(spool) == 0 {
294+
case spoolChan <- nextSpool.events:
295+
copy(r.spool, r.spool[1:])
296+
r.spool = r.spool[:len(r.spool)-1]
297+
r.spoolSize -= int64(nextSpool.size)
298+
if len(r.spool) == 0 {
270299
spoolChan = nil
271300
}
272301
}
@@ -385,6 +414,7 @@ func (r *Pool) updateReceivers(newConfig *config.Config) {
385414
receiverApi := &api.KeyValue{}
386415
receiverApi.SetEntry("listen", api.String(listen))
387416
receiverApi.SetEntry("maxPendingPayloads", api.Number(cfgEntry.MaxPendingPayloads))
417+
receiverApi.SetEntry("maxQueueSize", api.Number(cfgEntry.MaxQueueSize))
388418
r.apiListeners.AddEntry(listen, receiverApi)
389419
}
390420
}
@@ -414,3 +444,7 @@ func (r *Pool) shutdown() {
414444
receiver.Shutdown()
415445
}
416446
}
447+
448+
func calcSize(eventImpl transports.EventsEvent) int {
449+
return eventImpl.Size()
450+
}

lc-lib/receiver/status.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type poolConnectionStatus struct {
4040
metadataReceiver MetadataReceiver
4141
progress []*poolEventProgress
4242
lines int64
43+
bytes int
4344

4445
api.KeyValue
4546
}
@@ -69,6 +70,7 @@ func (p *poolConnectionStatus) Update() error {
6970
p.SetEntry("listener", api.String(p.listener))
7071
p.SetEntry("description", api.String(p.desc))
7172
p.SetEntry("completedLines", api.Number(p.lines))
73+
p.SetEntry("completedBytes", api.Bytes(p.bytes))
7274
p.SetEntry("pendingPayloads", api.Number(len(p.progress)))
7375
return nil
7476
}

lc-lib/transports/common.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,23 @@ import (
2424
"errors"
2525
"fmt"
2626
"os"
27+
"time"
2728

2829
"github.com/driskell/log-courier/lc-lib/config"
2930
)
3031

32+
const (
33+
defaultNetworkBackoff time.Duration = 5 * time.Second
34+
defaultNetworkBackoffMax time.Duration = 300 * time.Second
35+
defaultNetworkMaxPendingPayloads int64 = 10
36+
defaultNetworkMaxQueueSize int64 = 128 * 1024 * 1024 // 128 MiB
37+
defaultNetworkMethod string = "random"
38+
defaultNetworkRfc2782Service string = "courier"
39+
defaultNetworkRfc2782Srv bool = true
40+
defaultNetworkTimeout time.Duration = 15 * time.Second
41+
defaultNetworkTransport string = "tls"
42+
)
43+
3144
var (
3245
// ErrCongestion represents temporary congestion, rather than failure
3346
ErrCongestion error = errors.New("Congestion")
@@ -77,6 +90,7 @@ type EventsEvent interface {
7790
Events() []map[string]interface{}
7891
Nonce() *string
7992
Count() uint32
93+
Size() int
8094
}
8195

8296
// StatusEvent contains information about a status change for a transport
@@ -273,16 +287,18 @@ type eventsEvent struct {
273287
context context.Context
274288
nonce *string
275289
events []map[string]interface{}
290+
size int
276291
}
277292

278293
var _ EventsEvent = (*eventsEvent)(nil)
279294

280295
// NewEventsEvent generates a new EventsEvent for the given bundle of events
281-
func NewEventsEvent(context context.Context, nonce *string, events []map[string]interface{}) EventsEvent {
296+
func NewEventsEvent(context context.Context, nonce *string, events []map[string]interface{}, size int) EventsEvent {
282297
return &eventsEvent{
283298
context: context,
284299
nonce: nonce,
285300
events: events,
301+
size: size,
286302
}
287303
}
288304

@@ -306,6 +322,10 @@ func (e *eventsEvent) Count() uint32 {
306322
return uint32(len(e.events))
307323
}
308324

325+
func (e *eventsEvent) Size() int {
326+
return e.size
327+
}
328+
309329
// ParseTLSVersion parses a TLS version string into the tls library value for min/max config
310330
// We explicitly refuse SSLv3 to mitigate POODLE vulnerability
311331
func ParseTLSVersion(version string, fallback uint16) (uint16, error) {

0 commit comments

Comments
 (0)