effect-distributed-lock 0.0.2 → 0.0.4

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 (99) hide show
  1. package/README.md +97 -66
  2. package/examples/index.ts +49 -21
  3. package/package.json +2 -2
  4. package/redis-semaphore/.codeclimate.yml +5 -0
  5. package/redis-semaphore/.fossa.yml +14 -0
  6. package/redis-semaphore/.github/dependabot.yml +6 -0
  7. package/redis-semaphore/.github/workflows/branches.yml +39 -0
  8. package/redis-semaphore/.github/workflows/pull-requests.yml +35 -0
  9. package/redis-semaphore/.mocharc.yaml +6 -0
  10. package/redis-semaphore/.prettierrc +6 -0
  11. package/redis-semaphore/.snyk +4 -0
  12. package/redis-semaphore/.yarnrc.yml +2 -0
  13. package/redis-semaphore/CHANGELOG.md +70 -0
  14. package/redis-semaphore/Dockerfile +5 -0
  15. package/redis-semaphore/LICENSE +21 -0
  16. package/redis-semaphore/README.md +445 -0
  17. package/redis-semaphore/docker-compose.yml +31 -0
  18. package/redis-semaphore/eslint.config.mjs +73 -0
  19. package/redis-semaphore/package.json +79 -0
  20. package/redis-semaphore/setup-redis-servers.sh +2 -0
  21. package/redis-semaphore/src/Lock.ts +172 -0
  22. package/redis-semaphore/src/RedisMultiSemaphore.ts +56 -0
  23. package/redis-semaphore/src/RedisMutex.ts +45 -0
  24. package/redis-semaphore/src/RedisSemaphore.ts +49 -0
  25. package/redis-semaphore/src/RedlockMultiSemaphore.ts +56 -0
  26. package/redis-semaphore/src/RedlockMutex.ts +52 -0
  27. package/redis-semaphore/src/RedlockSemaphore.ts +49 -0
  28. package/redis-semaphore/src/errors/LostLockError.ts +1 -0
  29. package/redis-semaphore/src/errors/TimeoutError.ts +1 -0
  30. package/redis-semaphore/src/index.ts +23 -0
  31. package/redis-semaphore/src/misc.ts +12 -0
  32. package/redis-semaphore/src/multiSemaphore/acquire/index.ts +53 -0
  33. package/redis-semaphore/src/multiSemaphore/acquire/lua.ts +31 -0
  34. package/redis-semaphore/src/multiSemaphore/refresh/index.ts +32 -0
  35. package/redis-semaphore/src/multiSemaphore/refresh/lua.ts +31 -0
  36. package/redis-semaphore/src/multiSemaphore/release/index.ts +22 -0
  37. package/redis-semaphore/src/multiSemaphore/release/lua.ts +17 -0
  38. package/redis-semaphore/src/mutex/acquire.ts +42 -0
  39. package/redis-semaphore/src/mutex/refresh.ts +37 -0
  40. package/redis-semaphore/src/mutex/release.ts +30 -0
  41. package/redis-semaphore/src/redlockMultiSemaphore/acquire.ts +56 -0
  42. package/redis-semaphore/src/redlockMultiSemaphore/refresh.ts +68 -0
  43. package/redis-semaphore/src/redlockMultiSemaphore/release.ts +19 -0
  44. package/redis-semaphore/src/redlockMutex/acquire.ts +54 -0
  45. package/redis-semaphore/src/redlockMutex/refresh.ts +53 -0
  46. package/redis-semaphore/src/redlockMutex/release.ts +19 -0
  47. package/redis-semaphore/src/redlockSemaphore/acquire.ts +55 -0
  48. package/redis-semaphore/src/redlockSemaphore/refresh.ts +60 -0
  49. package/redis-semaphore/src/redlockSemaphore/release.ts +18 -0
  50. package/redis-semaphore/src/semaphore/acquire/index.ts +52 -0
  51. package/redis-semaphore/src/semaphore/acquire/lua.ts +25 -0
  52. package/redis-semaphore/src/semaphore/refresh/index.ts +31 -0
  53. package/redis-semaphore/src/semaphore/refresh/lua.ts +25 -0
  54. package/redis-semaphore/src/semaphore/release.ts +14 -0
  55. package/redis-semaphore/src/types.ts +63 -0
  56. package/redis-semaphore/src/utils/createEval.ts +45 -0
  57. package/redis-semaphore/src/utils/index.ts +13 -0
  58. package/redis-semaphore/src/utils/redlock.ts +7 -0
  59. package/redis-semaphore/test/init.test.ts +9 -0
  60. package/redis-semaphore/test/redisClient.ts +82 -0
  61. package/redis-semaphore/test/setup.ts +6 -0
  62. package/redis-semaphore/test/shell.test.ts +15 -0
  63. package/redis-semaphore/test/shell.ts +48 -0
  64. package/redis-semaphore/test/src/Lock.test.ts +37 -0
  65. package/redis-semaphore/test/src/RedisMultiSemaphore.test.ts +425 -0
  66. package/redis-semaphore/test/src/RedisMutex.test.ts +334 -0
  67. package/redis-semaphore/test/src/RedisSemaphore.test.ts +367 -0
  68. package/redis-semaphore/test/src/RedlockMultiSemaphore.test.ts +671 -0
  69. package/redis-semaphore/test/src/RedlockMutex.test.ts +328 -0
  70. package/redis-semaphore/test/src/RedlockSemaphore.test.ts +579 -0
  71. package/redis-semaphore/test/src/index.test.ts +22 -0
  72. package/redis-semaphore/test/src/multiSemaphore/acquire/index.test.ts +51 -0
  73. package/redis-semaphore/test/src/multiSemaphore/acquire/internal.test.ts +67 -0
  74. package/redis-semaphore/test/src/multiSemaphore/refresh/index.test.ts +52 -0
  75. package/redis-semaphore/test/src/multiSemaphore/release/index.test.ts +18 -0
  76. package/redis-semaphore/test/src/mutex/acquire.test.ts +78 -0
  77. package/redis-semaphore/test/src/mutex/refresh.test.ts +22 -0
  78. package/redis-semaphore/test/src/mutex/release.test.ts +17 -0
  79. package/redis-semaphore/test/src/redlockMutex/acquire.test.ts +90 -0
  80. package/redis-semaphore/test/src/redlockMutex/refresh.test.ts +27 -0
  81. package/redis-semaphore/test/src/redlockMutex/release.test.ts +17 -0
  82. package/redis-semaphore/test/src/semaphore/acquire/index.test.ts +49 -0
  83. package/redis-semaphore/test/src/semaphore/acquire/internal.test.ts +65 -0
  84. package/redis-semaphore/test/src/semaphore/refresh/index.test.ts +44 -0
  85. package/redis-semaphore/test/src/semaphore/release.test.ts +18 -0
  86. package/redis-semaphore/test/src/utils/eval.test.ts +22 -0
  87. package/redis-semaphore/test/src/utils/index.test.ts +19 -0
  88. package/redis-semaphore/test/src/utils/redlock.test.ts +31 -0
  89. package/redis-semaphore/test/unhandledRejection.ts +28 -0
  90. package/redis-semaphore/tsconfig.build-commonjs.json +9 -0
  91. package/redis-semaphore/tsconfig.build-es.json +9 -0
  92. package/redis-semaphore/tsconfig.json +11 -0
  93. package/redis-semaphore/yarn.lock +5338 -0
  94. package/src/Backing.ts +87 -0
  95. package/src/DistributedSemaphore.ts +384 -0
  96. package/src/Errors.ts +3 -39
  97. package/src/RedisBacking.ts +167 -165
  98. package/src/index.ts +28 -17
  99. package/src/DistributedMutex.ts +0 -304
@@ -0,0 +1,67 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { acquireLua } from '../../../../src/multiSemaphore/acquire/lua'
4
+ import { client1 as client } from '../../../redisClient'
5
+
6
+ interface Options {
7
+ identifier: string
8
+ lockTimeout: number
9
+ now: number
10
+ }
11
+
12
+ const opts = (id: string, nowOffset = 0): Options => ({
13
+ identifier: id,
14
+ lockTimeout: 500,
15
+ now: new Date().getTime() + nowOffset
16
+ })
17
+
18
+ async function acquire(options: Options) {
19
+ const { identifier, lockTimeout, now } = options
20
+ return await acquireLua(client, ['key', 1, 1, identifier, lockTimeout, now])
21
+ }
22
+
23
+ describe('multiSemaphore acquire internal', () => {
24
+ it('should return 1 for success acquire', async () => {
25
+ const result = await acquire(opts('111'))
26
+ expect(result).to.be.eql(1)
27
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['111_0'])
28
+ })
29
+ it('should return 0 for failure acquire', async () => {
30
+ const result1 = await acquire(opts('111'))
31
+ const result2 = await acquire(opts('112'))
32
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['111_0'])
33
+ expect(result1).to.be.eql(1)
34
+ expect(result2).to.be.eql(0)
35
+ })
36
+ describe('TIME SHIFT case', () => {
37
+ it('should handle time difference less than lockTimeout (nodeA has faster clocks)', async () => {
38
+ // lockTimeout = 500ms
39
+ // nodeA is for 450ms faster than nodeB
40
+ const resultA = await acquire(opts('111', 450))
41
+ const resultB = await acquire(opts('112', 0))
42
+ expect(resultA).to.be.eql(1)
43
+ expect(resultB).to.be.eql(0)
44
+ })
45
+ it('should handle time difference less than lockTimeout (nodeA has slower clocks)', async () => {
46
+ // lockTimeout = 500ms
47
+ // nodeB is for 450ms faster than nodeA
48
+ const resultA = await acquire(opts('111', 0))
49
+ const resultB = await acquire(opts('112', 450))
50
+ expect(resultA).to.be.eql(1)
51
+ expect(resultB).to.be.eql(0)
52
+ })
53
+ it('cant handle time difference greater than lockTimeout (nodeA has slower clocks)', async () => {
54
+ // lockTimeout = 500ms
55
+ // nodeB is for 550ms faster than nodeA
56
+ const resultA = await acquire(opts('111', 0))
57
+ const resultB = await acquire(opts('112', 550))
58
+ expect(resultA).to.be.eql(1)
59
+ expect(resultB).to.be.eql(1) // Semaphore stealed...
60
+
61
+ // This happens due removing "expired" nodeA lock (at nodeB "now" nodeA lock has been expired 50ms ago)
62
+ // Unfortunatelly "fair" semaphore described here
63
+ // https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-3-counting-semaphores/6-3-1-building-a-basic-counting-semaphore/
64
+ // also has the same problem
65
+ })
66
+ })
67
+ })
@@ -0,0 +1,52 @@
1
+ import { expect } from 'chai'
2
+
3
+ import {
4
+ Options,
5
+ refreshSemaphore as refresh
6
+ } from '../../../../src/multiSemaphore/refresh/index'
7
+ import { client1 as client } from '../../../redisClient'
8
+
9
+ const opts = (id: string): Options => ({
10
+ identifier: id,
11
+ lockTimeout: 100
12
+ })
13
+
14
+ describe('multiSemaphore refresh', () => {
15
+ it('should return false if resource is already acquired', async () => {
16
+ const now = '' + (Date.now() - 10)
17
+ await client.zadd('key', now, '222', now, '333', now, '444')
18
+ const result = await refresh(client, 'key', 3, 2, opts('111'))
19
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['222', '333', '444'])
20
+ expect(result).to.be.false
21
+ })
22
+ it('should return false if resource is already acquired, but some expired', async () => {
23
+ const now = '' + (Date.now() - 10)
24
+ const oldNow = '' + (Date.now() - 10000)
25
+ await client.zadd('key', oldNow, '222', oldNow, '333', now, '444')
26
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['222', '333', '444'])
27
+ const result = await refresh(client, 'key', 3, 2, opts('111'))
28
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['444'])
29
+ expect(result).to.be.false
30
+ })
31
+ it('should return false if resource is not acquired', async () => {
32
+ const result = await refresh(client, 'key', 3, 2, opts('111'))
33
+ expect(await client.zrange('key', 0, -1)).to.be.eql([])
34
+ expect(result).to.be.false
35
+ })
36
+ it('should return true for success refresh', async () => {
37
+ const now = '' + (Date.now() - 10)
38
+ await client.zadd('key', now, '111_0', now, '111_1', now, '333')
39
+ expect(await client.zrange('key', 0, -1)).to.be.eql([
40
+ '111_0',
41
+ '111_1',
42
+ '333'
43
+ ])
44
+ const result = await refresh(client, 'key', 3, 2, opts('111'))
45
+ expect(await client.zrange('key', 0, -1)).to.be.eql([
46
+ '333',
47
+ '111_0',
48
+ '111_1'
49
+ ])
50
+ expect(result).to.be.true
51
+ })
52
+ })
@@ -0,0 +1,18 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { releaseSemaphore as release } from '../../../../src/multiSemaphore/release/index'
4
+ import { client1 as client } from '../../../redisClient'
5
+
6
+ describe('multiSemaphore release', () => {
7
+ it('should remove key after success release', async () => {
8
+ await client.zadd('key', '' + Date.now(), '111_0')
9
+ expect(await client.zcard('key')).to.be.eql(1)
10
+ await release(client, 'key', 1, '111')
11
+ expect(await client.zcard('key')).to.be.eql(0)
12
+ })
13
+ it('should do nothing if resource is not locked', async () => {
14
+ expect(await client.zcard('key')).to.be.eql(0)
15
+ await release(client, 'key', 1, '111')
16
+ expect(await client.zcard('key')).to.be.eql(0)
17
+ })
18
+ })
@@ -0,0 +1,78 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { acquireMutex as acquire, Options } from '../../../src/mutex/acquire'
4
+ import { client1 as client } from '../../redisClient'
5
+
6
+ const opts = (id: string, overrides?: Partial<Options>): Options => ({
7
+ identifier: id,
8
+ acquireTimeout: 50,
9
+ acquireAttemptsLimit: Number.POSITIVE_INFINITY,
10
+ lockTimeout: 100,
11
+ retryInterval: 10,
12
+ ...overrides
13
+ })
14
+
15
+ describe('mutex acquire', () => {
16
+ it('should return true for success lock', async () => {
17
+ const result = await acquire(client, 'key', opts('111'))
18
+ expect(result).to.be.true
19
+ })
20
+ it('should return false when timeout', async () => {
21
+ const result1 = await acquire(client, 'key', opts('111'))
22
+ const result2 = await acquire(client, 'key', opts('222'))
23
+ expect(result1).to.be.true
24
+ expect(result2).to.be.false
25
+ })
26
+ it('should return false after acquireAttemptsLimit', async () => {
27
+ const result1 = await acquire(client, 'key', opts('111'))
28
+ const result2 = await acquire(
29
+ client,
30
+ 'key',
31
+ opts('222', {
32
+ acquireAttemptsLimit: 1,
33
+ acquireTimeout: Number.POSITIVE_INFINITY
34
+ })
35
+ )
36
+ expect(result1).to.be.true
37
+ expect(result2).to.be.false
38
+ })
39
+ it('should set identifier for key', async () => {
40
+ await acquire(client, 'key1', opts('111'))
41
+ const value = await client.get('key1')
42
+ expect(value).to.be.eql('111')
43
+ })
44
+ it('should set TTL for key', async () => {
45
+ await acquire(client, 'key2', opts('111'))
46
+ const ttl = await client.pttl('key2')
47
+ expect(ttl).to.be.gte(90)
48
+ expect(ttl).to.be.lte(100)
49
+ })
50
+ it('should wait for auto-release', async () => {
51
+ const start1 = Date.now()
52
+ await acquire(client, 'key', opts('111'))
53
+ const start2 = Date.now()
54
+ await acquire(client, 'key', opts('222'))
55
+ const now = Date.now()
56
+ expect(start2 - start1).to.be.gte(0)
57
+ expect(start2 - start1).to.be.lt(10)
58
+ expect(now - start1).to.be.gte(50)
59
+ expect(now - start2).to.be.gte(50)
60
+ })
61
+ it('should wait per key', async () => {
62
+ const start1 = Date.now()
63
+ await Promise.all([
64
+ acquire(client, 'key1', opts('a1')),
65
+ acquire(client, 'key2', opts('a2'))
66
+ ])
67
+ const start2 = Date.now()
68
+ await Promise.all([
69
+ acquire(client, 'key1', opts('b1')),
70
+ acquire(client, 'key2', opts('b2'))
71
+ ])
72
+ const now = Date.now()
73
+ expect(start2 - start1).to.be.gte(0)
74
+ expect(start2 - start1).to.be.lt(10)
75
+ expect(now - start1).to.be.gte(50)
76
+ expect(now - start2).to.be.gte(50)
77
+ })
78
+ })
@@ -0,0 +1,22 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { refreshMutex as refresh } from '../../../src/mutex/refresh'
4
+ import { client1 as client } from '../../redisClient'
5
+
6
+ describe('mutex refresh', () => {
7
+ it('should return false if resource is already acquired by different instance', async () => {
8
+ await client.set('key', '222')
9
+ const result = await refresh(client, 'key', '111', 10000)
10
+ expect(result).to.be.false
11
+ })
12
+ it('should return false if resource is not acquired', async () => {
13
+ const result = await refresh(client, 'key', '111', 10000)
14
+ expect(result).to.be.false
15
+ })
16
+ it('should return true for success refresh', async () => {
17
+ await client.set('key', '111')
18
+ const result = await refresh(client, 'key', '111', 20000)
19
+ expect(result).to.be.true
20
+ expect(await client.pttl('key')).to.be.gte(10000)
21
+ })
22
+ })
@@ -0,0 +1,17 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { releaseMutex as release } from '../../../src/mutex/release'
4
+ import { client1 as client } from '../../redisClient'
5
+
6
+ describe('Mutex release', () => {
7
+ it('should remove key after release', async () => {
8
+ await client.set('key', '111')
9
+ await release(client, 'key', '111')
10
+ expect(await client.get('key')).to.be.eql(null)
11
+ })
12
+ it('should do nothing if resource is not locked', async () => {
13
+ expect(await client.get('key')).to.be.eql(null)
14
+ await release(client, 'key', '111')
15
+ expect(await client.get('key')).to.be.eql(null)
16
+ })
17
+ })
@@ -0,0 +1,90 @@
1
+ import { expect } from 'chai'
2
+
3
+ import {
4
+ acquireRedlockMutex as acquire,
5
+ Options
6
+ } from '../../../src/redlockMutex/acquire'
7
+ import { allClients } from '../../redisClient'
8
+
9
+ const opts = (id: string, overrides?: Partial<Options>): Options => ({
10
+ identifier: id,
11
+ acquireTimeout: 50,
12
+ acquireAttemptsLimit: Number.POSITIVE_INFINITY,
13
+ lockTimeout: 100,
14
+ retryInterval: 10,
15
+ ...overrides
16
+ })
17
+
18
+ describe('redlockMutex acquire', () => {
19
+ it('should return true for success lock', async () => {
20
+ const result = await acquire(allClients, 'key', opts('111'))
21
+ expect(result).to.be.true
22
+ })
23
+ it('should return false when timeout', async () => {
24
+ const result1 = await acquire(allClients, 'key', opts('111'))
25
+ const result2 = await acquire(allClients, 'key', opts('222'))
26
+ expect(result1).to.be.true
27
+ expect(result2).to.be.false
28
+ })
29
+ it('should return false after acquireAttemptsLimit', async () => {
30
+ const result1 = await acquire(allClients, 'key', opts('111'))
31
+ const result2 = await acquire(
32
+ allClients,
33
+ 'key',
34
+ opts('222', {
35
+ acquireAttemptsLimit: 1,
36
+ acquireTimeout: Number.POSITIVE_INFINITY
37
+ })
38
+ )
39
+ expect(result1).to.be.true
40
+ expect(result2).to.be.false
41
+ })
42
+ it('should set identifier for key', async () => {
43
+ await acquire(allClients, 'key1', opts('111'))
44
+ const values = await Promise.all(
45
+ allClients.map(client => client.get('key1'))
46
+ )
47
+ expect(values).to.be.eql(['111', '111', '111'])
48
+ })
49
+ it('should set TTL for key', async () => {
50
+ await acquire(allClients, 'key2', opts('111'))
51
+ const ttls = await Promise.all(
52
+ allClients.map(client => client.pttl('key2'))
53
+ )
54
+ for (const ttl of ttls) {
55
+ if (ttl === -2) {
56
+ continue
57
+ }
58
+ expect(ttl).to.be.gte(90)
59
+ expect(ttl).to.be.lte(100)
60
+ }
61
+ })
62
+ it('should wait for auto-release', async () => {
63
+ const start1 = Date.now()
64
+ await acquire(allClients, 'key', opts('111'))
65
+ const start2 = Date.now()
66
+ await acquire(allClients, 'key', opts('222'))
67
+ const now = Date.now()
68
+ expect(start2 - start1).to.be.gte(0)
69
+ expect(start2 - start1).to.be.lt(10)
70
+ expect(now - start1).to.be.gte(50)
71
+ expect(now - start2).to.be.gte(50)
72
+ })
73
+ it('should wait per key', async () => {
74
+ const start1 = Date.now()
75
+ await Promise.all([
76
+ acquire(allClients, 'key1', opts('a1')),
77
+ acquire(allClients, 'key2', opts('a2'))
78
+ ])
79
+ const start2 = Date.now()
80
+ await Promise.all([
81
+ acquire(allClients, 'key1', opts('b1')),
82
+ acquire(allClients, 'key2', opts('b2'))
83
+ ])
84
+ const now = Date.now()
85
+ expect(start2 - start1).to.be.gte(0)
86
+ expect(start2 - start1).to.be.lt(10)
87
+ expect(now - start1).to.be.gte(50)
88
+ expect(now - start2).to.be.gte(50)
89
+ })
90
+ })
@@ -0,0 +1,27 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { refreshRedlockMutex as refresh } from '../../../src/redlockMutex/refresh'
4
+ import { allClients, client1, client2, client3 } from '../../redisClient'
5
+
6
+ describe('redlockMutex refresh', () => {
7
+ it('should return false if resource is acquired by different instance on quorum', async () => {
8
+ await client1.set('key', '111')
9
+ await client2.set('key', '222')
10
+ await client3.set('key', '222')
11
+ const result = await refresh(allClients, 'key', '111', 10000)
12
+ expect(result).to.be.false
13
+ })
14
+ it('should return true if resource is acquired on quorum', async () => {
15
+ await client1.set('key', '111')
16
+ await client2.set('key', '111')
17
+ const result = await refresh(allClients, 'key', '111', 20000)
18
+ expect(result).to.be.true
19
+ expect(await client1.pttl('key')).to.be.gte(10000)
20
+ expect(await client2.pttl('key')).to.be.gte(10000)
21
+ })
22
+ it('should return false if resource is not acquired on quorum', async () => {
23
+ await client1.set('key', '111')
24
+ const result = await refresh(allClients, 'key', '111', 10000)
25
+ expect(result).to.be.false
26
+ })
27
+ })
@@ -0,0 +1,17 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { releaseRedlockMutex as release } from '../../../src/redlockMutex/release'
4
+ import { allClients, client1 } from '../../redisClient'
5
+
6
+ describe('redlockMutex release', () => {
7
+ it('should remove key after release', async () => {
8
+ await client1.set('key', '111')
9
+ await release(allClients, 'key', '111')
10
+ expect(await client1.get('key')).to.be.eql(null)
11
+ })
12
+ it('should do nothing if resource is not locked', async () => {
13
+ expect(await client1.get('key')).to.be.eql(null)
14
+ await release(allClients, 'key', '111')
15
+ expect(await client1.get('key')).to.be.eql(null)
16
+ })
17
+ })
@@ -0,0 +1,49 @@
1
+ import { expect } from 'chai'
2
+
3
+ import {
4
+ acquireSemaphore as acquire,
5
+ Options
6
+ } from '../../../../src/semaphore/acquire/index'
7
+ import { client1 as client } from '../../../redisClient'
8
+
9
+ const opts = (id: string, overrides?: Partial<Options>): Options => ({
10
+ identifier: id,
11
+ acquireTimeout: 50,
12
+ acquireAttemptsLimit: Number.POSITIVE_INFINITY,
13
+ lockTimeout: 100,
14
+ retryInterval: 10,
15
+ ...overrides
16
+ })
17
+
18
+ describe('semaphore acquire', () => {
19
+ it('should return true for success acquire', async () => {
20
+ const result = await acquire(client, 'key', 1, opts('111'))
21
+ expect(result).to.be.true
22
+ })
23
+ it('should return false when timeout', async () => {
24
+ const result1 = await acquire(client, 'key', 2, opts('111')) // expire after 100ms
25
+ const result2 = await acquire(client, 'key', 2, opts('112')) // expire after 100ms
26
+ const result3 = await acquire(client, 'key', 2, opts('113')) // timeout after 50ms
27
+
28
+ expect(result1).to.be.true
29
+ expect(result2).to.be.true
30
+ expect(result3).to.be.false
31
+ })
32
+ it('should return false after acquireAttemptsLimit', async () => {
33
+ const result1 = await acquire(client, 'key', 2, opts('111')) // expire after 100ms
34
+ const result2 = await acquire(client, 'key', 2, opts('112')) // expire after 100ms
35
+ const result3 = await acquire(
36
+ client,
37
+ 'key',
38
+ 2,
39
+ opts('113', {
40
+ acquireAttemptsLimit: 1,
41
+ acquireTimeout: Number.POSITIVE_INFINITY
42
+ })
43
+ ) // no timeout, acquire limit = 1
44
+
45
+ expect(result1).to.be.true
46
+ expect(result2).to.be.true
47
+ expect(result3).to.be.false
48
+ })
49
+ })
@@ -0,0 +1,65 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { acquireLua } from '../../../../src/semaphore/acquire/lua'
4
+ import { client1 as client } from '../../../redisClient'
5
+
6
+ interface Options {
7
+ identifier: string
8
+ lockTimeout: number
9
+ now: number
10
+ }
11
+
12
+ const opts = (id: string, nowOffset = 0): Options => ({
13
+ identifier: id,
14
+ lockTimeout: 500,
15
+ now: new Date().getTime() + nowOffset
16
+ })
17
+
18
+ async function acquire(options: Options) {
19
+ const { identifier, lockTimeout, now } = options
20
+ return await acquireLua(client, ['key', 1, identifier, lockTimeout, now])
21
+ }
22
+
23
+ describe('semaphore acquire internal', () => {
24
+ it('should return 1 for success acquire', async () => {
25
+ const result = await acquire(opts('111'))
26
+ expect(result).to.be.eql(1)
27
+ })
28
+ it('should return 0 for failure acquire', async () => {
29
+ const result1 = await acquire(opts('111'))
30
+ const result2 = await acquire(opts('112'))
31
+ expect(result1).to.be.eql(1)
32
+ expect(result2).to.be.eql(0)
33
+ })
34
+ describe('TIME SHIFT case', () => {
35
+ it('should handle time difference less than lockTimeout (nodeA has faster clocks)', async () => {
36
+ // lockTimeout = 500ms
37
+ // nodeA is for 450ms faster than nodeB
38
+ const resultA = await acquire(opts('111', 450))
39
+ const resultB = await acquire(opts('112', 0))
40
+ expect(resultA).to.be.eql(1)
41
+ expect(resultB).to.be.eql(0)
42
+ })
43
+ it('should handle time difference less than lockTimeout (nodeA has slower clocks)', async () => {
44
+ // lockTimeout = 500ms
45
+ // nodeB is for 450ms faster than nodeA
46
+ const resultA = await acquire(opts('111', 0))
47
+ const resultB = await acquire(opts('112', 450))
48
+ expect(resultA).to.be.eql(1)
49
+ expect(resultB).to.be.eql(0)
50
+ })
51
+ it('cant handle time difference greater than lockTimeout (nodeA has slower clocks)', async () => {
52
+ // lockTimeout = 500ms
53
+ // nodeB is for 550ms faster than nodeA
54
+ const resultA = await acquire(opts('111', 0))
55
+ const resultB = await acquire(opts('112', 550))
56
+ expect(resultA).to.be.eql(1)
57
+ expect(resultB).to.be.eql(1) // Semaphore stealed...
58
+
59
+ // This happens due removing "expired" nodeA lock (at nodeB "now" nodeA lock has been expired 50ms ago)
60
+ // Unfortunatelly "fair" semaphore described here
61
+ // https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-3-counting-semaphores/6-3-1-building-a-basic-counting-semaphore/
62
+ // also has the same problem
63
+ })
64
+ })
65
+ })
@@ -0,0 +1,44 @@
1
+ import { expect } from 'chai'
2
+
3
+ import {
4
+ Options,
5
+ refreshSemaphore as refresh
6
+ } from '../../../../src/semaphore/refresh/index'
7
+ import { client1 as client } from '../../../redisClient'
8
+
9
+ const opts = (id: string): Options => ({
10
+ identifier: id,
11
+ lockTimeout: 100
12
+ })
13
+
14
+ describe('semaphore refresh', () => {
15
+ it('should return false if resource is already acquired', async () => {
16
+ const now = '' + (Date.now() - 10)
17
+ await client.zadd('key', now, '222', now, '333', now, '444')
18
+ const result = await refresh(client, 'key', 3, opts('111'))
19
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['222', '333', '444'])
20
+ expect(result).to.be.false
21
+ })
22
+ it('should return false if resource is already acquired, but some expired', async () => {
23
+ const now = '' + (Date.now() - 10)
24
+ const oldNow = '' + (Date.now() - 10000)
25
+ await client.zadd('key', oldNow, '222', now, '333', now, '444')
26
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['222', '333', '444'])
27
+ const result = await refresh(client, 'key', 3, opts('111'))
28
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['333', '444'])
29
+ expect(result).to.be.false
30
+ })
31
+ it('should return false if resource is not acquired', async () => {
32
+ const result = await refresh(client, 'key', 3, opts('111'))
33
+ expect(await client.zrange('key', 0, -1)).to.be.eql([])
34
+ expect(result).to.be.false
35
+ })
36
+ it('should return true for success refresh', async () => {
37
+ const now = '' + (Date.now() - 10)
38
+ await client.zadd('key', now, '111', now, '222', now, '333')
39
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['111', '222', '333'])
40
+ const result = await refresh(client, 'key', 3, opts('111'))
41
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['222', '333', '111'])
42
+ expect(result).to.be.true
43
+ })
44
+ })
@@ -0,0 +1,18 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { releaseSemaphore as release } from '../../../src/semaphore/release'
4
+ import { client1 as client } from '../../redisClient'
5
+
6
+ describe('semaphore release', () => {
7
+ it('should remove key after success release', async () => {
8
+ await client.zadd('key', '' + Date.now(), '111')
9
+ expect(await client.zcard('key')).to.be.eql(1)
10
+ await release(client, 'key', '111')
11
+ expect(await client.zcard('key')).to.be.eql(0)
12
+ })
13
+ it('should do nothing if resource is not locked', async () => {
14
+ expect(await client.zcard('key')).to.be.eql(0)
15
+ await release(client, 'key', '111')
16
+ expect(await client.zcard('key')).to.be.eql(0)
17
+ })
18
+ })
@@ -0,0 +1,22 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { createEval } from '../../../src/utils/index'
4
+ import { client1 as client } from '../../redisClient'
5
+
6
+ describe('utils createEval', () => {
7
+ it('should return function', async () => {
8
+ expect(createEval('return 5', 0)).to.be.a('function')
9
+ })
10
+ it('should call evalsha or fallback to eval', async () => {
11
+ const now = Date.now()
12
+ const SCRIPT = `return ${now}`
13
+ const execScript = createEval(SCRIPT, 0)
14
+ const result = await execScript(client, [])
15
+ expect(result).to.be.eql(now)
16
+ expect(Date.now() - now).to.be.lt(50)
17
+ })
18
+ it('should handle eval errors', async () => {
19
+ const execScript = createEval('return asdfkasjdf', 0)
20
+ await expect(execScript(client, [])).to.be.rejected
21
+ })
22
+ })
@@ -0,0 +1,19 @@
1
+ import { expect } from 'chai'
2
+ import Redis from 'ioredis'
3
+ import { getConnectionName } from '../../../src/utils/index'
4
+ import { client1 } from '../../redisClient'
5
+
6
+ describe('utils getConnectionName', () => {
7
+ it('should return connection name', async () => {
8
+ expect(getConnectionName(client1)).to.be.eql('<client1>')
9
+ })
10
+ it('should return unknown if connection name not configured', () => {
11
+ const client = new Redis('redis://127.0.0.1:6000', {
12
+ lazyConnect: true,
13
+ enableOfflineQueue: false,
14
+ autoResendUnfulfilledCommands: false, // dont queue commands while server is offline (dont break test logic)
15
+ maxRetriesPerRequest: 0 // dont retry, fail faster (default is 20)
16
+ })
17
+ expect(getConnectionName(client)).to.be.eql('<unknown client>')
18
+ })
19
+ })
@@ -0,0 +1,31 @@
1
+ import { expect } from 'chai'
2
+
3
+ import { getQuorum } from '../../../src/utils/redlock'
4
+
5
+ describe('redlockMutex utils', () => {
6
+ describe('getQuorum', () => {
7
+ function makeTest(count: number, expectedResult: number) {
8
+ it(`should return valid majority for ${count} nodes`, () => {
9
+ expect(getQuorum(count)).to.be.eql(expectedResult)
10
+ expect(getQuorum(2)).to.be.eql(2)
11
+ expect(getQuorum(3)).to.be.eql(2)
12
+ expect(getQuorum(4)).to.be.eql(3)
13
+ expect(getQuorum(5)).to.be.eql(3)
14
+ expect(getQuorum(6)).to.be.eql(4)
15
+ expect(getQuorum(7)).to.be.eql(4)
16
+ expect(getQuorum(8)).to.be.eql(5)
17
+ expect(getQuorum(9)).to.be.eql(5)
18
+ })
19
+ }
20
+ // makeTest(0, 1)
21
+ makeTest(1, 1)
22
+ makeTest(2, 2)
23
+ makeTest(3, 2)
24
+ makeTest(4, 3)
25
+ makeTest(5, 3)
26
+ makeTest(6, 4)
27
+ makeTest(7, 4)
28
+ makeTest(8, 5)
29
+ makeTest(9, 5)
30
+ })
31
+ })