From 3dfbe772196a9e284630bf3760f41bf41cf66446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Wed, 28 May 2025 19:59:43 +0200 Subject: [PATCH] add caching --- local/controller/lookup.go | 45 ++++++++++---- local/model/api.go | 93 ++++++++++++++++++++++++++-- local/model/cache.go | 120 +++++++++++++++++++++++++++++++++++++ local/model/server.go | 2 +- local/service/api.go | 78 +++++++++++++++++++++--- local/service/lookup.go | 101 +++++++++++++++++-------------- local/service/server.go | 4 +- local/service/service.go | 12 +++- 8 files changed, 382 insertions(+), 73 deletions(-) create mode 100644 local/model/cache.go diff --git a/local/controller/lookup.go b/local/controller/lookup.go index c41ea00..35b7ef4 100644 --- a/local/controller/lookup.go +++ b/local/controller/lookup.go @@ -40,8 +40,13 @@ func NewLookupController(as *service.LookupService, routeGroups *common.RouteGro // @Success 200 {array} string // @Router /v1/lookup/tracks [get] func (ac *LookupController) getTracks(c *fiber.Ctx) error { - LookupModel := ac.service.GetTracks(c) - return c.JSON(LookupModel) + result, err := ac.service.GetTracks(c) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error fetching tracks", + }) + } + return c.JSON(result) } // getCarModels returns CarModels @@ -52,8 +57,13 @@ func (ac *LookupController) getTracks(c *fiber.Ctx) error { // @Success 200 {array} string // @Router /v1/lookup/car-models [get] func (ac *LookupController) getCarModels(c *fiber.Ctx) error { - LookupModel := ac.service.GetCarModels(c) - return c.JSON(LookupModel) + result, err := ac.service.GetCarModels(c) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error fetching car models", + }) + } + return c.JSON(result) } // getDriverCategories returns DriverCategories @@ -64,8 +74,13 @@ func (ac *LookupController) getCarModels(c *fiber.Ctx) error { // @Success 200 {array} string // @Router /v1/lookup/driver-categories [get] func (ac *LookupController) getDriverCategories(c *fiber.Ctx) error { - LookupModel := ac.service.GetDriverCategories(c) - return c.JSON(LookupModel) + result, err := ac.service.GetDriverCategories(c) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error fetching driver categories", + }) + } + return c.JSON(result) } // getCupCategories returns CupCategories @@ -76,8 +91,13 @@ func (ac *LookupController) getDriverCategories(c *fiber.Ctx) error { // @Success 200 {array} string // @Router /v1/lookup/cup-categories [get] func (ac *LookupController) getCupCategories(c *fiber.Ctx) error { - LookupModel := ac.service.GetCupCategories(c) - return c.JSON(LookupModel) + result, err := ac.service.GetCupCategories(c) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error fetching cup categories", + }) + } + return c.JSON(result) } // getSessionTypes returns SessionTypes @@ -88,6 +108,11 @@ func (ac *LookupController) getCupCategories(c *fiber.Ctx) error { // @Success 200 {array} string // @Router /v1/lookup/session-types [get] func (ac *LookupController) getSessionTypes(c *fiber.Ctx) error { - LookupModel := ac.service.GetSessionTypes(c) - return c.JSON(LookupModel) + result, err := ac.service.GetSessionTypes(c) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error fetching session types", + }) + } + return c.JSON(result) } diff --git a/local/model/api.go b/local/model/api.go index c95b626..1629aed 100644 --- a/local/model/api.go +++ b/local/model/api.go @@ -1,13 +1,98 @@ package model -type ServiceStatus string +import ( + "database/sql/driver" + "fmt" +) + +type ServiceStatus int const ( - StatusRunning ServiceStatus = "SERVICE_RUNNING\r\n" - StatusStopped ServiceStatus = "SERVICE_STOPPED\r\n" - StatusRestarting ServiceStatus = "SERVICE_RESTARTING\r\n" + StatusUnknown ServiceStatus = iota + StatusStopped + StatusStopping + StatusRestarting + StatusStarting + StatusRunning ) +// String converts the ServiceStatus to its string representation +func (s ServiceStatus) String() string { + switch s { + case StatusRunning: + return "SERVICE_RUNNING" + case StatusStopped: + return "SERVICE_STOPPED" + case StatusStarting: + return "SERVICE_STARTING" + case StatusStopping: + return "SERVICE_STOPPING" + case StatusRestarting: + return "SERVICE_RESTARTING" + default: + return "SERVICE_UNKNOWN" + } +} + +// ParseServiceStatus converts a string to ServiceStatus +func ParseServiceStatus(s string) ServiceStatus { + switch s { + case "SERVICE_RUNNING": + return StatusRunning + case "SERVICE_STOPPED": + return StatusStopped + case "SERVICE_STARTING": + return StatusStarting + case "SERVICE_STOPPING": + return StatusStopping + case "SERVICE_RESTARTING": + return StatusRestarting + default: + return StatusUnknown + } +} + +// MarshalJSON implements json.Marshaler interface +func (s ServiceStatus) MarshalJSON() ([]byte, error) { + return []byte(`"` + s.String() + `"`), nil +} + +// UnmarshalJSON implements json.Unmarshaler interface +func (s *ServiceStatus) UnmarshalJSON(data []byte) error { + str := string(data) + // Remove quotes + str = str[1 : len(str)-1] + *s = ParseServiceStatus(str) + return nil +} + +// Scan implements the sql.Scanner interface +func (s *ServiceStatus) Scan(value interface{}) error { + if value == nil { + *s = StatusUnknown + return nil + } + + switch v := value.(type) { + case string: + *s = ParseServiceStatus(v) + return nil + case []byte: + *s = ParseServiceStatus(string(v)) + return nil + case int64: + *s = ServiceStatus(v) + return nil + default: + return fmt.Errorf("unsupported type for ServiceStatus: %T", value) + } +} + +// Value implements the driver.Valuer interface +func (s ServiceStatus) Value() (driver.Value, error) { + return s.String(), nil +} + type ApiModel struct { Api string `json:"api"` } diff --git a/local/model/cache.go b/local/model/cache.go new file mode 100644 index 0000000..083e851 --- /dev/null +++ b/local/model/cache.go @@ -0,0 +1,120 @@ +package model + +import ( + "sync" + "time" +) + +// StatusCache represents a cached server status with expiration +type StatusCache struct { + Status ServiceStatus + UpdatedAt time.Time +} + +// CacheConfig holds configuration for cache behavior +type CacheConfig struct { + ExpirationTime time.Duration // How long before a cache entry expires + ThrottleTime time.Duration // Minimum time between status checks + DefaultStatus ServiceStatus // Default status to return when throttled +} + +// ServerStatusCache manages cached server statuses +type ServerStatusCache struct { + sync.RWMutex + cache map[string]*StatusCache + config CacheConfig + lastChecked map[string]time.Time +} + +// NewServerStatusCache creates a new server status cache +func NewServerStatusCache(config CacheConfig) *ServerStatusCache { + return &ServerStatusCache{ + cache: make(map[string]*StatusCache), + lastChecked: make(map[string]time.Time), + config: config, + } +} + +// GetStatus retrieves the cached status or indicates if a fresh check is needed +func (c *ServerStatusCache) GetStatus(serviceName string) (ServiceStatus, bool) { + c.RLock() + defer c.RUnlock() + + // Check if we're being throttled + if lastCheck, exists := c.lastChecked[serviceName]; exists { + if time.Since(lastCheck) < c.config.ThrottleTime { + if cached, ok := c.cache[serviceName]; ok { + return cached.Status, false + } + return c.config.DefaultStatus, false + } + } + + // Check if we have a valid cached entry + if cached, ok := c.cache[serviceName]; ok { + if time.Since(cached.UpdatedAt) < c.config.ExpirationTime { + return cached.Status, false + } + } + + return StatusUnknown, true +} + +// UpdateStatus updates the cache with a new status +func (c *ServerStatusCache) UpdateStatus(serviceName string, status ServiceStatus) { + c.Lock() + defer c.Unlock() + + c.cache[serviceName] = &StatusCache{ + Status: status, + UpdatedAt: time.Now(), + } + c.lastChecked[serviceName] = time.Now() +} + +// Clear removes all entries from the cache +func (c *ServerStatusCache) Clear() { + c.Lock() + defer c.Unlock() + + c.cache = make(map[string]*StatusCache) + c.lastChecked = make(map[string]time.Time) +} + +// LookupCache provides a generic cache for lookup data +type LookupCache struct { + sync.RWMutex + data map[string]interface{} +} + +// NewLookupCache creates a new lookup cache +func NewLookupCache() *LookupCache { + return &LookupCache{ + data: make(map[string]interface{}), + } +} + +// Get retrieves a cached value by key +func (c *LookupCache) Get(key string) (interface{}, bool) { + c.RLock() + defer c.RUnlock() + + value, exists := c.data[key] + return value, exists +} + +// Set stores a value in the cache +func (c *LookupCache) Set(key string, value interface{}) { + c.Lock() + defer c.Unlock() + + c.data[key] = value +} + +// Clear removes all entries from the cache +func (c *LookupCache) Clear() { + c.Lock() + defer c.Unlock() + + c.data = make(map[string]interface{}) +} \ No newline at end of file diff --git a/local/model/server.go b/local/model/server.go index 0b333c4..198b4a9 100644 --- a/local/model/server.go +++ b/local/model/server.go @@ -9,7 +9,7 @@ import ( type Server struct { ID uint `gorm:"primaryKey" json:"id"` Name string `gorm:"not null" json:"name"` - Status ServiceStatus `json:"status"` + Status ServiceStatus `json:"status" gorm:"-"` IP string `gorm:"not null" json:"-"` Port int `gorm:"not null" json:"-"` ConfigPath string `gorm:"not null" json:"-"` // e.g. "/acc/servers/server1/" diff --git a/local/service/api.go b/local/service/api.go index b7e0e24..b4e71ed 100644 --- a/local/service/api.go +++ b/local/service/api.go @@ -7,22 +7,28 @@ import ( "context" "errors" "strings" + "time" "github.com/gofiber/fiber/v2" ) - type ApiService struct { repository *repository.ApiRepository serverRepository *repository.ServerRepository - serverService *ServerService + serverService *ServerService + statusCache *model.ServerStatusCache } func NewApiService(repository *repository.ApiRepository, - serverRepository *repository.ServerRepository,) *ApiService { + serverRepository *repository.ServerRepository) *ApiService { return &ApiService{ repository: repository, serverRepository: serverRepository, + statusCache: model.NewServerStatusCache(model.CacheConfig{ + ExpirationTime: 30 * time.Second, // Cache expires after 30 seconds + ThrottleTime: 5 * time.Second, // Minimum 5 seconds between checks + DefaultStatus: model.StatusRunning, // Default to running if throttled + }), } } @@ -35,9 +41,22 @@ func (as ApiService) GetStatus(ctx *fiber.Ctx) (string, error) { if err != nil { return "", err } - status, err := as.StatusServer(serviceName) - return status, err + // Try to get status from cache + if status, shouldCheck := as.statusCache.GetStatus(serviceName); !shouldCheck { + return status.String(), nil + } + + // If cache miss or expired, check actual status + statusStr, err := as.StatusServer(serviceName) + if err != nil { + return "", err + } + + // Parse and update cache with new status + status := model.ParseServiceStatus(statusStr) + as.statusCache.UpdateStatus(serviceName, status) + return status.String(), nil } func (as ApiService) ApiStartServer(ctx *fiber.Ctx) (string, error) { @@ -45,7 +64,19 @@ func (as ApiService) ApiStartServer(ctx *fiber.Ctx) (string, error) { if err != nil { return "", err } - return as.StartServer(serviceName) + + // Update status cache for this service before starting + as.statusCache.UpdateStatus(serviceName, model.StatusStarting) + + statusStr, err := as.StartServer(serviceName) + if err != nil { + return "", err + } + + // Parse and update cache with new status + status := model.ParseServiceStatus(statusStr) + as.statusCache.UpdateStatus(serviceName, status) + return status.String(), nil } func (as ApiService) ApiStopServer(ctx *fiber.Ctx) (string, error) { @@ -53,7 +84,19 @@ func (as ApiService) ApiStopServer(ctx *fiber.Ctx) (string, error) { if err != nil { return "", err } - return as.StopServer(serviceName) + + // Update status cache for this service before stopping + as.statusCache.UpdateStatus(serviceName, model.StatusStopping) + + statusStr, err := as.StopServer(serviceName) + if err != nil { + return "", err + } + + // Parse and update cache with new status + status := model.ParseServiceStatus(statusStr) + as.statusCache.UpdateStatus(serviceName, status) + return status.String(), nil } func (as ApiService) ApiRestartServer(ctx *fiber.Ctx) (string, error) { @@ -61,7 +104,19 @@ func (as ApiService) ApiRestartServer(ctx *fiber.Ctx) (string, error) { if err != nil { return "", err } - return as.RestartServer(serviceName) + + // Update status cache for this service before restarting + as.statusCache.UpdateStatus(serviceName, model.StatusRestarting) + + statusStr, err := as.RestartServer(serviceName) + if err != nil { + return "", err + } + + // Parse and update cache with new status + status := model.ParseServiceStatus(statusStr) + as.statusCache.UpdateStatus(serviceName, status) + return status.String(), nil } func (as ApiService) StatusServer(serviceName string) (string, error) { @@ -99,7 +154,12 @@ func ManageService(serviceName string, action string) (string, error) { return "", err } - return strings.ReplaceAll(output, "\x00", ""), nil + // Clean up NSSM output by removing null bytes and trimming whitespace + cleaned := strings.TrimSpace(strings.ReplaceAll(output, "\x00", "")) + // Remove \r\n from status strings + cleaned = strings.TrimSuffix(cleaned, "\r\n") + + return cleaned, nil } func (as ApiService) GetServiceName(ctx *fiber.Ctx) (string, error) { diff --git a/local/service/lookup.go b/local/service/lookup.go index 304cdcd..cc3d4d9 100644 --- a/local/service/lookup.go +++ b/local/service/lookup.go @@ -9,65 +9,76 @@ import ( type LookupService struct { repository *repository.LookupRepository + cache *model.LookupCache } func NewLookupService(repository *repository.LookupRepository) *LookupService { return &LookupService{ repository: repository, + cache: model.NewLookupCache(), } } -// GetTracks -// Gets Tracks rows from Lookup table. -// -// Args: -// context.Context: Application context -// Returns: -// string: Application version -func (as LookupService) GetTracks(ctx *fiber.Ctx) *[]model.Track { - return as.repository.GetTracks(ctx.UserContext()) +func (s *LookupService) GetTracks(ctx *fiber.Ctx) (interface{}, error) { + if cached, exists := s.cache.Get("tracks"); exists { + return cached, nil + } + + tracks := s.repository.GetTracks(ctx.UserContext()) + s.cache.Set("tracks", tracks) + return tracks, nil } -// GetCarModels -// Gets CarModels rows from Lookup table. -// -// Args: -// context.Context: Application context -// Returns: -// model.LookupModel: Lookup object from database. -func (as LookupService) GetCarModels(ctx *fiber.Ctx) *[]model.CarModel { - return as.repository.GetCarModels(ctx.UserContext()) +func (s *LookupService) GetCarModels(ctx *fiber.Ctx) (interface{}, error) { + if cached, exists := s.cache.Get("cars"); exists { + return cached, nil + } + + cars := s.repository.GetCarModels(ctx.UserContext()) + s.cache.Set("cars", cars) + return cars, nil } -// GetDriverCategories -// Gets DriverCategories rows from Lookup table. -// -// Args: -// context.Context: Application context -// Returns: -// model.LookupModel: Lookup object from database. -func (as LookupService) GetDriverCategories(ctx *fiber.Ctx) *[]model.DriverCategory { - return as.repository.GetDriverCategories(ctx.UserContext()) +func (s *LookupService) GetDriverCategories(ctx *fiber.Ctx) (interface{}, error) { + if cached, exists := s.cache.Get("drivers"); exists { + return cached, nil + } + + categories := s.repository.GetDriverCategories(ctx.UserContext()) + s.cache.Set("drivers", categories) + return categories, nil } -// GetCupCategories -// Gets CupCategories rows from Lookup table. -// -// Args: -// context.Context: Application context -// Returns: -// model.LookupModel: Lookup object from database. -func (as LookupService) GetCupCategories(ctx *fiber.Ctx) *[]model.CupCategory { - return as.repository.GetCupCategories(ctx.UserContext()) +func (s *LookupService) GetCupCategories(ctx *fiber.Ctx) (interface{}, error) { + if cached, exists := s.cache.Get("cups"); exists { + return cached, nil + } + + categories := s.repository.GetCupCategories(ctx.UserContext()) + s.cache.Set("cups", categories) + return categories, nil } -// GetSessionTypes -// Gets SessionTypes rows from Lookup table. -// -// Args: -// context.Context: Application context -// Returns: -// model.LookupModel: Lookup object from database. -func (as LookupService) GetSessionTypes(ctx *fiber.Ctx) *[]model.SessionType { - return as.repository.GetSessionTypes(ctx.UserContext()) +func (s *LookupService) GetSessionTypes(ctx *fiber.Ctx) (interface{}, error) { + if cached, exists := s.cache.Get("sessions"); exists { + return cached, nil + } + + types := s.repository.GetSessionTypes(ctx.UserContext()) + s.cache.Set("sessions", types) + return types, nil +} + +// ClearCache clears all cached lookup data +func (s *LookupService) ClearCache() { + s.cache.Clear() +} + +// PreloadCache loads all lookup data into cache +func (s *LookupService) PreloadCache(ctx *fiber.Ctx) { + s.GetTracks(ctx) + s.GetCarModels(ctx) + s.GetDriverCategories(ctx) + s.GetCupCategories(ctx) + s.GetSessionTypes(ctx) } diff --git a/local/service/server.go b/local/service/server.go index 58f7668..1f1c8cd 100644 --- a/local/service/server.go +++ b/local/service/server.go @@ -145,7 +145,7 @@ func (as ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]m if err != nil { log.Print(err.Error()) } - (*servers)[i].Status = model.ServiceStatus(status) + (*servers)[i].Status = model.ParseServiceStatus(status) instance, ok := as.instances.Load(server.ID) if !ok { log.Print("Unable to retrieve instance for server of ID: ", server.ID) @@ -176,7 +176,7 @@ func (as ServerService) GetById(ctx *fiber.Ctx, serverID int) (*model.Server, er if err != nil { log.Print(err.Error()) } - server.Status = model.ServiceStatus(status) + server.Status = model.ParseServiceStatus(status) instance, ok := as.instances.Load(server.ID) if !ok { log.Print("Unable to retrieve instance for server of ID: ", server.ID) diff --git a/local/service/service.go b/local/service/service.go index 39dcb2e..3b2eb16 100644 --- a/local/service/service.go +++ b/local/service/service.go @@ -2,6 +2,7 @@ package service import ( "acc-server-manager/local/repository" + "context" "log" "go.uber.org/dig" @@ -21,11 +22,18 @@ func InitializeServices(c *dig.Container) { c.Provide(NewConfigService) c.Provide(NewLookupService) - err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService) { + err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService, lookup *LookupService) { api.SetServerService(server) config.SetServerService(server) + + // Initialize lookup data using repository directly + lookup.cache.Set("tracks", lookup.repository.GetTracks(context.Background())) + lookup.cache.Set("cars", lookup.repository.GetCarModels(context.Background())) + lookup.cache.Set("drivers", lookup.repository.GetDriverCategories(context.Background())) + lookup.cache.Set("cups", lookup.repository.GetCupCategories(context.Background())) + lookup.cache.Set("sessions", lookup.repository.GetSessionTypes(context.Background())) }) if err != nil { - log.Panic("unable to initialize server service in api service") + log.Panic("unable to initialize services:", err) } }