diff --git a/main.go b/main.go index 0bb6679..bcb8766 100644 --- a/main.go +++ b/main.go @@ -1,27 +1,31 @@ package main import ( + "context" + "fmt" "net/http" + "os" + "os/signal" "sync" + "syscall" "time" - //"errors" - "github.com/gocql/gocql" - "fmt" helpers "github.com/Lineblocs/go-helpers" _ "github.com/go-sql-driver/mysql" + "github.com/go-redis/redis" + "github.com/gocql/gocql" "github.com/labstack/echo/v4" "github.com/mrwaggel/golimiter" - "github.com/go-redis/redis" "github.com/sirupsen/logrus" + "lineblocs.com/api/database" "lineblocs.com/api/handler" "lineblocs.com/api/model" "lineblocs.com/api/router" "lineblocs.com/api/store" "lineblocs.com/api/utils" - "lineblocs.com/api/database" ) +// Global service variables var dbConn *database.MySQLConn var rdb *redis.Client var cqlCluster *gocql.ClusterConfig @@ -29,11 +33,12 @@ var cqlSess *gocql.Session var data *model.ServerData var customizations *helpers.CustomizationSettings -func updateCustomizationSettings() (error) { +// updateCustomizationSettings fetches configuration updates from helper utilities. +// Any retrieval failure is logged and returned as an error to be handled by the caller. +func updateCustomizationSettings() error { record, err := helpers.GetCustomizationSettings() if err != nil { - utils.Log(logrus.PanicLevel, err.Error()) - panic(err) + utils.Log(logrus.ErrorLevel, err.Error()) return err } @@ -42,103 +47,138 @@ func updateCustomizationSettings() (error) { } func main() { - ticker := time.NewTicker(60 * time.Second) - defer ticker.Stop() - - // Init Logrus and configure channels + // Initialize Logrus and configure targets based on configurations logDestination := utils.Config("LOG_DESTINATIONS") helpers.InitLogrus(logDestination) utils.Log(logrus.InfoLevel, "Running setup methods for api server..") - // Load media_server list from db and create media server - var err error + + // Fetch and load media_server listing. Clean exit on error to allow orchestrator (K8s/Docker) to restart. servers, err := helpers.CreateMediaServers() + if err != nil { + utils.Log(logrus.ErrorLevel, fmt.Sprintf("Failed to create media servers: %v", err)) + os.Exit(1) + } data = &model.ServerData{ Mutex: sync.RWMutex{}, - Servers: servers} - - if err != nil { - utils.Log(logrus.PanicLevel, err.Error()) - panic(err) + Servers: servers, } - // Create DB Connection with MySQL + // Establish connection with the MySQL database. utils.Log(logrus.InfoLevel, "Connecting to database...") db, err := helpers.CreateDBConn() if err != nil { - utils.Log(logrus.PanicLevel, err.Error()) - panic(err) + utils.Log(logrus.ErrorLevel, fmt.Sprintf("Failed to connect to database: %v", err)) + os.Exit(1) } dbConn = database.NewMySQLConn(db) + // Establish connection with the Redis cache. rdb, err = helpers.CreateRedisConn() - - // get customization record before starting server - err = updateCustomizationSettings() if err != nil { - utils.Log(logrus.PanicLevel, err.Error()) - panic(err) + utils.Log(logrus.ErrorLevel, fmt.Sprintf("Failed to connect to Redis: %v", err)) + os.Exit(1) } - // connect to cassandra - /* - utils.Log(logrus.InfoLevel, "Connecting to cassandra...") - cassandraAddr := utils.Config("CASSANDRA_HOST") + ":9042" - cqlCluster = gocql.NewCluster(cassandraAddr) - cqlCluster.Keyspace = utils.Config("CASSANDRA_KEYSPACE") - cqlCluster.ProtoVersion = 4 - cqlSess, err = cqlCluster.CreateSession() + // Retrieve primary configuration records prior to triggering routing. + err = updateCustomizationSettings() if err != nil { - utils.Log(logrus.PanicLevel, err.Error()) - panic(err) + utils.Log(logrus.ErrorLevel, fmt.Sprintf("Failed to fetch initial customizations: %v", err)) + os.Exit(1) } - */ utils.Log(logrus.InfoLevel, fmt.Sprintf("got customization settings. billing frequency = %s", customizations.BillingFrequency)) - var wg sync.WaitGroup - wg.Add(1) + // Setup context for background routines to stop cleanly during shutdown. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Initialize background sync intervals + ticker := time.NewTicker(60 * time.Second) + defer ticker.Stop() + // Spin off asynchronous settings-update routine go func() { - for { - utils.Log(logrus.InfoLevel, fmt.Sprintf("updating customization settings")) - <-ticker.C - err := updateCustomizationSettings() - if err != nil { - utils.Log(logrus.DebugLevel, fmt.Sprintf("failed to get updated customizations record")) - continue + for { + select { + case <-ticker.C: + utils.Log(logrus.InfoLevel, "updating customization settings") + err := updateCustomizationSettings() + if err != nil { + utils.Log(logrus.DebugLevel, "failed to get updated customizations record") + continue + } + utils.Log(logrus.InfoLevel, "updated settings successfully.") + case <-ctx.Done(): + utils.Log(logrus.InfoLevel, "stopping customization settings updater routine") + return } + } + }() - utils.Log(logrus.InfoLevel, fmt.Sprintf("updated settings successfully.")) - } - }() + // Construct HTTP router framework and inject data stores + r := buildServer() + // Spin up Echo engine in a non-blocking goroutine go func() { - // Start Internals-API Backend server utils.Log(logrus.InfoLevel, "Starting API...") - startServer() - wg.Done() + if utils.Config("USE_TLS") == "on" { + certPath := utils.Config("TLS_CERT_PATH") + keyPath := utils.Config("TLS_KEY_PATH") + httpsPort := utils.ReadEnv("HTTPS_PORT", "443") + utils.Log(logrus.InfoLevel, fmt.Sprintf("Starting HTTP server with TLS. cert=%s, key=%s\r\n", certPath, keyPath)) + if err := r.StartTLS(":"+httpsPort, certPath, keyPath); err != nil && err != http.ErrServerClosed { + utils.Log(logrus.ErrorLevel, fmt.Sprintf("TLS server error: %v", err)) + os.Exit(1) + } + } else { + httpPort := utils.ReadEnv("HTTP_PORT", "80") + utils.Log(logrus.InfoLevel, fmt.Sprintf("HTTP port %s\r\n", httpPort)) + if err := r.Start(":" + httpPort); err != nil && err != http.ErrServerClosed { + utils.Log(logrus.ErrorLevel, fmt.Sprintf("HTTP server error: %v", err)) + os.Exit(1) + } + } }() - wg.Wait() + // Monitor OS termination signals to coordinate shutdown + shutdownSig := make(chan os.Signal, 1) + signal.Notify(shutdownSig, syscall.SIGINT, syscall.SIGTERM) + + // Wait for termination signal + <-shutdownSig + utils.Log(logrus.InfoLevel, "Shutting down service gracefully...") + + // Cancel background routines immediately + cancel() + + // Implement graceful shutdown with safety timeout context (10 seconds) + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() + + if err := r.Shutdown(shutdownCtx); err != nil { + utils.Log(logrus.ErrorLevel, fmt.Sprintf("Server forced to shutdown: %v", err)) + os.Exit(1) + } + + utils.Log(logrus.InfoLevel, "Service stopped cleanly.") + os.Exit(0) } -// Start Internals-API Backend server -// Configure Handler, limit middleware, TLS -func startServer() { +// buildServer constructs the Echo framework, creates stores, and configures route targets +func buildServer() *echo.Echo { utils.SetSetting(model.GlobalSettings{ValidateCallerId: false}) - // Start Server with Echo r := router.New() - utils.Log(logrus.InfoLevel, "Starting HTTP server...") - // Configure Limit Handler if USE_LIMIT_MIDDLEWARE is "on" + utils.Log(logrus.InfoLevel, "Configuring HTTP server...") + if utils.Config("USE_LIMIT_MIDDLEWARE") == "on" { r.Any("", limitHandler) } - // Configure Handler with Global DB + // Instantiate operational store nodes as := store.NewAdminStore(dbConn) cs := store.NewCallStore(dbConn) crs := store.NewCarrierStore(dbConn) @@ -149,29 +189,12 @@ func startServer() { us := store.NewUserStore(dbConn, rdb) h := handler.NewHandler(as, cs, crs, ds, fs, ls, rs, us) - // Register Handler for Echo context + // Bind endpoints h.Register(r) - - // Start with 443 port if TLS is ON - utils.Log(logrus.InfoLevel, "Starting HTTP server without TLS\r\n") - if utils.Config("USE_TLS") == "on" { - certPath := utils.Config("TLS_CERT_PATH") - keyPath := utils.Config("TLS_KEY_PATH") - httpsPort := utils.ReadEnv("HTTPS_PORT", "443") - utils.Log(logrus.InfoLevel, fmt.Sprintf("Starting HTTP server with TLS. cert=%s, key=%s\r\n", certPath, keyPath)) - r.Logger.Fatal(r.StartTLS(":"+httpsPort, certPath, keyPath)) - utils.Log(logrus.InfoLevel, "Started server...") - return - } - - // Start with 80 port if TLS is OFF - httpPort := utils.ReadEnv("HTTP_PORT", "80") - utils.Log(logrus.InfoLevel, fmt.Sprintf("HTTP port %s\r\n", httpPort)) - r.Logger.Fatal(r.Start(":" + httpPort)) - utils.Log(logrus.InfoLevel, "Started server...") + return r } -// Configure Limit Handler for Echo context +// limitHandler intercepts API calls to prevent brute force or denial attempts func limitHandler(c echo.Context) error { var addr string requestedAddr := c.QueryParam("addr") @@ -188,8 +211,6 @@ func limitHandler(c echo.Context) error { isCarrier = utils.CheckIfCarrier(carrier) } - // Limit for users - var limit int = 60 if isCarrier { limit = 3600 @@ -197,11 +218,9 @@ func limitHandler(c echo.Context) error { var indexLimiter = golimiter.New(limit, time.Minute) - // Check if the given IP is rate limited if indexLimiter.IsLimited(addr) { return c.String(http.StatusTooManyRequests, fmt.Sprintf("Rate limit exhausted from %s", addr)) } - // Add a request to the count for the Ip indexLimiter.Increment(addr) totalRequestPastMinute := indexLimiter.Count(addr) totalRemaining := limit - totalRequestPastMinute @@ -211,4 +230,4 @@ func limitHandler(c echo.Context) error { "You are allowed to make %d more request.\n"+ "Maximum request you can make per minute is %d.", addr, totalRequestPastMinute, totalRemaining, limit)) -} +} \ No newline at end of file