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