278 lines
9.2 KiB
Go
278 lines
9.2 KiB
Go
// Package manager implements the core orchestration logic for data providers and client sessions
|
|
// in the tessera data_service. It manages provider registration, session lifecycle, client attachment,
|
|
// stream configuration, and routing of messages between clients and providers.
|
|
package manager
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"gitlab.michelsen.id/phillmichelsen/tessera/services/data_service/internal/domain"
|
|
"gitlab.michelsen.id/phillmichelsen/tessera/services/data_service/internal/provider"
|
|
"gitlab.michelsen.id/phillmichelsen/tessera/services/data_service/internal/router"
|
|
"gitlab.michelsen.id/phillmichelsen/tessera/services/data_service/internal/worker"
|
|
)
|
|
|
|
var (
|
|
ErrSessionNotFound = errors.New("session not found")
|
|
ErrClientAlreadyAttached = errors.New("client already attached")
|
|
ErrClientNotAttached = errors.New("client not attached")
|
|
)
|
|
|
|
// Manager is a single-goroutine actor that owns all state.
|
|
type Manager struct {
|
|
// Command channel
|
|
cmdCh chan any
|
|
|
|
// State (loop-owned)
|
|
providers map[string]provider.Provider
|
|
sessions map[uuid.UUID]*session
|
|
|
|
// Router
|
|
router *router.Router
|
|
}
|
|
|
|
// NewManager creates a manager and starts its run loop.
|
|
func NewManager(router *router.Router, workerRegistry *worker.Registry) *Manager {
|
|
m := &Manager{
|
|
cmdCh: make(chan any, 256),
|
|
providers: make(map[string]provider.Provider),
|
|
sessions: make(map[uuid.UUID]*session),
|
|
router: router,
|
|
}
|
|
go router.Start()
|
|
go m.run()
|
|
|
|
slog.Default().Info("manager started", slog.String("cmp", "manager"))
|
|
|
|
return m
|
|
}
|
|
|
|
// API
|
|
|
|
// AddProvider adds and starts a new provider.
|
|
func (m *Manager) AddProvider(name string, p provider.Provider) error {
|
|
slog.Default().Debug("add provider request", slog.String("cmp", "manager"), slog.String("name", name))
|
|
resp := make(chan addProviderResult, 1)
|
|
m.cmdCh <- addProviderCmd{name: name, p: p, resp: resp}
|
|
|
|
r := <-resp
|
|
|
|
slog.Default().Info("provider added", slog.String("cmp", "manager"), slog.String("name", name))
|
|
return r.err
|
|
}
|
|
|
|
// RemoveProvider stops and removes a provider, cleaning up all sessions.
|
|
func (m *Manager) RemoveProvider(name string) error {
|
|
slog.Default().Debug("remove provider request", slog.String("cmp", "manager"), slog.String("name", name))
|
|
resp := make(chan removeProviderResult, 1)
|
|
m.cmdCh <- removeProviderCmd{name: name, resp: resp}
|
|
|
|
r := <-resp
|
|
|
|
slog.Default().Info("provider removed", slog.String("cmp", "manager"), slog.String("name", name))
|
|
return r.err
|
|
}
|
|
|
|
// NewSession creates a new session with the given idle timeout.
|
|
func (m *Manager) NewSession(idleAfter time.Duration) uuid.UUID {
|
|
slog.Default().Debug("new session request", slog.String("cmp", "manager"), slog.Duration("idle_after", idleAfter))
|
|
resp := make(chan newSessionResult, 1)
|
|
m.cmdCh <- newSessionCmd{idleAfter: idleAfter, resp: resp}
|
|
|
|
r := <-resp
|
|
|
|
slog.Default().Info("new session created", slog.String("cmp", "manager"), slog.String("session", r.id.String()))
|
|
return r.id
|
|
}
|
|
|
|
// AttachClient attaches a client to a session, creates and returns client channels for the session.
|
|
func (m *Manager) AttachClient(id uuid.UUID, inBuf, outBuf int) (chan<- domain.Message, <-chan domain.Message, error) {
|
|
slog.Default().Debug("attach client request", slog.String("cmp", "manager"), slog.String("session", id.String()), slog.Int("in_buf", inBuf), slog.Int("out_buf", outBuf))
|
|
resp := make(chan attachResult, 1)
|
|
m.cmdCh <- attachCmd{sid: id, inBuf: inBuf, outBuf: outBuf, resp: resp}
|
|
|
|
r := <-resp
|
|
|
|
slog.Default().Info("client attached", slog.String("cmp", "manager"), slog.String("session", id.String()))
|
|
return r.cin, r.cout, r.err
|
|
}
|
|
|
|
// DetachClient detaches the client from the session, closes client channels and arms timeout.
|
|
func (m *Manager) DetachClient(id uuid.UUID) error {
|
|
slog.Default().Debug("detach client request", slog.String("cmp", "manager"), slog.String("session", id.String()))
|
|
resp := make(chan detachResult, 1)
|
|
m.cmdCh <- detachCmd{sid: id, resp: resp}
|
|
|
|
r := <-resp
|
|
|
|
slog.Default().Info("client detached", slog.String("cmp", "manager"), slog.String("session", id.String()))
|
|
return r.err
|
|
}
|
|
|
|
// ConfigureSession sets the next set of patterns for the session, starting and stopping streams as needed.
|
|
func (m *Manager) ConfigureSession(id uuid.UUID, next []domain.Pattern) error {
|
|
slog.Default().Debug("configure session request", slog.String("cmp", "manager"), slog.String("session", id.String()), slog.Int("patterns", len(next)))
|
|
resp := make(chan configureResult, 1)
|
|
m.cmdCh <- configureCmd{sid: id, next: next, resp: resp}
|
|
|
|
r := <-resp
|
|
|
|
slog.Default().Info("session configured", slog.String("cmp", "manager"), slog.String("session", id.String()), slog.String("err", fmt.Sprintf("%v", r.err)))
|
|
return r.err
|
|
}
|
|
|
|
// CloseSession closes and removes the session, cleaning up all bindings.
|
|
func (m *Manager) CloseSession(id uuid.UUID) error {
|
|
slog.Default().Debug("close session request", slog.String("cmp", "manager"), slog.String("session", id.String()))
|
|
resp := make(chan closeSessionResult, 1)
|
|
m.cmdCh <- closeSessionCmd{sid: id, resp: resp}
|
|
|
|
r := <-resp
|
|
|
|
slog.Default().Info("session closed", slog.String("cmp", "manager"), slog.String("session", id.String()))
|
|
return r.err
|
|
}
|
|
|
|
// The main loop of the manager, processing commands serially.
|
|
func (m *Manager) run() {
|
|
for {
|
|
msg := <-m.cmdCh
|
|
switch c := msg.(type) {
|
|
case addProviderCmd:
|
|
m.handleAddProvider(c)
|
|
case removeProviderCmd:
|
|
m.handleRemoveProvider(c)
|
|
case newSessionCmd:
|
|
m.handleNewSession(c)
|
|
case attachCmd:
|
|
m.handleAttach(c)
|
|
case detachCmd:
|
|
m.handleDetach(c)
|
|
case configureCmd:
|
|
m.handleConfigure(c)
|
|
case closeSessionCmd:
|
|
m.handleCloseSession(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Command handlers, run in loop goroutine. With a single goroutine, no locking is needed.
|
|
|
|
// handleAddProvider adds and starts a new provider.
|
|
func (m *Manager) handleAddProvider(cmd addProviderCmd) {
|
|
if _, ok := m.providers[cmd.name]; ok {
|
|
slog.Default().Warn("provider already exists", slog.String("cmp", "manager"), slog.String("name", cmd.name))
|
|
cmd.resp <- addProviderResult{err: fmt.Errorf("provider exists: %s", cmd.name)}
|
|
return
|
|
}
|
|
if err := cmd.p.Start(); err != nil {
|
|
slog.Default().Warn("failed to start provider", slog.String("cmp", "manager"), slog.String("name", cmd.name), slog.String("err", err.Error()))
|
|
cmd.resp <- addProviderResult{err: fmt.Errorf("failed to start provider %s: %w", cmd.name, err)}
|
|
return
|
|
}
|
|
m.providers[cmd.name] = cmd.p
|
|
cmd.resp <- addProviderResult{err: nil}
|
|
}
|
|
|
|
// handleRemoveProvider stops and removes a provider, removing the bindings from all sessions that use streams from it.
|
|
// TODO: Implement this function.
|
|
func (m *Manager) handleRemoveProvider(_ removeProviderCmd) {
|
|
panic("unimplemented")
|
|
}
|
|
|
|
// handleNewSession creates a new session with the given idle timeout. The idle timeout is typically not set by the client, but by the server configuration.
|
|
func (m *Manager) handleNewSession(cmd newSessionCmd) {
|
|
s := newSession(cmd.idleAfter)
|
|
|
|
// Only arm the idle timer if the timeout is positive. We allow a zero or negative timeout to indicate "never timeout".
|
|
if s.idleAfter <= 0 {
|
|
s.armIdleTimer(func() {
|
|
resp := make(chan closeSessionResult, 1)
|
|
m.cmdCh <- closeSessionCmd{sid: s.id, resp: resp}
|
|
<-resp
|
|
})
|
|
}
|
|
|
|
m.sessions[s.id] = s
|
|
|
|
cmd.resp <- newSessionResult{id: s.id}
|
|
}
|
|
|
|
// handleAttach attaches a client to a session, creating new client channels for the session. If the session is already attached, returns an error.
|
|
func (m *Manager) handleAttach(cmd attachCmd) {
|
|
s, ok := m.sessions[cmd.sid]
|
|
if !ok {
|
|
cmd.resp <- attachResult{nil, nil, ErrSessionNotFound}
|
|
return
|
|
}
|
|
if s.attached {
|
|
cmd.resp <- attachResult{nil, nil, ErrClientAlreadyAttached}
|
|
return
|
|
}
|
|
|
|
cin, cout := s.generateNewChannels(cmd.inBuf, cmd.outBuf)
|
|
s.attached = true
|
|
s.disarmIdleTimer()
|
|
|
|
cmd.resp <- attachResult{cin: cin, cout: cout, err: nil}
|
|
}
|
|
|
|
// handleDetach detaches the client from the session, closing client channels and arming the idle timeout. If the session is not attached, returns an error.
|
|
func (m *Manager) handleDetach(cmd detachCmd) {
|
|
s, ok := m.sessions[cmd.sid]
|
|
if !ok {
|
|
cmd.resp <- detachResult{ErrSessionNotFound}
|
|
return
|
|
}
|
|
if !s.attached {
|
|
cmd.resp <- detachResult{ErrClientNotAttached}
|
|
return
|
|
}
|
|
|
|
s.clearChannels()
|
|
|
|
// Only rearm the idle timer if the timeout is positive.
|
|
if s.idleAfter > 0 {
|
|
s.armIdleTimer(func() {
|
|
resp := make(chan closeSessionResult, 1)
|
|
m.cmdCh <- closeSessionCmd{sid: s.id, resp: resp}
|
|
<-resp
|
|
})
|
|
}
|
|
|
|
s.attached = false
|
|
|
|
cmd.resp <- detachResult{nil}
|
|
}
|
|
|
|
// handleConfigure updates the session bindings, starting and stopping streams as needed. Currently only supports Raw streams.
|
|
// TODO: Change this configuration to be an atomic operation, so that partial failures do not end in a half-configured state.
|
|
func (m *Manager) handleConfigure(cmd configureCmd) {
|
|
_, ok := m.sessions[cmd.sid]
|
|
if !ok {
|
|
cmd.resp <- configureResult{ErrSessionNotFound}
|
|
return
|
|
}
|
|
|
|
var errs error
|
|
|
|
cmd.resp <- configureResult{err: errs}
|
|
}
|
|
|
|
// handleCloseSession closes and removes the session, cleaning up all bindings.
|
|
func (m *Manager) handleCloseSession(cmd closeSessionCmd) {
|
|
_, ok := m.sessions[cmd.sid]
|
|
if !ok {
|
|
cmd.resp <- closeSessionResult{err: ErrSessionNotFound}
|
|
return
|
|
}
|
|
|
|
var errs error
|
|
|
|
cmd.resp <- closeSessionResult{err: errs}
|
|
}
|