add caching

This commit is contained in:
Fran Jurmanović
2025-05-28 19:59:43 +02:00
parent 0ced45ce55
commit 3dfbe77219
8 changed files with 382 additions and 73 deletions

View File

@@ -40,8 +40,13 @@ func NewLookupController(as *service.LookupService, routeGroups *common.RouteGro
// @Success 200 {array} string // @Success 200 {array} string
// @Router /v1/lookup/tracks [get] // @Router /v1/lookup/tracks [get]
func (ac *LookupController) getTracks(c *fiber.Ctx) error { func (ac *LookupController) getTracks(c *fiber.Ctx) error {
LookupModel := ac.service.GetTracks(c) result, err := ac.service.GetTracks(c)
return c.JSON(LookupModel) if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error fetching tracks",
})
}
return c.JSON(result)
} }
// getCarModels returns CarModels // getCarModels returns CarModels
@@ -52,8 +57,13 @@ func (ac *LookupController) getTracks(c *fiber.Ctx) error {
// @Success 200 {array} string // @Success 200 {array} string
// @Router /v1/lookup/car-models [get] // @Router /v1/lookup/car-models [get]
func (ac *LookupController) getCarModels(c *fiber.Ctx) error { func (ac *LookupController) getCarModels(c *fiber.Ctx) error {
LookupModel := ac.service.GetCarModels(c) result, err := ac.service.GetCarModels(c)
return c.JSON(LookupModel) if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error fetching car models",
})
}
return c.JSON(result)
} }
// getDriverCategories returns DriverCategories // getDriverCategories returns DriverCategories
@@ -64,8 +74,13 @@ func (ac *LookupController) getCarModels(c *fiber.Ctx) error {
// @Success 200 {array} string // @Success 200 {array} string
// @Router /v1/lookup/driver-categories [get] // @Router /v1/lookup/driver-categories [get]
func (ac *LookupController) getDriverCategories(c *fiber.Ctx) error { func (ac *LookupController) getDriverCategories(c *fiber.Ctx) error {
LookupModel := ac.service.GetDriverCategories(c) result, err := ac.service.GetDriverCategories(c)
return c.JSON(LookupModel) if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error fetching driver categories",
})
}
return c.JSON(result)
} }
// getCupCategories returns CupCategories // getCupCategories returns CupCategories
@@ -76,8 +91,13 @@ func (ac *LookupController) getDriverCategories(c *fiber.Ctx) error {
// @Success 200 {array} string // @Success 200 {array} string
// @Router /v1/lookup/cup-categories [get] // @Router /v1/lookup/cup-categories [get]
func (ac *LookupController) getCupCategories(c *fiber.Ctx) error { func (ac *LookupController) getCupCategories(c *fiber.Ctx) error {
LookupModel := ac.service.GetCupCategories(c) result, err := ac.service.GetCupCategories(c)
return c.JSON(LookupModel) if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error fetching cup categories",
})
}
return c.JSON(result)
} }
// getSessionTypes returns SessionTypes // getSessionTypes returns SessionTypes
@@ -88,6 +108,11 @@ func (ac *LookupController) getCupCategories(c *fiber.Ctx) error {
// @Success 200 {array} string // @Success 200 {array} string
// @Router /v1/lookup/session-types [get] // @Router /v1/lookup/session-types [get]
func (ac *LookupController) getSessionTypes(c *fiber.Ctx) error { func (ac *LookupController) getSessionTypes(c *fiber.Ctx) error {
LookupModel := ac.service.GetSessionTypes(c) result, err := ac.service.GetSessionTypes(c)
return c.JSON(LookupModel) if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error fetching session types",
})
}
return c.JSON(result)
} }

View File

@@ -1,13 +1,98 @@
package model package model
type ServiceStatus string import (
"database/sql/driver"
"fmt"
)
type ServiceStatus int
const ( const (
StatusRunning ServiceStatus = "SERVICE_RUNNING\r\n" StatusUnknown ServiceStatus = iota
StatusStopped ServiceStatus = "SERVICE_STOPPED\r\n" StatusStopped
StatusRestarting ServiceStatus = "SERVICE_RESTARTING\r\n" 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 { type ApiModel struct {
Api string `json:"api"` Api string `json:"api"`
} }

120
local/model/cache.go Normal file
View File

@@ -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{})
}

View File

@@ -9,7 +9,7 @@ import (
type Server struct { type Server struct {
ID uint `gorm:"primaryKey" json:"id"` ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"` Name string `gorm:"not null" json:"name"`
Status ServiceStatus `json:"status"` Status ServiceStatus `json:"status" gorm:"-"`
IP string `gorm:"not null" json:"-"` IP string `gorm:"not null" json:"-"`
Port int `gorm:"not null" json:"-"` Port int `gorm:"not null" json:"-"`
ConfigPath string `gorm:"not null" json:"-"` // e.g. "/acc/servers/server1/" ConfigPath string `gorm:"not null" json:"-"` // e.g. "/acc/servers/server1/"

View File

@@ -7,22 +7,28 @@ import (
"context" "context"
"errors" "errors"
"strings" "strings"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type ApiService struct { type ApiService struct {
repository *repository.ApiRepository repository *repository.ApiRepository
serverRepository *repository.ServerRepository serverRepository *repository.ServerRepository
serverService *ServerService serverService *ServerService
statusCache *model.ServerStatusCache
} }
func NewApiService(repository *repository.ApiRepository, func NewApiService(repository *repository.ApiRepository,
serverRepository *repository.ServerRepository,) *ApiService { serverRepository *repository.ServerRepository) *ApiService {
return &ApiService{ return &ApiService{
repository: repository, repository: repository,
serverRepository: serverRepository, 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 { if err != nil {
return "", err 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) { 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 { if err != nil {
return "", err 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) { 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 { if err != nil {
return "", err 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) { 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 { if err != nil {
return "", err 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) { func (as ApiService) StatusServer(serviceName string) (string, error) {
@@ -99,7 +154,12 @@ func ManageService(serviceName string, action string) (string, error) {
return "", err 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) { func (as ApiService) GetServiceName(ctx *fiber.Ctx) (string, error) {

View File

@@ -9,65 +9,76 @@ import (
type LookupService struct { type LookupService struct {
repository *repository.LookupRepository repository *repository.LookupRepository
cache *model.LookupCache
} }
func NewLookupService(repository *repository.LookupRepository) *LookupService { func NewLookupService(repository *repository.LookupRepository) *LookupService {
return &LookupService{ return &LookupService{
repository: repository, repository: repository,
cache: model.NewLookupCache(),
} }
} }
// GetTracks func (s *LookupService) GetTracks(ctx *fiber.Ctx) (interface{}, error) {
// Gets Tracks rows from Lookup table. if cached, exists := s.cache.Get("tracks"); exists {
// return cached, nil
// Args:
// context.Context: Application context
// Returns:
// string: Application version
func (as LookupService) GetTracks(ctx *fiber.Ctx) *[]model.Track {
return as.repository.GetTracks(ctx.UserContext())
} }
// GetCarModels tracks := s.repository.GetTracks(ctx.UserContext())
// Gets CarModels rows from Lookup table. s.cache.Set("tracks", tracks)
// return tracks, nil
// 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())
} }
// GetDriverCategories func (s *LookupService) GetCarModels(ctx *fiber.Ctx) (interface{}, error) {
// Gets DriverCategories rows from Lookup table. if cached, exists := s.cache.Get("cars"); exists {
// return cached, nil
// 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())
} }
// GetCupCategories cars := s.repository.GetCarModels(ctx.UserContext())
// Gets CupCategories rows from Lookup table. s.cache.Set("cars", cars)
// return cars, nil
// 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())
} }
// GetSessionTypes func (s *LookupService) GetDriverCategories(ctx *fiber.Ctx) (interface{}, error) {
// Gets SessionTypes rows from Lookup table. if cached, exists := s.cache.Get("drivers"); exists {
// return cached, nil
// Args: }
// context.Context: Application context
// Returns: categories := s.repository.GetDriverCategories(ctx.UserContext())
// model.LookupModel: Lookup object from database. s.cache.Set("drivers", categories)
func (as LookupService) GetSessionTypes(ctx *fiber.Ctx) *[]model.SessionType { return categories, nil
return as.repository.GetSessionTypes(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
}
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)
} }

View File

@@ -145,7 +145,7 @@ func (as ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]m
if err != nil { if err != nil {
log.Print(err.Error()) log.Print(err.Error())
} }
(*servers)[i].Status = model.ServiceStatus(status) (*servers)[i].Status = model.ParseServiceStatus(status)
instance, ok := as.instances.Load(server.ID) instance, ok := as.instances.Load(server.ID)
if !ok { if !ok {
log.Print("Unable to retrieve instance for server of ID: ", server.ID) 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 { if err != nil {
log.Print(err.Error()) log.Print(err.Error())
} }
server.Status = model.ServiceStatus(status) server.Status = model.ParseServiceStatus(status)
instance, ok := as.instances.Load(server.ID) instance, ok := as.instances.Load(server.ID)
if !ok { if !ok {
log.Print("Unable to retrieve instance for server of ID: ", server.ID) log.Print("Unable to retrieve instance for server of ID: ", server.ID)

View File

@@ -2,6 +2,7 @@ package service
import ( import (
"acc-server-manager/local/repository" "acc-server-manager/local/repository"
"context"
"log" "log"
"go.uber.org/dig" "go.uber.org/dig"
@@ -21,11 +22,18 @@ func InitializeServices(c *dig.Container) {
c.Provide(NewConfigService) c.Provide(NewConfigService)
c.Provide(NewLookupService) 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) api.SetServerService(server)
config.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 { if err != nil {
log.Panic("unable to initialize server service in api service") log.Panic("unable to initialize services:", err)
} }
} }