mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-06 22:23:56 -05:00
**What type of PR is this?** Feature **What does this PR do? Why is it needed?** Adds data column support to backfill. **Acknowledgements** - [x] I have read [CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md). - [x] I have included a uniquely named [changelog fragment file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd). - [x] I have added a description to this PR with sufficient context for reviewers to understand this PR. --------- Co-authored-by: Kasey <kasey@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Preston Van Loon <preston@pvl.dev>
380 lines
11 KiB
Go
380 lines
11 KiB
Go
package backfill
|
|
|
|
import (
|
|
"bytes"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// trackingHook is a logrus hook that counts Log callCount for testing.
|
|
type trackingHook struct {
|
|
mu sync.RWMutex
|
|
entries []*logrus.Entry
|
|
}
|
|
|
|
func (h *trackingHook) Levels() []logrus.Level {
|
|
return logrus.AllLevels
|
|
}
|
|
|
|
func (h *trackingHook) Fire(entry *logrus.Entry) error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.entries = append(h.entries, entry)
|
|
return nil
|
|
}
|
|
|
|
func (h *trackingHook) callCount() int {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return len(h.entries)
|
|
}
|
|
|
|
func (h *trackingHook) emitted(t *testing.T) []string {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
e := make([]string, len(h.entries))
|
|
for i, entry := range h.entries {
|
|
entry.Buffer = new(bytes.Buffer)
|
|
serialized, err := entry.Logger.Formatter.Format(entry)
|
|
require.NoError(t, err)
|
|
e[i] = string(serialized)
|
|
}
|
|
return e
|
|
}
|
|
|
|
func entryWithHook() (*logrus.Entry, *trackingHook) {
|
|
logger := logrus.New()
|
|
logger.SetLevel(logrus.TraceLevel)
|
|
hook := &trackingHook{}
|
|
logger.AddHook(hook)
|
|
entry := logrus.NewEntry(logger)
|
|
return entry, hook
|
|
}
|
|
|
|
func intervalSecondsAndDuration(i int) (int64, time.Duration) {
|
|
return int64(i), time.Duration(i) * time.Second
|
|
}
|
|
|
|
// mockClock provides a controllable time source for testing.
|
|
// It allows tests to set the current time and advance it as needed.
|
|
type mockClock struct {
|
|
t time.Time
|
|
}
|
|
|
|
// now returns the current time.
|
|
func (c *mockClock) now() time.Time {
|
|
return c.t
|
|
}
|
|
|
|
func setupMockClock(il *intervalLogger) *mockClock {
|
|
// initialize now so that the time aligns with the start of the
|
|
// interval bucket. This ensures that adding less than an interval
|
|
// of time to the timestamp can never move into the next bucket.
|
|
interval := intervalNumber(time.Now(), il.seconds)
|
|
now := time.Unix(interval*il.seconds, 0)
|
|
clock := &mockClock{t: now}
|
|
il.now = clock.now
|
|
return clock
|
|
}
|
|
|
|
// TestNewIntervalLogger verifies logger is properly initialized
|
|
func TestNewIntervalLogger(t *testing.T) {
|
|
base := logrus.NewEntry(logrus.New())
|
|
intSec := int64(10)
|
|
|
|
il := newIntervalLogger(base, intSec)
|
|
|
|
require.NotNil(t, il)
|
|
require.Equal(t, intSec, il.seconds)
|
|
require.Equal(t, int64(0), il.last.Load())
|
|
require.Equal(t, base, il.Entry)
|
|
}
|
|
|
|
// TestLogOncePerInterval verifies that Log is called only once within an interval window
|
|
func TestLogOncePerInterval(t *testing.T) {
|
|
entry, hook := entryWithHook()
|
|
|
|
il := newIntervalLogger(entry, 10)
|
|
_ = setupMockClock(il) // use a fixed time to make sure no race is possible
|
|
|
|
// First log should call the underlying Log method
|
|
il.Log(logrus.InfoLevel, "test message 1")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Second log in same interval should not call Log
|
|
il.Log(logrus.InfoLevel, "test message 2")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Third log still in same interval should not call Log
|
|
il.Log(logrus.InfoLevel, "test message 3")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Verify last is set to current interval
|
|
require.Equal(t, il.intervalNumber(), il.last.Load())
|
|
}
|
|
|
|
// TestLogAcrossIntervalBoundary verifies logging at interval boundaries resets correctly
|
|
func TestLogAcrossIntervalBoundary(t *testing.T) {
|
|
iSec, iDur := intervalSecondsAndDuration(10)
|
|
|
|
entry, hook := entryWithHook()
|
|
il := newIntervalLogger(entry, iSec)
|
|
clock := setupMockClock(il)
|
|
|
|
il.Log(logrus.InfoLevel, "first interval")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Log in new interval should succeed
|
|
clock.t = clock.t.Add(2 * iDur)
|
|
il.Log(logrus.InfoLevel, "second interval")
|
|
require.Equal(t, 2, hook.callCount())
|
|
}
|
|
|
|
// TestWithFieldChaining verifies WithField returns logger and can be chained
|
|
func TestWithFieldChaining(t *testing.T) {
|
|
entry, hook := entryWithHook()
|
|
iSec, iDur := intervalSecondsAndDuration(10)
|
|
il := newIntervalLogger(entry, iSec)
|
|
clock := setupMockClock(il)
|
|
|
|
result := il.WithField("key1", "value1")
|
|
require.NotNil(t, result)
|
|
result.Info("test")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// make sure there was no mutation of the base as a side effect
|
|
clock.t = clock.t.Add(iDur)
|
|
il.Info("another")
|
|
|
|
// Verify field is present in logged entry
|
|
emitted := hook.emitted(t)
|
|
require.Contains(t, emitted[0], "test")
|
|
require.Contains(t, emitted[0], "key1=value1")
|
|
require.Contains(t, emitted[1], "another")
|
|
require.NotContains(t, emitted[1], "key1=value1")
|
|
}
|
|
|
|
// TestWithFieldsChaining verifies WithFields properly adds multiple fields
|
|
func TestWithFieldsChaining(t *testing.T) {
|
|
entry, hook := entryWithHook()
|
|
iSec, iDur := intervalSecondsAndDuration(10)
|
|
il := newIntervalLogger(entry, iSec)
|
|
clock := setupMockClock(il)
|
|
|
|
fields := logrus.Fields{
|
|
"key1": "value1",
|
|
"key2": "value2",
|
|
}
|
|
result := il.WithFields(fields)
|
|
require.NotNil(t, result)
|
|
result.Info("test")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// make sure there was no mutation of the base as a side effect
|
|
clock.t = clock.t.Add(iDur)
|
|
il.Info("another")
|
|
|
|
// Verify field is present in logged entry
|
|
emitted := hook.emitted(t)
|
|
require.Contains(t, emitted[0], "test")
|
|
require.Contains(t, emitted[0], "key1=value1")
|
|
require.Contains(t, emitted[0], "key2=value2")
|
|
require.Contains(t, emitted[1], "another")
|
|
require.NotContains(t, emitted[1], "key1=value1")
|
|
require.NotContains(t, emitted[1], "key2=value2")
|
|
}
|
|
|
|
// TestWithErrorChaining verifies WithError properly adds error field
|
|
func TestWithErrorChaining(t *testing.T) {
|
|
entry, hook := entryWithHook()
|
|
iSec, iDur := intervalSecondsAndDuration(10)
|
|
il := newIntervalLogger(entry, iSec)
|
|
clock := setupMockClock(il)
|
|
|
|
expected := errors.New("lowercase words")
|
|
result := il.WithError(expected)
|
|
require.NotNil(t, result)
|
|
result.Error("test")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
require.NotNil(t, result)
|
|
|
|
// make sure there was no mutation of the base as a side effect
|
|
clock.t = clock.t.Add(iDur)
|
|
il.Info("different")
|
|
|
|
// Verify field is present in logged entry
|
|
emitted := hook.emitted(t)
|
|
require.Contains(t, emitted[0], expected.Error())
|
|
require.Contains(t, emitted[0], "test")
|
|
require.Contains(t, emitted[1], "different")
|
|
require.NotContains(t, emitted[1], "test")
|
|
require.NotContains(t, emitted[1], "lowercase words")
|
|
}
|
|
|
|
// TestLogLevelMethods verifies all log level methods work and respect rate limiting
|
|
func TestLogLevelMethods(t *testing.T) {
|
|
entry, hook := entryWithHook()
|
|
il := newIntervalLogger(entry, 10)
|
|
_ = setupMockClock(il) // use a fixed time to make sure no race is possible
|
|
|
|
// First call from each level-specific method should succeed
|
|
il.Trace("trace message")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Subsequent callCount in same interval should be suppressed
|
|
il.Debug("debug message")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
il.Info("info message")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
il.Print("print message")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
il.Warn("warn message")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
il.Warning("warning message")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
il.Error("error message")
|
|
require.Equal(t, 1, hook.callCount())
|
|
}
|
|
|
|
// TestConcurrentLogging verifies multiple goroutines can safely call Log concurrently
|
|
func TestConcurrentLogging(t *testing.T) {
|
|
entry, hook := entryWithHook()
|
|
il := newIntervalLogger(entry, 10)
|
|
_ = setupMockClock(il) // use a fixed time to make sure no race is possible
|
|
|
|
var wg sync.WaitGroup
|
|
wait := make(chan struct{})
|
|
for range 10 {
|
|
wg.Add(1)
|
|
go func() {
|
|
<-wait
|
|
defer wg.Done()
|
|
il.Log(logrus.InfoLevel, "concurrent message")
|
|
}()
|
|
}
|
|
close(wait) // maximize raciness by unblocking goroutines together
|
|
wg.Wait()
|
|
|
|
// Only one Log call should succeed across all goroutines in the same interval
|
|
require.Equal(t, 1, hook.callCount())
|
|
}
|
|
|
|
// TestZeroInterval verifies behavior with small interval (logs every second)
|
|
func TestZeroInterval(t *testing.T) {
|
|
entry, hook := entryWithHook()
|
|
il := newIntervalLogger(entry, 1)
|
|
clock := setupMockClock(il)
|
|
|
|
il.Log(logrus.InfoLevel, "first")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Move to next second
|
|
clock.t = clock.t.Add(time.Second)
|
|
il.Log(logrus.InfoLevel, "second")
|
|
require.Equal(t, 2, hook.callCount())
|
|
}
|
|
|
|
// TestCompleteLoggingFlow tests realistic scenario with repeated logging
|
|
func TestCompleteLoggingFlow(t *testing.T) {
|
|
entry, hook := entryWithHook()
|
|
iSec, iDur := intervalSecondsAndDuration(10)
|
|
il := newIntervalLogger(entry, iSec)
|
|
clock := setupMockClock(il)
|
|
|
|
// Add field
|
|
il = il.WithField("request_id", "12345")
|
|
|
|
// Log multiple times in same interval - only first succeeds
|
|
il.Info("message 1")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
il.Warn("message 2")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Move to next interval
|
|
clock.t = clock.t.Add(iDur)
|
|
|
|
// Should be able to log again in new interval
|
|
il.Error("message 3")
|
|
require.Equal(t, 2, hook.callCount())
|
|
|
|
require.NotNil(t, il)
|
|
}
|
|
|
|
// TestAtomicSwapCorrectness verifies atomic swap works correctly
|
|
func TestAtomicSwapCorrectness(t *testing.T) {
|
|
il := newIntervalLogger(logrus.NewEntry(logrus.New()), 10)
|
|
_ = setupMockClock(il) // use a fixed time to make sure no race is possible
|
|
|
|
// Swap operation should return different value on first call
|
|
current := il.intervalNumber()
|
|
old := il.last.Swap(current)
|
|
require.Equal(t, int64(0), old) // initial value is 0
|
|
require.Equal(t, current, il.last.Load())
|
|
|
|
// Swap with same value should return the same value
|
|
old = il.last.Swap(current)
|
|
require.Equal(t, current, old)
|
|
}
|
|
|
|
// TestLogMethodsWithClockAdvancement verifies that log methods respect rate limiting
|
|
// within an interval but emit again after the interval passes.
|
|
func TestLogMethodsWithClockAdvancement(t *testing.T) {
|
|
entry, hook := entryWithHook()
|
|
|
|
iSec, iDur := intervalSecondsAndDuration(10)
|
|
il := newIntervalLogger(entry, iSec)
|
|
clock := setupMockClock(il)
|
|
|
|
// First Error call should log
|
|
il.Error("error 1")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Warn call in same interval should be suppressed
|
|
il.Warn("warn 1")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Info call in same interval should be suppressed
|
|
il.Info("info 1")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Debug call in same interval should be suppressed
|
|
il.Debug("debug 1")
|
|
require.Equal(t, 1, hook.callCount())
|
|
|
|
// Move forward 5 seconds - still in same 10-second interval
|
|
require.Equal(t, 5*time.Second, iDur/2)
|
|
clock.t = clock.t.Add(iDur / 2)
|
|
il.Error("error 2")
|
|
require.Equal(t, 1, hook.callCount(), "should still be suppressed within same interval")
|
|
firstInterval := il.intervalNumber()
|
|
|
|
// Move forward to next interval (10 second interval boundary)
|
|
clock.t = clock.t.Add(iDur / 2)
|
|
nextInterval := il.intervalNumber()
|
|
require.NotEqual(t, firstInterval, nextInterval, "should be in new interval now")
|
|
|
|
il.Error("error 3")
|
|
require.Equal(t, 2, hook.callCount(), "should emit in new interval")
|
|
|
|
// Another call in the new interval should be suppressed
|
|
il.Warn("warn 2")
|
|
require.Equal(t, 2, hook.callCount())
|
|
|
|
// Move forward to yet another interval
|
|
clock.t = clock.t.Add(iDur)
|
|
il.Info("info 2")
|
|
require.Equal(t, 3, hook.callCount(), "should emit in third interval")
|
|
}
|