Skip to content

Commit 6948ba4

Browse files
authored
feat!: clusters improvements (#109)
1 parent 66ac0ad commit 6948ba4

7 files changed

Lines changed: 228 additions & 21 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,17 @@ on:
99
jobs:
1010
test:
1111
runs-on: ubuntu-latest
12-
services:
13-
redis:
14-
image: redis
15-
ports:
16-
- 6379:6379
17-
options: --entrypoint redis-server
1812
strategy:
1913
matrix:
2014
node-version: [14.x, 16.x, 18.x]
2115

2216
steps:
2317
- uses: actions/checkout@v3
2418

19+
- name: Start Redis
20+
working-directory: ./docker
21+
run: docker-compose up -d
22+
2523
- name: Use Node.js
2624
uses: actions/setup-node@v3
2725
with:
@@ -32,3 +30,6 @@ jobs:
3230
- name: Run tests
3331
run: |
3432
npm run test
33+
- name: Run tests - clusters
34+
run: |
35+
node test-clusters.js

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ aedesPersistenceRedis({
6262
}, {
6363
port: 6380,
6464
host: '127.0.0.1'
65-
}])
65+
}]),
66+
cluster: true
6667
})
6768
```
6869

UPGRADE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Upgrade
22

33
## x.x.x to 9.x.x
4+
45
The database schema has changed between 8.x.x to 9.x.x.
56

67
Start with a clean database if you migrate from x.x.x to 9.x.x
8+
9+
# x.x.x to 10.x.x
10+
11+
The database schema has changed between 9.x.x to 10.x.x **IF YOU ARE USING CLUSTERS**.
12+
13+
Start with a clean database **IF YOU ARE USING CLUSTERS** migrate from x.x.x to 10.x.x or use `migrations.js` `from9to10` function.

docker/docker-compose.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
version: '3'
2+
3+
services:
4+
redis-default:
5+
image: redis:6.2.5-alpine
6+
command: redis-server --port 6379 --appendonly yes
7+
network_mode: "host"
8+
9+
redis-cluster:
10+
image: gsccheng/redis-cluster
11+
ports:
12+
- '6378:7000'
13+
- '6380:7001'
14+
- '6381:7002'
15+
- '6382:7003'
16+
- '6383:7004'
17+
- '6384:7005'
18+
environment:
19+
SENTINEL: 'true'
20+
INITIAL_PORT: 7000,
21+
MASTERS: 3,
22+
SLAVES_PER_MASTER: 1

migrations.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
async function from9to10 (db, cb) {
2+
// move retained messages from hash to keys
3+
const RETAINEDKEY = 'retained'
4+
function retainedKey (topic) {
5+
return `${RETAINEDKEY}:${encodeURIComponent(topic)}`
6+
}
7+
8+
// get all topics
9+
db.hkeys(RETAINEDKEY, function (err, topics) {
10+
if (err) {
11+
return cb(err)
12+
}
13+
14+
Promise.all(topics.map(t => {
15+
return new Promise((resolve, reject) => {
16+
// get packet payload
17+
db.hgetBuffer(RETAINEDKEY, t, function (err, payload) {
18+
if (err) {
19+
return reject(err)
20+
}
21+
// set packet with new format
22+
db.set(retainedKey(t), payload, function (err) {
23+
if (err) {
24+
return reject(err)
25+
}
26+
// remove old packet
27+
db.hdel(RETAINEDKEY, t, function (err) {
28+
if (err) {
29+
return reject(err)
30+
}
31+
resolve()
32+
})
33+
})
34+
})
35+
})
36+
})).then(() => {
37+
cb(null)
38+
}).catch(err => {
39+
cb(err)
40+
})
41+
})
42+
}
43+
44+
module.exports = {
45+
from9to10
46+
}

persistence.js

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const CLIENTSKEY = 'clients'
1919
const WILLSKEY = 'will'
2020
const WILLKEY = 'will:'
2121
const RETAINEDKEY = 'retained'
22+
const ALL_RETAINEDKEYS = `${RETAINEDKEY}:*`
2223
const OUTGOINGKEY = 'outgoing:'
2324
const OUTGOINGIDKEY = 'outgoing-id:'
2425
const INCOMINGKEY = 'incoming:'
@@ -52,6 +53,10 @@ function packetKey (brokerId, brokerCounter) {
5253
return `${PACKETKEY}${brokerId}:${brokerCounter}`
5354
}
5455

56+
function retainedKey (topic) {
57+
return `${RETAINEDKEY}:${encodeURIComponent(topic)}`
58+
}
59+
5560
function packetCountKey (brokerId, brokerCounter) {
5661
return `${PACKETKEY}${brokerId}:${brokerCounter}:offlineCount`
5762
}
@@ -64,25 +69,70 @@ class RedisPersistence extends CachedPersistence {
6469

6570
this.messageIdCache = HLRU(100000)
6671

67-
if (opts.cluster) {
72+
if (opts.cluster && Array.isArray(opts.cluster)) {
6873
this._db = new Redis.Cluster(opts.cluster)
6974
} else {
7075
this._db = opts.conn || new Redis(opts)
7176
}
7277

73-
this._getRetainedChunkBound = this._getRetainedChunk.bind(this)
78+
this.hasClusters = !!opts.cluster
79+
this._getRetainedChunkBound = (this.hasClusters ? this._getRetainedChunkCluster : this._getRetainedChunk).bind(this)
80+
this._getRetainedKeysBound = (this.hasClusters ? this._getRetainedKeysCluster : this._getRetainedKeys).bind(this)
7481
}
7582

76-
storeRetained (packet, cb) {
83+
/**
84+
* When using clusters we store it using a compound key instead of an hash
85+
* to spread the load across the clusters. See issue #85.
86+
*/
87+
_storeRetainedCluster (packet, cb) {
88+
if (packet.payload.length === 0) {
89+
this._db.del(retainedKey(packet.topic), cb)
90+
} else {
91+
this._db.set(retainedKey(packet.topic), msgpack.encode(packet), cb)
92+
}
93+
}
94+
95+
_storeRetained (packet, cb) {
7796
if (packet.payload.length === 0) {
7897
this._db.hdel(RETAINEDKEY, packet.topic, cb)
7998
} else {
8099
this._db.hset(RETAINEDKEY, packet.topic, msgpack.encode(packet), cb)
81100
}
82101
}
83102

84-
_getRetainedChunk (chunk, enc, cb) {
85-
this._db.hgetBuffer(RETAINEDKEY, chunk, cb)
103+
storeRetained (packet, cb) {
104+
if (this.hasClusters) {
105+
this._storeRetainedCluster(packet, cb)
106+
} else {
107+
this._storeRetained(packet, cb)
108+
}
109+
}
110+
111+
_getRetainedChunkCluster (topic, enc, cb) {
112+
this._db.getBuffer(retainedKey(topic), cb)
113+
}
114+
115+
_getRetainedChunk (topic, enc, cb) {
116+
this._db.hgetBuffer(RETAINEDKEY, topic, cb)
117+
}
118+
119+
_getRetainedKeysCluster (cb) {
120+
// Get keys of all the masters:
121+
const masters = this._db.nodes('master')
122+
Promise.all(
123+
masters
124+
.map((node) => node.keys(ALL_RETAINEDKEYS))
125+
).then((keys) => {
126+
// keys: [['key1', 'key2'], ['key3', 'key4']]
127+
// flatten the array
128+
cb(null, keys.reduce((acc, val) => acc.concat(val), []))
129+
}).catch((err) => {
130+
cb(err)
131+
})
132+
}
133+
134+
_getRetainedKeys (cb) {
135+
this._db.hkeys(RETAINEDKEY, cb)
86136
}
87137

88138
createRetainedStreamCombi (patterns) {
@@ -95,11 +145,11 @@ class RedisPersistence extends CachedPersistence {
95145

96146
const stream = through.obj(that._getRetainedChunkBound)
97147

98-
this._db.hkeys(RETAINEDKEY, function getKeys (err, keys) {
148+
this._getRetainedKeysBound(function getKeys (err, keys) {
99149
if (err) {
100150
stream.emit('error', err)
101151
} else {
102-
matchRetained(stream, keys, qlobber)
152+
matchRetained(stream, keys, qlobber, that.hasClusters)
103153
}
104154
})
105155

@@ -269,7 +319,15 @@ class RedisPersistence extends CachedPersistence {
269319

270320
const encoded = msgpack.encode(new Packet(packet))
271321

272-
this._db.mset(pktKey, encoded, countKey, subs.length, finish)
322+
if (this.hasClusters) {
323+
// do not do this using `mset`, fails in clusters
324+
outstanding += 1
325+
this._db.set(pktKey, encoded, finish)
326+
this._db.set(countKey, subs.length, finish)
327+
} else {
328+
this._db.mset(pktKey, encoded, countKey, subs.length, finish)
329+
}
330+
273331
if (ttl > 0) {
274332
outstanding += 2
275333
this._db.expire(pktKey, ttl, finish)
@@ -319,6 +377,7 @@ class RedisPersistence extends CachedPersistence {
319377
}
320378

321379
let count = 0
380+
let expected = 3
322381
let errored = false
323382

324383
// TODO can be cached in case of wildcard deliveries
@@ -354,7 +413,14 @@ class RedisPersistence extends CachedPersistence {
354413
return cb(err)
355414
}
356415
if (remained === 0) {
357-
that._db.del(pktKey, countKey, finish)
416+
if (that.hasClusters) {
417+
expected++
418+
// do not remove multiple keys at once, fails in clusters
419+
that._db.del(pktKey, finish)
420+
that._db.del(countKey, finish)
421+
} else {
422+
that._db.del(pktKey, countKey, finish)
423+
}
358424
} else {
359425
finish()
360426
}
@@ -366,7 +432,7 @@ class RedisPersistence extends CachedPersistence {
366432
errored = err
367433
return cb(err)
368434
}
369-
if (count === 3 && !errored) {
435+
if (count === expected && !errored) {
370436
cb(err, origPacket)
371437
}
372438
}
@@ -525,10 +591,11 @@ class RedisPersistence extends CachedPersistence {
525591
}
526592
}
527593

528-
function matchRetained (stream, keys, qlobber) {
529-
for (const key of keys) {
530-
if (qlobber.test(key)) {
531-
stream.write(key)
594+
function matchRetained (stream, topics, qlobber, hasClusters) {
595+
for (let t of topics) {
596+
t = hasClusters ? decodeURIComponent(t.split(':')[1]) : t
597+
if (qlobber.test(t)) {
598+
stream.write(t)
532599
}
533600
}
534601
stream.end()

test-clusters.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const test = require('tape').test
2+
const persistence = require('./persistence')
3+
const Redis = require('ioredis')
4+
const mqemitterRedis = require('mqemitter-redis')
5+
const abs = require('aedes-cached-persistence/abstract')
6+
7+
function unref () {
8+
this.connector.stream.unref()
9+
}
10+
11+
const nodes = [
12+
{ host: 'localhost', port: 6378 },
13+
{ host: 'localhost', port: 6380 },
14+
{ host: 'localhost', port: 6381 },
15+
{ host: 'localhost', port: 6382 },
16+
{ host: 'localhost', port: 6383 },
17+
{ host: 'localhost', port: 6384 }
18+
]
19+
20+
const db = new Redis.Cluster(nodes)
21+
22+
db.on('error', e => {
23+
console.trace(e)
24+
})
25+
26+
db.on('ready', function () {
27+
abs({
28+
test,
29+
buildEmitter () {
30+
const emitter = mqemitterRedis()
31+
emitter.subConn.on('connect', unref)
32+
emitter.pubConn.on('connect', unref)
33+
34+
return emitter
35+
},
36+
persistence (cb) {
37+
const slaves = db.nodes('master')
38+
Promise.all(slaves.map(function (node) {
39+
return node.flushdb().catch(err => {
40+
console.error('flushRedisKeys-error:', err)
41+
})
42+
})).then(() => {
43+
const conn = new Redis.Cluster(nodes)
44+
45+
conn.on('error', e => {
46+
console.trace(e)
47+
})
48+
49+
conn.on('ready', function () {
50+
cb(null, persistence({
51+
conn,
52+
cluster: true
53+
}))
54+
})
55+
})
56+
},
57+
waitForReady: true
58+
})
59+
60+
test.onFinish(() => {
61+
process.exit(0)
62+
})
63+
})

0 commit comments

Comments
 (0)