Skip to content

Commit 6c7f1c1

Browse files
authored
Merge pull request #1192 from shawnmcknight/redlock-opt-out
Allow opt out of redlock in redis cacher
2 parents 80f27f3 + 0f2bbb9 commit 6c7f1c1

File tree

3 files changed

+150
-80
lines changed

3 files changed

+150
-80
lines changed

index.d.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,10 @@ declare namespace Moleculer {
815815
* @param mixinSchema Mixin schema
816816
* @param svcSchema Service schema
817817
*/
818-
mergeSchemas(mixinSchema: Partial<ServiceSchema>, svcSchema: Partial<ServiceSchema>): Partial<ServiceSchema>;
818+
mergeSchemas(
819+
mixinSchema: Partial<ServiceSchema>,
820+
svcSchema: Partial<ServiceSchema>
821+
): Partial<ServiceSchema>;
819822

820823
/**
821824
* Merge `settings` property in schema
@@ -1424,7 +1427,7 @@ declare namespace Moleculer {
14241427
interface RedisCacherOptions extends CacherOptions {
14251428
prefix?: string;
14261429
redis?: GenericObject;
1427-
redlock?: GenericObject;
1430+
redlock?: boolean | GenericObject;
14281431
monitor?: boolean;
14291432
pingInterval?: number;
14301433
}
@@ -1743,7 +1746,9 @@ declare namespace Moleculer {
17431746
actions: any;
17441747
events: any;
17451748

1746-
getServiceList<S = ServiceSettingSchema>(opts?: ServiceListCatalogOptions): ServiceSchema<S>[];
1749+
getServiceList<S = ServiceSettingSchema>(
1750+
opts?: ServiceListCatalogOptions
1751+
): ServiceSchema<S>[];
17471752
getActionList(opts?: ActionCatalogListOptions): ActionSchema[];
17481753
}
17491754

src/cachers/redis.js

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
"use strict";
88

9-
let Redis, Redlock;
9+
let Redis;
1010
const BaseCacher = require("./base");
1111
const _ = require("lodash");
1212
const { METRIC } = require("../metrics");
@@ -38,6 +38,13 @@ class RedisCacher extends BaseCacher {
3838
});
3939

4040
this.pingIntervalHandle = null;
41+
42+
/**
43+
* Redlock client instance
44+
* @memberof RedisCacher
45+
*/
46+
this.redlock = null;
47+
this.redlockNonBlocking = null;
4148
}
4249

4350
/**
@@ -94,27 +101,26 @@ class RedisCacher extends BaseCacher {
94101
/* istanbul ignore next */
95102
this.logger.error(err);
96103
});
97-
try {
98-
Redlock = require("redlock");
99-
} catch (err) {
100-
/* istanbul ignore next */
101-
this.logger.warn(
102-
"The 'redlock' package is missing. If you want to enable cache lock, please install it with 'npm install redlock --save' command."
103-
);
104-
}
105-
if (Redlock) {
106-
let redlockClients = (this.opts.redlock ? this.opts.redlock.clients : null) || [
107-
this.client
108-
];
109-
/**
110-
* redlock client instance
111-
* @memberof RedisCacher
112-
*/
113-
this.redlock = new Redlock(redlockClients, _.omit(this.opts.redlock, ["clients"]));
114-
// Non-blocking redlock client, used for tryLock()
115-
this.redlockNonBlocking = new Redlock(redlockClients, {
116-
retryCount: 0
117-
});
104+
105+
// check for !== false for backwards compatibility purposes; should be changed to opt-in in a breaking change release
106+
if (this.opts.redlock !== false) {
107+
let Redlock;
108+
try {
109+
Redlock = require("redlock");
110+
} catch (err) {
111+
Redlock = null;
112+
}
113+
if (Redlock != null) {
114+
let redlockClients = (this.opts.redlock ? this.opts.redlock.clients : null) || [
115+
this.client
116+
];
117+
118+
this.redlock = new Redlock(redlockClients, _.omit(this.opts.redlock, ["clients"]));
119+
// Non-blocking redlock client, used for tryLock()
120+
this.redlockNonBlocking = new Redlock(redlockClients, {
121+
retryCount: 0
122+
});
123+
}
118124
}
119125
if (this.opts.monitor) {
120126
/* istanbul ignore next */
@@ -344,6 +350,10 @@ class RedisCacher extends BaseCacher {
344350
* @memberof RedisCacher
345351
*/
346352
lock(key, ttl = 15000) {
353+
if (this.redlock == null) {
354+
return this._handleMissingRedlock();
355+
}
356+
347357
key = this.prefix + key + "-lock";
348358
return this.redlock.lock(key, ttl).then(lock => {
349359
return () => lock.unlock();
@@ -360,12 +370,27 @@ class RedisCacher extends BaseCacher {
360370
* @memberof RedisCacher
361371
*/
362372
tryLock(key, ttl = 15000) {
373+
if (this.redlockNonBlocking == null) {
374+
return this._handleMissingRedlock();
375+
}
376+
363377
key = this.prefix + key + "-lock";
364378
return this.redlockNonBlocking.lock(key, ttl).then(lock => {
365379
return () => lock.unlock();
366380
});
367381
}
368382

383+
/**
384+
* Common code for handling unavailable Redlock
385+
* @returns {Promise}
386+
*/
387+
_handleMissingRedlock() {
388+
this.logger.error(
389+
"The 'redlock' package is missing or redlock is disabled. If you want to enable cache lock, please install it with 'npm install redlock --save' command."
390+
);
391+
return Promise.resolve();
392+
}
393+
369394
_sequentialPromises(elements) {
370395
return elements.reduce((chain, element) => {
371396
return chain.then(() => this._scanDel(element));

test/unit/cachers/redis.spec.js

Lines changed: 95 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const C = require("../../../src/constants");
55

66
jest.mock("ioredis");
77
const Redis = require("ioredis");
8+
const BaseLogger = require("../../../src/loggers/base");
89

910
Redis.mockImplementation(() => {
1011
let onCallbacks = {};
@@ -646,79 +647,118 @@ describe("Test RedisCacher getWithTTL method", () => {
646647
});
647648
});
648649

649-
describe("Test RedisCacher lock method", () => {
650-
const key = "abcd134";
651-
let broker = new ServiceBroker({ logger: false });
652-
let cacher = new RedisCacher({
653-
ttl: 30,
654-
lock: true
655-
});
656-
cacher.init(broker); // for empty logger
657-
let unlock1, unlock2;
658-
beforeEach(() => {
659-
unlock1 = jest.fn(() => Promise.resolve());
660-
unlock2 = jest.fn(() => Promise.resolve());
661-
cacher.redlock.lock = jest.fn(() => {
662-
return Promise.resolve({
663-
unlock: unlock1
664-
});
650+
describe("redlock enabled", () => {
651+
describe("Test RedisCacher lock method", () => {
652+
const key = "abcd134";
653+
let broker = new ServiceBroker({ logger: false });
654+
let cacher = new RedisCacher({
655+
ttl: 30,
656+
lock: true
665657
});
666-
cacher.redlockNonBlocking.lock = jest.fn(() => {
667-
return Promise.resolve({
668-
unlock: unlock2
658+
cacher.init(broker); // for empty logger
659+
let unlock1, unlock2;
660+
beforeEach(() => {
661+
unlock1 = jest.fn(() => Promise.resolve());
662+
unlock2 = jest.fn(() => Promise.resolve());
663+
cacher.redlock.lock = jest.fn(() => {
664+
return Promise.resolve({
665+
unlock: unlock1
666+
});
667+
});
668+
cacher.redlockNonBlocking.lock = jest.fn(() => {
669+
return Promise.resolve({
670+
unlock: unlock2
671+
});
669672
});
670673
});
671-
});
672674

673-
it("should call redlock.lock when calling cacher.lock", () => {
674-
return cacher.lock(key, 20).then(() => {
675-
expect(cacher.redlock.lock).toHaveBeenCalledTimes(1);
676-
expect(cacher.redlock.lock).toHaveBeenCalledWith(cacher.prefix + key + "-lock", 20);
675+
it("should call redlock.lock when calling cacher.lock", () => {
676+
return cacher.lock(key, 20).then(() => {
677+
expect(cacher.redlock.lock).toHaveBeenCalledTimes(1);
678+
expect(cacher.redlock.lock).toHaveBeenCalledWith(cacher.prefix + key + "-lock", 20);
679+
});
677680
});
678-
});
679681

680-
it("should call redlock.unlock when calling unlock callback", () => {
681-
return cacher.lock(key, 20).then(unlock => {
682-
return unlock().then(() => {
683-
expect(unlock1).toBeCalled();
682+
it("should call redlock.unlock when calling unlock callback", () => {
683+
return cacher.lock(key, 20).then(unlock => {
684+
return unlock().then(() => {
685+
expect(unlock1).toBeCalled();
686+
});
684687
});
685688
});
686-
});
687689

688-
it("should call redlock.lock when calling cacher.tryLock", () => {
689-
return cacher.tryLock(key, 20).then(() => {
690-
expect(cacher.redlockNonBlocking.lock).toHaveBeenCalledTimes(1);
691-
expect(cacher.redlockNonBlocking.lock).toHaveBeenCalledWith(
692-
cacher.prefix + key + "-lock",
693-
20
694-
);
690+
it("should call redlock.lock when calling cacher.tryLock", () => {
691+
return cacher.tryLock(key, 20).then(() => {
692+
expect(cacher.redlockNonBlocking.lock).toHaveBeenCalledTimes(1);
693+
expect(cacher.redlockNonBlocking.lock).toHaveBeenCalledWith(
694+
cacher.prefix + key + "-lock",
695+
20
696+
);
697+
});
695698
});
696-
});
697699

698-
it("should call redlock.unlock when calling unlock callback", () => {
699-
const err = new Error("Already locked.");
700-
cacher.redlockNonBlocking.lock = jest.fn(() => {
701-
return Promise.reject(err);
700+
it("should call redlock.unlock when calling unlock callback", () => {
701+
const err = new Error("Already locked.");
702+
cacher.redlockNonBlocking.lock = jest.fn(() => {
703+
return Promise.reject(err);
704+
});
705+
return cacher.tryLock(key, 20).catch(e => {
706+
expect(e).toBe(err);
707+
});
702708
});
703-
return cacher.tryLock(key, 20).catch(e => {
704-
expect(e).toBe(err);
709+
710+
it("should failed to acquire a lock when redlock client throw an error", () => {
711+
return cacher.tryLock(key, 20);
705712
});
706713
});
707714

708-
it("should failed to acquire a lock when redlock client throw an error", () => {
709-
return cacher.tryLock(key, 20);
715+
describe("Test RedisCacher with opts.lock", () => {
716+
it("should create redlock clients", () => {
717+
let broker = new ServiceBroker({ logger: false });
718+
let cacher = new RedisCacher({
719+
ttl: 30
720+
});
721+
cacher.init(broker);
722+
expect(cacher.redlock).toBeDefined();
723+
expect(cacher.redlockNonBlocking).toBeDefined();
724+
});
710725
});
711726
});
712727

713-
describe("Test RedisCacher with opts.lock", () => {
714-
it("should create redlock clients", () => {
715-
let broker = new ServiceBroker({ logger: false });
716-
let cacher = new RedisCacher({
717-
ttl: 30
718-
});
719-
cacher.init(broker);
720-
expect(cacher.redlock).toBeDefined();
721-
expect(cacher.redlockNonBlocking).toBeDefined();
728+
describe("redlock disabled", () => {
729+
const errorMock = jest.fn();
730+
class TestLogger extends BaseLogger {
731+
getLogHandler() {
732+
return (type, args) => {
733+
if (type === "error") {
734+
return errorMock(...args);
735+
}
736+
};
737+
}
738+
}
739+
const logger = new TestLogger();
740+
741+
const broker = new ServiceBroker({ logger });
742+
const cacher = new RedisCacher({ redlock: false });
743+
cacher.init(broker);
744+
745+
afterEach(() => {
746+
errorMock.mockClear();
747+
});
748+
749+
it("should not add a redlock instance", () => {
750+
expect(cacher.redlock).toBeNull();
751+
expect(cacher.redlockNonBlocking).toBeNull();
752+
});
753+
754+
it("should resolve but emit error on lock call", async () => {
755+
await expect(cacher.lock()).resolves.toBeUndefined();
756+
expect(errorMock).toHaveBeenCalledTimes(1);
757+
});
758+
759+
it("should resolve but emit error on tryLock call", async () => {
760+
await expect(cacher.tryLock()).resolves.toBeUndefined();
761+
expect(errorMock).toHaveBeenCalledTimes(1);
722762
});
723763
});
724764

0 commit comments

Comments
 (0)