beacon/light/request: simple server event rate limit

This commit is contained in:
Zsolt Felfoldi 2024-01-30 03:24:33 +01:00 committed by Felix Lange
parent 544d75daf7
commit 8884782e4e
3 changed files with 74 additions and 31 deletions

View file

@ -28,18 +28,12 @@ func TestEventFilter(t *testing.T) {
}) })
// let module1 send a request // let module1 send a request
srv.canRequest = 1 srv.canRequest = 1
module1.reqc = testRequest module1.sendReq = testRequest
s.Trigger() s.Trigger()
// first triggered round sends the request, no events yet // in first triggered round module1 sends the request, no events yet
s.testWaitCh <- struct{}{} s.testWaitCh <- struct{}{}
module1.expProcess(t, nil) module1.expProcess(t, nil)
module2.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 // server emits EvTimeout; only module1 should receive it
srv.eventCb(Event{Type: EvTimeout, Data: RequestResponse{ID: 1, Request: testRequest}}) srv.eventCb(Event{Type: EvTimeout, Data: RequestResponse{ID: 1, Request: testRequest}})
s.testWaitCh <- struct{}{} s.testWaitCh <- struct{}{}
@ -80,32 +74,32 @@ func (s *testServer) subscribe(eventCb func(Event)) {
s.eventCb = eventCb s.eventCb = eventCb
} }
func (s *testServer) canRequestNow() (bool, float32) { func (s *testServer) canRequestNow() bool {
return s.canRequest > 0, 0 return s.canRequest > 0
} }
func (s *testServer) sendRequest(req Request) ID { func (s *testServer) sendRequest(req Request) ID {
s.canRequest-- s.canRequest--
s.lastID++ s.lastID++
s.eventCb(Event{Type: EvRequest, Data: RequestResponse{ID: s.lastID, Request: req}})
return s.lastID return s.lastID
} }
func (s *testServer) Fail(string) {} func (s *testServer) fail(string) {}
func (s *testServer) unsubscribe() {} func (s *testServer) unsubscribe() {}
type testModule struct { type testModule struct {
name string name string
processed [][]Event processed [][]Event
reqc Request // request candidate sendReq Request
} }
func (m *testModule) Process(events []Event) { func (m *testModule) Process(requester Requester, events []Event) {
m.processed = append(m.processed, events) m.processed = append(m.processed, events)
} if m.sendReq != nil {
if cs := requester.CanSendTo(); len(cs) > 0 {
func (m *testModule) MakeRequest(Server) (Request, float32) { requester.Send(cs[0], m.sendReq)
return m.reqc, 0 }
}
} }
func (m *testModule) expProcess(t *testing.T, expEvents []Event) { func (m *testModule) expProcess(t *testing.T, expEvents []Event) {

View file

@ -49,6 +49,8 @@ const (
defaultParallelLimit = 3 // parallelLimit initial value defaultParallelLimit = 3 // parallelLimit initial value
minFailureDelay = time.Millisecond * 100 // minimum disable time in case of request failure minFailureDelay = time.Millisecond * 100 // minimum disable time in case of request failure
maxFailureDelay = time.Minute // maximum disable time in case of request failure maxFailureDelay = time.Minute // maximum disable time in case of request failure
maxServerEventBuffer = 5 // server event allowance buffer limit
maxServerEventRate = time.Second // server event allowance buffer recharge rate
) )
// requestServer can send requests in a non-blocking way and feed back events // requestServer can send requests in a non-blocking way and feed back events
@ -226,13 +228,11 @@ func (s *serverWithTimeout) unsubscribe() {
// serverWithLimits wraps serverWithTimeout and implements server. It limits the // serverWithLimits wraps serverWithTimeout and implements server. It limits the
// number of parallel in-flight requests and prevents sending new requests when a // number of parallel in-flight requests and prevents sending new requests when a
// pending one has already timed out. It also implements a failure delay mechanism // pending one has already timed out. Server events are also rate limited.
// that adds an exponentially growing delay each time a request fails (wrong answer // It also implements a failure delay mechanism that adds an exponentially growing
// or hard timeout). This makes the syncing mechanism less brittle as temporary // delay each time a request fails (wrong answer or hard timeout). This makes the
// failures of the server might happen sometimes, but still avoids hammering a // syncing mechanism less brittle as temporary failures of the server might happen
// non-functional server with requests. // sometimes, but still avoids hammering a non-functional server with requests.
//
// TODO protect against excessive server events
type serverWithLimits struct { type serverWithLimits struct {
serverWithTimeout serverWithTimeout
lock sync.Mutex lock sync.Mutex
@ -245,12 +245,15 @@ type serverWithLimits struct {
delayCounter int delayCounter int
failureDelayEnd mclock.AbsTime failureDelayEnd mclock.AbsTime
failureDelay float64 failureDelay float64
serverEventBuffer int
eventBufferUpdated mclock.AbsTime
} }
// init initializes serverWithLimits // init initializes serverWithLimits
func (s *serverWithLimits) init() { func (s *serverWithLimits) init() {
s.softTimeouts = make(map[ID]struct{}) s.softTimeouts = make(map[ID]struct{})
s.parallelLimit = defaultParallelLimit s.parallelLimit = defaultParallelLimit
s.serverEventBuffer = maxServerEventBuffer
} }
// subscribe subscribes to events which include parent (serverWithTimeout) events // subscribe subscribes to events which include parent (serverWithTimeout) events
@ -267,6 +270,7 @@ func (s *serverWithLimits) subscribe(eventCallback func(event Event)) {
func (s *serverWithLimits) eventCallback(event Event) { func (s *serverWithLimits) eventCallback(event Event) {
s.lock.Lock() s.lock.Lock()
var sendCanRequestAgain bool var sendCanRequestAgain bool
passEvent := true
switch event.Type { switch event.Type {
case EvTimeout: case EvTimeout:
id := event.Data.(RequestResponse).ID id := event.Data.(RequestResponse).ID
@ -295,10 +299,31 @@ func (s *serverWithLimits) eventCallback(event Event) {
if event.Type == EvFail { if event.Type == EvFail {
s.failLocked("failed request") s.failLocked("failed request")
} }
default:
// server event; check rate limit
if s.serverEventBuffer < maxServerEventBuffer {
now := s.clock.Now()
sinceUpdate := time.Duration(now - s.eventBufferUpdated)
if sinceUpdate >= maxServerEventRate*time.Duration(maxServerEventBuffer-s.serverEventBuffer) {
s.serverEventBuffer = maxServerEventBuffer
s.eventBufferUpdated = now
} else {
addBuffer := int(sinceUpdate / maxServerEventRate)
s.serverEventBuffer += addBuffer
s.eventBufferUpdated += mclock.AbsTime(maxServerEventRate * time.Duration(addBuffer))
}
}
if s.serverEventBuffer > 0 {
s.serverEventBuffer--
} else {
passEvent = false
}
} }
childEventCb := s.childEventCb childEventCb := s.childEventCb
s.lock.Unlock() s.lock.Unlock()
childEventCb(event) if passEvent {
childEventCb(event)
}
if sendCanRequestAgain { if sendCanRequestAgain {
childEventCb(Event{Type: EvCanRequestAgain}) childEventCb(Event{Type: EvCanRequestAgain})
} }

View file

@ -36,7 +36,6 @@ func TestServerEvents(t *testing.T) {
expEvent(testEventType) expEvent(testEventType)
// send request, soft timeout, then valid response // send request, soft timeout, then valid response
srv.sendRequest(testRequest) srv.sendRequest(testRequest)
expEvent(EvRequest)
clock.WaitForTimers(1) clock.WaitForTimers(1)
clock.Run(softRequestTimeout) clock.Run(softRequestTimeout)
expEvent(EvTimeout) expEvent(EvTimeout)
@ -44,7 +43,6 @@ func TestServerEvents(t *testing.T) {
expEvent(EvResponse) expEvent(EvResponse)
// send request, hard timeout (response after hard timeout should be ignored) // send request, hard timeout (response after hard timeout should be ignored)
srv.sendRequest(testRequest) srv.sendRequest(testRequest)
expEvent(EvRequest)
clock.WaitForTimers(1) clock.WaitForTimers(1)
clock.Run(softRequestTimeout) clock.Run(softRequestTimeout)
expEvent(EvTimeout) expEvent(EvTimeout)
@ -63,7 +61,7 @@ func TestServerParallel(t *testing.T) {
expSend := func(expSent int) { expSend := func(expSent int) {
var sent int var sent int
for sent <= expSent { for sent <= expSent {
if ok, _ := srv.canRequestNow(); !ok { if !srv.canRequestNow() {
break break
} }
sent++ sent++
@ -94,7 +92,7 @@ func TestServerFail(t *testing.T) {
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 {
t.Errorf("Wrong result for canRequestNow (expected %v, got %v)", expCanRequest, canRequest) t.Errorf("Wrong result for canRequestNow (expected %v, got %v)", expCanRequest, canRequest)
} }
} }
@ -108,7 +106,7 @@ func TestServerFail(t *testing.T) {
rs.eventCb(Event{Type: EvResponse, Data: RequestResponse{ID: 1, Request: testRequest, Response: testResponse}}) rs.eventCb(Event{Type: EvResponse, Data: RequestResponse{ID: 1, Request: testRequest, Response: testResponse}})
expCanRequest(true) expCanRequest(true)
// explicit server.Fail // explicit server.Fail
srv.Fail("") srv.fail("")
clock.WaitForTimers(1) clock.WaitForTimers(1)
expCanRequest(false) // cannot request for a while after a failure expCanRequest(false) // cannot request for a while after a failure
clock.Run(minFailureDelay) clock.Run(minFailureDelay)
@ -125,6 +123,32 @@ func TestServerFail(t *testing.T) {
srv.unsubscribe() srv.unsubscribe()
} }
func TestServerEventRateLimit(t *testing.T) {
rs := &testRequestServer{}
clock := &mclock.Simulated{}
srv := NewServer(rs, clock)
var eventCount int
srv.subscribe(func(event Event) {
if !event.IsRequestEvent() {
eventCount++
}
})
expEvents := func(send, expAllowed int) {
eventCount = 0
for sent := 0; sent < send; sent++ {
rs.eventCb(Event{Type: testEventType})
}
if eventCount != expAllowed {
t.Errorf("Wrong number of server events passing rate limitation (sent %d, expected %d, got %d)", send, expAllowed, eventCount)
}
}
expEvents(maxServerEventBuffer+5, maxServerEventBuffer)
clock.Run(maxServerEventRate)
expEvents(5, 1)
clock.Run(maxServerEventRate * maxServerEventBuffer * 2)
expEvents(maxServerEventBuffer+5, maxServerEventBuffer)
}
type testRequestServer struct { type testRequestServer struct {
eventCb func(Event) eventCb func(Event)
} }