Merge pull request #15875 from civicrm/5.20
[civicrm-core.git] / CRM / Utils / Cache / Memcached.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17 class CRM_Utils_Cache_Memcached implements CRM_Utils_Cache_Interface {
18
19 // TODO Consider native implementation.
20 use CRM_Utils_Cache_NaiveMultipleTrait;
21
22 const DEFAULT_HOST = 'localhost';
23 const DEFAULT_PORT = 11211;
24 const DEFAULT_TIMEOUT = 3600;
25 const DEFAULT_PREFIX = '';
26 const MAX_KEY_LEN = 200;
27
28 /**
29 * If another process clears namespace, we'll find out in ~5 sec.
30 */
31 const NS_LOCAL_TTL = 5;
32
33 /**
34 * The host name of the memcached server
35 *
36 * @var string
37 */
38 protected $_host = self::DEFAULT_HOST;
39
40 /**
41 * The port on which to connect on
42 *
43 * @var int
44 */
45 protected $_port = self::DEFAULT_PORT;
46
47 /**
48 * The default timeout to use
49 *
50 * @var int
51 */
52 protected $_timeout = self::DEFAULT_TIMEOUT;
53
54 /**
55 * The prefix prepended to cache keys.
56 *
57 * If we are using the same memcache instance for multiple CiviCRM
58 * installs, we must have a unique prefix for each install to prevent
59 * the keys from clobbering each other.
60 *
61 * @var string
62 */
63 protected $_prefix = self::DEFAULT_PREFIX;
64
65 /**
66 * The actual memcache object.
67 *
68 * @var Memcached
69 */
70 protected $_cache;
71
72 /**
73 * @var NULL|array
74 *
75 * This is the effective prefix. It may be bumped up whenever the dataset is flushed.
76 *
77 * @see https://github.com/memcached/memcached/wiki/ProgrammingTricks#deleting-by-namespace
78 */
79 protected $_truePrefix = NULL;
80
81 /**
82 * Constructor.
83 *
84 * @param array $config
85 * An array of configuration params.
86 *
87 * @return \CRM_Utils_Cache_Memcached
88 */
89 public function __construct($config) {
90 if (isset($config['host'])) {
91 $this->_host = $config['host'];
92 }
93 if (isset($config['port'])) {
94 $this->_port = $config['port'];
95 }
96 if (isset($config['timeout'])) {
97 $this->_timeout = $config['timeout'];
98 }
99 if (isset($config['prefix'])) {
100 $this->_prefix = $config['prefix'];
101 }
102
103 $this->_cache = new Memcached();
104
105 if (!$this->_cache->addServer($this->_host, $this->_port)) {
106 // dont use fatal here since we can go in an infinite loop
107 echo 'Could not connect to Memcached server';
108 CRM_Utils_System::civiExit();
109 }
110 }
111
112 /**
113 * @param $key
114 * @param $value
115 * @param null|int|\DateInterval $ttl
116 *
117 * @return bool
118 * @throws Exception
119 */
120 public function set($key, $value, $ttl = NULL) {
121 CRM_Utils_Cache::assertValidKey($key);
122 if (is_int($ttl) && $ttl <= 0) {
123 return $this->delete($key);
124 }
125 $expires = CRM_Utils_Date::convertCacheTtlToExpires($ttl, $this->_timeout);
126
127 $key = $this->cleanKey($key);
128 if (!$this->_cache->set($key, serialize($value), $expires)) {
129 if (PHP_SAPI === 'cli' || (Civi\Core\Container::isContainerBooted() && CRM_Core_Permission::check('view debug output'))) {
130 throw new CRM_Utils_Cache_CacheException("Memcached::set($key) failed: " . $this->_cache->getResultMessage());
131 }
132 else {
133 Civi::log()->error("Memcached::set($key) failed: " . $this->_cache->getResultMessage());
134 throw new CRM_Utils_Cache_CacheException("Memcached::set($key) failed");
135 }
136 return FALSE;
137
138 }
139 return TRUE;
140 }
141
142 /**
143 * @param $key
144 * @param mixed $default
145 *
146 * @return mixed
147 */
148 public function get($key, $default = NULL) {
149 CRM_Utils_Cache::assertValidKey($key);
150 $key = $this->cleanKey($key);
151 $result = $this->_cache->get($key);
152 switch ($this->_cache->getResultCode()) {
153 case Memcached::RES_SUCCESS:
154 return unserialize($result);
155
156 case Memcached::RES_NOTFOUND:
157 return $default;
158
159 default:
160 Civi::log()->error("Memcached::get($key) failed: " . $this->_cache->getResultMessage());
161 throw new CRM_Utils_Cache_CacheException("Memcached set ($key) failed");
162 }
163 }
164
165 /**
166 * @param string $key
167 *
168 * @return bool
169 * @throws \Psr\SimpleCache\CacheException
170 */
171 public function has($key) {
172 CRM_Utils_Cache::assertValidKey($key);
173 $key = $this->cleanKey($key);
174 if ($this->_cache->get($key) !== FALSE) {
175 return TRUE;
176 }
177 switch ($this->_cache->getResultCode()) {
178 case Memcached::RES_NOTFOUND:
179 return FALSE;
180
181 case Memcached::RES_SUCCESS:
182 return TRUE;
183
184 default:
185 Civi::log()->error("Memcached::has($key) failed: " . $this->_cache->getResultMessage());
186 throw new CRM_Utils_Cache_CacheException("Memcached set ($key) failed");
187 }
188 }
189
190 /**
191 * @param $key
192 *
193 * @return mixed
194 */
195 public function delete($key) {
196 CRM_Utils_Cache::assertValidKey($key);
197 $key = $this->cleanKey($key);
198 if ($this->_cache->delete($key)) {
199 return TRUE;
200 }
201 $code = $this->_cache->getResultCode();
202 return ($code == Memcached::RES_DELETED || $code == Memcached::RES_NOTFOUND);
203 }
204
205 /**
206 * @param $key
207 *
208 * @return mixed|string
209 */
210 public function cleanKey($key) {
211 $truePrefix = $this->getTruePrefix();
212 $maxLen = self::MAX_KEY_LEN - strlen($truePrefix);
213 $key = preg_replace('/\s+|\W+/', '_', $key);
214 if (strlen($key) > $maxLen) {
215 // this should be 32 characters in length
216 $md5Key = md5($key);
217 $subKeyLen = $maxLen - 1 - strlen($md5Key);
218 $key = substr($key, 0, $subKeyLen) . "_" . $md5Key;
219 }
220 return $truePrefix . $key;
221 }
222
223 /**
224 * @return bool
225 */
226 public function flush() {
227 $this->_truePrefix = NULL;
228 if ($this->_cache->delete($this->_prefix)) {
229 return TRUE;
230 }
231 $code = $this->_cache->getResultCode();
232 return ($code == Memcached::RES_DELETED || $code == Memcached::RES_NOTFOUND);
233 }
234
235 public function clear() {
236 return $this->flush();
237 }
238
239 protected function getTruePrefix() {
240 if ($this->_truePrefix === NULL || $this->_truePrefix['expires'] < time()) {
241 $key = $this->_prefix;
242 $value = $this->_cache->get($key);
243 if ($this->_cache->getResultCode() === Memcached::RES_NOTFOUND) {
244 $value = uniqid();
245 // Indefinite.
246 $this->_cache->add($key, $value, 0);
247 }
248 $this->_truePrefix = [
249 'value' => $value,
250 'expires' => time() + self::NS_LOCAL_TTL,
251 ];
252 }
253 return $this->_prefix . $this->_truePrefix['value'] . '/';
254 }
255
256 }