Files
tendermint/internal/libs/queue/queue_test.go
M. J. Fromberger d32913c889 pubsub: Use a dynamic queue for buffered subscriptions (#7177)
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.
2021-11-01 10:38:27 -07:00

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
})
}