snapshot_cache.go 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. package admin
  2. import (
  3. "crypto/sha256"
  4. "encoding/hex"
  5. "encoding/json"
  6. "strings"
  7. "sync"
  8. "time"
  9. "golang.org/x/sync/singleflight"
  10. )
  11. type snapshotCacheEntry struct {
  12. ETag string
  13. Payload any
  14. ExpiresAt time.Time
  15. }
  16. type snapshotCache struct {
  17. mu sync.RWMutex
  18. ttl time.Duration
  19. items map[string]snapshotCacheEntry
  20. sf singleflight.Group
  21. }
  22. type snapshotCacheLoadResult struct {
  23. Entry snapshotCacheEntry
  24. Hit bool
  25. }
  26. func newSnapshotCache(ttl time.Duration) *snapshotCache {
  27. if ttl <= 0 {
  28. ttl = 30 * time.Second
  29. }
  30. return &snapshotCache{
  31. ttl: ttl,
  32. items: make(map[string]snapshotCacheEntry),
  33. }
  34. }
  35. func (c *snapshotCache) Get(key string) (snapshotCacheEntry, bool) {
  36. if c == nil || key == "" {
  37. return snapshotCacheEntry{}, false
  38. }
  39. now := time.Now()
  40. c.mu.RLock()
  41. entry, ok := c.items[key]
  42. c.mu.RUnlock()
  43. if !ok {
  44. return snapshotCacheEntry{}, false
  45. }
  46. if now.After(entry.ExpiresAt) {
  47. c.mu.Lock()
  48. delete(c.items, key)
  49. c.mu.Unlock()
  50. return snapshotCacheEntry{}, false
  51. }
  52. return entry, true
  53. }
  54. func (c *snapshotCache) Set(key string, payload any) snapshotCacheEntry {
  55. if c == nil {
  56. return snapshotCacheEntry{}
  57. }
  58. entry := snapshotCacheEntry{
  59. ETag: buildETagFromAny(payload),
  60. Payload: payload,
  61. ExpiresAt: time.Now().Add(c.ttl),
  62. }
  63. if key == "" {
  64. return entry
  65. }
  66. c.mu.Lock()
  67. c.items[key] = entry
  68. c.mu.Unlock()
  69. return entry
  70. }
  71. func (c *snapshotCache) GetOrLoad(key string, load func() (any, error)) (snapshotCacheEntry, bool, error) {
  72. if load == nil {
  73. return snapshotCacheEntry{}, false, nil
  74. }
  75. if entry, ok := c.Get(key); ok {
  76. return entry, true, nil
  77. }
  78. if c == nil || key == "" {
  79. payload, err := load()
  80. if err != nil {
  81. return snapshotCacheEntry{}, false, err
  82. }
  83. return c.Set(key, payload), false, nil
  84. }
  85. value, err, _ := c.sf.Do(key, func() (any, error) {
  86. if entry, ok := c.Get(key); ok {
  87. return snapshotCacheLoadResult{Entry: entry, Hit: true}, nil
  88. }
  89. payload, err := load()
  90. if err != nil {
  91. return nil, err
  92. }
  93. return snapshotCacheLoadResult{Entry: c.Set(key, payload), Hit: false}, nil
  94. })
  95. if err != nil {
  96. return snapshotCacheEntry{}, false, err
  97. }
  98. result, ok := value.(snapshotCacheLoadResult)
  99. if !ok {
  100. return snapshotCacheEntry{}, false, nil
  101. }
  102. return result.Entry, result.Hit, nil
  103. }
  104. func buildETagFromAny(payload any) string {
  105. raw, err := json.Marshal(payload)
  106. if err != nil {
  107. return ""
  108. }
  109. sum := sha256.Sum256(raw)
  110. return "\"" + hex.EncodeToString(sum[:]) + "\""
  111. }
  112. func parseBoolQueryWithDefault(raw string, def bool) bool {
  113. value := strings.TrimSpace(strings.ToLower(raw))
  114. if value == "" {
  115. return def
  116. }
  117. switch value {
  118. case "1", "true", "yes", "on":
  119. return true
  120. case "0", "false", "no", "off":
  121. return false
  122. default:
  123. return def
  124. }
  125. }