Skip to content

Commit

Permalink
introduces mockable ticker
Browse files Browse the repository at this point in the history
  • Loading branch information
kjezek committed Sep 25, 2024
1 parent be5fc26 commit 199eea6
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 20 deletions.
43 changes: 43 additions & 0 deletions go/common/ticker/ticker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) 2024 Fantom Foundation
//
// Use of this software is governed by the Business Source License included
// in the LICENSE file and at fantom.foundation/bsl11.
//
// Change Date: 2028-4-16
//
// On the date above, in accordance with the Business Source License, use of
// this software will be governed by the GNU Lesser General Public License v3.

package ticker

import "time"

//go:generate mockgen -source ticker.go -destination ticker_mocks.go -package ticker

// Ticker is an abstraction of a ticker
type Ticker interface {

// C returns the channel on which the ticks are delivered.
C() <-chan time.Time

// Stop turns off a ticker. After Stop, no more ticks will be sent.
Stop()
}

// TimeTicker is a wrapper around time.Ticker
type TimeTicker struct {
ticker *time.Ticker
}

// NewTimeTicker creates a new TimeTicker
func NewTimeTicker(d time.Duration) TimeTicker {
return TimeTicker{time.NewTicker(d)}
}

func (t TimeTicker) C() <-chan time.Time {
return t.ticker.C
}

func (t TimeTicker) Stop() {
t.ticker.Stop()
}
75 changes: 75 additions & 0 deletions go/common/ticker/ticker_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions go/common/ticker/ticker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) 2024 Fantom Foundation
//
// Use of this software is governed by the Business Source License included
// in the LICENSE file and at fantom.foundation/bsl11.
//
// Change Date: 2028-4-16
//
// On the date above, in accordance with the Business Source License, use of
// this software will be governed by the GNU Lesser General Public License v3.

package ticker

import (
"testing"
"time"
)

func TestTicker_Ticks(t *testing.T) {
last := time.Now()

ticker := NewTimeTicker(10 * time.Millisecond)

const loops = 100
for i := 0; i < loops; i++ {
tick := <-ticker.C()
if tick.Before(last) {
t.Errorf("tick %d is before last tick: %v < %v", i, last, tick)
}
last = tick
}
ticker.Stop()

// no more ticks should be received
select {
case tick := <-ticker.C():
t.Errorf("unexpected tick: %v", tick)
case <-time.After(1000 * time.Millisecond):
// done
}
}
4 changes: 1 addition & 3 deletions go/database/mpt/forest.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,7 @@ func makeForest(
sink := writeBufferSink{res}

// Start a background worker flushing dirty nodes to disk.
res.flusher = startNodeFlusher(res.nodeCache, sink, nodeFlusherConfig{
period: forestConfig.BackgroundFlushPeriod,
})
res.flusher = startNodeFlusher(res.nodeCache, sink, newTimeTickerNodeFlusherConfig(forestConfig.BackgroundFlushPeriod))

// Run a background worker releasing entire tries of nodes on demand.
go func() {
Expand Down
26 changes: 23 additions & 3 deletions go/database/mpt/node_flusher.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package mpt

import (
"errors"
"github.com/Fantom-foundation/Carmen/go/common/ticker"
"slices"
"time"

Expand All @@ -25,7 +26,19 @@ type nodeFlusher struct {
}

type nodeFlusherConfig struct {
period time.Duration // uses a default period if zero and disables flushing if negative
period time.Duration // uses a default period if zero and disables flushing if negative
tickerFactory func(time.Duration) ticker.Ticker
}

// newTimeTickerNodeFlusherConfig creates a new node flusher configuration with standard time ticker
func newTimeTickerNodeFlusherConfig(d time.Duration) nodeFlusherConfig {
ticker.NewTimeTicker(d)
return nodeFlusherConfig{
period: d,
tickerFactory: func(duration time.Duration) ticker.Ticker {
return ticker.NewTimeTicker(duration)
},
}
}

func startNodeFlusher(cache NodeCache, sink NodeSink, config nodeFlusherConfig) *nodeFlusher {
Expand All @@ -43,16 +56,23 @@ func startNodeFlusher(cache NodeCache, sink NodeSink, config nodeFlusherConfig)
period = 5 * time.Second
}

tickerFactory := config.tickerFactory
if tickerFactory == nil {
tickerFactory = func(duration time.Duration) ticker.Ticker {
return ticker.NewTimeTicker(duration)
}
}

if period > 0 {
go func() {
defer close(done)
ticker := time.NewTicker(period)
ticker := tickerFactory(period)
defer ticker.Stop()
for {
select {
case <-shutdown:
return
case <-ticker.C:
case <-ticker.C():
if err := tryFlushDirtyNodes(cache, sink); err != nil {
res.errs = append(res.errs, err)
}
Expand Down
38 changes: 24 additions & 14 deletions go/database/mpt/node_flusher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package mpt
import (
"errors"
"fmt"
"github.com/Fantom-foundation/Carmen/go/common/ticker"
"testing"
"time"

Expand Down Expand Up @@ -44,32 +45,43 @@ func TestNodeFlusher_StartAndStopWithDisabledFlusher(t *testing.T) {
}

func TestNodeFlusher_TriggersFlushesPeriodically(t *testing.T) {
const period = 1 * time.Second
ctrl := gomock.NewController(t)
cache := NewMockNodeCache(ctrl)

const loops = 3
flushSignal := make(chan struct{}, 1)
cache.EXPECT().ForEach(gomock.Any()).Times(3).Do(func(f func(id NodeId, node *shared.Shared[Node])) {
cache.EXPECT().ForEach(gomock.Any()).Times(loops).Do(func(f func(id NodeId, node *shared.Shared[Node])) {
flushSignal <- struct{}{}
}).Return()

tickerC := make(chan time.Time, loops)
for i := 0; i < loops; i++ {
tickerC <- time.Now()
}

mockTicker := ticker.NewMockTicker(ctrl)
mockTicker.EXPECT().C().Return(tickerC).AnyTimes()
mockTicker.EXPECT().Stop()

flusher := startNodeFlusher(cache, nil, nodeFlusherConfig{
period: period,
tickerFactory: func(duration time.Duration) ticker.Ticker {
return mockTicker
},
})

last := time.Now()
for i := 0; i < 3; i++ {
var numTicks int
for i := 0; i < loops; i++ {
select {
case <-flushSignal:
if time.Since(last) < period/2 {
t.Fatalf("flush signal received too early")
}
last = time.Now()
case <-time.After(period * 2):
t.Fatalf("flush signal not received")
numTicks++
case <-time.After(30 * time.Second):
}
}

if got, want := numTicks, loops; got != want {
t.Errorf("unexpected number of ticks: got %v, want %v", got, want)
}

if err := flusher.Stop(); err != nil {
t.Fatalf("failed to stop node flusher: %v", err)
}
Expand Down Expand Up @@ -98,9 +110,7 @@ func TestNodeFlusher_ErrorsAreCollected(t *testing.T) {
cache.EXPECT().Get(RefTo(id)).Return(node, true).AnyTimes()
sink.EXPECT().Write(id, gomock.Any()).Return(injectedError).AnyTimes()

flusher := startNodeFlusher(cache, sink, nodeFlusherConfig{
period: period,
})
flusher := startNodeFlusher(cache, sink, newTimeTickerNodeFlusherConfig(period))

<-done

Expand Down

0 comments on commit 199eea6

Please sign in to comment.