[#1210] timers: Add IterationsTicker

It allows specifying duration in blocks and a desired number of handler
calls in that period.

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
This commit is contained in:
Pavel Karpy 2022-03-02 19:26:57 +03:00 committed by Alex Vanin
parent 13af4e6046
commit 77d847dbea
2 changed files with 208 additions and 0 deletions

View file

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

View file

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