160
internal/service/service.go
Normal file
160
internal/service/service.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"zwift-activity-loader/internal/client"
|
||||
"zwift-activity-loader/internal/config"
|
||||
"zwift-activity-loader/internal/monitor"
|
||||
"zwift-activity-loader/internal/state"
|
||||
"zwift-activity-loader/internal/storage"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Service represents the main monitoring service
|
||||
type Service struct {
|
||||
config *config.Config
|
||||
client *client.Client
|
||||
monitor *monitor.Monitor
|
||||
state *state.State
|
||||
storage *storage.Storage
|
||||
logger *zap.Logger
|
||||
ticker *time.Ticker
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// New creates a new Service instance
|
||||
func New(cfg *config.Config, logger *zap.Logger) (*Service, error) {
|
||||
// Create client
|
||||
zwiftClient, err := client.NewClient(cfg.Username, cfg.Password, cfg.RateLimit, cfg.MaxRetries)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Zwift client: %w", err)
|
||||
}
|
||||
|
||||
// Create storage
|
||||
stor, err := storage.New(cfg.OutputDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create storage: %w", err)
|
||||
}
|
||||
|
||||
// Create state
|
||||
st := state.New(cfg.StateFile)
|
||||
if err := st.Load(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load state: %w", err)
|
||||
}
|
||||
|
||||
// Create monitor
|
||||
mon := monitor.New(zwiftClient, st, stor, logger)
|
||||
|
||||
// Create context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &Service{
|
||||
config: cfg,
|
||||
client: zwiftClient,
|
||||
monitor: mon,
|
||||
state: st,
|
||||
storage: stor,
|
||||
logger: logger,
|
||||
ticker: time.NewTicker(cfg.PollInterval),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run starts the service loop
|
||||
func (s *Service) Run() error {
|
||||
s.logger.Info("Starting Zwift Activity Monitor",
|
||||
zap.Duration("poll_interval", s.config.PollInterval),
|
||||
zap.String("output_dir", s.config.OutputDir))
|
||||
|
||||
// Get initial stats
|
||||
totalDownloaded, lastCheck := s.state.GetStats()
|
||||
s.logger.Info("Loaded state",
|
||||
zap.Int("total_downloaded", totalDownloaded),
|
||||
zap.Time("last_check", lastCheck))
|
||||
|
||||
// Run initial check immediately
|
||||
s.logger.Info("Running initial check...")
|
||||
if err := s.runCheck(); err != nil {
|
||||
s.logger.Error("Initial check failed", zap.Error(err))
|
||||
}
|
||||
|
||||
// Start ticker loop
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
s.logger.Info("Service context cancelled, shutting down")
|
||||
return nil
|
||||
|
||||
case <-s.ticker.C:
|
||||
if err := s.runCheck(); err != nil {
|
||||
s.logger.Error("Check failed", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runCheck runs a single check for new activities
|
||||
func (s *Service) runCheck() error {
|
||||
// Recover from panics
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
s.logger.Error("Panic recovered in runCheck",
|
||||
zap.Any("panic", r),
|
||||
zap.Stack("stack"))
|
||||
}
|
||||
}()
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(s.ctx, 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Run check and download
|
||||
return s.monitor.CheckAndDownload(ctx)
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the service
|
||||
func (s *Service) Shutdown() error {
|
||||
s.logger.Info("Shutting down gracefully...")
|
||||
|
||||
// Stop ticker
|
||||
s.ticker.Stop()
|
||||
|
||||
// Cancel context (with timeout for current operation)
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Wait for current operation to complete
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
s.cancel()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
s.logger.Info("Service stopped")
|
||||
case <-shutdownCtx.Done():
|
||||
s.logger.Warn("Shutdown timeout, forcing exit")
|
||||
}
|
||||
|
||||
// Save final state
|
||||
if err := s.state.Save(); err != nil {
|
||||
s.logger.Error("Failed to save state on shutdown", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// Close client
|
||||
if err := s.client.Close(); err != nil {
|
||||
s.logger.Error("Failed to close client", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Shutdown complete")
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user