|
|
- package eventlog_test
-
- import (
- "context"
- "errors"
- "fmt"
- "math/rand"
- "strconv"
- "sync"
- "testing"
- "time"
-
- "github.com/fortytw2/leaktest"
- "github.com/google/go-cmp/cmp"
-
- "github.com/tendermint/tendermint/internal/eventlog"
- "github.com/tendermint/tendermint/internal/eventlog/cursor"
- "github.com/tendermint/tendermint/types"
- )
-
- // fakeTime is a fake clock to use to control cursor assignment.
- // The timeIndex method reports the current "time" and advance manually updates
- // the apparent time.
- type fakeTime struct{ now int64 }
-
- func newFakeTime(init int64) *fakeTime { return &fakeTime{now: init} }
-
- func (f *fakeTime) timeIndex() int64 { return f.now }
-
- func (f *fakeTime) advance(d time.Duration) { f.now += int64(d) }
-
- // eventData is a placeholder event data implementation for testing.
- type eventData string
-
- func (eventData) TypeTag() string { return "eventData" }
-
- func TestNewError(t *testing.T) {
- lg, err := eventlog.New(eventlog.LogSettings{})
- if err == nil {
- t.Fatalf("New: got %+v, wanted error", lg)
- } else {
- t.Logf("New: got expected error: %v", err)
- }
- }
-
- func TestPruneTime(t *testing.T) {
- clk := newFakeTime(0)
-
- // Construct a log with a 60-second time window.
- lg, err := eventlog.New(eventlog.LogSettings{
- WindowSize: 60 * time.Second,
- Source: cursor.Source{
- TimeIndex: clk.timeIndex,
- },
- })
- if err != nil {
- t.Fatalf("New unexpectedly failed: %v", err)
- }
-
- // Add events up to the time window, at seconds 0, 15, 30, 45, 60.
- // None of these should be pruned (yet).
- var want []string // cursor strings
- for i := 1; i <= 5; i++ {
- want = append(want, fmt.Sprintf("%016x-%04x", clk.timeIndex(), i))
- mustAdd(t, lg, "test-event", eventData("whatever"))
- clk.advance(15 * time.Second)
- }
- // time now: 75 sec.
-
- // Verify that all the events we added are present.
- got := cursors(t, lg)
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("Cursors before pruning: (-want, +got)\n%s", diff)
- }
-
- // Add an event past the end of the window at second 90, and verify that
- // this triggered an age-based prune of the oldest events (0, 15) that are
- // outside the 60-second window.
-
- clk.advance(15 * time.Second) // time now: 90 sec.
- want = append(want[2:], fmt.Sprintf("%016x-%04x", clk.timeIndex(), 6))
-
- mustAdd(t, lg, "test-event", eventData("extra"))
- got = cursors(t, lg)
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("Cursors after pruning: (-want, +got)\n%s", diff)
- }
- }
-
- // Run a publisher and concurrent subscribers to tickle the race detector with
- // concurrent add and scan operations.
- func TestConcurrent(t *testing.T) {
- defer leaktest.Check(t)
- if testing.Short() {
- t.Skip("Skipping concurrency exercise because -short is set")
- }
-
- lg, err := eventlog.New(eventlog.LogSettings{
- WindowSize: 30 * time.Second,
- })
- if err != nil {
- t.Fatalf("New unexpectedly failed: %v", err)
- }
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- var wg sync.WaitGroup
-
- // Publisher: Add events and handle expirations.
- wg.Add(1)
- go func() {
- defer wg.Done()
-
- tick := time.NewTimer(0)
- defer tick.Stop()
- for {
- select {
- case <-ctx.Done():
- return
- case t := <-tick.C:
- _ = lg.Add("test-event", eventData(t.Format(time.RFC3339Nano)))
- tick.Reset(time.Duration(rand.Intn(50)) * time.Millisecond)
- }
- }
- }()
-
- // Subscribers: Wait for new events at the head of the queue. This
- // simulates the typical operation of a subscriber by waiting for the head
- // cursor to change and then scanning down toward the unconsumed item.
- const numSubs = 16
- for i := 0; i < numSubs; i++ {
- task := i
- wg.Add(1)
- go func() {
- defer wg.Done()
-
- tick := time.NewTimer(0)
- var cur cursor.Cursor
- for {
- // Simulate the subscriber being busy with other things.
- select {
- case <-ctx.Done():
- return
- case <-tick.C:
- tick.Reset(time.Duration(rand.Intn(150)) * time.Millisecond)
- }
-
- // Wait for new data to arrive.
- info, err := lg.WaitScan(ctx, cur, func(itm *eventlog.Item) error {
- if itm.Cursor == cur {
- return eventlog.ErrStopScan
- }
- return nil
- })
- if err != nil {
- if !errors.Is(err, context.Canceled) {
- t.Errorf("Wait scan for task %d failed: %v", task, err)
- }
- return
- }
- cur = info.Newest
- }
- }()
- }
-
- time.AfterFunc(2*time.Second, cancel)
- wg.Wait()
- }
-
- func TestPruneSize(t *testing.T) {
- const maxItems = 25
- lg, err := eventlog.New(eventlog.LogSettings{
- WindowSize: 60 * time.Second,
- MaxItems: maxItems,
- })
- if err != nil {
- t.Fatalf("New unexpectedly failed: %v", err)
- }
-
- // Add a lot of items to the log and verify that we never exceed the
- // specified cap.
- for i := 0; i < 60; i++ {
- mustAdd(t, lg, "test-event", eventData(strconv.Itoa(i+1)))
-
- if got := lg.Info().Size; got > maxItems {
- t.Errorf("After add %d: log size is %d, want ≤ %d", i+1, got, maxItems)
- }
- }
- }
-
- // mustAdd adds a single event to lg. If Add reports an error other than for
- // pruning, the test fails; otherwise the error is returned.
- func mustAdd(t *testing.T, lg *eventlog.Log, etype string, data types.EventData) {
- t.Helper()
- err := lg.Add(etype, data)
- if err != nil && !errors.Is(err, eventlog.ErrLogPruned) {
- t.Fatalf("Add %q failed: %v", etype, err)
- }
- }
-
- // cursors extracts the cursors from lg in ascending order of time.
- func cursors(t *testing.T, lg *eventlog.Log) []string {
- t.Helper()
-
- var cursors []string
- if _, err := lg.Scan(func(itm *eventlog.Item) error {
- cursors = append(cursors, itm.Cursor.String())
- return nil
- }); err != nil {
- t.Fatalf("Scan failed: %v", err)
- }
- reverse(cursors) // put in forward-time order for comparison
- return cursors
- }
-
- func reverse(ss []string) {
- for i, j := 0, len(ss)-1; i < j; {
- ss[i], ss[j] = ss[j], ss[i]
- i++
- j--
- }
- }
|