first commit

This commit is contained in:
2025-05-21 13:51:10 -04:00
commit b61c4b59ec
23 changed files with 3097 additions and 0 deletions

219
app/app.go Normal file
View File

@ -0,0 +1,219 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2023 mysubarumq
// SPDX-FileContributor: alex-savin
package app
import (
"context"
"errors"
"log/slog"
"runtime"
"sync/atomic"
"time"
"git.savin.nyc/alex/mysubaru-mq/bus"
"git.savin.nyc/alex/mysubaru-mq/config"
"git.savin.nyc/alex/mysubaru-mq/listeners"
"git.savin.nyc/alex/mysubaru-mq/system"
"git.savin.nyc/alex/mysubaru-mq/workers"
)
const (
AppVersion = "1.0.1" // the current application version.
AppName = "mysubarumq" // the short app name
)
var (
ErrWorkerIDExists = errors.New("worker id already exists")
ErrListenerIDExists = errors.New("listener id already exists")
)
type App struct {
Options *config.Config // configurable server options
Workers *workers.Workers // worker are network interfaces which listen for new connections
Listeners *listeners.Listeners // listeners are network interfaces which listen for new connections
Info *system.Info // values about the server commonly known as $SYS topics
Log *slog.Logger // minimal no-alloc logger
bus *bus.Bus //
hooks *Hooks // hooks contains hooks for extra functionality such as auth and persistent storage
loop *loop // loop contains tickers for the system event loop
cancel context.CancelFunc //
// done chan bool // indicate that the server is ending
}
// loop contains interval tickers for the system events loop.
type loop struct {
sysInfo *time.Ticker // interval ticker for sending updating $SYS topics
}
type Info struct {
AppVersion string `json:"version"` // the current version of the server
Started int64 `json:"started"` // the time the server started in unix seconds
MemoryAlloc int64 `json:"memory_alloc"` // memory currently allocated
Threads int64 `json:"threads"` // number of active goroutines, named as threads for platform ambiguity
Time int64 `json:"time"` // current time on the server
Uptime int64 `json:"uptime"` // the number of seconds the server has been online
}
// EstablishConnection establishes a new client when a listener accepts a new connection.
// func (a *App) EstablishConnection(listener string, c net.Conn) error {
// cl := a.NewClient(c, listener, "", false)
// return a.attachClient(cl, listener)
// }
// New returns a new instance of mochi mqtt broker. Optional parameters
// can be specified to override some default settings (see Options).
func New(opts *config.Config, bus *bus.Bus, logger *slog.Logger) *App {
if opts == nil {
slog.Error("empty config")
}
ctx, cancel := context.WithCancel(context.Background())
var m runtime.MemStats
runtime.ReadMemStats(&m)
a := &App{
Options: opts,
Workers: workers.New(),
Listeners: listeners.New(),
Info: &system.Info{
Version: AppVersion,
Started: time.Now().Unix(),
MemoryAlloc: int64(m.HeapInuse),
Threads: int64(runtime.NumGoroutine()),
// Time: time.Now().Unix(),
// Uptime: time.Now().Unix() - atomic.LoadInt64(&a.Started),
},
Log: logger,
bus: bus,
hooks: &Hooks{
Log: logger,
},
loop: &loop{
sysInfo: time.NewTicker(time.Second * time.Duration(1)),
},
cancel: cancel,
// done: make(chan bool),
}
chAppWorker, err := a.bus.Subscribe("app:connected", "mysubarumq", a.Options.SubscriptionSize["app:connected"])
if err != nil {
a.Log.Error("couldn't subscribe to a channel", "error", err.Error())
}
go a.eventLoop(ctx, chAppWorker)
return a
}
// AddHook attaches a new Hook to the server. Ideally, this should be called
// before the server is started with s.Serve().
func (a *App) AddHook(hook Hook, config any) error {
nl := a.Log.With("hook", hook.ID())
hook.SetOpts(nl)
a.Log.Info("added hook", "hook", hook.ID())
return a.hooks.Add(hook, config)
}
// AddListener adds a new network listener to the server, for receiving incoming client connections.
func (a *App) AddWorker(w workers.Worker) error {
if _, ok := a.Workers.Get(w.ID()); ok {
return ErrWorkerIDExists
}
nl := a.Log.With(slog.String("worker", w.ID()))
err := w.Init(nl)
if err != nil {
return err
}
a.Workers.Add(w)
a.Log.Info("attached worker", "id", w.ID(), "type", w.Type())
return nil
}
// AddListener adds a new network listener to the server, for receiving incoming client connections.
func (a *App) AddListener(l listeners.Listener) error {
if _, ok := a.Listeners.Get(l.ID()); ok {
return ErrListenerIDExists
}
nl := a.Log.With(slog.String("listener", l.ID()))
err := l.Init(nl)
if err != nil {
return err
}
a.Listeners.Add(l)
a.Log.Info("attached listener", "id", l.ID(), "protocol", l.Protocol(), "address", l.Address())
return nil
}
// Serve starts the event loops responsible for establishing client connections
// on all attached listeners, publishing the system topics, and starting all hooks.
func (a *App) Serve() error {
a.Log.Info("mysubarumq starting", "version", AppVersion)
defer a.Log.Info("mysubarumq started")
a.Workers.ServeAll() // start listening on all workers.
a.Listeners.ServeAll() // start listening on all listeners.
a.publishSysTopics()
a.hooks.OnStarted()
return nil
}
// Close attempts to gracefully shut down the server, all listeners, clients, and stores.
func (a *App) Close() error {
// close(a.done)
a.cancel()
a.Log.Info("gracefully stopping mysubarumq")
a.Workers.CloseAll()
a.Listeners.CloseAll()
a.hooks.OnStopped()
a.hooks.Stop()
a.Log.Info("mysubarumq stopped")
return nil
}
// eventLoop loops forever
func (a *App) eventLoop(ctx context.Context, chAppWorker chan bus.Event) {
a.Log.Info("app communication event loop started")
defer a.Log.Info("app communication event loop halted")
for {
select {
case <-a.loop.sysInfo.C:
a.publishSysTopics()
case event := <-chAppWorker:
message := event.Data.(*bus.ConnectionStatus)
a.Log.Info("got a message from worker", "worker", message.WorkerID, "type", message.WorkerType, "isconnected", message.IsConnected)
if !message.IsConnected {
a.hooks.OnWorkerDisconnected()
a.Workers.Close(message.WorkerID)
}
a.hooks.OnWorkerConnected()
case <-ctx.Done():
a.Log.Info("stopping app communication event loop")
return
}
}
}
// publishSysTopics publishes the current values to the server $SYS topics.
// Due to the int to string conversions this method is not as cheap as
// some of the others so the publishing interval should be set appropriately.
func (a *App) publishSysTopics() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
atomic.StoreInt64(&a.Info.MemoryAlloc, int64(m.HeapInuse))
atomic.StoreInt64(&a.Info.Threads, int64(runtime.NumGoroutine()))
atomic.StoreInt64(&a.Info.Time, time.Now().Unix())
atomic.StoreInt64(&a.Info.Uptime, time.Now().Unix()-atomic.LoadInt64(&a.Info.Started))
a.hooks.OnSysInfoTick(a.Info)
}

220
app/hooks.go Normal file
View File

@ -0,0 +1,220 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2023 mysubarumq
// SPDX-FileContributor: alex-savin
package app
import (
"errors"
"fmt"
"log/slog"
"sync"
"sync/atomic"
"git.savin.nyc/alex/mysubaru-mq/system"
)
const (
SetOptions byte = iota
OnSysInfoTick
OnStarted
OnStopped
OnWorkerConnected
OnWorkerDisconnected
// OnSysInfoTick
)
var (
// ErrInvalidConfigType indicates a different Type of config value was expected to what was received.
ErrInvalidConfigType = errors.New("invalid config type provided")
)
// Hook provides an interface of handlers for different events which occur
// during the lifecycle of the broker.
type Hook interface {
ID() string
Provides(b byte) bool
Init(config any) error
Stop() error
SetOpts(l *slog.Logger)
OnSysInfoTick(*system.Info)
OnStarted()
OnStopped()
OnWorkerConnected()
OnWorkerDisconnected()
// OnSysInfoTick(*system.Info)
}
// Hooks is a slice of Hook interfaces to be called in sequence.
type Hooks struct {
Log *slog.Logger // a logger for the hook (from the server)
internal atomic.Value // a slice of []Hook
wg sync.WaitGroup // a waitgroup for syncing hook shutdown
qty int64 // the number of hooks in use
sync.Mutex // a mutex for locking when adding hooks
}
// Len returns the number of hooks added.
func (h *Hooks) Len() int64 {
return atomic.LoadInt64(&h.qty)
}
// Provides returns true if any one hook provides any of the requested hook methods.
func (h *Hooks) Provides(b ...byte) bool {
for _, hook := range h.GetAll() {
for _, hb := range b {
if hook.Provides(hb) {
return true
}
}
}
return false
}
// Add adds and initializes a new hook.
func (h *Hooks) Add(hook Hook, config any) error {
h.Lock()
defer h.Unlock()
err := hook.Init(config)
if err != nil {
return fmt.Errorf("failed initialising %s hook: %w", hook.ID(), err)
}
i, ok := h.internal.Load().([]Hook)
if !ok {
i = []Hook{}
}
i = append(i, hook)
h.internal.Store(i)
atomic.AddInt64(&h.qty, 1)
h.wg.Add(1)
return nil
}
// GetAll returns a slice of all the hooks.
func (h *Hooks) GetAll() []Hook {
i, ok := h.internal.Load().([]Hook)
if !ok {
return []Hook{}
}
return i
}
// Stop indicates all attached hooks to gracefully end.
func (h *Hooks) Stop() {
go func() {
for _, hook := range h.GetAll() {
h.Log.Info("stopping hook", "hook", hook.ID())
if err := hook.Stop(); err != nil {
h.Log.Debug("problem stopping hook", "error", err, "hook", hook.ID())
}
h.wg.Done()
}
}()
h.wg.Wait()
}
// OnSysInfoTick is called when the $SYS topic values are published out.
func (h *Hooks) OnSysInfoTick(sys *system.Info) {
for _, hook := range h.GetAll() {
if hook.Provides(OnSysInfoTick) {
hook.OnSysInfoTick(sys)
}
}
}
// OnStarted is called when the server has successfully started.
func (h *Hooks) OnStarted() {
for _, hook := range h.GetAll() {
if hook.Provides(OnStarted) {
hook.OnStarted()
}
}
}
// OnStopped is called when the server has successfully stopped.
func (h *Hooks) OnStopped() {
for _, hook := range h.GetAll() {
if hook.Provides(OnStopped) {
hook.OnStopped()
}
}
}
// OnWorkerConnected is called when the worker has successfully connected.
func (h *Hooks) OnWorkerConnected() {
for _, hook := range h.GetAll() {
if hook.Provides(OnWorkerConnected) {
hook.OnWorkerConnected()
}
}
}
// OnWorkerDisconnected is called when the worker has disconnected.
func (h *Hooks) OnWorkerDisconnected() {
for _, hook := range h.GetAll() {
if hook.Provides(OnWorkerDisconnected) {
hook.OnWorkerDisconnected()
}
}
}
// HookBase provides a set of default methods for each hook. It should be embedded in
// all hooks.
type HookBase struct {
Hook
Log *slog.Logger
}
// ID returns the ID of the hook.
func (h *HookBase) ID() string {
return "base"
}
// Provides indicates which methods a hook provides. The default is none - this method
// should be overridden by the embedding hook.
func (h *HookBase) Provides(b byte) bool {
return false
}
// Init performs any pre-start initializations for the hook, such as connecting to databases
// or opening files.
func (h *HookBase) Init(config any) error {
return nil
}
// SetOpts is called by the server to propagate internal values and generally should
// not be called manually.
func (h *HookBase) SetOpts(l *slog.Logger) {
h.Log = l
}
// Stop is called to gracefully shut down the hook.
func (h *HookBase) Stop() error {
return nil
}
// OnSysInfoTick is called when the server publishes system info.
func (h *HookBase) OnSysInfoTick(*system.Info) {}
// OnStarted is called when the server starts.
func (h *HookBase) OnStarted() {}
// OnStopped is called when the server stops.
func (h *HookBase) OnStopped() {}
// OnWorkerConnected is called when the worker connected.
func (h *HookBase) OnWorkerConnected() {}
// OnWorkerDisconnected is called when the worker disconnected.
func (h *HookBase) OnWorkerDisconnected() {}
// // OnSysInfoTick is called when the server publishes system info.
// func (h *HookBase) OnSysInfoTick(*system.Info) {}