Add configurable timeouts and cache validity to ESI and SSO components
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go-eve-pi/options"
|
||||||
"go-eve-pi/types"
|
"go-eve-pi/types"
|
||||||
|
|
||||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||||
@@ -22,6 +23,7 @@ type CachedESI struct {
|
|||||||
GetCacheEntry(urlHash string) (*types.CacheEntry, error)
|
GetCacheEntry(urlHash string) (*types.CacheEntry, error)
|
||||||
SaveCacheEntry(entry *types.CacheEntry) error
|
SaveCacheEntry(entry *types.CacheEntry) error
|
||||||
}
|
}
|
||||||
|
cacheValidity time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCachedESI creates a new CachedESI instance
|
// NewCachedESI creates a new CachedESI instance
|
||||||
@@ -29,44 +31,43 @@ func NewCachedESI(direct ESIInterface, db interface {
|
|||||||
GetCacheEntry(urlHash string) (*types.CacheEntry, error)
|
GetCacheEntry(urlHash string) (*types.CacheEntry, error)
|
||||||
SaveCacheEntry(entry *types.CacheEntry) error
|
SaveCacheEntry(entry *types.CacheEntry) error
|
||||||
}) *CachedESI {
|
}) *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{
|
return &CachedESI{
|
||||||
direct: direct,
|
direct: direct,
|
||||||
db: db,
|
db: db,
|
||||||
|
cacheValidity: cacheValidity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCharacterPlanets retrieves a list of planets for a character with caching
|
// GetCharacterPlanets retrieves a list of planets for a character with caching
|
||||||
func (c *CachedESI) GetCharacterPlanets(ctx context.Context, characterID int, accessToken string) ([]Planet, error) {
|
func (c *CachedESI) GetCharacterPlanets(ctx context.Context, characterID int, accessToken string) ([]Planet, error) {
|
||||||
url := fmt.Sprintf("/v1/characters/%d/planets/", characterID)
|
url := fmt.Sprintf("/v1/characters/%d/planets/", characterID)
|
||||||
result, err := func() (interface{}, error) {
|
|
||||||
var fetchFunc func() (interface{}, error) = func() (interface{}, error) {
|
fetchFunc := func() ([]Planet, error) {
|
||||||
return c.direct.GetCharacterPlanets(ctx, characterID, accessToken)
|
return c.direct.GetCharacterPlanets(ctx, characterID, accessToken)
|
||||||
}
|
|
||||||
return c.getCachedResponse(url, fetchFunc)
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return result.([]Planet), nil
|
|
||||||
|
return getCachedResponse(c, url, fetchFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPlanetDetails retrieves detailed information about a specific planet with caching
|
// GetPlanetDetails retrieves detailed information about a specific planet with caching
|
||||||
func (c *CachedESI) GetPlanetDetails(ctx context.Context, characterID, planetID int, accessToken string) (*PlanetDetail, error) {
|
func (c *CachedESI) GetPlanetDetails(ctx context.Context, characterID, planetID int, accessToken string) (*PlanetDetail, error) {
|
||||||
url := fmt.Sprintf("/v3/characters/%d/planets/%d/", characterID, planetID)
|
url := fmt.Sprintf("/v3/characters/%d/planets/%d/", characterID, planetID)
|
||||||
result, err := func() (interface{}, error) {
|
fetchFunc := func() (*PlanetDetail, error) {
|
||||||
var fetchFunc func() (interface{}, error) = func() (interface{}, error) {
|
return c.direct.GetPlanetDetails(ctx, characterID, planetID, accessToken)
|
||||||
return c.direct.GetPlanetDetails(ctx, characterID, planetID, accessToken)
|
|
||||||
}
|
|
||||||
return c.getCachedResponse(url, fetchFunc)
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return result.(*PlanetDetail), nil
|
|
||||||
|
return getCachedResponse(c, url, fetchFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCachedResponse handles caching logic
|
// getCachedResponse handles caching logic with generics
|
||||||
func (c *CachedESI) getCachedResponse(url string, fetchFunc func() (interface{}, error)) (interface{}, error) {
|
func getCachedResponse[T any](c *CachedESI, url string, fetchFunc func() (T, error)) (T, error) {
|
||||||
// Generate cache key
|
// Generate cache key
|
||||||
hash := sha256.Sum256([]byte(url))
|
hash := sha256.Sum256([]byte(url))
|
||||||
urlHash := hex.EncodeToString(hash[:])
|
urlHash := hex.EncodeToString(hash[:])
|
||||||
@@ -75,22 +76,12 @@ func (c *CachedESI) getCachedResponse(url string, fetchFunc func() (interface{},
|
|||||||
cacheEntry, err := c.db.GetCacheEntry(urlHash)
|
cacheEntry, err := c.db.GetCacheEntry(urlHash)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Check if cache is still valid
|
// Check if cache is still valid
|
||||||
cacheValidity, _ := time.ParseDuration("10m") // Default 10 minutes
|
if time.Since(cacheEntry.CachedAt) < c.cacheValidity {
|
||||||
if time.Since(cacheEntry.CachedAt) < cacheValidity {
|
|
||||||
logger.Debug("Cache hit for URL: %s", url)
|
logger.Debug("Cache hit for URL: %s", url)
|
||||||
// Parse cached response based on URL pattern
|
// Parse cached response
|
||||||
if url[len(url)-1:] == "/" {
|
var result T
|
||||||
// Planets endpoint
|
if err := json.Unmarshal([]byte(cacheEntry.Response), &result); err == nil {
|
||||||
var planets []Planet
|
return result, nil
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +90,8 @@ func (c *CachedESI) getCachedResponse(url string, fetchFunc func() (interface{},
|
|||||||
logger.Debug("Cache miss for URL: %s", url)
|
logger.Debug("Cache miss for URL: %s", url)
|
||||||
result, err := fetchFunc()
|
result, err := fetchFunc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
var zero T
|
||||||
|
return zero, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store in cache
|
// Store in cache
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go-eve-pi/options"
|
||||||
|
|
||||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,9 +107,16 @@ type DirectESI struct {
|
|||||||
|
|
||||||
// NewDirectESI creates a new DirectESI instance
|
// NewDirectESI creates a new DirectESI instance
|
||||||
func NewDirectESI() *DirectESI {
|
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{
|
return &DirectESI{
|
||||||
httpClient: &http.Client{
|
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("Authorization", "Bearer "+accessToken)
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
resp, err := d.httpClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to fetch character planets: %v", err)
|
logger.Error("Failed to fetch character planets: %v", err)
|
||||||
return nil, 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("Authorization", "Bearer "+accessToken)
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
resp, err := d.httpClient.Do(req)
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to fetch planet details: %v", err)
|
logger.Error("Failed to fetch planet details: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
18
esi/sso.go
18
esi/sso.go
@@ -16,6 +16,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go-eve-pi/options"
|
||||||
"go-eve-pi/repositories"
|
"go-eve-pi/repositories"
|
||||||
"go-eve-pi/types"
|
"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) {
|
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
|
// Wait for callback through channel
|
||||||
select {
|
select {
|
||||||
case result := <-s.callbackChan:
|
case result := <-s.callbackChan:
|
||||||
@@ -289,8 +297,8 @@ func (s *SSO) waitForCallback() (code, state string, err error) {
|
|||||||
}
|
}
|
||||||
logger.Debug("Callback received successfully")
|
logger.Debug("Callback received successfully")
|
||||||
return result.code, result.state, result.err
|
return result.code, result.state, result.err
|
||||||
case <-time.After(30 * time.Second):
|
case <-time.After(callbackTimeout):
|
||||||
logger.Error("Callback timeout after 30 seconds")
|
logger.Error("Callback timeout after %v", callbackTimeout)
|
||||||
return "", "", errors.New("callback timeout")
|
return "", "", errors.New("callback timeout")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +343,7 @@ func (s *SSO) exchangeCodeForToken(ctx context.Context, code, verifier string) (
|
|||||||
CharacterName: name,
|
CharacterName: name,
|
||||||
AccessToken: tr.AccessToken,
|
AccessToken: tr.AccessToken,
|
||||||
RefreshToken: tr.RefreshToken,
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +384,7 @@ func (s *SSO) refreshToken(ctx context.Context, char *types.Character) error {
|
|||||||
char.RefreshToken = tr.RefreshToken
|
char.RefreshToken = tr.RefreshToken
|
||||||
}
|
}
|
||||||
if tr.ExpiresIn > 0 {
|
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)
|
logger.Debug("Saving refreshed token to database for character %s", char.CharacterName)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||||
@@ -29,7 +30,12 @@ type Options struct {
|
|||||||
ClientID string `env:"ESI_CLIENT_ID" description:"EVE SSO client ID"`
|
ClientID string `env:"ESI_CLIENT_ID" description:"EVE SSO client ID"`
|
||||||
RedirectURI string `env:"ESI_REDIRECT_URI" default:"http://localhost:3000/callback" description:"EVE SSO redirect URI"`
|
RedirectURI string `env:"ESI_REDIRECT_URI" default:"http://localhost:3000/callback" description:"EVE SSO redirect URI"`
|
||||||
Scopes []string `env:"ESI_SCOPES" default:"esi-planets.manage_planets.v1" description:"EVE SSO scopes (space-separated)"`
|
Scopes []string `env:"ESI_SCOPES" default:"esi-planets.manage_planets.v1" description:"EVE SSO scopes (space-separated)"`
|
||||||
CacheValidity string `env:"ESI_CACHE_VALIDITY" default:"PT20M" description:"ESI cache validity in duration (e.g. PT20M for 20 minutes)"`
|
CacheValidity string `env:"ESI_CACHE_VALIDITY" default:"PT20M" description:"ESI cache validity in ISO8601 duration (e.g. PT20M for 20 minutes)"`
|
||||||
|
|
||||||
|
// HTTP timeouts
|
||||||
|
HTTPTimeout string `env:"HTTP_TIMEOUT" default:"PT30S" description:"HTTP client timeout in ISO8601 duration (e.g. PT30S for 30 seconds)"`
|
||||||
|
SSOCallbackTimeout string `env:"SSO_CALLBACK_TIMEOUT" default:"PT30S" description:"SSO callback timeout in ISO8601 duration (e.g. PT30S for 30 seconds)"`
|
||||||
|
TokenExpiryBuffer int `env:"TOKEN_EXPIRY_BUFFER" default:"30" description:"Token expiry buffer in seconds"`
|
||||||
|
|
||||||
WebhookURL string `env:"WEBHOOK_URL" description:"Webhook URL for notifications"`
|
WebhookURL string `env:"WEBHOOK_URL" description:"Webhook URL for notifications"`
|
||||||
WebhookEmail string `env:"WEBHOOK_EMAIL" description:"Webhook authentication email"`
|
WebhookEmail string `env:"WEBHOOK_EMAIL" description:"Webhook authentication email"`
|
||||||
@@ -80,6 +86,12 @@ func setFieldValue(field reflect.Value, value string, fieldType reflect.Type) er
|
|||||||
switch fieldType.Kind() {
|
switch fieldType.Kind() {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
field.SetString(value)
|
field.SetString(value)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
intValue, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid integer value: %w", err)
|
||||||
|
}
|
||||||
|
field.SetInt(intValue)
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
if fieldType.Elem().Kind() == reflect.String {
|
if fieldType.Elem().Kind() == reflect.String {
|
||||||
// Handle []string by splitting on spaces
|
// Handle []string by splitting on spaces
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go-eve-pi/options"
|
||||||
|
|
||||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,12 +25,19 @@ type ZulipWebhook struct {
|
|||||||
func NewZulipWebhook(url, email, token string) *ZulipWebhook {
|
func NewZulipWebhook(url, email, token string) *ZulipWebhook {
|
||||||
logger.Info("Zulip webhook client initialized with email: %s", email)
|
logger.Info("Zulip webhook client initialized with email: %s", email)
|
||||||
|
|
||||||
|
// 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 &ZulipWebhook{
|
return &ZulipWebhook{
|
||||||
url: url,
|
url: url,
|
||||||
email: email,
|
email: email,
|
||||||
token: token,
|
token: token,
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: httpTimeout,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user