Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controllers refactor to improve maintainability #47

Merged
merged 2 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 96 additions & 49 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import (
logger "github.com/richbl/go-ble-sync-cycle/internal/logging"
speed "github.com/richbl/go-ble-sync-cycle/internal/speed"
video "github.com/richbl/go-ble-sync-cycle/internal/video-player"

"tinygo.org/x/bluetooth"
)

// Application constants
Expand All @@ -38,44 +36,46 @@ func main() {
// Hello world!
log.Println(appPrefix, "Starting", appName, appVersion)

// Load configuration
cfg := loadConfig("config.toml")

// Initialize the shutdown manager and exit handler
sm := NewShutdownManager(shutdownTimeout)
exitHandler := NewExitHandler(sm)
sm.Start()
// Initialize utility services
sm, exitHandler := initializeUtilityServices(cfg)

// Initialize the logger with the configured log level and exit handler
logger.Initialize(cfg.App.LogLevel)
logger.SetExitHandler(func() {
sm.initiateShutdown()
exitHandler.HandleExit()
})
// Initialize application controllers
controllers := initializeControllers(cfg, exitHandler)

// Initialize the application controllers
controllers, componentType, err := setupAppControllers(*cfg)
if err != nil {
logger.Fatal(componentType, "failed to create controllers:", err.Error())
return
}
// Scan for BLE device
bleDeviceDiscovery(sm.shutdownCtx.ctx, controllers, exitHandler)

// Scan for the BLE characteristic and handle context cancellation
bleChar, err := scanForBLECharacteristic(sm.Context(), controllers)
if err != nil {
// Start and monitor services for BLE and video components
monitorServiceRunners(startServiceRunners(sm, controllers))

if err != context.Canceled {
logger.Fatal(logger.BLE, "failed to scan for BLE characteristic:", err.Error())
// Wait for final shutdown sequences to complete and wave goodbye!
sm.Wait()
waveGoodbye()
}

// monitorServiceRunners monitors the services and logs any errors encountered
func monitorServiceRunners(runners []*ServiceRunner) {

for _, runner := range runners {

if err := runner.Error(); err != nil {
logger.Fatal(logger.APP, "service error:", err.Error())
return
}

exitHandler.HandleExit()
return
}
}

// startServiceRunners starts the BLE and video service runners and returns a slice of service runners
func startServiceRunners(sm *ShutdownManager, controllers appControllers) []*ServiceRunner {

// Create and run the BLE service runner
bleRunner := NewServiceRunner(sm, "BLE")
bleRunner.Run(func(ctx context.Context) error {
return controllers.bleController.GetBLEUpdates(ctx, controllers.speedController, bleChar)
return controllers.bleController.GetBLEUpdates(ctx, controllers.speedController)
})

// Create and run the video service runner
Expand All @@ -84,17 +84,57 @@ func main() {
return controllers.videoPlayer.Start(ctx, controllers.speedController)
})

// Wait for services to complete and check for errors
for _, runner := range []*ServiceRunner{bleRunner, videoRunner} {
if err := runner.Error(); err != nil {
logger.Fatal(logger.APP, "service error:", err.Error())
return []*ServiceRunner{bleRunner, videoRunner}
}

// bleDeviceDiscovery scans for the BLE device and CSC speed characteristic
func bleDeviceDiscovery(ctx context.Context, controllers appControllers, exitHandler *ExitHandler) {

err := scanForBLECharacteristic(ctx, controllers)
if err != nil {

if err != context.Canceled {
logger.Fatal(logger.BLE, "failed to scan for BLE characteristic:", err.Error())
return
}

exitHandler.HandleExit()
}
}

// Wait for final shutdown sequences to complete and wave goodbye!
sm.Wait()
waveGoodbye()
// initializeUtilityServices initializes the core components of the application, including the shutdown manager,
// exit handler, and logger
func initializeUtilityServices(cfg *config.Config) (*ShutdownManager, *ExitHandler) {

// Initialize the shutdown manager and exit handler
sm := NewShutdownManager(shutdownTimeout)
exitHandler := NewExitHandler(sm)
sm.Start()

// Initialize the logger
logger.Initialize(cfg.App.LogLevel)

// Set the exit handler for the shutdown manager
logger.SetExitHandler(func() {
sm.initiateShutdown()
exitHandler.HandleExit()
})

return sm, exitHandler
}

// initializeControllers initializes the application controllers, including the speed controller,
// video player, and BLE controller. It returns the initialized controllers
func initializeControllers(cfg *config.Config, exitHandler *ExitHandler) appControllers {

controllers, componentType, err := setupAppControllers(*cfg)

if err != nil {
logger.Fatal(componentType, "failed to create controllers:", err.Error())
exitHandler.HandleExit()
}

return controllers
}

// setupAppControllers creates and initializes all application controllers
Expand All @@ -119,31 +159,38 @@ func setupAppControllers(cfg config.Config) (appControllers, logger.ComponentTyp
}

// scanForBLECharacteristic handles the initial BLE device discovery and characteristic scanning
// using a context for cancellation and returns the discovered characteristic or an error
func scanForBLECharacteristic(ctx context.Context, controllers appControllers) (*bluetooth.DeviceCharacteristic, error) {
func scanForBLECharacteristic(ctx context.Context, controllers appControllers) error {

// Create a channel to receive the result of the BLE characteristic scan
resultsChan := make(chan struct {
char *bluetooth.DeviceCharacteristic
err error
}, 1)
// Create a channel to receive errors from the scan goroutine
errChan := make(chan error, 1)

// BLE peripheral scan and connect
go func() {
defer close(resultsChan)
char, err := controllers.bleController.GetBLECharacteristic(ctx, controllers.speedController)
resultsChan <- struct {
char *bluetooth.DeviceCharacteristic
err error
}{char, err}
defer close(errChan)
scanResult, err := controllers.bleController.ScanForBLEPeripheral(ctx)
if err != nil {
errChan <- err
return
}

connectResult, err := controllers.bleController.ConnectToBLEPeripheral(scanResult)
if err != nil {
errChan <- err
return
}

// Get the BLE characteristic from the connected device
err = controllers.bleController.GetBLECharacteristic(connectResult)
errChan <- err
}()

select {
case <-ctx.Done():
fmt.Print("\r") // Clear the ^C character from the terminal line
logger.Info(logger.BLE, "user-generated interrupt, stopping BLE discovery...")
return nil, ctx.Err()
case result := <-resultsChan:
return result.char, result.err
return ctx.Err()
case result := <-errChan:
return result
}
}

Expand Down
129 changes: 9 additions & 120 deletions cmd/service_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@ package main

import (
"context"
"os"
"os/signal"
"sync"
"syscall"
"time"

logger "github.com/richbl/go-ble-sync-cycle/internal/logging"
)

// ServiceRunner manages individual service goroutines and their lifecycle
Expand All @@ -18,101 +11,6 @@ type ServiceRunner struct {
errChan chan error
}

// ShutdownManager handles graceful shutdown of application components and coordinates cleanup
// operations with context cancellations and timeout management
type ShutdownManager struct {
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
cleanupFuncs []func()
timeout time.Duration
terminated chan struct{}
cleanupOnce sync.Once
}

// ExitHandler coordinates the final application shutdown sequence
type ExitHandler struct {
sm *ShutdownManager
}

// NewShutdownManager creates a new ShutdownManager with the specified timeout duration
func NewShutdownManager(timeout time.Duration) *ShutdownManager {

ctx, cancel := context.WithCancel(context.Background())

return &ShutdownManager{
ctx: ctx,
cancel: cancel,
wg: sync.WaitGroup{},
terminated: make(chan struct{}),
timeout: timeout,
}
}

// Context returns the ShutdownManager's context for cancellation propagation
func (sm *ShutdownManager) Context() context.Context {
return sm.ctx
}

// Wait blocks until the shutdown sequence is complete
func (sm *ShutdownManager) Wait() {
<-sm.terminated
}

// WaitGroup returns the ShutdownManager's WaitGroup for goroutine synchronization
func (sm *ShutdownManager) WaitGroup() *sync.WaitGroup {
return &sm.wg
}

// AddCleanupFn adds a cleanup function to be executed during shutdown
// Note that cleanup functions are executed in reverse order of registration
func (sm *ShutdownManager) AddCleanupFn(fn func()) {
sm.cleanupFuncs = append(sm.cleanupFuncs, fn)
}

// Start begins listening for shutdown signals (SIGINT, SIGTERM)
// When a signal is received, it initiates the shutdown sequence
func (sm *ShutdownManager) Start() {

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

go func() {
<-sigChan
sm.initiateShutdown()
}()
}

// initiateShutdown coordinates the shutdown sequence, including timeout management and cleanup
// function execution, and ensures the shutdown sequence runs only once
func (sm *ShutdownManager) initiateShutdown() {

sm.cleanupOnce.Do(func() {
sm.cancel()
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), sm.timeout)
defer timeoutCancel()
done := make(chan struct{})

go func() {
sm.wg.Wait()
close(done)
}()

select {
case <-done:
case <-timeoutCtx.Done():
logger.Warn(logger.APP, "shutdown timed out, some goroutines may not have cleaned up properly")
}

// Execute cleanup functions (reverse order)
for i := len(sm.cleanupFuncs) - 1; i >= 0; i-- {
sm.cleanupFuncs[i]()
}

close(sm.terminated)
})
}

// NewServiceRunner creates a new ServiceRunner with the specified name and ShutdownManager
func NewServiceRunner(sm *ShutdownManager, name string) *ServiceRunner {

Expand All @@ -127,14 +25,19 @@ func NewServiceRunner(sm *ShutdownManager, name string) *ServiceRunner {
// handling cleanup and error propagation
func (sr *ServiceRunner) Run(fn func(context.Context) error) {

sr.sm.wg.Add(1)
sr.sm.WaitGroup().Add(1)

go func() {
defer sr.sm.wg.Done()
defer sr.sm.WaitGroup().Done()

if err := fn(sr.sm.ctx); err != nil && err != context.Canceled {
if err := fn(sr.sm.shutdownCtx.ctx); err != nil && err != context.Canceled {
sr.errChan <- err
sr.sm.cancel()

// Initiate shutdown on error
if sm := sr.sm; sm != nil {
sm.shutdownCtx.cancel()
}

}

close(sr.errChan)
Expand All @@ -150,19 +53,5 @@ func (sr *ServiceRunner) Error() error {
default:
return nil
}
}

// NewExitHandler creates a new ExitHandler with the specified shutdown manager
func NewExitHandler(sm *ShutdownManager) *ExitHandler {
return &ExitHandler{sm: sm}
}

// HandleExit coordinates the final shutdown sequence and exits the application
func (h *ExitHandler) HandleExit() {

if h.sm != nil {
h.sm.Wait()
}

waveGoodbye()
}
Loading
Loading