139 lines
4.5 KiB
Go
139 lines
4.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gocql/gocql"
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/scheduler"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/scheduler/tasks"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/storage/database/cassandradb"
|
|
)
|
|
|
|
// Application represents the main application using Wire DI
|
|
type Application struct {
|
|
config *config.Config
|
|
httpServer *http.WireServer
|
|
logger *zap.Logger
|
|
migrator *cassandradb.Migrator
|
|
scheduler *scheduler.Scheduler
|
|
ipAnonymizationTask *tasks.IPAnonymizationTask
|
|
dbSession *gocql.Session
|
|
}
|
|
|
|
// ProvideApplication creates the application instance for Wire
|
|
func ProvideApplication(
|
|
cfg *config.Config,
|
|
httpServer *http.WireServer,
|
|
logger *zap.Logger,
|
|
migrator *cassandradb.Migrator,
|
|
sched *scheduler.Scheduler,
|
|
ipAnonymizationTask *tasks.IPAnonymizationTask,
|
|
dbSession *gocql.Session,
|
|
) *Application {
|
|
return &Application{
|
|
config: cfg,
|
|
httpServer: httpServer,
|
|
logger: logger,
|
|
migrator: migrator,
|
|
scheduler: sched,
|
|
ipAnonymizationTask: ipAnonymizationTask,
|
|
dbSession: dbSession,
|
|
}
|
|
}
|
|
|
|
// Start starts the application
|
|
func (app *Application) Start() error {
|
|
app.logger.Info("🚀 MapleFile Backend Starting (Wire DI)",
|
|
zap.String("version", app.config.App.Version),
|
|
zap.String("environment", app.config.App.Environment),
|
|
zap.String("di_framework", "Google Wire"))
|
|
|
|
// Run database migrations automatically on startup if enabled
|
|
if app.config.Database.AutoMigrate {
|
|
app.logger.Info("Auto-migration enabled, running database migrations...")
|
|
if err := app.migrator.Up(); err != nil {
|
|
app.logger.Error("Failed to run database migrations", zap.Error(err))
|
|
return fmt.Errorf("migration failed: %w", err)
|
|
}
|
|
app.logger.Info("✅ Database migrations completed successfully")
|
|
|
|
// Wait for schema agreement across all Cassandra nodes
|
|
// This ensures all nodes have the new schema before we start accepting requests
|
|
app.logger.Info("⏳ Waiting for Cassandra schema agreement...")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
if err := app.dbSession.AwaitSchemaAgreement(ctx); err != nil {
|
|
app.logger.Warn("Schema agreement wait failed, continuing anyway",
|
|
zap.Error(err),
|
|
zap.String("note", "This may cause transient errors on first requests"))
|
|
} else {
|
|
app.logger.Info("✅ Cassandra schema agreement reached")
|
|
}
|
|
} else {
|
|
app.logger.Info("Auto-migration disabled (DATABASE_AUTO_MIGRATE=false), skipping migrations")
|
|
}
|
|
|
|
// Register scheduled tasks
|
|
app.logger.Info("Registering scheduled tasks...")
|
|
if err := app.scheduler.RegisterTask(app.ipAnonymizationTask); err != nil {
|
|
app.logger.Error("Failed to register IP anonymization task", zap.Error(err))
|
|
return fmt.Errorf("task registration failed: %w", err)
|
|
}
|
|
|
|
// Start scheduler
|
|
if err := app.scheduler.Start(); err != nil {
|
|
app.logger.Error("Failed to start scheduler", zap.Error(err))
|
|
return fmt.Errorf("scheduler startup failed: %w", err)
|
|
}
|
|
|
|
// Start HTTP server in goroutine
|
|
errChan := make(chan error, 1)
|
|
go func() {
|
|
if err := app.httpServer.Start(); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// Wait for interrupt signal or server error
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
select {
|
|
case err := <-errChan:
|
|
app.logger.Error("HTTP server failed", zap.Error(err))
|
|
return fmt.Errorf("server startup failed: %w", err)
|
|
case sig := <-quit:
|
|
app.logger.Info("Received shutdown signal", zap.String("signal", sig.String()))
|
|
}
|
|
|
|
app.logger.Info("👋 MapleFile Backend Shutting Down")
|
|
|
|
// Graceful shutdown with timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Stop scheduler first
|
|
app.logger.Info("Stopping scheduler...")
|
|
if err := app.scheduler.Stop(); err != nil {
|
|
app.logger.Error("Scheduler shutdown error", zap.Error(err))
|
|
// Continue with shutdown even if scheduler fails
|
|
}
|
|
|
|
// Stop HTTP server
|
|
if err := app.httpServer.Shutdown(ctx); err != nil {
|
|
app.logger.Error("Server shutdown error", zap.Error(err))
|
|
return fmt.Errorf("server shutdown failed: %w", err)
|
|
}
|
|
|
|
app.logger.Info("✅ MapleFile Backend Stopped Successfully")
|
|
return nil
|
|
}
|