1// Copyright 2020 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package cache
5
6import (
7 "fmt"
8 "strconv"
9 "time"
10
11 "forgejo.org/modules/graceful"
12 "forgejo.org/modules/nosql"
13
14 "code.forgejo.org/go-chi/cache"
15)
16
17// RedisCacher represents a redis cache adapter implementation.
18type RedisCacher struct {
19 c nosql.RedisClient
20 prefix string
21 hsetName string
22 occupyMode bool
23}
24
25// toStr convert string/int/int64 interface to string. it's only used by the RedisCacher.Put internally
26func toStr(v any) string {
27 if v == nil {
28 return ""
29 }
30 switch v := v.(type) {
31 case string:
32 return v
33 case []byte:
34 return string(v)
35 case int:
36 return strconv.FormatInt(int64(v), 10)
37 case int64:
38 return strconv.FormatInt(v, 10)
39 default:
40 return fmt.Sprint(v) // as what the old com.ToStr does in most cases
41 }
42}
43
44// Put puts value (string type) into cache with key and expire time.
45// If expired is 0, it lives forever.
46func (c *RedisCacher) Put(key string, val any, expire int64) error {
47 // this function is not well-designed, it only puts string values into cache
48 key = c.prefix + key
49 if expire == 0 {
50 if err := c.c.Set(graceful.GetManager().HammerContext(), key, toStr(val), 0).Err(); err != nil {
51 return err
52 }
53 } else {
54 dur := time.Duration(expire) * time.Second
55 if err := c.c.Set(graceful.GetManager().HammerContext(), key, toStr(val), dur).Err(); err != nil {
56 return err
57 }
58 }
59
60 if c.occupyMode {
61 return nil
62 }
63 return c.c.HSet(graceful.GetManager().HammerContext(), c.hsetName, key, "0").Err()
64}
65
66// Get gets cached value by given key.
67func (c *RedisCacher) Get(key string) any {
68 val, err := c.c.Get(graceful.GetManager().HammerContext(), c.prefix+key).Result()
69 if err != nil {
70 return nil
71 }
72 return val
73}
74
75// Delete deletes cached value by given key.
76func (c *RedisCacher) Delete(key string) error {
77 key = c.prefix + key
78 if err := c.c.Del(graceful.GetManager().HammerContext(), key).Err(); err != nil {
79 return err
80 }
81
82 if c.occupyMode {
83 return nil
84 }
85 return c.c.HDel(graceful.GetManager().HammerContext(), c.hsetName, key).Err()
86}
87
88// Incr increases cached int-type value by given key as a counter.
89func (c *RedisCacher) Incr(key string) error {
90 if !c.IsExist(key) {
91 return fmt.Errorf("key '%s' not exist", key)
92 }
93 return c.c.Incr(graceful.GetManager().HammerContext(), c.prefix+key).Err()
94}
95
96// Decr decreases cached int-type value by given key as a counter.
97func (c *RedisCacher) Decr(key string) error {
98 if !c.IsExist(key) {
99 return fmt.Errorf("key '%s' not exist", key)
100 }
101 return c.c.Decr(graceful.GetManager().HammerContext(), c.prefix+key).Err()
102}
103
104// IsExist returns true if cached value exists.
105func (c *RedisCacher) IsExist(key string) bool {
106 if c.c.Exists(graceful.GetManager().HammerContext(), c.prefix+key).Val() == 1 {
107 return true
108 }
109
110 if !c.occupyMode {
111 c.c.HDel(graceful.GetManager().HammerContext(), c.hsetName, c.prefix+key)
112 }
113 return false
114}
115
116// Flush deletes all cached data.
117func (c *RedisCacher) Flush() error {
118 if c.occupyMode {
119 return c.c.FlushDB(graceful.GetManager().HammerContext()).Err()
120 }
121
122 keys, err := c.c.HKeys(graceful.GetManager().HammerContext(), c.hsetName).Result()
123 if err != nil {
124 return err
125 }
126 if err = c.c.Del(graceful.GetManager().HammerContext(), keys...).Err(); err != nil {
127 return err
128 }
129 return c.c.Del(graceful.GetManager().HammerContext(), c.hsetName).Err()
130}
131
132// StartAndGC starts GC routine based on config string settings.
133// AdapterConfig: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180,hset_name=MacaronCache,prefix=cache:
134func (c *RedisCacher) StartAndGC(opts cache.Options) error {
135 c.hsetName = "MacaronCache"
136 c.occupyMode = opts.OccupyMode
137
138 uri := nosql.ToRedisURI(opts.AdapterConfig)
139
140 c.c = nosql.GetManager().GetRedisClient(uri.String())
141
142 for k, v := range uri.Query() {
143 switch k {
144 case "hset_name":
145 c.hsetName = v[0]
146 case "prefix":
147 c.prefix = v[0]
148 }
149 }
150
151 return c.c.Ping(graceful.GetManager().HammerContext()).Err()
152}
153
154// Ping tests if the cache is alive.
155func (c *RedisCacher) Ping() error {
156 return c.c.Ping(graceful.GetManager().HammerContext()).Err()
157}
158
159func init() {
160 cache.Register("redis", &RedisCacher{})
161}