Add configurable timeouts and cache validity to ESI and SSO components

This commit is contained in:
2025-10-10 22:38:09 +02:00
parent fe47d3f8dd
commit 0f0adac82a
5 changed files with 78 additions and 50 deletions

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"time"
"go-eve-pi/options"
"go-eve-pi/types"
logger "git.site.quack-lab.dev/dave/cylogger"
@@ -22,6 +23,7 @@ type CachedESI struct {
GetCacheEntry(urlHash string) (*types.CacheEntry, error)
SaveCacheEntry(entry *types.CacheEntry) error
}
cacheValidity time.Duration
}
// NewCachedESI creates a new CachedESI instance
@@ -29,44 +31,43 @@ func NewCachedESI(direct ESIInterface, db interface {
GetCacheEntry(urlHash string) (*types.CacheEntry, error)
SaveCacheEntry(entry *types.CacheEntry) error
}) *CachedESI {
// Parse cache validity ONCE at initialization
cacheValidity, err := time.ParseDuration(options.GlobalOptions.CacheValidity)
if err != nil {
logger.Warning("Invalid cache validity duration %s, using 10m default: %v", options.GlobalOptions.CacheValidity, err)
cacheValidity = 10 * time.Minute
}
return &CachedESI{
direct: direct,
db: db,
direct: direct,
db: db,
cacheValidity: cacheValidity,
}
}
// GetCharacterPlanets retrieves a list of planets for a character with caching
func (c *CachedESI) GetCharacterPlanets(ctx context.Context, characterID int, accessToken string) ([]Planet, error) {
url := fmt.Sprintf("/v1/characters/%d/planets/", characterID)
result, err := func() (interface{}, error) {
var fetchFunc func() (interface{}, error) = func() (interface{}, error) {
return c.direct.GetCharacterPlanets(ctx, characterID, accessToken)
}
return c.getCachedResponse(url, fetchFunc)
}()
if err != nil {
return nil, err
fetchFunc := func() ([]Planet, error) {
return c.direct.GetCharacterPlanets(ctx, characterID, accessToken)
}
return result.([]Planet), nil
return getCachedResponse(c, url, fetchFunc)
}
// GetPlanetDetails retrieves detailed information about a specific planet with caching
func (c *CachedESI) GetPlanetDetails(ctx context.Context, characterID, planetID int, accessToken string) (*PlanetDetail, error) {
url := fmt.Sprintf("/v3/characters/%d/planets/%d/", characterID, planetID)
result, err := func() (interface{}, error) {
var fetchFunc func() (interface{}, error) = func() (interface{}, error) {
return c.direct.GetPlanetDetails(ctx, characterID, planetID, accessToken)
}
return c.getCachedResponse(url, fetchFunc)
}()
if err != nil {
return nil, err
fetchFunc := func() (*PlanetDetail, error) {
return c.direct.GetPlanetDetails(ctx, characterID, planetID, accessToken)
}
return result.(*PlanetDetail), nil
return getCachedResponse(c, url, fetchFunc)
}
// getCachedResponse handles caching logic
func (c *CachedESI) getCachedResponse(url string, fetchFunc func() (interface{}, error)) (interface{}, error) {
// getCachedResponse handles caching logic with generics
func getCachedResponse[T any](c *CachedESI, url string, fetchFunc func() (T, error)) (T, error) {
// Generate cache key
hash := sha256.Sum256([]byte(url))
urlHash := hex.EncodeToString(hash[:])
@@ -75,22 +76,12 @@ func (c *CachedESI) getCachedResponse(url string, fetchFunc func() (interface{},
cacheEntry, err := c.db.GetCacheEntry(urlHash)
if err == nil {
// Check if cache is still valid
cacheValidity, _ := time.ParseDuration("10m") // Default 10 minutes
if time.Since(cacheEntry.CachedAt) < cacheValidity {
if time.Since(cacheEntry.CachedAt) < c.cacheValidity {
logger.Debug("Cache hit for URL: %s", url)
// Parse cached response based on URL pattern
if url[len(url)-1:] == "/" {
// Planets endpoint
var planets []Planet
if err := json.Unmarshal([]byte(cacheEntry.Response), &planets); err == nil {
return planets, nil
}
} else {
// Planet details endpoint
var planetDetail PlanetDetail
if err := json.Unmarshal([]byte(cacheEntry.Response), &planetDetail); err == nil {
return &planetDetail, nil
}
// Parse cached response
var result T
if err := json.Unmarshal([]byte(cacheEntry.Response), &result); err == nil {
return result, nil
}
}
}
@@ -99,7 +90,8 @@ func (c *CachedESI) getCachedResponse(url string, fetchFunc func() (interface{},
logger.Debug("Cache miss for URL: %s", url)
result, err := fetchFunc()
if err != nil {
return nil, err
var zero T
return zero, err
}
// Store in cache

View File

@@ -9,6 +9,8 @@ import (
"net/http"
"time"
"go-eve-pi/options"
logger "git.site.quack-lab.dev/dave/cylogger"
)
@@ -105,9 +107,16 @@ type DirectESI struct {
// NewDirectESI creates a new DirectESI instance
func NewDirectESI() *DirectESI {
// Parse HTTP timeout ONCE at initialization
httpTimeout, err := time.ParseDuration(options.GlobalOptions.HTTPTimeout)
if err != nil {
logger.Warning("Invalid HTTP timeout duration %s, using 30s default: %v", options.GlobalOptions.HTTPTimeout, err)
httpTimeout = 30 * time.Second
}
return &DirectESI{
httpClient: &http.Client{
Timeout: 30 * time.Second,
Timeout: httpTimeout,
},
}
}
@@ -127,8 +136,7 @@ func (d *DirectESI) GetCharacterPlanets(ctx context.Context, characterID int, ac
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
resp, err := d.httpClient.Do(req)
if err != nil {
logger.Error("Failed to fetch character planets: %v", err)
return nil, err
@@ -166,8 +174,7 @@ func (d *DirectESI) GetPlanetDetails(ctx context.Context, characterID, planetID
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
resp, err := d.httpClient.Do(req)
if err != nil {
logger.Error("Failed to fetch planet details: %v", err)
return nil, err

View File

@@ -16,6 +16,7 @@ import (
"sync"
"time"
"go-eve-pi/options"
"go-eve-pi/repositories"
"go-eve-pi/types"
@@ -279,7 +280,14 @@ func (s *SSO) processCallback(isGet bool, code, state string, writeResponse func
}
func (s *SSO) waitForCallback() (code, state string, err error) {
logger.Debug("Waiting for authentication callback (timeout: 30s)")
// Parse SSO callback timeout ONCE at initialization
callbackTimeout, err := time.ParseDuration(options.GlobalOptions.SSOCallbackTimeout)
if err != nil {
logger.Warning("Invalid SSO callback timeout duration %s, using 30s default: %v", options.GlobalOptions.SSOCallbackTimeout, err)
callbackTimeout = 30 * time.Second
}
logger.Debug("Waiting for authentication callback (timeout: %v)", callbackTimeout)
// Wait for callback through channel
select {
case result := <-s.callbackChan:
@@ -289,8 +297,8 @@ func (s *SSO) waitForCallback() (code, state string, err error) {
}
logger.Debug("Callback received successfully")
return result.code, result.state, result.err
case <-time.After(30 * time.Second):
logger.Error("Callback timeout after 30 seconds")
case <-time.After(callbackTimeout):
logger.Error("Callback timeout after %v", callbackTimeout)
return "", "", errors.New("callback timeout")
}
}
@@ -335,7 +343,7 @@ func (s *SSO) exchangeCodeForToken(ctx context.Context, code, verifier string) (
CharacterName: name,
AccessToken: tr.AccessToken,
RefreshToken: tr.RefreshToken,
ExpiresAt: time.Now().Add(time.Duration(tr.ExpiresIn-30) * time.Second),
ExpiresAt: time.Now().Add(time.Duration(tr.ExpiresIn-options.GlobalOptions.TokenExpiryBuffer) * time.Second),
}, nil
}
@@ -376,7 +384,7 @@ func (s *SSO) refreshToken(ctx context.Context, char *types.Character) error {
char.RefreshToken = tr.RefreshToken
}
if tr.ExpiresIn > 0 {
char.ExpiresAt = time.Now().Add(time.Duration(tr.ExpiresIn-30) * time.Second)
char.ExpiresAt = time.Now().Add(time.Duration(tr.ExpiresIn-options.GlobalOptions.TokenExpiryBuffer) * time.Second)
}
logger.Debug("Saving refreshed token to database for character %s", char.CharacterName)