@owlmeans/redis 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +517 -0
  3. package/build/.gitkeep +0 -0
  4. package/build/consts.d.ts +2 -0
  5. package/build/consts.d.ts.map +1 -0
  6. package/build/consts.js +3 -0
  7. package/build/consts.js.map +1 -0
  8. package/build/index.d.ts +4 -0
  9. package/build/index.d.ts.map +1 -0
  10. package/build/index.js +3 -0
  11. package/build/index.js.map +1 -0
  12. package/build/service.d.ts +9 -0
  13. package/build/service.d.ts.map +1 -0
  14. package/build/service.js +64 -0
  15. package/build/service.js.map +1 -0
  16. package/build/types.d.ts +6 -0
  17. package/build/types.d.ts.map +1 -0
  18. package/build/types.js +2 -0
  19. package/build/types.js.map +1 -0
  20. package/build/utils/cluster.d.ts +5 -0
  21. package/build/utils/cluster.d.ts.map +1 -0
  22. package/build/utils/cluster.js +186 -0
  23. package/build/utils/cluster.js.map +1 -0
  24. package/build/utils/config.d.ts +9 -0
  25. package/build/utils/config.d.ts.map +1 -0
  26. package/build/utils/config.js +31 -0
  27. package/build/utils/config.js.map +1 -0
  28. package/build/utils/index.d.ts +4 -0
  29. package/build/utils/index.d.ts.map +1 -0
  30. package/build/utils/index.js +4 -0
  31. package/build/utils/index.js.map +1 -0
  32. package/build/utils/instance.d.ts +4 -0
  33. package/build/utils/instance.d.ts.map +1 -0
  34. package/build/utils/instance.js +13 -0
  35. package/build/utils/instance.js.map +1 -0
  36. package/package.json +40 -0
  37. package/src/consts.ts +3 -0
  38. package/src/index.ts +4 -0
  39. package/src/service.ts +94 -0
  40. package/src/types.ts +6 -0
  41. package/src/utils/cluster.ts +215 -0
  42. package/src/utils/config.ts +35 -0
  43. package/src/utils/index.ts +4 -0
  44. package/src/utils/instance.ts +16 -0
  45. package/tsconfig.json +14 -0
  46. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,215 @@
1
+ import type { DbConfig } from '@owlmeans/resource'
2
+ import { Cluster } from 'ioredis'
3
+ import type { Redis } from 'ioredis'
4
+ import { prepareClusterRedisOptions } from './config.js'
5
+ import { createClient } from './instance.js'
6
+ import type { RedisMeta } from '../types.js'
7
+
8
+ export const ensuerCluster = async (config: DbConfig<RedisMeta>): Promise<Cluster> => {
9
+ if (!Array.isArray(config.host)) {
10
+ throw new SyntaxError('We may connect to redis cluster only knowing its nodes')
11
+ }
12
+ const setup = prepareClusterRedisOptions(config)
13
+ // 1. Create single clients for testing
14
+ const clients = await Promise.all(setup.nodes.map(
15
+ async node => {
16
+ if (typeof node !== 'object') {
17
+ throw new SyntaxError('Cluster node must be an object after cluster options initialization')
18
+ }
19
+ if (node.host == null) {
20
+ throw new SyntaxError('Cluster node must have a host property after cluster options initialization')
21
+ }
22
+ const client = await createClient({ ...config, host: node.host }) as Redis
23
+
24
+ return client
25
+ }
26
+ ))
27
+
28
+ // 2. Test clients and ensure they are configured
29
+
30
+ try {
31
+ const masters: ClusterNode[] = []
32
+ const slaves: ClusterNode[] = []
33
+ const masterQty = config.meta?.masterNumber
34
+ ?? Math.round(clients.length / (1 + (config.meta?.slaveNumber ?? 2)))
35
+ const slaveQty = config.meta?.slaveNumber ??
36
+ Math.round((clients.length - masterQty) / masterQty)
37
+ const requiredNodes = (slaveQty + 1) * masterQty
38
+
39
+ if (requiredNodes !== clients.length) {
40
+ throw new SyntaxError(`Number of nodes in redis cluster does not match the configuration requirements: ${clients.length} / ${requiredNodes}`)
41
+ }
42
+
43
+ for (const client of clients) {
44
+ let nodesInfo: string = await client.cluster('NODES') as string
45
+ let nodes = parseClusterNodeInfo(nodesInfo)
46
+ // Remove nodes that we don't nkow
47
+ for (const node of nodes) {
48
+ // We do not remove ourselves
49
+ if (node.flags.includes('myself')) {
50
+ continue;
51
+ }
52
+ if (!config.host.includes(node.addr)) {
53
+ await client.cluster('FORGET', node.nodeId)
54
+ }
55
+ }
56
+ const otherNodes = nodes.filter(node => !node.flags.includes('myself'))
57
+ // Add nodes that are missing
58
+ for (const host of config.host) {
59
+ if (!otherNodes.some(node => node.addr === host)) {
60
+ await client.cluster('MEET', host, config.port ?? 6379)
61
+ }
62
+ }
63
+ client.disconnect(true)
64
+ nodesInfo = await client.cluster('NODES') as string
65
+ nodes = parseClusterNodeInfo(nodesInfo)
66
+ const myself = nodes.find(node => node.flags.includes('myself')) as NodeInfo
67
+ if (myself == null) {
68
+ throw new SyntaxError('Cluster node does not contain information about itself')
69
+ }
70
+ if (myself.flags.includes('master')) {
71
+ masters.push({ info: myself, node: client })
72
+ } else {
73
+ slaves.push({ info: myself, node: client })
74
+ }
75
+ }
76
+
77
+ await Promise.all(clients.map(client => client.disconnect(true)))
78
+
79
+ const realMasters = masters.filter(master => master.info.slots != null)
80
+ const newcommers = masters.filter(master => master.info.slots == null)
81
+
82
+ const slotsChunk = Math.floor(16384 / masterQty)
83
+ const slotsSurplus = 16384 % masterQty
84
+
85
+ const resetNode = async (node: ClusterNode, flush: boolean = false) => {
86
+ flush && await node.node.flushall()
87
+ await node.node.cluster('RESET')
88
+ node.info.slots = null
89
+ node.info.master = '-'
90
+
91
+ newcommers.push(node)
92
+ }
93
+
94
+ const configureMaster = async (node: ClusterNode, idx: number) => {
95
+ const chunk = idx + 1 == masterQty ? slotsChunk + slotsSurplus : slotsChunk
96
+ const firstSlot = idx * slotsChunk
97
+ const lastSlot = firstSlot + chunk - 1
98
+ await node.node.flushall()
99
+ await node.node.cluster('RESET')
100
+ await node.node.cluster('ADDSLOTSRANGE', firstSlot, lastSlot)
101
+ node.info.slots = [[firstSlot, lastSlot]]
102
+ const nodeSlaves = slaves.filter(slave => slave.info.master === node.info.nodeId)
103
+ while (nodeSlaves.length > slaveQty) {
104
+ const slave = nodeSlaves.pop()
105
+ if (slave == null) {
106
+ throw new SyntaxError('Not enough slaves to forget in redis cluster')
107
+ }
108
+ slaves.splice(slaves.indexOf(slave), 1)
109
+ await resetNode(slave)
110
+ }
111
+ }
112
+
113
+ if (realMasters.length === 0) {
114
+ for (let i = 0; i < masterQty; ++i) {
115
+ const newcommer = newcommers.shift()
116
+ if (newcommer == null) {
117
+ throw new SyntaxError('(clean) Not enough nodes to add to redis cluster as master')
118
+ }
119
+ await configureMaster(newcommer, i)
120
+ realMasters.push(newcommer)
121
+ }
122
+ } else if (realMasters.length !== masterQty) {
123
+ while (realMasters.length > masterQty) {
124
+ const renegade = realMasters.pop()
125
+ if (renegade == null) {
126
+ throw new SyntaxError('Not enough nodes to forget in redis cluster')
127
+ }
128
+ const renegadeSlaves = slaves.filter(slave => slave.info.master === renegade.info.nodeId)
129
+ for (const slave of renegadeSlaves) {
130
+ slaves.splice(slaves.indexOf(slave), 1)
131
+ await resetNode(slave)
132
+ }
133
+ await resetNode(renegade, true)
134
+ }
135
+ while (realMasters.length < masterQty) {
136
+ const newcommer = newcommers.shift()
137
+ if (newcommer == null) {
138
+ throw new SyntaxError('(dirty) Not enough nodes to add to redis cluster as master')
139
+ }
140
+ realMasters.push(newcommer)
141
+ }
142
+ for (let i = 0; i < masterQty; ++i) {
143
+ await configureMaster(realMasters[i], i)
144
+ }
145
+ }
146
+ if (slaves.length !== slaveQty * masterQty) {
147
+ for (let master of realMasters) {
148
+ const masterSlaves = slaves.filter(slave => slave.info.master === master.info.nodeId)
149
+ while (masterSlaves.length > slaveQty) {
150
+ const slave = masterSlaves.pop()
151
+ if (slave == null) {
152
+ throw new SyntaxError('Not enough slaves to forget in redis cluster')
153
+ }
154
+ slaves.splice(slaves.indexOf(slave), 1)
155
+ await resetNode(slave)
156
+ }
157
+ while (masterSlaves.length < slaveQty) {
158
+ const newcommer = newcommers.shift()
159
+ if (newcommer == null) {
160
+ throw new SyntaxError('Not enough slaves to add to redis cluster')
161
+ }
162
+ newcommer.info.master = master.info.nodeId
163
+
164
+ await newcommer.node.cluster('REPLICATE', master.info.nodeId)
165
+ masterSlaves.push(newcommer)
166
+ slaves.push(newcommer)
167
+ }
168
+ }
169
+ }
170
+ if (newcommers.length !== 0) {
171
+ throw new SyntaxError('There are nodes that are not configured in redis cluster')
172
+ }
173
+ if (realMasters.length !== masterQty) {
174
+ throw new SyntaxError('Not enough master nodes in redis cluster after configuration')
175
+ }
176
+ if (slaves.length !== slaveQty * masterQty) {
177
+ throw new SyntaxError('Not enough slave nodes in redis cluster after configuration')
178
+ }
179
+ } catch (e) {
180
+ console.error(e)
181
+ throw e
182
+ }
183
+
184
+ // 3. Disconnect single clients
185
+
186
+ await Promise.all(clients.map(client => client.disconnect()))
187
+
188
+ // 4. Connect to cluster
189
+
190
+ return new Cluster(setup.nodes, setup.options)
191
+ }
192
+
193
+ const parseClusterNodeInfo = (clusterNodes: string) => {
194
+ const nodesInfo = clusterNodes.split('\n').filter(line => line.trim() !== '')
195
+ return nodesInfo.map(line => {
196
+ const [nodeId, addr, flags, master, , , , state, ...slots] = line.split(' ')
197
+ return {
198
+ nodeId,
199
+ addr: addr.trim().split(':')[0],
200
+ flags: flags.trim().split(','),
201
+ master,
202
+ state,
203
+ slots: slots != null && slots.length > 0 ? slots.map(
204
+ slot => (slot.includes('-') ? slot.split('-', 2) : [slot, slot]).map(v => parseInt(v))
205
+ ) : null
206
+ }
207
+ })
208
+ }
209
+
210
+ type NodeInfo = ReturnType<typeof parseClusterNodeInfo>[0]
211
+
212
+ interface ClusterNode {
213
+ node: Redis
214
+ info: NodeInfo
215
+ }
@@ -0,0 +1,35 @@
1
+ import type { DbConfig } from '@owlmeans/resource'
2
+ import type { RedisOptions, ClusterNode, ClusterOptions } from 'ioredis'
3
+ import type { RedisMeta } from '../types.js'
4
+
5
+ export const prepareSingleRedisOptions = (config: DbConfig<RedisMeta>, host?: string): RedisOptions => {
6
+ host = (host != null ? host : config.host) as string
7
+ if (typeof host !== 'string') {
8
+ throw new SyntaxError('Single redis options can be created only from config referencing single host')
9
+ }
10
+ return {
11
+ host: host,
12
+ port: config.port ?? 6379,
13
+ password: config.secret,
14
+ ...config.meta
15
+ }
16
+ }
17
+
18
+ export const prepareClusterRedisOptions = (config: DbConfig<RedisMeta>): { nodes: ClusterNode[], options: ClusterOptions } => {
19
+ if (!Array.isArray(config.host)) {
20
+ throw new SyntaxError('Cluster redis options can be created only from config referencing multiple hosts')
21
+ }
22
+ return {
23
+ nodes: config.host.map(host => ({ host, port: config.port })), options: {
24
+ dnsLookup: (address, callback) => {
25
+ callback(null, address)
26
+ },
27
+ slotsRefreshTimeout: 20000,
28
+ redisOptions: {
29
+ // tls: { rejectUnauthorized: false }, // @TODO check if it's working
30
+ password: config.secret,
31
+ ...config.meta
32
+ }
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,4 @@
1
+
2
+ export * from './cluster.js'
3
+ export * from './config.js'
4
+ export * from './instance.js'
@@ -0,0 +1,16 @@
1
+ import type { DbConfig } from '@owlmeans/resource'
2
+ import type { RedisClient } from '@owlmeans/redis-resource'
3
+ import {Redis} from 'ioredis'
4
+ import { prepareSingleRedisOptions } from './config.js'
5
+ import { ensuerCluster } from './cluster.js'
6
+
7
+ export const createClient = async (config: DbConfig): Promise<RedisClient> => {
8
+ if (Array.isArray(config.host) && config.host.length === 1) {
9
+ config.host = config.host[0]
10
+ }
11
+ if (typeof config.host === 'string') {
12
+ return new Redis(prepareSingleRedisOptions(config))
13
+ }
14
+
15
+ return await ensuerCluster(config)
16
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": [
3
+ "../tsconfig.default.json",
4
+ ],
5
+ "compilerOptions": {
6
+ "rootDir": "./src/", /* Specify the root folder within your source files. */
7
+ "outDir": "./build/", /* Specify an output folder for all emitted files. */
8
+ },
9
+ "exclude": [
10
+ "./dist/**/*",
11
+ "./build/**/*",
12
+ "./*.ts"
13
+ ]
14
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/consts.ts","./src/index.ts","./src/service.ts","./src/types.ts","./src/utils/cluster.ts","./src/utils/config.ts","./src/utils/index.ts","./src/utils/instance.ts"],"version":"5.6.3"}