239 lines
5.3 KiB
Go
239 lines
5.3 KiB
Go
package manager
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"gitlab.michelsen.id/phillmichelsen/tessera/services/data_service/internal/domain"
|
|
)
|
|
|
|
var (
|
|
// Lease lifecycle errors.
|
|
ErrAlreadyReleased = errors.New("lease already released")
|
|
ErrSenderAlreadyLeased = errors.New("sender already leased")
|
|
ErrReceiverAlreadyLeased = errors.New("receiver already leased")
|
|
ErrSenderNotLeased = errors.New("no sender lease active")
|
|
ErrReceiverNotLeased = errors.New("no receiver lease active")
|
|
|
|
// Config errors
|
|
ErrBadConfig = errors.New("config not valid")
|
|
ErrConfigActiveLeases = errors.New("cannot configure while a lease is active")
|
|
)
|
|
|
|
type WorkerEntry struct {
|
|
Type string
|
|
Spec []byte
|
|
Unit []byte
|
|
}
|
|
|
|
// SessionConfig carries non-live-tunable knobs for a session.
|
|
// Manager mutates this directly; session does not expose Configure anymore.
|
|
type SessionConfig struct {
|
|
IdleAfter time.Duration // <=0 disables idle timer
|
|
EgressBuffer int // receiver egress buffer size
|
|
Patterns []domain.Pattern
|
|
Workers []WorkerEntry
|
|
}
|
|
|
|
// session is manager-owned state. Single goroutine access.
|
|
type session struct {
|
|
id uuid.UUID
|
|
|
|
// Router pipes
|
|
ingress chan<- domain.Message // router.Incoming(); router-owned
|
|
egress chan domain.Message // current receiver lease egress; owned here
|
|
|
|
// Config and timers
|
|
cfg SessionConfig
|
|
idleTimer *time.Timer
|
|
idleCallback func() // stored on creation
|
|
|
|
// Sender lease
|
|
sendOpen bool
|
|
sendDone chan struct{}
|
|
|
|
// Receiver lease
|
|
receiveOpen bool
|
|
receiveDone chan struct{}
|
|
}
|
|
|
|
// newSession arms a 1-minute idle timer immediately. Manager must
|
|
// configure before it fires. idleCb is invoked by the timer.
|
|
func newSession(ingress chan<- domain.Message, idleCb func()) *session {
|
|
s := &session{
|
|
id: uuid.New(),
|
|
ingress: ingress,
|
|
cfg: SessionConfig{
|
|
IdleAfter: time.Minute, // default 1m on creation
|
|
EgressBuffer: 256, // default buffer
|
|
},
|
|
idleCallback: idleCb,
|
|
}
|
|
s.armIdleTimer()
|
|
return s
|
|
}
|
|
|
|
func (s *session) setConfig(cfg any) error {
|
|
if s.sendOpen || s.receiveOpen {
|
|
return ErrConfigActiveLeases
|
|
}
|
|
|
|
cfgParsed, ok := cfg.(SessionConfig)
|
|
if !ok {
|
|
return ErrBadConfig
|
|
}
|
|
|
|
s.cfg = cfgParsed
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *session) getEgress() (chan<- domain.Message, bool) {
|
|
if s.egress == nil {
|
|
return nil, false
|
|
}
|
|
return s.egress, true
|
|
}
|
|
|
|
func (s *session) getPatterns() []domain.Pattern {
|
|
return nil
|
|
}
|
|
|
|
// leaseSender opens a sender lease and returns send(m) error.
|
|
func (s *session) leaseSender() (func(domain.Message) error, error) {
|
|
if s.sendOpen {
|
|
return nil, ErrSenderAlreadyLeased
|
|
}
|
|
s.sendOpen = true
|
|
s.sendDone = make(chan struct{})
|
|
s.disarmIdleTimer()
|
|
|
|
// Snapshot for lease-scoped handle.
|
|
done := s.sendDone
|
|
|
|
sendFunc := func(m domain.Message) error {
|
|
select {
|
|
case <-done:
|
|
return ErrAlreadyReleased
|
|
case s.ingress <- m:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return sendFunc, nil
|
|
}
|
|
|
|
// releaseSender releases the current sender lease.
|
|
func (s *session) releaseSender() error {
|
|
if !s.sendOpen {
|
|
return ErrSenderNotLeased
|
|
}
|
|
s.sendOpen = false
|
|
if s.sendDone != nil {
|
|
close(s.sendDone) // invalidates all prior send funcs
|
|
s.sendDone = nil
|
|
}
|
|
if !s.receiveOpen {
|
|
s.armIdleTimer()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// leaseReceiver opens a receiver lease and returns receive() (Message, error).
|
|
func (s *session) leaseReceiver() (func() (domain.Message, error), error) {
|
|
if s.receiveOpen {
|
|
return nil, ErrReceiverAlreadyLeased
|
|
}
|
|
s.receiveOpen = true
|
|
s.receiveDone = make(chan struct{})
|
|
s.egress = make(chan domain.Message, s.cfg.EgressBuffer)
|
|
s.disarmIdleTimer()
|
|
|
|
// Snapshots for lease-scoped handles.
|
|
done := s.receiveDone
|
|
eg := s.egress
|
|
|
|
receiveFunc := func() (domain.Message, error) {
|
|
select {
|
|
case <-done:
|
|
return domain.Message{}, ErrAlreadyReleased
|
|
case msg, ok := <-eg:
|
|
if !ok {
|
|
return domain.Message{}, ErrAlreadyReleased
|
|
}
|
|
return msg, nil
|
|
}
|
|
}
|
|
|
|
return receiveFunc, nil
|
|
}
|
|
|
|
// releaseReceiver releases the current receiver lease.
|
|
// Manager must stop any routing into s.egress before calling this.
|
|
func (s *session) releaseReceiver() error {
|
|
if !s.receiveOpen {
|
|
return ErrReceiverNotLeased
|
|
}
|
|
s.receiveOpen = false
|
|
if s.receiveDone != nil {
|
|
close(s.receiveDone) // invalidates all prior receive funcs
|
|
s.receiveDone = nil
|
|
}
|
|
if s.egress != nil {
|
|
close(s.egress)
|
|
s.egress = nil
|
|
}
|
|
if !s.sendOpen {
|
|
s.armIdleTimer()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// closeAll force-releases both sender and receiver leases. Safe to call multiple times.
|
|
func (s *session) closeAll() {
|
|
// Sender
|
|
if s.sendOpen {
|
|
s.sendOpen = false
|
|
if s.sendDone != nil {
|
|
close(s.sendDone)
|
|
s.sendDone = nil
|
|
}
|
|
}
|
|
// Receiver
|
|
if s.receiveOpen {
|
|
s.receiveOpen = false
|
|
if s.receiveDone != nil {
|
|
close(s.receiveDone)
|
|
s.receiveDone = nil
|
|
}
|
|
if s.egress != nil {
|
|
close(s.egress)
|
|
s.egress = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// armIdleTimer arms a timer using stored cfg.IdleAfter and idleCb.
|
|
func (s *session) armIdleTimer() {
|
|
if s.idleCallback == nil {
|
|
slog.Warn("nil idle callback function provided to session")
|
|
}
|
|
if s.idleTimer != nil {
|
|
s.idleTimer.Stop()
|
|
s.idleTimer = nil
|
|
}
|
|
if s.cfg.IdleAfter > 0 && s.idleCallback != nil {
|
|
s.idleTimer = time.AfterFunc(s.cfg.IdleAfter, s.idleCallback)
|
|
}
|
|
}
|
|
|
|
// disarmIdleTimer disarms the idle timer if active.
|
|
func (s *session) disarmIdleTimer() {
|
|
if s.idleTimer != nil {
|
|
s.idleTimer.Stop()
|
|
s.idleTimer = nil
|
|
}
|
|
}
|