Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dec8b2b
add initial support for creating cluster redis connection
NickOvt Feb 17, 2026
cf23117
for locks use first node as main one, create separate connection for …
NickOvt Feb 18, 2026
581cb50
for locks using single master node remove hash tags on namespace
NickOvt Feb 19, 2026
cefb793
for locks using single master node remove hash tags on namespace | 2
NickOvt Feb 19, 2026
c05c521
for locks using single master node remove hash tags on namespace | 3
NickOvt Feb 19, 2026
951e4c9
for locks using single master node remove hash tags on namespace | 4 …
NickOvt Feb 19, 2026
d9ec347
ZMS-6: add backwards compatible initial support for redis cluster | f…
NickOvt Feb 19, 2026
c723490
ioredfour now support cluster mode, simplify config, db.js remove red…
NickOvt Feb 19, 2026
ecc09c6
use multi() only for same slot keys
NickOvt Feb 19, 2026
d54fc52
add cluster specific package json command
NickOvt Feb 19, 2026
f9263dc
fix cluster tests workflow
NickOvt Feb 19, 2026
b9a912f
grunt, don't hardcode node_env
NickOvt Feb 19, 2026
d106bde
imap-notifier, in redis cluster mode use sharded pub/sub
NickOvt Feb 20, 2026
9cf7228
imap-notifier, if using redis cluster set shardedSubscribers: true
NickOvt Feb 23, 2026
ff281c2
update ioredfour dependency
NickOvt Feb 26, 2026
34b5580
on cluster tests set log level error
NickOvt Feb 26, 2026
19149a6
revert test-cluster.toml change, increase protocol tests timeout
NickOvt Feb 26, 2026
2dd7b2a
revert protocol test timeout, imapnotifier use correct message event …
NickOvt Feb 26, 2026
50fc6f5
add api-methods endpoint on cluster tests too
NickOvt Feb 26, 2026
5b92026
keep redis multi calls for standalone redis
NickOvt Feb 26, 2026
6da02a5
hardcode new hash tagged key
NickOvt Mar 2, 2026
72d38de
for mailbox counters use per user hash tags
NickOvt Mar 2, 2026
b881528
tools. getMailboxCounter use const instead of let where possible
NickOvt Mar 2, 2026
1131607
user-handler flushreq use multi() instead of multiple promises | reve…
NickOvt Mar 2, 2026
f9bec69
hardcode wd:bull value as hash tag
NickOvt Mar 2, 2026
5ae2db4
merge master into ZMS-6
NickOvt Apr 23, 2026
48cf52c
fix imap notifier and migrations
NickOvt Apr 23, 2026
5daf4b4
remove unnecessary file
NickOvt Apr 23, 2026
d104267
remove dangling file
NickOvt Apr 23, 2026
5a246b9
add missing fixture
NickOvt Apr 23, 2026
e9baab6
fix package.json
NickOvt Apr 23, 2026
67be8e3
add hash tags to redis keys conditionally
NickOvt Apr 23, 2026
565af29
Merge branch 'master' of github.com:zone-eu/wildduck into ZMS-6
NickOvt May 25, 2026
b2ea96d
fix imap notifier
NickOvt May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,74 @@ jobs:
env:
APPCONF_dbs_redis: redis://127.0.0.1:6379/1
APPCONF_dbs_mongodb: 'mongodb://127.0.0.1:27017/wildduck-test?authSource=admin'

test-redis-cluster:
runs-on: ubuntu-latest
services:
redis-cluster:
image: grokzen/redis-cluster:7.0.10
env:
IP: 127.0.0.1
INITIAL_PORT: 7000
MASTERS: 3
SLAVES_PER_MASTER: 0
options: >-
--health-cmd "redis-cli -c -p 7000 ping"
--health-interval 10s
--health-timeout 5s
--health-retries 20
ports:
- 7000:7000
- 7001:7001
- 7002:7002

steps:
- uses: actions/checkout@v6

- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.12.1
with:
mongodb-version: 6.0
mongodb-port: 27017
mongodb-db: wildduck-test

- name: Use Node.js 24.x
uses: actions/setup-node@v6
with:
node-version: 24.x

- name: Install mongosh, mpop and redis-cli
run: |
sudo apt-get update
sudo apt-get install -y wget gnupg mpop redis-tools
wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list
sudo apt-get update
sudo apt-get install -y mongodb-mongosh

- run: which mongosh
- run: which mpop
- run: which redis-cli
- run: npm install

- name: Wait for Redis Cluster
run: |
for port in 7000 7001 7002; do
for i in $(seq 1 60); do
redis-cli -c -h 127.0.0.1 -p "$port" ping && break
sleep 1
done
done
redis-cli -c -h 127.0.0.1 -p 7000 cluster info | grep 'cluster_state:ok'

- name: Reset DB and Redis cluster
run: |
mongosh --eval 'db.dropDatabase()' wildduck-test
for port in 7000 7001 7002; do
redis-cli -c -h 127.0.0.1 -p "$port" flushdb
done

- name: Run tests against Redis cluster
run: NODE_ENV=test-cluster npx grunt
env:
APPCONF_dbs_mongodb: 'mongodb://127.0.0.1:27017/wildduck-test?authSource=admin'
2 changes: 0 additions & 2 deletions Gruntfile.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
'use strict';

process.env.NODE_ENV = 'test';

module.exports = function (grunt) {
// Project configuration.
grunt.initConfig({
Expand Down
2 changes: 1 addition & 1 deletion api.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ module.exports = done => {
settingsRoutes(db, server, settingsHandler);
healthRoutes(db, server, loggelf);

if (process.env.NODE_ENV === 'test') {
if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'test-cluster') {
server.get(
{ name: 'api-methods', path: '/api-methods' },
tools.responseWrapper(async (req, res) => {
Expand Down
41 changes: 23 additions & 18 deletions bin/access-tokens
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ let argv = yargs
await dbconnect();

let accessToken = crypto.randomBytes(20).toString('hex');
let tokenHash = crypto
.createHash('sha256')
.update(accessToken)
.digest('hex');
let tokenHash = crypto.createHash('sha256').update(accessToken).digest('hex');
let key = 'tn:token:' + tokenHash;

let tokenData = {
Expand All @@ -63,11 +60,15 @@ let argv = yargs
.digest('hex')
};

await db.redis
.multi()
.hmset(key, tokenData)
.sadd('tn:user:' + argv.user, tokenHash)
.exec();
if (db.redis.isCluster) {
await Promise.all([db.redis.hmset(key, tokenData), db.redis.sadd('tn:user:' + argv.user, tokenHash)]);
} else {
await db.redis
.multi()
.hmset(key, tokenData)
.sadd('tn:user:' + argv.user, tokenHash)
.exec();
}

console.error('Generated access token for %s[%s]:', tokenData.user, tokenData.role);

Expand All @@ -92,10 +93,7 @@ let argv = yargs

let accessToken = argv.token;

let tokenHash = crypto
.createHash('sha256')
.update(accessToken)
.digest('hex');
let tokenHash = crypto.createHash('sha256').update(accessToken).digest('hex');

let key = 'tn:token:' + tokenHash;

Expand Down Expand Up @@ -132,13 +130,20 @@ let argv = yargs
process.exit();
}

let query = await db.redis.multi().del('tn:user:' + user);
if (db.redis.isCluster) {
await db.redis.del('tn:user:' + user);

tokens.forEach(tokenHash => {
query = query.del('tn:token:' + tokenHash);
});
for (let tokenHash of tokens) {
await db.redis.del('tn:token:' + tokenHash);
}
} else {
let query = db.redis.multi().del('tn:user:' + user);
tokens.forEach(tokenHash => {
query = query.del('tn:token:' + tokenHash);
});

await query.exec();
await query.exec();
}

console.error('Deleted %s tokens for %s', tokens.length, user);

Expand Down
22 changes: 22 additions & 0 deletions config/dbs.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,25 @@ db = 3
# [[redis.sentinels]]
# host="54.36.85.115"
# port=26379

## Connect to Redis Cluster instead of a single master
# [redis]
# cluster = true
# password = "" # global/fallback password
# [[redis.nodes]]
# host="54.36.85.113"
# port=26379
# password="" # per node password
# [[redis.nodes]]
# host = "127.0.0.1"
# port = 7000
# [[redis.nodes]]
# host = "127.0.0.1"
# port = 7001
# [[redis.nodes]]
# host = "127.0.0.1"
# port = 7002
# [[redis.nodes]]
# host="54.36.85.115"
# port=26379
# password=""
41 changes: 41 additions & 0 deletions config/test-cluster.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[api]
port = 8080
host = "127.0.0.1"
secure = false

[log]
level = "silly"

[dbs]
# mongodb connection string for the main database
mongo = "mongodb://127.0.0.1:27017/wildduck-test"

dbname = "wildduck-test"

[dbs.redis]
cluster = true
password = ""

[[dbs.redis.nodes]]
host = "127.0.0.1"
port = 7000

[[dbs.redis.nodes]]
host = "127.0.0.1"
port = 7001

[[dbs.redis.nodes]]
host = "127.0.0.1"
port = 7002

[imap]
port = 9993
host = "127.0.0.1"

[lmtp]
enabled = true
port = 2424

[pwned]
enabled = true
type = "softfail"
8 changes: 5 additions & 3 deletions indexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const counters = require('./lib/counters');
const { ObjectId } = require('mongodb');
const libmime = require('libmime');
const punycode = require('punycode.js');
const tools = require('./lib/tools');
const { getClient } = require('./lib/elasticsearch');
const { normalizeLoggelfMessage } = require('./lib/loggelf-message');

Expand Down Expand Up @@ -284,14 +285,15 @@ function indexingJob(esclient) {

const dateKeyTdy = new Date().toISOString().substring(0, 10).replace(/-/g, '');
const dateKeyYdy = new Date(Date.now() - 24 * 3600 * 1000).toISOString().substring(0, 10).replace(/-/g, '');
const tombstoneTdy = `indexer:tomb:${dateKeyTdy}`;
const tombstoneYdy = `indexer:tomb:${dateKeyYdy}`;
const tombstoneTag = tools.redisHashTag(db.redis, 'indexer:tomb');
const tombstoneTdy = `${tombstoneTag}:${dateKeyTdy}`;
const tombstoneYdy = `${tombstoneTag}:${dateKeyYdy}`;

switch (data.action) {
case 'new': {
// check tombstone for race conditions (might be already deleted)

let [[err1, isDeleted1], [err2, isDeleted2]] = await db.redis
const [[err1, isDeleted1], [err2, isDeleted2]] = await db.redis
.multi()
.sismember(tombstoneTdy, data.message)
.sismember(tombstoneYdy, data.message)
Expand Down
10 changes: 6 additions & 4 deletions lib/api/addresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -2397,11 +2397,12 @@ module.exports = (db, server, userHandler, settingsHandler) => {

let response;
try {
const addressKey = tools.redisHashTag(db.redis, addressData._id.toString());
response = await db.redis
.multi()
// sending counters are stored in Redis
.get('wdf:' + addressData._id.toString())
.ttl('wdf:' + addressData._id.toString())
.get(`wdf:${addressKey}`)
.ttl(`wdf:${addressKey}`)
.exec();
} catch (err) {
// ignore
Expand Down Expand Up @@ -2565,11 +2566,12 @@ module.exports = (db, server, userHandler, settingsHandler) => {

let response;
try {
const addressKey = tools.redisHashTag(db.redis, addressData._id.toString());
response = await db.redis
.multi()
// sending counters are stored in Redis
.get('wdf:' + addressData._id.toString())
.ttl('wdf:' + addressData._id.toString())
.get(`wdf:${addressKey}`)
.ttl(`wdf:${addressKey}`)
.exec();
} catch (err) {
// ignore
Expand Down
8 changes: 4 additions & 4 deletions lib/api/mailboxes.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ module.exports = (db, server, mailboxHandler) => {
counterOps.push(
(async () => {
try {
total = await getMailboxCounter(db, mailboxData._id);
total = await getMailboxCounter(db, mailboxData._id, false, mailboxData.user);
} catch (err) {
// ignore
}
Expand All @@ -268,7 +268,7 @@ module.exports = (db, server, mailboxHandler) => {
counterOps.push(
(async () => {
try {
unseen = await getMailboxCounter(db, mailboxData._id, 'unseen');
unseen = await getMailboxCounter(db, mailboxData._id, 'unseen', mailboxData.user);
} catch (err) {
// ignore
}
Expand Down Expand Up @@ -533,13 +533,13 @@ module.exports = (db, server, mailboxHandler) => {
let total, unseen;

try {
total = await getMailboxCounter(db, mailboxData._id);
total = await getMailboxCounter(db, mailboxData._id, false, mailboxData.user);
} catch (err) {
// ignore
}

try {
unseen = await getMailboxCounter(db, mailboxData._id, 'unseen');
unseen = await getMailboxCounter(db, mailboxData._id, 'unseen', mailboxData.user);
} catch (err) {
// ignore
}
Expand Down
Loading