diff --git a/cmd/neofs-node/reputation/ticker/fixed.go b/cmd/neofs-node/reputation/ticker/fixed.go new file mode 100644 index 0000000000..4f73d4b542 --- /dev/null +++ b/cmd/neofs-node/reputation/ticker/fixed.go @@ -0,0 +1,90 @@ +package ticker + +import ( + "fmt" + "sync" +) + +// IterationHandler is a callback of a certain block advance. +type IterationHandler func() + +// IterationsTicker represents fixed tick number block timer. +// +// It can tick the blocks and perform certain actions +// on block time intervals. +type IterationsTicker struct { + m sync.Mutex + + curr uint64 + period uint64 + + times uint64 + + h IterationHandler +} + +// NewIterationsTicker creates a new IterationsTicker. +// +// It guaranties that handler would be called the +// specified amount of times in the specified amount +// of blocks. After the last meaningful Tick IterationsTicker +// becomes no-op timer. +// +// Returns error only if times is greater than totalBlocks. +func NewIterationsTicker(totalBlocks uint64, times uint64, h IterationHandler) (*IterationsTicker, error) { + period := totalBlocks / times + + if period == 0 { + return nil, fmt.Errorf("impossible to tick %d times in %d blocks", + times, totalBlocks, + ) + } + + var curr uint64 + + // try to make handler calls as rare as possible + if totalBlocks%times != 0 { + extraBlocks := (period+1)*times - totalBlocks + + if period >= extraBlocks { + curr = extraBlocks + (period-extraBlocks)/2 + period++ + } + } + + return &IterationsTicker{ + curr: curr, + period: period, + times: times, + h: h, + }, nil +} + +// Tick ticks one block in the IterationsTicker. +// +// Returns `false` if the timer has finished its operations +// and there will be no more handler calls. +// Calling Tick after the returned `false` is safe, no-op +// and also returns `false`. +func (ft *IterationsTicker) Tick() bool { + ft.m.Lock() + defer ft.m.Unlock() + + if ft.times == 0 { + return false + } + + ft.curr++ + + if ft.curr%ft.period == 0 { + ft.h() + + ft.times-- + + if ft.times == 0 { + return false + } + } + + return true +} diff --git a/cmd/neofs-node/reputation/ticker/fixed_test.go b/cmd/neofs-node/reputation/ticker/fixed_test.go new file mode 100644 index 0000000000..25e9bd08f7 --- /dev/null +++ b/cmd/neofs-node/reputation/ticker/fixed_test.go @@ -0,0 +1,118 @@ +package ticker + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFixedTimer_Tick(t *testing.T) { + tests := [...]struct { + duration uint64 + times uint64 + err error + }{ + { + duration: 20, + times: 4, + err: nil, + }, + { + duration: 6, + times: 6, + err: nil, + }, + { + duration: 10, + times: 6, + err: nil, + }, + { + duration: 5, + times: 6, + err: errors.New("impossible to tick 6 times in 5 blocks"), + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("duration:%d,times:%d", test.duration, test.times), func(t *testing.T) { + counter := uint64(0) + + timer, err := NewIterationsTicker(test.duration, test.times, func() { + counter++ + }) + if test.err != nil { + require.EqualError(t, err, test.err.Error()) + return + } + + require.NoError(t, err) + + for i := 0; i < int(test.duration); i++ { + if !timer.Tick() { + break + } + } + + require.Equal(t, false, timer.Tick()) + require.Equal(t, test.times, counter) + }) + } +} + +func TestFixedTimer_RareCalls(t *testing.T) { + tests := [...]struct { + duration uint64 + times uint64 + firstCall uint64 + period uint64 + }{ + { + duration: 11, + times: 6, + firstCall: 1, + period: 2, + }, + { + duration: 11, + times: 4, + firstCall: 2, + period: 3, + }, + { + duration: 20, + times: 3, + firstCall: 4, + period: 7, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("duration:%d,times:%d", test.duration, test.times), func(t *testing.T) { + var counter uint64 + + timer, err := NewIterationsTicker(test.duration, test.times, func() { + counter++ + }) + require.NoError(t, err) + + checked := false + + for i := 1; i <= int(test.duration); i++ { + if !timer.Tick() { + break + } + + if !checked && counter == 1 { + require.Equal(t, test.firstCall, uint64(i)) + checked = true + } + } + + require.Equal(t, false, timer.Tick()) + require.Equal(t, test.times, counter) + }) + } +}