beacon/light/request: add Scheduler unit test

This commit is contained in:
Zsolt Felfoldi 2024-01-24 04:35:00 +01:00 committed by Felix Lange
parent 5795351236
commit 245d166d45
5 changed files with 163 additions and 40 deletions

View file

@ -20,7 +20,6 @@ import (
"math" "math"
"sync" "sync"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
@ -68,7 +67,6 @@ type Module interface {
// allow new operations. // allow new operations.
type Scheduler struct { type Scheduler struct {
lock sync.Mutex lock sync.Mutex
clock mclock.Clock
modules []Module // first has highest priority modules []Module // first has highest priority
names map[Module]string names map[Module]string
servers map[server]struct{} servers map[server]struct{}
@ -83,6 +81,9 @@ type Scheduler struct {
stopCh chan chan struct{} stopCh chan chan struct{}
triggerCh chan struct{} // restarts waiting sync loop triggerCh chan struct{} // restarts waiting sync loop
// if trigger has already been fired then send to testWaitCh blocks until
// the triggered processing round is finished
testWaitCh chan struct{}
} }
type ( type (
@ -118,9 +119,8 @@ type pendingRequest struct {
} }
// NewScheduler creates a new Scheduler. // NewScheduler creates a new Scheduler.
func NewScheduler(clock mclock.Clock) *Scheduler { func NewScheduler() *Scheduler {
s := &Scheduler{ s := &Scheduler{
clock: clock,
servers: make(map[server]struct{}), servers: make(map[server]struct{}),
names: make(map[Module]string), names: make(map[Module]string),
pending: make(map[ServerAndID]pendingRequest), pending: make(map[ServerAndID]pendingRequest),
@ -130,7 +130,7 @@ func NewScheduler(clock mclock.Clock) *Scheduler {
// that after a trigger happens testWaitCh will block until the resulting // that after a trigger happens testWaitCh will block until the resulting
// processing round has been finished // processing round has been finished
triggerCh: make(chan struct{}, 1), triggerCh: make(chan struct{}, 1),
//testWaitCh: make(chan struct{}), testWaitCh: make(chan struct{}),
} }
return s return s
} }
@ -157,11 +157,10 @@ func (s *Scheduler) RegisterModule(m Module, name string) {
} }
// RegisterServer registers a new server. // RegisterServer registers a new server.
func (s *Scheduler) RegisterServer(rs requestServer) { func (s *Scheduler) RegisterServer(server server) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
server := newServer(rs, s.clock)
s.addEvent(Event{Type: EvRegistered, Server: server}) s.addEvent(Event{Type: EvRegistered, Server: server})
server.subscribe(func(event Event) { server.subscribe(func(event Event) {
event.Server = server event.Server = server
@ -170,17 +169,12 @@ func (s *Scheduler) RegisterServer(rs requestServer) {
} }
// UnregisterServer removes a registered server. // UnregisterServer removes a registered server.
func (s *Scheduler) UnregisterServer(rs requestServer) { func (s *Scheduler) UnregisterServer(server server) {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
for server := range s.servers {
if sl, ok := server.(*serverWithLimits); ok && sl.parent == rs {
server.unsubscribe() server.unsubscribe()
s.addEvent(Event{Type: EvUnregistered, Server: server}) s.addEvent(Event{Type: EvUnregistered, Server: server})
return
}
}
} }
// Start starts the scheduler. It should be called after registering all modules // Start starts the scheduler. It should be called after registering all modules
@ -191,15 +185,15 @@ func (s *Scheduler) Start() {
// Stop stops the scheduler. // Stop stops the scheduler.
func (s *Scheduler) Stop() { func (s *Scheduler) Stop() {
stop := make(chan struct{})
s.stopCh <- stop
<-stop
s.lock.Lock() s.lock.Lock()
for server := range s.servers { for server := range s.servers {
server.unsubscribe() server.unsubscribe()
} }
s.servers = nil s.servers = nil
s.lock.Unlock() s.lock.Unlock()
stop := make(chan struct{})
s.stopCh <- stop
<-stop
} }
// syncLoop is the main event loop responsible for event/data processing and // syncLoop is the main event loop responsible for event/data processing and
@ -219,7 +213,7 @@ func (s *Scheduler) syncLoop() {
return return
case <-s.triggerCh: case <-s.triggerCh:
break loop break loop
//case <-s.testWaitCh: case <-s.testWaitCh:
} }
} }
} }
@ -244,11 +238,11 @@ func (s *Scheduler) targetChanged() (changed bool) {
// requests are generated and sent if necessary and possible. // requests are generated and sent if necessary and possible.
func (s *Scheduler) processRound() { func (s *Scheduler) processRound() {
for { for {
serverEvents, requestEvents := s.filterEvents() filteredEvents := s.filterEvents()
log.Debug("Processing modules", "server events", len(serverEvents)) log.Debug("Processing modules")
for _, module := range s.modules { for _, module := range s.modules {
log.Debug("Processing module", "name", s.names[module], "request events", len(requestEvents[module])) log.Debug("Processing module", "name", s.names[module], "events", len(filteredEvents[module]))
module.Process(append(serverEvents, requestEvents[module]...)) module.Process(filteredEvents[module])
} }
if !s.targetChanged() { if !s.targetChanged() {
break break
@ -358,13 +352,13 @@ func (s *Scheduler) addEvent(event Event) {
// to the given server by adding a failed request event (EvFail), ensuring that // to the given server by adding a failed request event (EvFail), ensuring that
// all requests get finalized and thereby allowing the module logic to be safe // all requests get finalized and thereby allowing the module logic to be safe
// and simple. // and simple.
func (s *Scheduler) filterEvents() (serverEvents []Event, requestEvents map[Module][]Event) { func (s *Scheduler) filterEvents() map[Module][]Event {
s.eventLock.Lock() s.eventLock.Lock()
events := s.events events := s.events
s.events = nil s.events = nil
s.eventLock.Unlock() s.eventLock.Unlock()
requestEvents = make(map[Module][]Event) filteredEvents := make(map[Module][]Event)
for _, event := range events { for _, event := range events {
server, ok := event.Server.(server) server, ok := event.Server.(server)
if !ok { if !ok {
@ -384,27 +378,29 @@ func (s *Scheduler) filterEvents() (serverEvents []Event, requestEvents map[Modu
if event.Type == EvResponse || event.Type == EvFail { if event.Type == EvResponse || event.Type == EvFail {
delete(s.pending, sid) // final event, close pending request delete(s.pending, sid) // final event, close pending request
} }
requestEvents[pending.module] = append(requestEvents[pending.module], event) filteredEvents[pending.module] = append(filteredEvents[pending.module], event)
} else { } else {
switch event.Type { switch event.Type {
case EvRegistered: case EvRegistered:
s.servers[server] = struct{}{} s.servers[server] = struct{}{}
case EvUnregistered: case EvUnregistered:
s.closePending(event.Server, requestEvents) s.closePending(event.Server, filteredEvents)
delete(s.servers, server) delete(s.servers, server)
} }
serverEvents = append(serverEvents, event) for _, module := range s.modules {
filteredEvents[module] = append(filteredEvents[module], event)
} }
} }
return }
return filteredEvents
} }
// closePending closes all pending requests to the given server and adds an EvFail // closePending closes all pending requests to the given server and adds an EvFail
// event to properly finalize them // event to properly finalize them
func (s *Scheduler) closePending(server Server, requestEvents map[Module][]Event) { func (s *Scheduler) closePending(server Server, filteredEvents map[Module][]Event) {
for sid, pending := range s.pending { for sid, pending := range s.pending {
if sid.Server == server { if sid.Server == server {
requestEvents[pending.module] = append(requestEvents[pending.module], Event{ filteredEvents[pending.module] = append(filteredEvents[pending.module], Event{
Type: EvFail, Type: EvFail,
Server: server, Server: server,
Data: RequestResponse{ Data: RequestResponse{

View file

@ -0,0 +1,128 @@
package request
import (
"reflect"
"testing"
)
func TestEventFilter(t *testing.T) {
s := NewScheduler()
module1 := &testModule{name: "module1"}
module2 := &testModule{name: "module2"}
s.RegisterModule(module1, "module1")
s.RegisterModule(module2, "module2")
s.Start()
// startup process round without events
s.testWaitCh <- struct{}{}
module1.expProcess(t, nil)
module2.expProcess(t, nil)
srv := &testServer{}
// register server; both modules should receive server event
s.RegisterServer(srv)
s.testWaitCh <- struct{}{}
module1.expProcess(t, []Event{
Event{Type: EvRegistered, Server: srv},
})
module2.expProcess(t, []Event{
Event{Type: EvRegistered, Server: srv},
})
// let module1 send a request
srv.canRequest = 1
module1.reqc = testRequest
s.Trigger()
// first triggered round sends the request, no events yet
s.testWaitCh <- struct{}{}
module1.expProcess(t, nil)
module2.expProcess(t, nil)
// next round triggered by EvRequest; only module1 should receive it
s.testWaitCh <- struct{}{}
module1.expProcess(t, []Event{
Event{Type: EvRequest, Server: srv, Data: RequestResponse{ID: 1, Request: testRequest}},
})
module2.expProcess(t, nil)
// server emits EvTimeout; only module1 should receive it
srv.eventCb(Event{Type: EvTimeout, Data: RequestResponse{ID: 1, Request: testRequest}})
s.testWaitCh <- struct{}{}
module1.expProcess(t, []Event{
Event{Type: EvTimeout, Server: srv, Data: RequestResponse{ID: 1, Request: testRequest}},
})
module2.expProcess(t, nil)
// unregister server; both modules should receive server event
s.UnregisterServer(srv)
s.testWaitCh <- struct{}{}
module1.expProcess(t, []Event{
// module1 should also receive EvFail on its pending request
Event{Type: EvFail, Server: srv, Data: RequestResponse{ID: 1, Request: testRequest}},
Event{Type: EvUnregistered, Server: srv},
})
module2.expProcess(t, []Event{
Event{Type: EvUnregistered, Server: srv},
})
// response after server unregistered; should be discarded
srv.eventCb(Event{Type: EvResponse, Data: RequestResponse{ID: 1, Request: testRequest, Response: testResponse}})
s.testWaitCh <- struct{}{}
module1.expProcess(t, nil)
module2.expProcess(t, nil)
// no more process rounds expected; shut down
s.testWaitCh <- struct{}{}
module1.expNoMoreProcess(t)
module2.expNoMoreProcess(t)
s.Stop()
}
type testServer struct {
eventCb func(Event)
lastID ID
canRequest int
}
func (s *testServer) subscribe(eventCb func(Event)) {
s.eventCb = eventCb
}
func (s *testServer) canRequestNow() (bool, float32) {
return s.canRequest > 0, 0
}
func (s *testServer) sendRequest(req Request) ID {
s.canRequest--
s.lastID++
s.eventCb(Event{Type: EvRequest, Data: RequestResponse{ID: s.lastID, Request: req}})
return s.lastID
}
func (s *testServer) Fail(string) {}
func (s *testServer) unsubscribe() {}
type testModule struct {
name string
processed [][]Event
reqc Request // request candidate
}
func (m *testModule) Process(events []Event) {
m.processed = append(m.processed, events)
}
func (m *testModule) MakeRequest(Server) (Request, float32) {
return m.reqc, 0
}
func (m *testModule) expProcess(t *testing.T, expEvents []Event) {
if len(m.processed) == 0 {
t.Errorf("Missing call to %s.Process", m.name)
return
}
events := m.processed[0]
m.processed = m.processed[1:]
if !reflect.DeepEqual(events, expEvents) {
t.Errorf("Call to %s.Process with wrong events (expected %v, got %v)", m.name, expEvents, events)
}
}
func (m *testModule) expNoMoreProcess(t *testing.T) {
for len(m.processed) > 0 {
t.Errorf("Unexpected call to %s.Process with events %v", m.name, m.processed[0])
m.processed = m.processed[1:]
}
}

View file

@ -76,8 +76,8 @@ type server interface {
unsubscribe() unsubscribe()
} }
// newServer wraps a requestServer and returns a server // NewServer wraps a requestServer and returns a server
func newServer(rs requestServer, clock mclock.Clock) server { func NewServer(rs requestServer, clock mclock.Clock) server {
s := &serverWithLimits{} s := &serverWithLimits{}
s.parent = rs s.parent = rs
s.serverWithTimeout.init(clock) s.serverWithTimeout.init(clock)

View file

@ -16,7 +16,7 @@ var testEventType = &EventType{Name: "testEvent"}
func TestServerEvents(t *testing.T) { func TestServerEvents(t *testing.T) {
rs := &testRequestServer{} rs := &testRequestServer{}
clock := &mclock.Simulated{} clock := &mclock.Simulated{}
srv := newServer(rs, clock) srv := NewServer(rs, clock)
var lastEventType *EventType var lastEventType *EventType
srv.subscribe(func(event Event) { lastEventType = event.Type }) srv.subscribe(func(event Event) { lastEventType = event.Type })
evTypeName := func(evType *EventType) string { evTypeName := func(evType *EventType) string {
@ -57,7 +57,7 @@ func TestServerEvents(t *testing.T) {
func TestServerParallel(t *testing.T) { func TestServerParallel(t *testing.T) {
rs := &testRequestServer{} rs := &testRequestServer{}
srv := newServer(rs, &mclock.Simulated{}) srv := NewServer(rs, &mclock.Simulated{})
srv.subscribe(func(event Event) {}) srv.subscribe(func(event Event) {})
expSend := func(expSent int) { expSend := func(expSent int) {
@ -91,7 +91,7 @@ func TestServerParallel(t *testing.T) {
func TestServerFail(t *testing.T) { func TestServerFail(t *testing.T) {
rs := &testRequestServer{} rs := &testRequestServer{}
clock := &mclock.Simulated{} clock := &mclock.Simulated{}
srv := newServer(rs, clock) srv := NewServer(rs, clock)
srv.subscribe(func(event Event) {}) srv.subscribe(func(event Event) {})
expCanRequest := func(expCanRequest bool) { expCanRequest := func(expCanRequest bool) {
if canRequest, _ := srv.canRequestNow(); canRequest != expCanRequest { if canRequest, _ := srv.canRequestNow(); canRequest != expCanRequest {

View file

@ -115,8 +115,7 @@ func blsync(ctx *cli.Context) error {
headSync := sync.NewHeadSync(headTracker, committeeChain) headSync := sync.NewHeadSync(headTracker, committeeChain)
// set up scheduler and sync modules // set up scheduler and sync modules
scheduler := request.NewScheduler(&mclock.System{}) scheduler := request.NewScheduler()
checkpointInit := sync.NewCheckpointInit(committeeChain, chainConfig.Checkpoint) checkpointInit := sync.NewCheckpointInit(committeeChain, chainConfig.Checkpoint)
forwardSync := sync.NewForwardUpdateSync(committeeChain) forwardSync := sync.NewForwardUpdateSync(committeeChain)
beaconBlockSync := newBeaconBlockSync(headTracker) beaconBlockSync := newBeaconBlockSync(headTracker)
@ -132,7 +131,7 @@ func blsync(ctx *cli.Context) error {
// register server(s) // register server(s)
for _, url := range ctx.StringSlice(utils.BeaconApiFlag.Name) { for _, url := range ctx.StringSlice(utils.BeaconApiFlag.Name) {
beaconApi := api.NewBeaconLightApi(url, customHeader) beaconApi := api.NewBeaconLightApi(url, customHeader)
scheduler.RegisterServer(api.NewApiServer(beaconApi)) scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{}))
} }
// run until stopped // run until stopped
<-ctx.Done() <-ctx.Done()