mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-09 14:43:19 +00:00
Updates #7156, and a follow-up to #7070. Event subscriptions in Tendermint currently use a fixed-length Go channel as a queue. When the channel fills up, the publisher immediately terminates the subscription. This prevents slow subscribers from creating memory pressure on the node by not servicing their queue fast enough. Replace the buffered channel used to deliver events to buffered subscribers with an explicit queue. The queue provides a soft quota and burst credit mechanism: Clients that usually keep up can survive occasional bursts, without allowing truly slow clients to hog resources indefinitely.
189 lines
4.6 KiB
Go
189 lines
4.6 KiB
Go
package queue
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNew(t *testing.T) {
|
|
tests := []struct {
|
|
desc string
|
|
opts Options
|
|
want error
|
|
}{
|
|
{"empty options", Options{}, errHardLimit},
|
|
{"zero limit negative quota", Options{SoftQuota: -1}, errHardLimit},
|
|
{"zero limit and quota", Options{SoftQuota: 0}, errHardLimit},
|
|
{"zero limit", Options{SoftQuota: 1, HardLimit: 0}, errHardLimit},
|
|
{"limit less than quota", Options{SoftQuota: 5, HardLimit: 3}, errHardLimit},
|
|
{"negative credit", Options{SoftQuota: 1, HardLimit: 1, BurstCredit: -6}, errBurstCredit},
|
|
{"valid default credit", Options{SoftQuota: 1, HardLimit: 2, BurstCredit: 0}, nil},
|
|
{"valid explicit credit", Options{SoftQuota: 1, HardLimit: 5, BurstCredit: 10}, nil},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
got, err := New(test.opts)
|
|
if err != test.want {
|
|
t.Errorf("New(%+v): got (%+v, %v), want err=%v", test.opts, got, err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type testQueue struct {
|
|
t *testing.T
|
|
*Queue
|
|
}
|
|
|
|
func (q testQueue) mustAdd(item string) {
|
|
q.t.Helper()
|
|
if err := q.Add(item); err != nil {
|
|
q.t.Errorf("Add(%q): unexpected error: %v", item, err)
|
|
}
|
|
}
|
|
|
|
func (q testQueue) mustRemove(want string) {
|
|
q.t.Helper()
|
|
got, ok := q.Remove()
|
|
if !ok {
|
|
q.t.Error("Remove: queue is empty")
|
|
} else if got.(string) != want {
|
|
q.t.Errorf("Remove: got %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func mustQueue(t *testing.T, opts Options) testQueue {
|
|
t.Helper()
|
|
|
|
q, err := New(opts)
|
|
if err != nil {
|
|
t.Fatalf("New(%+v): unexpected error: %v", opts, err)
|
|
}
|
|
return testQueue{t: t, Queue: q}
|
|
}
|
|
|
|
func TestHardLimit(t *testing.T) {
|
|
q := mustQueue(t, Options{SoftQuota: 1, HardLimit: 1})
|
|
q.mustAdd("foo")
|
|
if err := q.Add("bar"); err != ErrQueueFull {
|
|
t.Errorf("Add: got err=%v, want %v", err, ErrQueueFull)
|
|
}
|
|
}
|
|
|
|
func TestSoftQuota(t *testing.T) {
|
|
q := mustQueue(t, Options{SoftQuota: 1, HardLimit: 4})
|
|
q.mustAdd("foo")
|
|
q.mustAdd("bar")
|
|
if err := q.Add("baz"); err != ErrNoCredit {
|
|
t.Errorf("Add: got err=%v, want %v", err, ErrNoCredit)
|
|
}
|
|
}
|
|
|
|
func TestBurstCredit(t *testing.T) {
|
|
q := mustQueue(t, Options{SoftQuota: 2, HardLimit: 5})
|
|
q.mustAdd("foo")
|
|
q.mustAdd("bar")
|
|
|
|
// We should still have all our initial credit.
|
|
if q.credit < 2 {
|
|
t.Errorf("Wrong credit: got %f, want ≥ 2", q.credit)
|
|
}
|
|
|
|
// Removing an item below soft quota should increase our credit.
|
|
q.mustRemove("foo")
|
|
if q.credit <= 2 {
|
|
t.Errorf("wrong credit: got %f, want > 2", q.credit)
|
|
}
|
|
|
|
// Credit should be capped by the hard limit.
|
|
q.mustRemove("bar")
|
|
q.mustAdd("baz")
|
|
q.mustRemove("baz")
|
|
if cap := float64(q.hardLimit - q.softQuota); q.credit > cap {
|
|
t.Errorf("Wrong credit: got %f, want ≤ %f", q.credit, cap)
|
|
}
|
|
}
|
|
|
|
func TestClose(t *testing.T) {
|
|
q := mustQueue(t, Options{SoftQuota: 2, HardLimit: 10})
|
|
q.mustAdd("alpha")
|
|
q.mustAdd("bravo")
|
|
q.mustAdd("charlie")
|
|
q.Close()
|
|
|
|
// After closing the queue, subsequent writes should fail.
|
|
if err := q.Add("foxtrot"); err == nil {
|
|
t.Error("Add should have failed after Close")
|
|
}
|
|
|
|
// However, the remaining contents of the queue should still work.
|
|
q.mustRemove("alpha")
|
|
q.mustRemove("bravo")
|
|
q.mustRemove("charlie")
|
|
}
|
|
|
|
func TestWait(t *testing.T) {
|
|
q := mustQueue(t, Options{SoftQuota: 2, HardLimit: 2})
|
|
|
|
// A wait on an empty queue should time out.
|
|
t.Run("WaitTimeout", func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
defer cancel()
|
|
got, err := q.Wait(ctx)
|
|
if err == nil {
|
|
t.Errorf("Wait: got %v, want error", got)
|
|
} else {
|
|
t.Logf("Wait correctly failed: %v", err)
|
|
}
|
|
})
|
|
|
|
// A wait on a non-empty queue should report an item.
|
|
t.Run("WaitNonEmpty", func(t *testing.T) {
|
|
const input = "figgy pudding"
|
|
q.mustAdd(input)
|
|
|
|
got, err := q.Wait(context.Background())
|
|
if err != nil {
|
|
t.Errorf("Wait: unexpected error: %v", err)
|
|
} else if got != input {
|
|
t.Errorf("Wait: got %q, want %q", got, input)
|
|
}
|
|
})
|
|
|
|
// Wait should block until an item arrives.
|
|
t.Run("WaitOnEmpty", func(t *testing.T) {
|
|
const input = "fleet footed kittens"
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
got, err := q.Wait(context.Background())
|
|
if err != nil {
|
|
t.Errorf("Wait: unexpected error: %v", err)
|
|
} else if got != input {
|
|
t.Errorf("Wait: got %q, want %q", got, input)
|
|
}
|
|
}()
|
|
|
|
q.mustAdd(input)
|
|
<-done
|
|
})
|
|
|
|
// Closing the queue unblocks a wait.
|
|
t.Run("UnblockOnClose", func(t *testing.T) {
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
got, err := q.Wait(context.Background())
|
|
if err != ErrQueueClosed {
|
|
t.Errorf("Wait: got (%v, %v), want %v", got, err, ErrQueueClosed)
|
|
}
|
|
}()
|
|
|
|
q.Close()
|
|
<-done
|
|
})
|
|
}
|