@iobroker/db-objects-redis 4.0.0-alpha.45-20220114-88aaebdb → 4.0.0-alpha.49-20220121-e7aa4308

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.
@@ -0,0 +1,12 @@
1
+ -- REDLOCK SCRIPT
2
+ -- call with key, value, ms (for set px)
3
+ local key = KEYS[1]
4
+
5
+ if redis.call("exists", key) == 1 then
6
+ -- lock already acquired by someone could be us too (we can only extend)
7
+ return 0
8
+ end
9
+
10
+ redis.call("set", key, KEYS[2], "PX", KEYS[3])
11
+ -- return 1 if lock acquired
12
+ return 1
@@ -0,0 +1,12 @@
1
+ -- REDLOCK SCRIPT
2
+ -- call with key, value, ms (for set px)
3
+ local key = KEYS[1]
4
+
5
+ if redis.call("get", key) ~= KEYS[2] then
6
+ -- we return 0 if lock is already taken by someone else
7
+ return 0
8
+ end
9
+
10
+ redis.call("set", key, KEYS[2], "PX", KEYS[3])
11
+ -- return 1 if we extended the lock successfully
12
+ return 1
@@ -0,0 +1,10 @@
1
+ -- REDLOCK SCRIPT
2
+ -- release a lock by deleting the key
3
+ -- call with key, value
4
+ local key = KEYS[1]
5
+
6
+ if redis.call("get", key) == KEYS[2] then
7
+ redis.pcall("del", key)
8
+ -- we need to publish a expire, because redis won't do it
9
+ redis.call("publish", "__keyevent@" .. KEYS[3] .. "__:expired", KEYS[4])
10
+ end
@@ -18,7 +18,7 @@
18
18
 
19
19
  const extend = require('node.extend');
20
20
  const Redis = require('ioredis');
21
- const tools = require('@iobroker/db-base').tools;
21
+ const { tools } = require('@iobroker/db-base');
22
22
  const fs = require('fs');
23
23
  const path = require('path');
24
24
  const crypto = require('crypto');
@@ -305,6 +305,30 @@ class ObjectsInRedisClient {
305
305
  this.subSystem = new Redis(this.settings.connection.options);
306
306
  this.subSystem.ioBrokerSubscriptions = {};
307
307
 
308
+ if (typeof this.settings.primaryHostLost === 'function') {
309
+ try {
310
+ // enable Expiry/Evicted events in server - same as states (could be same db)
311
+ await this.client.config('set', ['notify-keyspace-events', 'Exe']);
312
+ } catch (e) {
313
+ this.log.warn(
314
+ `${this.namespace} Unable to enable Expiry Keyspace events from Redis Server: ${e.message}`
315
+ );
316
+ }
317
+
318
+ this.subSystem.on('message', (channel, message) => {
319
+ if (
320
+ channel === `__keyevent@${this.settings.connection.options.db}__:expired` ||
321
+ channel === `__keyevent@${this.settings.connection.options.db}__:evicted`
322
+ ) {
323
+ this.log.silly(`${this.namespace} redis message expired/evicted ${channel}:${message}`);
324
+
325
+ if (message === `${this.metaNamespace}objects.primaryHost`) {
326
+ this.settings.primaryHostLost();
327
+ }
328
+ }
329
+ });
330
+ }
331
+
308
332
  if (typeof onChange === 'function') {
309
333
  this.subSystem.on('pmessage', (pattern, channel, message) =>
310
334
  setImmediate(() => {
@@ -394,27 +418,19 @@ class ObjectsInRedisClient {
394
418
  if (this.settings.connection.enhancedLogging) {
395
419
  this.subSystem.on('connect', () =>
396
420
  this.log.silly(
397
- this.namespace +
398
- ' PubSub System client Objects-Redis Event connect (stop=' +
399
- this.stop +
400
- ')'
421
+ `${this.namespace} PubSub System client Objects-Redis Event connect (stop=${this.stop})`
401
422
  )
402
423
  );
403
424
 
404
425
  this.subSystem.on('close', () =>
405
426
  this.log.silly(
406
- this.namespace + ' PubSub System client Objects-Redis Event close (stop=' + this.stop + ')'
427
+ `${this.namespace} PubSub System client Objects-Redis Event close (stop=${this.stop})`
407
428
  )
408
429
  );
409
430
 
410
431
  this.subSystem.on('reconnecting', reconnectCounter =>
411
432
  this.log.silly(
412
- this.namespace +
413
- ' PubSub System client Objects-Redis Event reconnect (reconnectCounter=' +
414
- reconnectCounter +
415
- ', stop=' +
416
- this.stop +
417
- ')'
433
+ `${this.namespace} PubSub System client Objects-Redis Event reconnect (reconnectCounter=${reconnectCounter}, stop=${this.stop})`
418
434
  )
419
435
  );
420
436
  }
@@ -423,21 +439,15 @@ class ObjectsInRedisClient {
423
439
  if (--initCounter < 1) {
424
440
  if (this.settings.connection.port === 0) {
425
441
  this.log.debug(
426
- this.namespace +
427
- ' Objects ' +
428
- (ready ? 'system re' : '') +
429
- 'connected to redis: ' +
442
+ `${this.namespace} Objects ${ready ? 'system re' : ''}connected to redis: ${
430
443
  this.settings.connection.host
444
+ }`
431
445
  );
432
446
  } else {
433
447
  this.log.debug(
434
- this.namespace +
435
- ' Objects ' +
436
- (ready ? 'system re' : '') +
437
- 'connected to redis: ' +
438
- this.settings.connection.host +
439
- ':' +
440
- this.settings.connection.port
448
+ `${this.namespace} Objects ${ready ? 'system re' : ''}connected to redis: ${
449
+ this.settings.connection.host
450
+ }:${this.settings.connection.port}`
441
451
  );
442
452
  }
443
453
  !ready && typeof this.settings.connected === 'function' && this.settings.connected();
@@ -473,14 +483,14 @@ class ObjectsInRedisClient {
473
483
 
474
484
  if (!this.sub && typeof onChangeUser === 'function') {
475
485
  initCounter++;
476
- this.log.debug(this.namespace + ' Objects create User PubSub Client');
486
+ this.log.debug(`${this.namespace} Objects create User PubSub Client`);
477
487
  this.sub = new Redis(this.settings.connection.options);
478
488
  this.sub.ioBrokerSubscriptions = {};
479
489
 
480
490
  this.sub.on('pmessage', (pattern, channel, message) => {
481
491
  setImmediate(() => {
482
492
  this.log.silly(
483
- this.namespace + ' Objects user redis pmessage ' + pattern + '/' + channel + ':' + message
493
+ `${this.namespace} Objects user redis pmessage ${pattern}/${channel}:${message}`
484
494
  );
485
495
  try {
486
496
  if (channel.startsWith(this.objNamespace) && channel.length > this.objNamespaceL) {
@@ -502,15 +512,11 @@ class ObjectsInRedisClient {
502
512
  }
503
513
  } catch (e) {
504
514
  this.log.warn(
505
- this.namespace +
506
- ' Objects user pmessage ' +
507
- channel +
508
- ' ' +
509
- JSON.stringify(message) +
510
- ' ' +
515
+ `${this.namespace} Objects user pmessage ${channel} ${JSON.stringify(message)} ${
511
516
  e.message
517
+ }`
512
518
  );
513
- this.log.warn(this.namespace + ' ' + e.stack);
519
+ this.log.warn(`${this.namespace} ${e.stack}`);
514
520
  }
515
521
  });
516
522
  });
@@ -534,24 +540,19 @@ class ObjectsInRedisClient {
534
540
  if (this.settings.connection.enhancedLogging) {
535
541
  this.sub.on('connect', () =>
536
542
  this.log.silly(
537
- this.namespace + ' PubSub user client Objects-Redis Event connect (stop=' + this.stop + ')'
543
+ `${this.namespace} PubSub user client Objects-Redis Event connect (stop=${this.stop})`
538
544
  )
539
545
  );
540
546
 
541
547
  this.sub.on('close', () =>
542
548
  this.log.silly(
543
- this.namespace + ' PubSub user client Objects-Redis Event close (stop=' + this.stop + ')'
549
+ `${this.namespace} PubSub user client Objects-Redis Event close (stop=${this.stop})`
544
550
  )
545
551
  );
546
552
 
547
553
  this.sub.on('reconnecting', reconnectCounter =>
548
554
  this.log.silly(
549
- this.namespace +
550
- ' PubSub user client Objects-Redis Event reconnect (reconnectCounter=' +
551
- reconnectCounter +
552
- ', stop=' +
553
- this.stop +
554
- ')'
555
+ `${this.namespace} PubSub user client Objects-Redis Event reconnect (reconnectCounter=${reconnectCounter}, stop=${this.stop})`
555
556
  )
556
557
  );
557
558
  }
@@ -560,21 +561,15 @@ class ObjectsInRedisClient {
560
561
  if (--initCounter < 1) {
561
562
  if (this.settings.connection.port === 0) {
562
563
  this.log.debug(
563
- this.namespace +
564
- ' Objects ' +
565
- (ready ? 'user re' : '') +
566
- 'connected to redis: ' +
564
+ `${this.namespace} Objects ${ready ? 'user re' : ''}connected to redis: ${
567
565
  this.settings.connection.host
566
+ }`
568
567
  );
569
568
  } else {
570
569
  this.log.debug(
571
- this.namespace +
572
- ' Objects ' +
573
- (ready ? 'user re' : '') +
574
- 'connected to redis: ' +
575
- this.settings.connection.host +
576
- ':' +
577
- this.settings.connection.port
570
+ `${this.namespace} Objects ${ready ? 'user re' : ''}connected to redis: ${
571
+ this.settings.connection.host
572
+ }:${this.settings.connection.port}`
578
573
  );
579
574
  }
580
575
  !ready && typeof this.settings.connected === 'function' && this.settings.connected();
@@ -611,7 +606,8 @@ class ObjectsInRedisClient {
611
606
 
612
607
  // filter out obvious non-host objects
613
608
  keys = keys.filter(id => /^system\.host\.[^.]+$/.test(id));
614
- this.useSets = true;
609
+ /** if false we have a host smaller 4 (no proto version for this existing) */
610
+ this.noLegacyMultihost = true;
615
611
 
616
612
  try {
617
613
  if (keys.length) {
@@ -627,7 +623,7 @@ class ObjectsInRedisClient {
627
623
  semver.lt(obj.common.installedVersion, '4.0.0')
628
624
  ) {
629
625
  // one of the host has a version smaller 4, we have to use legacy db
630
- this.useSets = false;
626
+ this.noLegacyMultihost = false;
631
627
  this.log.info('Sets unsupported');
632
628
  }
633
629
  }
@@ -2357,7 +2353,7 @@ class ObjectsInRedisClient {
2357
2353
  const message = JSON.stringify(obj);
2358
2354
  try {
2359
2355
  const commands = [];
2360
- if (this.useSets) {
2356
+ if (this.noLegacyMultihost) {
2361
2357
  if (obj.type) {
2362
2358
  // e.g. _design/ has no type
2363
2359
  // add the object to the set + set object atomic
@@ -2376,12 +2372,7 @@ class ObjectsInRedisClient {
2376
2372
  } else {
2377
2373
  // set all commands atomic
2378
2374
  commands.push(['set', id, message]);
2379
- // if we are sending all together as a pipeline it is slow for sim
2380
- this.client.multi({ pipeline: false });
2381
- for (const command of commands) {
2382
- this.client[command.shift()](...command);
2383
- }
2384
- await this.client.exec();
2375
+ await this.client.multi(commands).exec();
2385
2376
  }
2386
2377
  await this.client.publish(id, message);
2387
2378
  } catch (e) {
@@ -3104,7 +3095,7 @@ class ObjectsInRedisClient {
3104
3095
  const message = JSON.stringify(obj);
3105
3096
 
3106
3097
  const commands = [];
3107
- if (this.useSets) {
3098
+ if (this.noLegacyMultihost) {
3108
3099
  if (obj.type && (!oldObj || !oldObj.type)) {
3109
3100
  // new object or oldObj had no type -> add to set + set object
3110
3101
  commands.push(['sadd', `${this.setNamespace}object.type.${obj.type}`, this.objNamespace + id]);
@@ -3133,12 +3124,7 @@ class ObjectsInRedisClient {
3133
3124
  } else {
3134
3125
  // set all commands atomic
3135
3126
  commands.push(['set', this.objNamespace + id, message]);
3136
- // if we are sending all together as a pipeline it is slow for sim
3137
- this.client.multi({ pipeline: false });
3138
- for (const command of commands) {
3139
- this.client[command.shift()](...command);
3140
- }
3141
- await this.client.exec();
3127
+ await this.client.multi(commands).exec();
3142
3128
  }
3143
3129
 
3144
3130
  //this.settings.connection.enhancedLogging && this.log.silly(this.namespace + ' redis publish ' + this.objNamespace + id + ' ' + message);
@@ -3231,7 +3217,7 @@ class ObjectsInRedisClient {
3231
3217
  try {
3232
3218
  const commands = [];
3233
3219
 
3234
- if (this.useSets) {
3220
+ if (this.noLegacyMultihost) {
3235
3221
  if (oldObj.type) {
3236
3222
  // e.g. _design/ has no type
3237
3223
  // del the object from the set + del object atomic
@@ -3254,12 +3240,7 @@ class ObjectsInRedisClient {
3254
3240
  } else {
3255
3241
  // set all commands atomic
3256
3242
  commands.push(['del', this.objNamespace + id]);
3257
- // if we are sending all together as a pipeline it is slow for sim
3258
- this.client.multi({ pipeline: false });
3259
- for (const command of commands) {
3260
- this.client[command.shift()](...command);
3261
- }
3262
- await this.client.exec();
3243
+ await this.client.multi(commands).exec();
3263
3244
  }
3264
3245
 
3265
3246
  // object has been deleted -> remove from cached meta if there
@@ -4072,7 +4053,7 @@ class ObjectsInRedisClient {
4072
4053
 
4073
4054
  try {
4074
4055
  const commands = [];
4075
- if (this.useSets) {
4056
+ if (this.noLegacyMultihost) {
4076
4057
  // what is called oldObj is acutally the obj we set, because it has been extended
4077
4058
  if (oldObj.type && !oldType) {
4078
4059
  // new object or oldObj had no type -> add to set + set object
@@ -4102,12 +4083,7 @@ class ObjectsInRedisClient {
4102
4083
  } else {
4103
4084
  // set all commands atomic
4104
4085
  commands.push(['set', this.objNamespace + id, message]);
4105
- // if we are sending all together as a pipeline it is slow for sim
4106
- this.client.multi({ pipeline: false });
4107
- for (const command of commands) {
4108
- this.client[command.shift()](...command);
4109
- }
4110
- await this.client.exec();
4086
+ await this.client.multi(commands).exec();
4111
4087
  }
4112
4088
 
4113
4089
  // extended -> if its now type meta and currently marked as not -> cache
@@ -4353,7 +4329,7 @@ class ObjectsInRedisClient {
4353
4329
  }
4354
4330
 
4355
4331
  async loadLuaScripts() {
4356
- const luaPath = path.join(__dirname, this.useSets ? 'lua' : 'lua-v3');
4332
+ const luaPath = path.join(__dirname, this.noLegacyMultihost ? 'lua' : 'lua-v3');
4357
4333
  const scripts = fs.readdirSync(luaPath).map(name => {
4358
4334
  const shasum = crypto.createHash('sha1');
4359
4335
  const script = fs.readFileSync(path.join(luaPath, name));
@@ -4446,7 +4422,7 @@ class ObjectsInRedisClient {
4446
4422
  * @return {Promise<number>}
4447
4423
  */
4448
4424
  async migrateToSets() {
4449
- if (!this.useSets) {
4425
+ if (!this.noLegacyMultihost) {
4450
4426
  return 0;
4451
4427
  }
4452
4428
 
@@ -4499,6 +4475,103 @@ class ObjectsInRedisClient {
4499
4475
  return this.client.get(`${this.metaNamespace}objects.protocolVersion`);
4500
4476
  }
4501
4477
 
4478
+ /**
4479
+ * Extend the primary host lock time
4480
+ * Value will expire after ms milliseconds
4481
+ *
4482
+ * {number} ms - ms until value expires
4483
+ * @return {Promise<number>} 1 if extended else 0
4484
+ */
4485
+ extendPrimaryHostLock(ms) {
4486
+ if (!this.client) {
4487
+ throw new Error(utils.ERRORS.ERROR_DB_CLOSED);
4488
+ }
4489
+
4490
+ // we have a host version smaller 3 no one can be primary
4491
+ if (!this.noLegacyMultihost) {
4492
+ return Promise.resolve(0);
4493
+ }
4494
+
4495
+ // try to extend lock
4496
+ return this.client.evalsha([
4497
+ this.scripts['redlock_extend'],
4498
+ 3,
4499
+ `${this.metaNamespace}objects.primaryHost`,
4500
+ this.settings.hostname,
4501
+ ms
4502
+ ]);
4503
+ }
4504
+
4505
+ /**
4506
+ * Sets current host as primary if no primary host active
4507
+ * Value will expire after ms milliseconds
4508
+ *
4509
+ * {number} ms - ms until value expires
4510
+ * @return {Promise<number>} 1 if lock acquired else 0
4511
+ */
4512
+ setPrimaryHost(ms) {
4513
+ if (!this.client) {
4514
+ throw new Error(utils.ERRORS.ERROR_DB_CLOSED);
4515
+ }
4516
+
4517
+ // we have a host version smaller 3 no one can be primary
4518
+ if (!this.noLegacyMultihost) {
4519
+ return Promise.resolve(0);
4520
+ }
4521
+
4522
+ // try to acquire lock
4523
+ return this.client.evalsha([
4524
+ this.scripts['redlock_acquire'],
4525
+ 3,
4526
+ `${this.metaNamespace}objects.primaryHost`,
4527
+ this.settings.hostname,
4528
+ ms
4529
+ ]);
4530
+ }
4531
+
4532
+ /**
4533
+ * Get name of the primary host
4534
+ *
4535
+ * @return {Promise<string>}
4536
+ */
4537
+ getPrimaryHost() {
4538
+ if (!this.client) {
4539
+ throw new Error(utils.ERRORS.ERROR_DB_CLOSED);
4540
+ }
4541
+
4542
+ // we have a host version smaller 3 no one can be primary
4543
+ if (!this.noLegacyMultihost) {
4544
+ return new Promise.resolve('');
4545
+ }
4546
+
4547
+ return this.client.get(`${this.metaNamespace}objects.primaryHost`);
4548
+ }
4549
+
4550
+ /**
4551
+ * Ensure we are no longer the primary host
4552
+ *
4553
+ * @return Promise<void>
4554
+ */
4555
+ releasePrimaryHost() {
4556
+ if (!this.client) {
4557
+ throw new Error(utils.ERRORS.ERROR_DB_CLOSED);
4558
+ }
4559
+
4560
+ if (!this.noLegacyMultihost) {
4561
+ return new Promise.resolve();
4562
+ }
4563
+
4564
+ // try to release lock
4565
+ return this.client.evalsha([
4566
+ this.scripts['redlock_release'],
4567
+ 4,
4568
+ `${this.metaNamespace}objects.primaryHost`,
4569
+ this.settings.hostname,
4570
+ this.settings.options.db,
4571
+ `${this.metaNamespace}objects.primaryHost`
4572
+ ]);
4573
+ }
4574
+
4502
4575
  /**
4503
4576
  * Sets the protocol version to the DB
4504
4577
  * @param {number|string} version - protocol version
@@ -4518,6 +4591,17 @@ class ObjectsInRedisClient {
4518
4591
  throw new Error('Cannot set an unsupported protocol version on the current host');
4519
4592
  }
4520
4593
  }
4594
+
4595
+ /**
4596
+ * Subscribe to expired events to get expiration of primary host
4597
+ * @return {Promise<void>}
4598
+ */
4599
+ async subscribePrimaryHost() {
4600
+ if (this.subSystem) {
4601
+ await this.subSystem.subscribe(`__keyevent@${this.settings.connection.options.db}__:expired`);
4602
+ await this.subSystem.subscribe(`__keyevent@${this.settings.connection.options.db}__:evicted`);
4603
+ }
4604
+ }
4521
4605
  }
4522
4606
 
4523
4607
  module.exports = ObjectsInRedisClient;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@iobroker/db-objects-redis",
3
- "version": "4.0.0-alpha.45-20220114-88aaebdb",
3
+ "version": "4.0.0-alpha.49-20220121-e7aa4308",
4
4
  "engines": {
5
5
  "node": ">=12.0.0"
6
6
  },
7
7
  "dependencies": {
8
- "@iobroker/db-base": "4.0.0-alpha.45-20220114-88aaebdb",
8
+ "@iobroker/db-base": "4.0.0-alpha.49-20220121-e7aa4308",
9
9
  "deep-clone": "^3.0.3",
10
10
  "ioredis": "^4.28.2",
11
11
  "node.extend": "^2.0.2",
@@ -36,5 +36,5 @@
36
36
  "lib/",
37
37
  "index.js"
38
38
  ],
39
- "gitHead": "e2fd3e7af91eb83c30f8ae45f71d6e178edc7b28"
39
+ "gitHead": "f42f117b74d3c4bfac0bc872c087d382ca897b7c"
40
40
  }