Update copyright
[friendica.git/.git] / src / Core / Cache / RedisCache.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Core\Cache;
23
24 use Exception;
25 use Friendica\Core\BaseCache;
26 use Friendica\Core\Config\IConfig;
27 use Redis;
28
29 /**
30  * Redis Cache. This driver is based on Memcache driver
31  */
32 class RedisCache extends BaseCache implements IMemoryCache
33 {
34         /**
35          * @var Redis
36          */
37         private $redis;
38
39         /**
40          * @throws Exception
41          */
42         public function __construct(string $hostname, IConfig $config)
43         {
44                 if (!class_exists('Redis', false)) {
45                         throw new Exception('Redis class isn\'t available');
46                 }
47
48                 parent::__construct($hostname);
49
50                 $this->redis = new Redis();
51
52                 $redis_host = $config->get('system', 'redis_host');
53                 $redis_port = $config->get('system', 'redis_port');
54                 $redis_pw   = $config->get('system', 'redis_password');
55                 $redis_db   = $config->get('system', 'redis_db', 0);
56
57                 if (isset($redis_port) && !@$this->redis->connect($redis_host, $redis_port)) {
58                         throw new Exception('Expected Redis server at ' . $redis_host . ':' . $redis_port . ' isn\'t available');
59                 } elseif (!@$this->redis->connect($redis_host)) {
60                         throw new Exception('Expected Redis server at ' . $redis_host . ' isn\'t available');
61                 }
62
63                 if (isset($redis_pw) && !$this->redis->auth($redis_pw)) {
64                         throw new Exception('Cannot authenticate redis server at ' . $redis_host . ':' . $redis_port);
65                 }
66
67                 if ($redis_db !== 0 && !$this->redis->select($redis_db)) {
68                         throw new Exception('Cannot switch to redis db ' . $redis_db . ' at ' . $redis_host . ':' . $redis_port);
69                 }
70         }
71
72         /**
73          * (@inheritdoc)
74          */
75         public function getAllKeys($prefix = null)
76         {
77                 if (empty($prefix)) {
78                         $search = '*';
79                 } else {
80                         $search = $prefix . '*';
81                 }
82
83                 $list = $this->redis->keys($this->getCacheKey($search));
84
85                 return $this->getOriginalKeys($list);
86         }
87
88         /**
89          * (@inheritdoc)
90          */
91         public function get($key)
92         {
93                 $return = null;
94                 $cachekey = $this->getCacheKey($key);
95
96                 $cached = $this->redis->get($cachekey);
97                 if ($cached === false && !$this->redis->exists($cachekey)) {
98                         return null;
99                 }
100
101                 $value = unserialize($cached);
102
103                 // Only return a value if the serialized value is valid.
104                 // We also check if the db entry is a serialized
105                 // boolean 'false' value (which we want to return).
106                 if ($cached === serialize(false) || $value !== false) {
107                         $return = $value;
108                 }
109
110                 return $return;
111         }
112
113         /**
114          * (@inheritdoc)
115          */
116         public function set($key, $value, $ttl = Duration::FIVE_MINUTES)
117         {
118                 $cachekey = $this->getCacheKey($key);
119
120                 $cached = serialize($value);
121
122                 if ($ttl > 0) {
123                         return $this->redis->setex(
124                                 $cachekey,
125                                 $ttl,
126                                 $cached
127                         );
128                 } else {
129                         return $this->redis->set(
130                                 $cachekey,
131                                 $cached
132                         );
133                 }
134         }
135
136         /**
137          * (@inheritdoc)
138          */
139         public function delete($key)
140         {
141                 $cachekey = $this->getCacheKey($key);
142                 $this->redis->del($cachekey);
143                 // Redis doesn't have an error state for del()
144                 return true;
145         }
146
147         /**
148          * (@inheritdoc)
149          */
150         public function clear($outdated = true)
151         {
152                 if ($outdated) {
153                         return true;
154                 } else {
155                         return $this->redis->flushAll();
156                 }
157         }
158
159         /**
160          * (@inheritdoc)
161          */
162         public function add($key, $value, $ttl = Duration::FIVE_MINUTES)
163         {
164                 $cachekey = $this->getCacheKey($key);
165                 $cached = serialize($value);
166
167                 return $this->redis->setnx($cachekey, $cached);
168         }
169
170         /**
171          * (@inheritdoc)
172          */
173         public function compareSet($key, $oldValue, $newValue, $ttl = Duration::FIVE_MINUTES)
174         {
175                 $cachekey = $this->getCacheKey($key);
176
177                 $newCached = serialize($newValue);
178
179                 $this->redis->watch($cachekey);
180                 // If the old value isn't what we expected, somebody else changed the key meanwhile
181                 if ($this->get($key) === $oldValue) {
182                         if ($ttl > 0) {
183                                 $result = $this->redis->multi()
184                                         ->setex($cachekey, $ttl, $newCached)
185                                         ->exec();
186                         } else {
187                                 $result = $this->redis->multi()
188                                         ->set($cachekey, $newCached)
189                                         ->exec();
190                         }
191                         return $result !== false;
192                 }
193                 $this->redis->unwatch();
194                 return false;
195         }
196
197         /**
198          * (@inheritdoc)
199          */
200         public function compareDelete($key, $value)
201         {
202                 $cachekey = $this->getCacheKey($key);
203
204                 $this->redis->watch($cachekey);
205                 // If the old value isn't what we expected, somebody else changed the key meanwhile
206                 if ($this->get($key) === $value) {
207                         $result = $this->redis->multi()
208                                 ->del($cachekey)
209                                 ->exec();
210                         return $result !== false;
211                 }
212                 $this->redis->unwatch();
213                 return false;
214         }
215
216         /**
217          * {@inheritDoc}
218          */
219         public function getName()
220         {
221                 return Type::REDIS;
222         }
223 }