aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Memcache/Redis.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Memcache/Redis.php')
-rw-r--r--lib/private/Memcache/Redis.php194
1 files changed, 118 insertions, 76 deletions
diff --git a/lib/private/Memcache/Redis.php b/lib/private/Memcache/Redis.php
index 9b07da2d99c..f8c51570c4f 100644
--- a/lib/private/Memcache/Redis.php
+++ b/lib/private/Memcache/Redis.php
@@ -1,72 +1,86 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Stefan Weil <sw@weilnetz.de>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Memcache;
use OCP\IMemcacheTTL;
class Redis extends Cache implements IMemcacheTTL {
+ /** name => [script, sha1] */
+ public const LUA_SCRIPTS = [
+ 'dec' => [
+ 'if redis.call("exists", KEYS[1]) == 1 then return redis.call("decrby", KEYS[1], ARGV[1]) else return "NEX" end',
+ '720b40cb66cef1579f2ef16ec69b3da8c85510e9',
+ ],
+ 'cas' => [
+ 'if redis.call("get", KEYS[1]) == ARGV[1] then redis.call("set", KEYS[1], ARGV[2]) return 1 else return 0 end',
+ '94eac401502554c02b811e3199baddde62d976d4',
+ ],
+ 'cad' => [
+ 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end',
+ 'cf0e94b2e9ffc7e04395cf88f7583fc309985910',
+ ],
+ 'ncad' => [
+ 'if redis.call("get", KEYS[1]) ~= ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end',
+ '75526f8048b13ce94a41b58eee59c664b4990ab2',
+ ],
+ 'caSetTtl' => [
+ 'if redis.call("get", KEYS[1]) == ARGV[1] then redis.call("expire", KEYS[1], ARGV[2]) return 1 else return 0 end',
+ 'fa4acbc946d23ef41d7d3910880b60e6e4972d72',
+ ],
+ ];
+
+ private const MAX_TTL = 30 * 24 * 60 * 60; // 1 month
+
/**
- * @var \Redis $cache
+ * @var \Redis|\RedisCluster $cache
*/
private static $cache = null;
public function __construct($prefix = '', string $logFile = '') {
parent::__construct($prefix);
+ }
+
+ /**
+ * @return \Redis|\RedisCluster|null
+ * @throws \Exception
+ */
+ public function getCache() {
if (is_null(self::$cache)) {
- self::$cache = \OC::$server->getGetRedisFactory()->getInstance();
+ self::$cache = \OC::$server->get('RedisFactory')->getInstance();
}
+ return self::$cache;
}
public function get($key) {
- $result = self::$cache->get($this->getPrefix() . $key);
- if ($result === false && !self::$cache->exists($this->getPrefix() . $key)) {
+ $result = $this->getCache()->get($this->getPrefix() . $key);
+ if ($result === false) {
return null;
- } else {
- return json_decode($result, true);
}
+
+ return self::decodeValue($result);
}
public function set($key, $value, $ttl = 0) {
- if ($ttl > 0) {
- return self::$cache->setex($this->getPrefix() . $key, $ttl, json_encode($value));
- } else {
- return self::$cache->set($this->getPrefix() . $key, json_encode($value));
+ $value = self::encodeValue($value);
+ if ($ttl === 0) {
+ // having infinite TTL can lead to leaked keys as the prefix changes with version upgrades
+ $ttl = self::DEFAULT_TTL;
}
+ $ttl = min($ttl, self::MAX_TTL);
+ return $this->getCache()->setex($this->getPrefix() . $key, $ttl, $value);
}
public function hasKey($key) {
- return (bool)self::$cache->exists($this->getPrefix() . $key);
+ return (bool)$this->getCache()->exists($this->getPrefix() . $key);
}
public function remove($key) {
- if (self::$cache->del($this->getPrefix() . $key)) {
+ if ($this->getCache()->unlink($this->getPrefix() . $key)) {
return true;
} else {
return false;
@@ -74,11 +88,12 @@ class Redis extends Cache implements IMemcacheTTL {
}
public function clear($prefix = '') {
+ // TODO: this is slow and would fail with Redis cluster
$prefix = $this->getPrefix() . $prefix . '*';
- $keys = self::$cache->keys($prefix);
- $deleted = self::$cache->del($keys);
+ $keys = $this->getCache()->keys($prefix);
+ $deleted = $this->getCache()->del($keys);
- return count($keys) === $deleted;
+ return (is_array($keys) && (count($keys) === $deleted));
}
/**
@@ -90,17 +105,17 @@ class Redis extends Cache implements IMemcacheTTL {
* @return bool
*/
public function add($key, $value, $ttl = 0) {
- // don't encode ints for inc/dec
- if (!is_int($value)) {
- $value = json_encode($value);
+ $value = self::encodeValue($value);
+ if ($ttl === 0) {
+ // having infinite TTL can lead to leaked keys as the prefix changes with version upgrades
+ $ttl = self::DEFAULT_TTL;
}
+ $ttl = min($ttl, self::MAX_TTL);
$args = ['nx'];
- if ($ttl !== 0 && is_int($ttl)) {
- $args['ex'] = $ttl;
- }
+ $args['ex'] = $ttl;
- return self::$cache->set($this->getPrefix() . $key, $value, $args);
+ return $this->getCache()->set($this->getPrefix() . $key, $value, $args);
}
/**
@@ -111,7 +126,7 @@ class Redis extends Cache implements IMemcacheTTL {
* @return int | bool
*/
public function inc($key, $step = 1) {
- return self::$cache->incrBy($this->getPrefix() . $key, $step);
+ return $this->getCache()->incrBy($this->getPrefix() . $key, $step);
}
/**
@@ -122,10 +137,8 @@ class Redis extends Cache implements IMemcacheTTL {
* @return int | bool
*/
public function dec($key, $step = 1) {
- if (!$this->hasKey($key)) {
- return false;
- }
- return self::$cache->decrBy($this->getPrefix() . $key, $step);
+ $res = $this->evalLua('dec', [$key], [$step]);
+ return ($res === 'NEX') ? false : $res;
}
/**
@@ -137,18 +150,10 @@ class Redis extends Cache implements IMemcacheTTL {
* @return bool
*/
public function cas($key, $old, $new) {
- if (!is_int($new)) {
- $new = json_encode($new);
- }
- self::$cache->watch($this->getPrefix() . $key);
- if ($this->get($key) === $old) {
- $result = self::$cache->multi()
- ->set($this->getPrefix() . $key, $new)
- ->exec();
- return $result !== false;
- }
- self::$cache->unwatch();
- return false;
+ $old = self::encodeValue($old);
+ $new = self::encodeValue($new);
+
+ return $this->evalLua('cas', [$key], [$old, $new]) > 0;
}
/**
@@ -159,22 +164,59 @@ class Redis extends Cache implements IMemcacheTTL {
* @return bool
*/
public function cad($key, $old) {
- self::$cache->watch($this->getPrefix() . $key);
- if ($this->get($key) === $old) {
- $result = self::$cache->multi()
- ->del($this->getPrefix() . $key)
- ->exec();
- return $result !== false;
- }
- self::$cache->unwatch();
- return false;
+ $old = self::encodeValue($old);
+
+ return $this->evalLua('cad', [$key], [$old]) > 0;
+ }
+
+ public function ncad(string $key, mixed $old): bool {
+ $old = self::encodeValue($old);
+
+ return $this->evalLua('ncad', [$key], [$old]) > 0;
}
public function setTTL($key, $ttl) {
- self::$cache->expire($this->getPrefix() . $key, $ttl);
+ if ($ttl === 0) {
+ // having infinite TTL can lead to leaked keys as the prefix changes with version upgrades
+ $ttl = self::DEFAULT_TTL;
+ }
+ $ttl = min($ttl, self::MAX_TTL);
+ $this->getCache()->expire($this->getPrefix() . $key, $ttl);
+ }
+
+ public function getTTL(string $key): int|false {
+ $ttl = $this->getCache()->ttl($this->getPrefix() . $key);
+ return $ttl > 0 ? (int)$ttl : false;
+ }
+
+ public function compareSetTTL(string $key, mixed $value, int $ttl): bool {
+ $value = self::encodeValue($value);
+
+ return $this->evalLua('caSetTtl', [$key], [$value, $ttl]) > 0;
}
public static function isAvailable(): bool {
- return \OC::$server->getGetRedisFactory()->isAvailable();
+ return \OC::$server->get('RedisFactory')->isAvailable();
+ }
+
+ protected function evalLua(string $scriptName, array $keys, array $args) {
+ $keys = array_map(fn ($key) => $this->getPrefix() . $key, $keys);
+ $args = array_merge($keys, $args);
+ $script = self::LUA_SCRIPTS[$scriptName];
+
+ $result = $this->getCache()->evalSha($script[1], $args, count($keys));
+ if ($result === false) {
+ $result = $this->getCache()->eval($script[0], $args, count($keys));
+ }
+
+ return $result;
+ }
+
+ protected static function encodeValue(mixed $value): string {
+ return is_int($value) ? (string)$value : json_encode($value);
+ }
+
+ protected static function decodeValue(string $value): mixed {
+ return is_numeric($value) ? (int)$value : json_decode($value, true);
}
}