Skip to content

Commit 3967680

Browse files
susnuxprovokateurincome-nc
committed
feat: add predis based Key-Value cache (redis / valkey etc)
Assisted-by: ClaudeCode:claude-opus-4-8 Co-authored-by: Kate <26026535+provokateurin@users.noreply.github.com> Co-authored-by: Côme Chilliet <91878298+come-nc@users.noreply.github.com> Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 7eed70f commit 3967680

16 files changed

Lines changed: 1192 additions & 4 deletions
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: MIT
3+
4+
name: PHPUnit key-value store
5+
6+
on:
7+
pull_request:
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: phpunit-kvstore-${{ github.head_ref || github.run_id }}
14+
cancel-in-progress: true
15+
16+
env:
17+
VALKEY_IMAGE: valkey/valkey:8
18+
19+
jobs:
20+
changes:
21+
runs-on: ubuntu-latest-low
22+
23+
outputs:
24+
src: ${{ steps.changes.outputs.src}}
25+
26+
steps:
27+
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
28+
id: changes
29+
continue-on-error: true
30+
with:
31+
filters: |
32+
src:
33+
- '.github/workflows/phpunit-kvstore.yml'
34+
- '3rdparty/**'
35+
- '**/appinfo/**'
36+
- '**.php'
37+
38+
phpunit-kvstore:
39+
runs-on: ubuntu-latest
40+
41+
needs: changes
42+
if: needs.changes.outputs.src != 'false'
43+
44+
strategy:
45+
fail-fast: false
46+
matrix:
47+
php-versions: ["8.3", "8.5"]
48+
# The three supported topologies for the key-value store cache
49+
topology: ["single", "cluster", "sentinel"]
50+
51+
name: KV store ${{ matrix.topology }} (PHP ${{ matrix.php-versions }})
52+
53+
steps:
54+
- name: Checkout server
55+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
56+
with:
57+
persist-credentials: false
58+
submodules: true
59+
60+
- name: Set up php ${{ matrix.php-versions }}
61+
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 #v2.37.2
62+
timeout-minutes: 5
63+
with:
64+
php-version: ${{ matrix.php-versions }}
65+
# https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation
66+
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, pdo_sqlite, posix, session, simplexml, sqlite, xmlreader, xmlwriter, zip, zlib
67+
coverage: none
68+
ini-file: development
69+
ini-values: disable_functions=""
70+
env:
71+
fail-fast: true
72+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73+
74+
- name: Start Valkey (single server)
75+
if: matrix.topology == 'single'
76+
run: |
77+
docker run -d --name kv-single -p 6379:6379 "${VALKEY_IMAGE}"
78+
timeout 60 sh -c 'until docker exec kv-single valkey-cli ping | grep -q PONG; do sleep 1; done'
79+
80+
- name: Start Valkey (cluster)
81+
if: matrix.topology == 'cluster'
82+
run: |
83+
for port in 7000 7001 7002 7003 7004 7005; do
84+
docker run -d --name "kv-cluster-$port" --network host "${VALKEY_IMAGE}" \
85+
valkey-server --port "$port" --cluster-enabled yes \
86+
--cluster-config-file "nodes-$port.conf" --cluster-node-timeout 5000 \
87+
--appendonly no --save ""
88+
done
89+
# Wait for every node to answer before forming the cluster
90+
for port in 7000 7001 7002 7003 7004 7005; do
91+
timeout 60 sh -c "until docker exec kv-cluster-$port valkey-cli -p $port ping | grep -q PONG; do sleep 1; done"
92+
done
93+
docker exec kv-cluster-7000 valkey-cli --cluster create \
94+
127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
95+
127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
96+
--cluster-replicas 1 --cluster-yes
97+
98+
- name: Start Valkey (sentinel)
99+
if: matrix.topology == 'sentinel'
100+
run: |
101+
docker run -d --name kv-master --network host "${VALKEY_IMAGE}" \
102+
valkey-server --port 6379
103+
docker run -d --name kv-replica --network host "${VALKEY_IMAGE}" \
104+
valkey-server --port 6380 --replicaof 127.0.0.1 6379
105+
printf 'port 26379\nsentinel monitor mymaster 127.0.0.1 6379 1\nsentinel down-after-milliseconds mymaster 5000\nsentinel failover-timeout mymaster 10000\n' > sentinel.conf
106+
docker run -d --name kv-sentinel --network host -v "$PWD/sentinel.conf:/etc/valkey/sentinel.conf" \
107+
"${VALKEY_IMAGE}" valkey-sentinel /etc/valkey/sentinel.conf
108+
timeout 60 sh -c 'until docker exec kv-sentinel valkey-cli -p 26379 ping | grep -q PONG; do sleep 1; done'
109+
110+
- name: Set up dependencies
111+
run: composer i
112+
113+
- name: Set up Nextcloud
114+
run: |
115+
mkdir data
116+
cp tests/kvstore-${{ matrix.topology }}.config.php config/
117+
cp tests/preseed-config.php config/config.php
118+
./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
119+
php -f tests/enable_all.php
120+
121+
- name: PHPUnit key-value store tests
122+
run: composer run test -- --group KeyValueCache --log-junit junit.xml
123+
124+
- name: Print logs
125+
if: always()
126+
run: |
127+
cat data/nextcloud.log
128+
129+
summary:
130+
permissions:
131+
contents: none
132+
runs-on: ubuntu-latest-low
133+
needs: [changes, phpunit-kvstore]
134+
135+
if: always()
136+
137+
name: phpunit-kvstore-summary
138+
139+
steps:
140+
- name: Summary status
141+
run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-kvstore.result != 'success' }}; then exit 1; fi

3rdparty

Submodule 3rdparty updated 652 files

build/psalm-baseline.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3026,6 +3026,11 @@
30263026
<code><![CDATA[$this->timeFactory->getTime()]]></code>
30273027
</InvalidScalarArgument>
30283028
</file>
3029+
<file src="core/Command/Memcache/RedisCommand.php">
3030+
<DeprecatedClass>
3031+
<code><![CDATA[protected]]></code>
3032+
</DeprecatedClass>
3033+
</file>
30293034
<file src="core/Command/Security/BruteforceAttempts.php">
30303035
<DeprecatedMethod>
30313036
<code><![CDATA[getAttempts]]></code>
@@ -4246,6 +4251,11 @@
42464251
<code><![CDATA[array{X-Request-Id: string, Cache-Control: string, Content-Security-Policy: string, Feature-Policy: string, X-Robots-Tag: string, Last-Modified?: string, ETag?: string, ...H}]]></code>
42474252
</MoreSpecificReturnType>
42484253
</file>
4254+
<file src="lib/public/ICache.php">
4255+
<AmbiguousConstantInheritance>
4256+
<code><![CDATA[DEFAULT_TTL]]></code>
4257+
</AmbiguousConstantInheritance>
4258+
</file>
42494259
<file src="public.php">
42504260
<DeprecatedMethod>
42514261
<code><![CDATA[getAppValue]]></code>

config/config.sample.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,6 +1783,87 @@
17831783
*/
17841784
'memcache_customprefix' => 'mycustomprefix',
17851785

1786+
/**
1787+
* Connection details for the Key-Value store used for in-memory caching,
1788+
* for example when using Valkey or Redis.
1789+
*
1790+
* This is the brand-independent successor of the ``redis`` and
1791+
* ``redis.cluster`` options below and supports the latest Valkey and Redis
1792+
* releases. Three topologies are supported:
1793+
* a single server, a Sentinel managed replication set, and a
1794+
* server cluster. Configure exactly one of ``server``, ``sentinel`` or
1795+
* ``seeds``.
1796+
*
1797+
* For enhanced security, it is recommended to configure ACLs in
1798+
* the cache server and configure the ``user`` and ``password`` (or a TLS
1799+
* client certificate). Alternatively, you can also configure the cache
1800+
* server to just use a ``password``.
1801+
* See https://valkey.io/topics/security/ for more information when using Valkey.
1802+
*/
1803+
'memcache.kvstore' => [
1804+
/**
1805+
* Single server setup.
1806+
*
1807+
* Also used as the connection template for the ``sentinel`` managed
1808+
* primary / replica connections.
1809+
*/
1810+
'server' => [
1811+
'host' => 'localhost', // can also be a Unix domain socket: '/tmp/cache.sock'
1812+
'port' => 6379, // ignored for Unix domain sockets
1813+
// Protocol used to connect. One of 'tcp', 'tls' or 'unix'.
1814+
// When omitted it is derived from the host (a leading '/' means 'unix').
1815+
'protocol' => 'tcp',
1816+
],
1817+
1818+
/**
1819+
* Sentinel managed replication setup.
1820+
*
1821+
* Provide the name of the monitored service and one entry per Sentinel
1822+
* node. Each seed uses the same format as the ``server`` entry above.
1823+
* Uncomment to enable and remove the ``server`` / ``seeds`` entries.
1824+
*/
1825+
//'sentinel' => [
1826+
// 'service' => 'mymaster',
1827+
// 'seeds' => [
1828+
// ['host' => 'localhost', 'port' => 26379],
1829+
// ['host' => 'localhost', 'port' => 26380],
1830+
// ],
1831+
//],
1832+
1833+
/**
1834+
* Cluster setup.
1835+
*
1836+
* Provide some or all of the cluster nodes to bootstrap discovery.
1837+
* Each seed uses the same format as the ``server`` entry above.
1838+
* Uncomment to enable and remove the ``server`` / ``sentinel`` entries.
1839+
*/
1840+
//'seeds' => [
1841+
// ['host' => 'localhost', 'port' => 7000],
1842+
// ['host' => 'localhost', 'port' => 7001],
1843+
//],
1844+
1845+
// Optional: username, only sent when the cache server uses ACLs.
1846+
'user' => '',
1847+
// Optional: if not defined, no password will be used.
1848+
'password' => '',
1849+
// Optional: select a numbered database. Only supported for single
1850+
// servers and Sentinel setups, clusters always use database 0.
1851+
'dbindex' => 0,
1852+
// Optional: connection timeout in seconds (float). 0 means no timeout.
1853+
'timeout' => 0.0,
1854+
// Optional: read/write timeout in seconds (float). 0 means no timeout.
1855+
'read_timeout' => 0.0,
1856+
// Optional: keep the connection open across requests. Defaults to false.
1857+
'persistent' => false,
1858+
// Optional: when the 'tls' protocol is used, provide the SSL context.
1859+
// SSL context options, see https://www.php.net/manual/en/context.ssl.php
1860+
//'ssl_context' => [
1861+
// 'local_cert' => '/certs/cache.crt',
1862+
// 'local_pk' => '/certs/cache.key',
1863+
// 'cafile' => '/certs/ca.crt',
1864+
//],
1865+
],
1866+
17861867
/**
17871868
* Connection details for Redis to use for memory caching in a single server configuration.
17881869
*
@@ -1792,6 +1873,8 @@
17921873
*
17931874
* We also support Redis SSL/TLS encryption as of version 6.
17941875
* See https://redis.io/topics/encryption for more information.
1876+
*
1877+
* @deprecated 34.0.0 use `memcache.kvstore` instead which supports also Valkey.
17951878
*/
17961879
'redis' => [
17971880
'host' => 'localhost', // can also be a Unix domain socket: '/tmp/redis.sock'
@@ -1831,6 +1914,8 @@
18311914
*
18321915
* Authentication works with phpredis version 4.2.1+. See
18331916
* https://github.com/phpredis/phpredis/commit/c5994f2a42b8a348af92d3acb4edff1328ad8ce1
1917+
*
1918+
* @deprecated 34.0.0 use `memcache.kvstore` instead which supports also Valkey.
18341919
*/
18351920
'redis.cluster' => [
18361921
'seeds' => [ // provide some or all of the cluster servers to bootstrap discovery, port required

lib/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,6 +1953,8 @@
19531953
'OC\\Memcache\\CASTrait' => $baseDir . '/lib/private/Memcache/CASTrait.php',
19541954
'OC\\Memcache\\Cache' => $baseDir . '/lib/private/Memcache/Cache.php',
19551955
'OC\\Memcache\\Factory' => $baseDir . '/lib/private/Memcache/Factory.php',
1956+
'OC\\Memcache\\KeyValueCache' => $baseDir . '/lib/private/Memcache/KeyValueCache.php',
1957+
'OC\\Memcache\\KeyValueCacheFactory' => $baseDir . '/lib/private/Memcache/KeyValueCacheFactory.php',
19561958
'OC\\Memcache\\LoggerWrapperCache' => $baseDir . '/lib/private/Memcache/LoggerWrapperCache.php',
19571959
'OC\\Memcache\\Memcached' => $baseDir . '/lib/private/Memcache/Memcached.php',
19581960
'OC\\Memcache\\NullCache' => $baseDir . '/lib/private/Memcache/NullCache.php',

lib/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1994,6 +1994,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
19941994
'OC\\Memcache\\CASTrait' => __DIR__ . '/../../..' . '/lib/private/Memcache/CASTrait.php',
19951995
'OC\\Memcache\\Cache' => __DIR__ . '/../../..' . '/lib/private/Memcache/Cache.php',
19961996
'OC\\Memcache\\Factory' => __DIR__ . '/../../..' . '/lib/private/Memcache/Factory.php',
1997+
'OC\\Memcache\\KeyValueCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/KeyValueCache.php',
1998+
'OC\\Memcache\\KeyValueCacheFactory' => __DIR__ . '/../../..' . '/lib/private/Memcache/KeyValueCacheFactory.php',
19971999
'OC\\Memcache\\LoggerWrapperCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/LoggerWrapperCache.php',
19982000
'OC\\Memcache\\Memcached' => __DIR__ . '/../../..' . '/lib/private/Memcache/Memcached.php',
19992001
'OC\\Memcache\\NullCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/NullCache.php',

lib/private/Memcache/Factory.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public function withServerVersionPrefix(\Closure $closure): void {
164164
#[\Override]
165165
public function createLocking(string $prefix = ''): IMemcache {
166166
$cache = new $this->lockingCacheClass($this->getGlobalPrefix() . '/' . $prefix);
167-
if ($this->lockingCacheClass === Redis::class) {
167+
if ($this->supportsInstrumentation($this->lockingCacheClass)) {
168168
if ($this->profiler->isEnabled()) {
169169
// We only support the profiler with Redis
170170
$cache = new ProfilerWrapperCache($cache, 'Locking');
@@ -187,7 +187,7 @@ public function createLocking(string $prefix = ''): IMemcache {
187187
#[\Override]
188188
public function createDistributed(string $prefix = ''): ICache {
189189
$cache = new $this->distributedCacheClass($this->getGlobalPrefix() . '/' . $prefix);
190-
if ($this->distributedCacheClass === Redis::class) {
190+
if ($this->supportsInstrumentation($this->distributedCacheClass)) {
191191
if ($this->profiler->isEnabled()) {
192192
// We only support the profiler with Redis
193193
$cache = new ProfilerWrapperCache($cache, 'Distributed');
@@ -210,7 +210,7 @@ public function createDistributed(string $prefix = ''): ICache {
210210
#[\Override]
211211
public function createLocal(string $prefix = ''): ICache {
212212
$cache = new $this->localCacheClass($this->getGlobalPrefix() . '/' . $prefix);
213-
if ($this->localCacheClass === Redis::class) {
213+
if ($this->supportsInstrumentation($this->localCacheClass)) {
214214
if ($this->profiler->isEnabled()) {
215215
// We only support the profiler with Redis
216216
$cache = new ProfilerWrapperCache($cache, 'Local');
@@ -249,6 +249,15 @@ public function isLocalCacheAvailable(): bool {
249249
return $this->localCacheClass !== self::NULL_CACHE;
250250
}
251251

252+
/**
253+
* Whether the given cache backend supports the profiler and logger wrappers.
254+
*
255+
* @param class-string<ICache> $cacheClass
256+
*/
257+
private function supportsInstrumentation(string $cacheClass): bool {
258+
return $cacheClass === Redis::class || $cacheClass === KeyValueCache::class;
259+
}
260+
252261
public function clearAll(): void {
253262
$this->createLocal()->clear();
254263
$this->createDistributed()->clear();

0 commit comments

Comments
 (0)