Manager: force a poll of the farm status when a job/worker changes state
This introduces the concept of 'event listener', which is now used by the farm status service to respond to events on the event bus. This makes it possible to reduce the regular poll period from 5 to 30 seconds. That's now only necessary as backup, just in case events are missed or otherwise things change without the event bus logic noticing.
This commit is contained in:
parent
9bfb53a7f6
commit
c1a9b1e877
@ -10,17 +10,25 @@ type (
|
|||||||
EventTopic string
|
EventTopic string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Listener is the interface for internal components that want to respond to events.
|
||||||
|
type Listener interface {
|
||||||
|
OnEvent(topic EventTopic, payload interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forwarder is the interface for components that forward events to external systems.
|
||||||
type Forwarder interface {
|
type Forwarder interface {
|
||||||
Broadcast(topic EventTopic, payload interface{})
|
Broadcast(topic EventTopic, payload interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type Broker struct {
|
type Broker struct {
|
||||||
|
listeners []Listener
|
||||||
forwarders []Forwarder
|
forwarders []Forwarder
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBroker() *Broker {
|
func NewBroker() *Broker {
|
||||||
return &Broker{
|
return &Broker{
|
||||||
|
listeners: []Listener{},
|
||||||
forwarders: []Forwarder{},
|
forwarders: []Forwarder{},
|
||||||
mutex: sync.Mutex{},
|
mutex: sync.Mutex{},
|
||||||
}
|
}
|
||||||
@ -32,10 +40,20 @@ func (b *Broker) AddForwarder(forwarder Forwarder) {
|
|||||||
b.forwarders = append(b.forwarders, forwarder)
|
b.forwarders = append(b.forwarders, forwarder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Broker) AddListener(listener Listener) {
|
||||||
|
b.mutex.Lock()
|
||||||
|
defer b.mutex.Unlock()
|
||||||
|
b.listeners = append(b.listeners, listener)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Broker) broadcast(topic EventTopic, payload interface{}) {
|
func (b *Broker) broadcast(topic EventTopic, payload interface{}) {
|
||||||
b.mutex.Lock()
|
b.mutex.Lock()
|
||||||
defer b.mutex.Unlock()
|
defer b.mutex.Unlock()
|
||||||
|
|
||||||
|
for _, listener := range b.listeners {
|
||||||
|
listener.OnEvent(topic, payload)
|
||||||
|
}
|
||||||
|
|
||||||
for _, forwarder := range b.forwarders {
|
for _, forwarder := range b.forwarders {
|
||||||
forwarder.Broadcast(topic, payload)
|
forwarder.Broadcast(topic, payload)
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var socketIOEventTypes = map[string]string{
|
var socketIOEventTypes = map[string]string{
|
||||||
|
reflect.TypeOf(api.EventLifeCycle{}).Name(): "/lifecycle",
|
||||||
|
reflect.TypeOf(api.EventFarmStatus{}).Name(): "/status",
|
||||||
reflect.TypeOf(api.EventJobUpdate{}).Name(): "/jobs",
|
reflect.TypeOf(api.EventJobUpdate{}).Name(): "/jobs",
|
||||||
reflect.TypeOf(api.EventTaskUpdate{}).Name(): "/task",
|
reflect.TypeOf(api.EventTaskUpdate{}).Name(): "/task",
|
||||||
reflect.TypeOf(api.EventLastRenderedUpdate{}).Name(): "/last-rendered",
|
reflect.TypeOf(api.EventLastRenderedUpdate{}).Name(): "/last-rendered",
|
||||||
@ -91,6 +93,10 @@ func (s *SocketIOForwarder) registerSIOEventHandlers() {
|
|||||||
_ = sio.On(gosocketio.OnConnection, func(c *gosocketio.Channel) {
|
_ = sio.On(gosocketio.OnConnection, func(c *gosocketio.Channel) {
|
||||||
logger := sioLogger(c)
|
logger := sioLogger(c)
|
||||||
logger.Debug().Msg("socketIO: connected")
|
logger.Debug().Msg("socketIO: connected")
|
||||||
|
|
||||||
|
// All SocketIO connections get these events, regardless of their subscription.
|
||||||
|
_ = c.Join(string(TopicLifeCycle))
|
||||||
|
_ = c.Join(string(TopicFarmStatus))
|
||||||
})
|
})
|
||||||
|
|
||||||
// socket disconnection
|
// socket disconnection
|
||||||
|
@ -20,7 +20,7 @@ const (
|
|||||||
//
|
//
|
||||||
// Note that this indicates the time between polls, so between a poll
|
// Note that this indicates the time between polls, so between a poll
|
||||||
// operation being done, and the next one starting.
|
// operation being done, and the next one starting.
|
||||||
pollWait = 5 * time.Second
|
pollWait = 30 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service keeps track of the overall farm status.
|
// Service keeps track of the overall farm status.
|
||||||
@ -30,17 +30,24 @@ type Service struct {
|
|||||||
|
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
lastReport api.FarmStatusReport
|
lastReport api.FarmStatusReport
|
||||||
|
forcePoll chan struct{} // Send anything here to force a poll, if none is running yet.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewService returns a 'farm status' service. Run its Run() function in a
|
||||||
|
// goroutine to make it actually do something.
|
||||||
func NewService(persist PersistenceService, eventbus EventBus) *Service {
|
func NewService(persist PersistenceService, eventbus EventBus) *Service {
|
||||||
return &Service{
|
service := Service{
|
||||||
persist: persist,
|
persist: persist,
|
||||||
eventbus: eventbus,
|
eventbus: eventbus,
|
||||||
mutex: sync.Mutex{},
|
mutex: sync.Mutex{},
|
||||||
|
forcePoll: make(chan struct{}, 1),
|
||||||
lastReport: api.FarmStatusReport{
|
lastReport: api.FarmStatusReport{
|
||||||
Status: api.FarmStatusStarting,
|
Status: api.FarmStatusStarting,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventbus.AddListener(&service)
|
||||||
|
return &service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the farm status polling loop.
|
// Run the farm status polling loop.
|
||||||
@ -54,10 +61,43 @@ func (s *Service) Run(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
case <-time.After(pollWait):
|
case <-time.After(pollWait):
|
||||||
s.poll(ctx)
|
s.poll(ctx)
|
||||||
|
case <-s.forcePoll:
|
||||||
|
s.poll(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) OnEvent(topic eventbus.EventTopic, payload interface{}) {
|
||||||
|
forcePoll := false
|
||||||
|
eventSubject := ""
|
||||||
|
|
||||||
|
switch event := payload.(type) {
|
||||||
|
case api.EventJobUpdate:
|
||||||
|
forcePoll = event.PreviousStatus != nil && *event.PreviousStatus != event.Status
|
||||||
|
eventSubject = "job"
|
||||||
|
case api.EventWorkerUpdate:
|
||||||
|
forcePoll = event.PreviousStatus != nil && *event.PreviousStatus != event.Status
|
||||||
|
eventSubject = "worker"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !forcePoll {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("event", string(topic)).
|
||||||
|
Msgf("farm status: investigating after %s status change", eventSubject)
|
||||||
|
|
||||||
|
// Polling queries the database, and thus can have a non-trivial duration.
|
||||||
|
// Better to run in the Run() goroutine.
|
||||||
|
select {
|
||||||
|
case s.forcePoll <- struct{}{}:
|
||||||
|
default:
|
||||||
|
// If sending to the channel fails, there is already a struct{}{} in
|
||||||
|
// there, and thus a poll will be triggered ASAP anyway.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Report returns the last-known farm status report.
|
// Report returns the last-known farm status report.
|
||||||
//
|
//
|
||||||
// It is updated every few seconds, from the Run() function.
|
// It is updated every few seconds, from the Run() function.
|
||||||
|
@ -225,6 +225,8 @@ func fixtures(t *testing.T) *Fixtures {
|
|||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calling NewService() immediate registers as a listener with the event bus.
|
||||||
|
f.eventbus.EXPECT().AddListener(gomock.Any())
|
||||||
f.service = NewService(f.persist, f.eventbus)
|
f.service = NewService(f.persist, f.eventbus)
|
||||||
|
|
||||||
return &f
|
return &f
|
||||||
|
@ -19,6 +19,7 @@ type PersistenceService interface {
|
|||||||
var _ PersistenceService = (*persistence.DB)(nil)
|
var _ PersistenceService = (*persistence.DB)(nil)
|
||||||
|
|
||||||
type EventBus interface {
|
type EventBus interface {
|
||||||
|
AddListener(listener eventbus.Listener)
|
||||||
BroadcastFarmStatusEvent(event api.EventFarmStatus)
|
BroadcastFarmStatusEvent(event api.EventFarmStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
eventbus "projects.blender.org/studio/flamenco/internal/manager/eventbus"
|
||||||
persistence "projects.blender.org/studio/flamenco/internal/manager/persistence"
|
persistence "projects.blender.org/studio/flamenco/internal/manager/persistence"
|
||||||
api "projects.blender.org/studio/flamenco/pkg/api"
|
api "projects.blender.org/studio/flamenco/pkg/api"
|
||||||
)
|
)
|
||||||
@ -89,6 +90,18 @@ func (m *MockEventBus) EXPECT() *MockEventBusMockRecorder {
|
|||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddListener mocks base method.
|
||||||
|
func (m *MockEventBus) AddListener(arg0 eventbus.Listener) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "AddListener", arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddListener indicates an expected call of AddListener.
|
||||||
|
func (mr *MockEventBusMockRecorder) AddListener(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddListener", reflect.TypeOf((*MockEventBus)(nil).AddListener), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// BroadcastFarmStatusEvent mocks base method.
|
// BroadcastFarmStatusEvent mocks base method.
|
||||||
func (m *MockEventBus) BroadcastFarmStatusEvent(arg0 api.EventFarmStatus) {
|
func (m *MockEventBus) BroadcastFarmStatusEvent(arg0 api.EventFarmStatus) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user