[#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:
parent
13af4e6046
commit
77d847dbea
2 changed files with 208 additions and 0 deletions
90
cmd/neofs-node/reputation/ticker/fixed.go
Normal file
90
cmd/neofs-node/reputation/ticker/fixed.go
Normal 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
|
||||
}
|
118
cmd/neofs-node/reputation/ticker/fixed_test.go
Normal file
118
cmd/neofs-node/reputation/ticker/fixed_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue