Ctrl K
ComunidadGuías…

server/internal/handlers/webhook_secret_cache.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handlers

import (
	"strings"
	"sync"
	"time"

	"golang.org/x/sync/singleflight"
	"gorm.io/gorm"
)

const webhookSecretCacheTTL = 60 * time.Second

type cachedSecret struct {
	secret    string
	expiresAt time.Time
}

type cachedSecretMap struct {
	secrets   map[string]string
	expiresAt time.Time
}

var (
	webhookSecretCache      sync.Map
	webhookSecretRefreshes  singleflight.Group
	webhookSecretsRefreshes singleflight.Group
)

func PrimeWebhookSecretCache(db *gorm.DB, sourceType, envFallback string) string {
	return refreshWebhookSecretCache(db, sourceType, envFallback)
}

// GetCachedWebhookSecret returns the webhook secret for sourceType, reading from
// the DB at most once per webhookSecretCacheTTL. Changes made via the admin UI
// take effect immediately because refreshWebhookSecretCacheIfNeeded is called
// synchronously after every create/update/delete. The TTL is a safety net for
// external DB edits and for expiring stale entries after a delete.
func GetCachedWebhookSecret(db *gorm.DB, sourceType, envFallback string) string {
	if v, ok := webhookSecretCache.Load(sourceType); ok {
		if entry, ok := v.(cachedSecret); ok && time.Now().Before(entry.expiresAt) {
			if entry.secret != "" {
				return entry.secret
			}
			// Empty secret cached — still respect TTL to avoid hammering the DB,
			// but fall through to env fallback.
			return envFallback
		}
		webhookSecretCache.Delete(sourceType)
	}
	return refreshWebhookSecretCache(db, sourceType, envFallback)
}

// GetCachedWebhookSecrets returns a map of sourceID → secret for all enabled
// instances of sourceType. Uses the same TTL and invalidation logic as the
// singleton cache. Returns an empty map (never nil) if no instances are found.
// The returned map is a copy; callers may not mutate the cache through it.
func GetCachedWebhookSecrets(db *gorm.DB, sourceType string) map[string]string {
	key := sourceType + ":multi"
	if v, ok := webhookSecretCache.Load(key); ok {
		if entry, ok := v.(cachedSecretMap); ok && time.Now().Before(entry.expiresAt) {
			return cloneStringMap(entry.secrets)
		}
		webhookSecretCache.Delete(key)
	}
	return refreshWebhookSecretsCache(db, sourceType)
}

// PrimeWebhookSecretsCache eagerly loads the multi-instance secret cache for
// sourceType at startup so the first inbound request does not hit the DB.
func PrimeWebhookSecretsCache(db *gorm.DB, sourceType string) map[string]string {
	return refreshWebhookSecretsCache(db, sourceType)
}

func refreshWebhookSecretCache(db *gorm.DB, sourceType, envFallback string) string {
	resolved, _, _ := webhookSecretRefreshes.Do(sourceType, func() (interface{}, error) {
		secret := GetWebhookSecret(db, sourceType)
		if secret == "" {
			secret = envFallback
		}
		webhookSecretCache.Store(sourceType, cachedSecret{
			secret:    secret,
			expiresAt: time.Now().Add(webhookSecretCacheTTL),
		})
		return secret, nil
	})
	if secret, ok := resolved.(string); ok {
		return secret
	}
	return envFallback
}

func refreshWebhookSecretsCache(db *gorm.DB, sourceType string) map[string]string {
	key := sourceType + ":multi"
	resolved, _, _ := webhookSecretsRefreshes.Do(sourceType, func() (interface{}, error) {
		secrets := GetAllWebhookSecrets(db, sourceType)
		webhookSecretCache.Store(key, cachedSecretMap{
			secrets:   cloneStringMap(secrets),
			expiresAt: time.Now().Add(webhookSecretCacheTTL),
		})
		return secrets, nil
	})
	if secrets, ok := resolved.(map[string]string); ok {
		return cloneStringMap(secrets)
	}
	return map[string]string{}
}

func cloneStringMap(m map[string]string) map[string]string {
	c := make(map[string]string, len(m))
	for k, v := range m {
		c[k] = v
	}
	return c
}

func refreshWebhookSecretCacheIfNeeded(db *gorm.DB, sourceType string) {
	if sourceType == "" || db == nil || !strings.HasPrefix(sourceType, "webhook_") {
		return
	}
	webhookSecretCache.Delete(sourceType)
	webhookSecretCache.Delete(sourceType + ":multi")
	refreshWebhookSecretCache(db, sourceType, "")
	refreshWebhookSecretsCache(db, sourceType)
}

func ResetWebhookSecretCacheForTesting(sourceTypes ...string) {
	for _, sourceType := range sourceTypes {
		webhookSecretCache.Delete(sourceType)
		webhookSecretCache.Delete(sourceType + ":multi")
	}
}