1package cache
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "time"
8
9 "github.com/redis/go-redis/v9"
10)
11
12type Client struct {
13 rdb *redis.Client
14 prefix string
15 metrics *Metrics
16}
17
18type Config struct {
19 Addr string
20 Password string
21 DB int
22 PoolSize int
23 MinIdleConns int
24 KeyPrefix string
25}
26
27func NewClient(cfg Config) (*Client, error) {
28 rdb := redis.NewClient(&redis.Options{
29 Addr: cfg.Addr,
30 Password: cfg.Password,
31 DB: cfg.DB,
32 PoolSize: cfg.PoolSize,
33 MinIdleConns: cfg.MinIdleConns,
34 ReadTimeout: 100 * time.Millisecond,
35 WriteTimeout: 100 * time.Millisecond,
36 DialTimeout: 5 * time.Second,
37 })
38
39 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
40 defer cancel()
41
42 if err := rdb.Ping(ctx).Err(); err != nil {
43 return nil, fmt.Errorf("redis ping: %w", err)
44 }
45
46 return &Client{
47 rdb: rdb,
48 prefix: cfg.KeyPrefix,
49 metrics: NewMetrics(),
50 }, nil
51}
52
53func (c *Client) Get(ctx context.Context, key string, dest interface{}) error {
54 start := time.Now()
55 fullKey := c.prefixKey(key)
56
57 raw, err := c.rdb.Get(ctx, fullKey).Bytes()
58 if err == redis.Nil {
59 c.metrics.RecordMiss(time.Since(start))
60 return ErrCacheMiss
61 }
62 if err != nil {
63 return fmt.Errorf("redis get: %w", err)
64 }
65
66 c.metrics.RecordHit(time.Since(start))
67 return json.Unmarshal(raw, dest)
68}
69
70func (c *Client) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
71 fullKey := c.prefixKey(key)
72 data, err := json.Marshal(value)
73 if err != nil {
74 return fmt.Errorf("marshal: %w", err)
75 }
76 return c.rdb.Set(ctx, fullKey, data, ttl).Err()
77}
78
79func (c *Client) Delete(ctx context.Context, keys ...string) error {
80 fullKeys := make([]string, len(keys))
81 for i, k := range keys {
82 fullKeys[i] = c.prefixKey(k)
83 }
84 return c.rdb.Del(ctx, fullKeys...).Err()
85}
86
87func (c *Client) GetOrLoad(ctx context.Context, key string, dest interface{}, ttl time.Duration, loader func() (interface{}, error)) error {
88 err := c.Get(ctx, key, dest)
89 if err == nil {
90 return nil
91 }
92 if err != ErrCacheMiss {
93 return err
94 }
95
96 value, err := loader()
97 if err != nil {
98 return err
99 }
100
101 if err := c.Set(ctx, key, value, ttl); err != nil {
102
103 }
104
105 data, _ := json.Marshal(value)
106 return json.Unmarshal(data, dest)
107}
108
109func (c *Client) prefixKey(key string) string {
110 if c.prefix == "" {
111 return key
112 }
113 return c.prefix + ":" + key
114}
115
116var ErrCacheMiss = fmt.Errorf("cache miss")
117