Sybren A. Stüvel 76a24243f0 Manager: Introduce event bus system
Introduce an "event bus"-like system. It's more like a fan-out
broadcaster for certain events. Instead of directly sending events to
SocketIO, they are now sent to the broker, which in turn sends it to any
registered "forwarder". Currently there is ony one forwarder, for
SocketIO.

This opens the door for a proper MQTT client that sends the same events
to an MQTT server.
2024-02-03 22:55:23 +01:00

169 lines
5.1 KiB
Go

package eventbus
import (
"fmt"
"reflect"
gosocketio "github.com/graarh/golang-socketio"
"github.com/graarh/golang-socketio/transport"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"projects.blender.org/studio/flamenco/internal/uuid"
"projects.blender.org/studio/flamenco/pkg/api"
)
type SocketIOEventType string
const (
SIOEventSubscription SocketIOEventType = "/subscription" // clients send api.SocketIOSubscription
)
var socketIOEventTypes = map[string]string{
reflect.TypeOf(api.SocketIOJobUpdate{}).Name(): "/jobs",
reflect.TypeOf(api.SocketIOTaskUpdate{}).Name(): "/task",
reflect.TypeOf(api.SocketIOLastRenderedUpdate{}).Name(): "/last-rendered",
reflect.TypeOf(api.SocketIOTaskLogUpdate{}).Name(): "/tasklog",
reflect.TypeOf(api.SocketIOWorkerTagUpdate{}).Name(): "/workertags",
reflect.TypeOf(api.SocketIOWorkerUpdate{}).Name(): "/workers",
}
// SocketIOForwarder is an event forwarder via SocketIO.
type SocketIOForwarder struct {
sockserv *gosocketio.Server
}
var _ Forwarder = (*SocketIOForwarder)(nil)
type Message struct {
Name string `json:"name"`
Text string `json:"text"`
}
func NewSocketIOForwarder() *SocketIOForwarder {
siof := SocketIOForwarder{
sockserv: gosocketio.NewServer(transport.GetDefaultWebsocketTransport()),
}
siof.registerSIOEventHandlers()
return &siof
}
func (s *SocketIOForwarder) RegisterHandlers(router *echo.Echo) {
router.Any("/socket.io/", echo.WrapHandler(s.sockserv))
}
func (s *SocketIOForwarder) Broadcast(topic EventTopic, payload interface{}) {
// SocketIO has a concept of 'event types'. MQTT doesn't have this, and thus the Flamenco event
// system doesn't rely on it. We use the payload type name as event type.
payloadType := reflect.TypeOf(payload).Name()
eventType := socketIOEventTypes[payloadType]
log.Debug().
Str("topic", string(topic)).
Str("eventType", eventType).
Interface("payload", payload).
Msg("socketIO: broadcasting message")
s.sockserv.BroadcastTo(string(topic), eventType, payload)
}
func (s *SocketIOForwarder) registerSIOEventHandlers() {
log.Debug().Msg("initialising SocketIO")
sio := s.sockserv
// the sio.On() and c.Join() calls only return an error when there is no
// server connected to them, but that's not possible with our setup.
// Errors are explicitly silenced (by assigning to _) to reduce clutter.
// socket connection
_ = sio.On(gosocketio.OnConnection, func(c *gosocketio.Channel) {
logger := sioLogger(c)
logger.Debug().Msg("socketIO: connected")
})
// socket disconnection
_ = sio.On(gosocketio.OnDisconnection, func(c *gosocketio.Channel) {
logger := sioLogger(c)
logger.Debug().Msg("socketIO: disconnected")
})
_ = sio.On(gosocketio.OnError, func(c *gosocketio.Channel) {
logger := sioLogger(c)
logger.Warn().Msg("socketIO: socketio error")
})
s.registerRoomEventHandlers()
}
func sioLogger(c *gosocketio.Channel) zerolog.Logger {
logger := log.With().
Str("clientID", c.Id()).
Str("remoteAddr", c.Ip()).
Logger()
return logger
}
func (s *SocketIOForwarder) registerRoomEventHandlers() {
_ = s.sockserv.On(string(SIOEventSubscription), s.handleRoomSubscription)
}
func (s *SocketIOForwarder) handleRoomSubscription(c *gosocketio.Channel, subs api.SocketIOSubscription) string {
logger := sioLogger(c)
logCtx := logger.With().
Str("op", string(subs.Op)).
Str("type", string(subs.Type))
if subs.Uuid != nil {
logCtx = logCtx.Str("uuid", string(*subs.Uuid))
}
logger = logCtx.Logger()
if subs.Uuid != nil && !uuid.IsValid(*subs.Uuid) {
logger.Warn().Msg("socketIO: invalid UUID, ignoring subscription request")
return "invalid UUID, ignoring request"
}
var sioRoom EventTopic
switch subs.Type {
case api.SocketIOSubscriptionTypeAllJobs:
sioRoom = TopicJobUpdate
case api.SocketIOSubscriptionTypeAllWorkers:
sioRoom = TopicWorkerUpdate
case api.SocketIOSubscriptionTypeAllLastRendered:
sioRoom = TopicLastRenderedImage
case api.SocketIOSubscriptionTypeAllWorkerTags:
sioRoom = TopicWorkerTagUpdate
case api.SocketIOSubscriptionTypeJob:
if subs.Uuid == nil {
logger.Warn().Msg("socketIO: trying to (un)subscribe to job without UUID")
return "operation on job requires a UUID"
}
sioRoom = topicForJob(*subs.Uuid)
case api.SocketIOSubscriptionTypeTasklog:
if subs.Uuid == nil {
logger.Warn().Msg("socketIO: trying to (un)subscribe to task without UUID")
return "operation on task requires a UUID"
}
sioRoom = topicForTaskLog(*subs.Uuid)
default:
logger.Warn().Msg("socketIO: unknown subscription type, ignoring")
return "unknown subscription type, ignoring request"
}
var err error
switch subs.Op {
case api.SocketIOSubscriptionOperationSubscribe:
err = c.Join(string(sioRoom))
case api.SocketIOSubscriptionOperationUnsubscribe:
err = c.Leave(string(sioRoom))
default:
logger.Warn().Msg("socketIO: invalid subscription operation, ignoring")
return "invalid subscription operation, ignoring request"
}
if err != nil {
logger.Warn().Err(err).Msg("socketIO: performing subscription operation")
return fmt.Sprintf("unable to perform subscription operation: %v", err)
}
logger.Debug().Msg("socketIO: subscription")
return "ok"
}