Files
openfsd/bootstrap/bootstrap.go
Reese Norris 57d54d6705 v0.1.0-alpha
Changes:
- Implement bootstrapping library for managing several concurrent internal services
- Refactor concurrency model for connections/logical clients and their associated I/O
- Refactor server context singleton
- Refactor error handling
    - Most errors are now gracefully sent to the FSD client directly encoded as an $ER packet,
      enhancing visibility and debugging
    - Most errors are now rightfully treated as non-fatal
- Refactor package/dependency graph
- Refactor calling conventions/interfaces for many packages
- Refactor database package
- Refactor post office

Features:
- Add VATSIM-esque HTTP/JSON "data feed"
- Add ephemeral in-memory database option
- Add user management REST API
- Add improved web interface
- Add MySQL support (drop SQLite support)
2024-10-07 12:50:39 -07:00

147 lines
3.6 KiB
Go

package bootstrap
import (
"context"
"errors"
"github.com/renorris/openfsd/bootstrap/service"
"github.com/renorris/openfsd/servercontext"
"reflect"
)
// Bootstrap bootstraps/manages multiple services running concurrently
type Bootstrap struct {
services []Service
startErrCh chan error
doneErrs []chan error
Done chan struct{}
}
type Service interface {
// Start is the function call to start a service.
//
// Start should return once the service is healthily running.
// Start should return diligently, blocking for minimal time.
//
// A service is expected to promptly shut itself down on ctx close.
//
// (It is convention that a service runs concurrently on its own,
// using ctx as the signal to eventually shut down.)
// The service must send an error over doneErr when it stops in
// response to the context closing or due to an internal error.
Start(ctx context.Context, doneErr chan<- error) error
}
// NewDefaultBootstrap makes a new bootstrapper for the default openfsd services
func NewDefaultBootstrap() *Bootstrap {
servercontext.InitializeServerContextSingleton(servercontext.New())
services := []Service{&service.FSDService{}, &service.HTTPService{}, &service.DataFeedService{}}
if servercontext.Config().InMemoryDB {
services = append(services, &service.InMemoryDatabaseService{})
}
return NewBootstrap(services)
}
func NewBootstrap(services []Service) *Bootstrap {
return &Bootstrap{
services: services,
startErrCh: make(chan error),
doneErrs: make([]chan error, 0),
Done: make(chan struct{}),
}
}
// Start starts the bootstrapping process.
// Returns when all services have started successfully.
func (b *Bootstrap) Start(c context.Context) error {
ctx, cancel := context.WithCancel(c)
for _, svc := range b.services {
doneErr := make(chan error)
b.doneErrs = append(b.doneErrs, doneErr)
go func(s Service, doneErr chan error) {
b.startErrCh <- s.Start(ctx, doneErr)
}(svc, doneErr)
}
// Wait until all services finish starting
capturedStartErrs := make([]error, 0)
for range b.services {
var err error
if err = <-b.startErrCh; err != nil {
// Fire cancel so all services spin down
cancel()
}
capturedStartErrs = append(capturedStartErrs, err)
}
// Start bootstrap monitor
go b.monitor(cancel)
// Return an error if >0 services ready'd with an error
var errs error
for _, err := range capturedStartErrs {
if err != nil {
errs = errors.Join(errs, err)
}
}
if errs != nil {
return errs
}
return nil
}
func (b *Bootstrap) monitor(cancel func()) {
// Dynamically select the doneErr channel from each service.
// If a signal is received, check the error.
// If non-nil, spin down the other services.
// If nil, noop.
// Once the error from each service has been
// captured, signal that we're closed and return.
cases := make([]reflect.SelectCase, len(b.doneErrs))
for i, ch := range b.doneErrs {
cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
}
capturedDoneSigs := make([]error, 0)
for {
i, val, ok := reflect.Select(cases)
if !ok {
// Remove this channel from the case list
cases[i] = cases[len(cases)-1]
cases = cases[:len(cases)-1]
continue
}
var err error
if val.IsNil() {
err = nil
} else {
err = val.Interface().(error)
}
capturedDoneSigs = append(capturedDoneSigs, err)
// Spin down all services if this service returned an error
if err != nil {
cancel()
}
// Check if all services have returned
if len(capturedDoneSigs) == len(b.doneErrs) {
// Mark the completion of this bootstrapping
// process by closing the done channel.
close(b.Done)
return
}
}
}