Skip to content
Open
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ require (
github.com/golang-migrate/migrate/v4 v4.18.3
github.com/google/uuid v1.6.0
github.com/lib/pq v1.10.9
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/prometheus/client_golang v1.14.0
github.com/segmentio/kafka-go v0.4.38
github.com/segmentio/kafka-go/sasl/aws_msk_iam_v2 v0.1.0
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
Expand Down
148 changes: 148 additions & 0 deletions server/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package server

import (
"sync"
"time"

"github.com/brave-intl/challenge-bypass-server/model"
)

type CachingConfig struct {
Enabled bool `json:"enabled"`
ExpirationSec int `json:"expirationSec"`
}

type Cache[T any] interface {
Get(k string) (T, bool)
Delete(k string)
SetDefault(k string, x T)
}

// Clock interface allows us to mock time in tests
type Clock interface {
Now() time.Time
}

type RealClock struct{}

func (RealClock) Now() time.Time {
return time.Now()
}

type SimpleCache[T any] struct {
items sync.Map
defaultExpiration time.Duration
cleanupInterval time.Duration
stopCleanup chan bool
clock Clock
}

type cacheItem[T any] struct {
value T
expiration int64
}

// NewSimpleCache creates a new cache with the given default expiration and cleanup interval
func NewSimpleCache[T any](defaultExpiration, cleanupInterval time.Duration) *SimpleCache[T] {
return newSimpleCacheWithClock[T](defaultExpiration, cleanupInterval, RealClock{})
}

func newSimpleCacheWithClock[T any](defaultExpiration, cleanupInterval time.Duration, clock Clock) *SimpleCache[T] {
cache := &SimpleCache[T]{
defaultExpiration: defaultExpiration,
cleanupInterval: cleanupInterval,
stopCleanup: make(chan bool),
clock: clock,
}

if cleanupInterval > 0 {
go cache.startCleanupTimer()
}

return cache
}

func (c *SimpleCache[T]) startCleanupTimer() {
ticker := time.NewTicker(c.cleanupInterval)
defer ticker.Stop()

for {
select {
case <-ticker.C:
c.deleteExpired()
case <-c.stopCleanup:
return
}
}
}

func (c *SimpleCache[T]) deleteExpired() {
now := c.clock.Now().UnixNano()
c.items.Range(func(key, value any) bool {
item, ok := value.(cacheItem[T])
if ok && item.expiration > 0 && item.expiration < now {
c.items.Delete(key)
}
return true
})
}

func (c *SimpleCache[T]) Get(k string) (T, bool) {
var zero T
value, found := c.items.Load(k)
if !found {
return zero, false
}

item, ok := value.(cacheItem[T])
if !ok {
return zero, false
}

if item.expiration > 0 && item.expiration < c.clock.Now().UnixNano() {
c.items.Delete(k)
return zero, false
}

return item.value, true
}

func (c *SimpleCache[T]) Delete(k string) {
c.items.Delete(k)
}

// SetDefault adds an item to the cache with the default expiration time
func (c *SimpleCache[T]) SetDefault(k string, x T) {
var expiration int64 = 0
if c.defaultExpiration > 0 {
expiration = c.clock.Now().Add(c.defaultExpiration).UnixNano()
}

c.items.Store(k, cacheItem[T]{
value: x,
expiration: expiration,
})
}

type CacheCollection struct {
Issuer *SimpleCache[*model.Issuer]
Issuers *SimpleCache[[]model.Issuer]
Redemptions *SimpleCache[*Redemption]
IssuerCohort *SimpleCache[[]model.Issuer]
}

func bootstrapCache(cfg DBConfig) *CacheCollection {
if !cfg.CachingConfig.Enabled {
return nil
}

defaultDuration := time.Duration(cfg.CachingConfig.ExpirationSec) * time.Second
cleanupInterval := defaultDuration * 2

return &CacheCollection{
Issuer: NewSimpleCache[*model.Issuer](defaultDuration, cleanupInterval),
Issuers: NewSimpleCache[[]model.Issuer](defaultDuration, cleanupInterval),
Redemptions: NewSimpleCache[*Redemption](defaultDuration, cleanupInterval),
IssuerCohort: NewSimpleCache[[]model.Issuer](defaultDuration, cleanupInterval),
}
}
Loading
Loading