Skip to content

Commit 94ce278

Browse files
committed
refactor!: do not throw anymore when can't acquire lock
1 parent 14a529a commit 94ce278

4 files changed

Lines changed: 141 additions & 88 deletions

File tree

packages/verrou/src/errors.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
import { createError } from '@poppinss/utils'
22

3-
/**
4-
* Thrown when the lock is not acquired in the allotted time
5-
*/
6-
export const E_LOCK_TIMEOUT = createError(
7-
`Lock was not acquired in the allotted time`,
8-
'E_LOCK_TIMEOUT',
9-
)
10-
113
/**
124
* Thrown when user tries to update/release/extend a lock that is not acquired by them
135
*/
146
export const E_LOCK_NOT_OWNED = createError(
15-
'Looks like you are trying to update a lock that is not acquired by you',
7+
'Looks like you are trying to update or release a lock that is not acquired by you',
168
'E_LOCK_NOT_OWNED',
179
)
1810

@@ -23,11 +15,3 @@ export const E_LOCK_STORAGE_ERROR = createError<[{ message: string }]>(
2315
'Lock storage error: %s',
2416
'E_LOCK_STORAGE_ERROR',
2517
)
26-
27-
/**
28-
* Thrown when user tries to acquire a lock that is already acquired by someone else
29-
*/
30-
export const E_LOCK_ALREADY_ACQUIRED = createError(
31-
'Lock is already acquired',
32-
'E_LOCK_ALREDY_ACQUIRED',
33-
)

packages/verrou/src/lock.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { setTimeout } from 'node:timers/promises'
22
import { InvalidArgumentsException } from '@poppinss/utils'
33

44
import { resolveDuration } from './helpers.js'
5-
import { E_LOCK_ALREADY_ACQUIRED, E_LOCK_TIMEOUT } from './errors.js'
65
import type {
76
Duration,
87
LockAcquireOptions,
@@ -88,13 +87,13 @@ export class Lock {
8887
/**
8988
* Check if we reached the maximum number of attempts
9089
*/
91-
if (attemptsDone === attemptsMax) throw new E_LOCK_TIMEOUT()
90+
if (attemptsDone === attemptsMax) return false
9291

9392
/**
9493
* Or check if we reached the timeout
9594
*/
9695
const elapsed = Date.now() - start
97-
if (timeout && elapsed > timeout) throw new E_LOCK_TIMEOUT()
96+
if (timeout && elapsed > timeout) return false
9897

9998
/**
10099
* Otherwise wait for the delay and try again
@@ -103,28 +102,33 @@ export class Lock {
103102
}
104103

105104
this.#config.logger.debug({ key: this.#key }, 'Lock acquired')
105+
return true
106106
}
107107

108108
/**
109109
* Try to acquire the lock immediately or throw an error
110110
*/
111111
async acquireImmediately() {
112112
const result = await this.#lockStore.save(this.#key, this.#owner, this.#ttl)
113-
if (!result) throw new E_LOCK_ALREADY_ACQUIRED()
113+
if (!result) return false
114114
this.#expirationTime = this.#ttl ? Date.now() + this.#ttl : null
115115

116116
this.#config.logger.debug({ key: this.#key }, 'Lock acquired with acquireImmediately()')
117+
return true
117118
}
118119

119120
/**
120121
* Acquire the lock, run the callback and release the lock automatically
121122
* after the callback is done.
122123
* Also returns the callback return value
123124
*/
124-
async run<T>(callback: () => Promise<T>): Promise<T> {
125+
async run<T>(callback: () => Promise<T>): Promise<[true, T] | [false, null]> {
126+
const handle = await this.acquire()
127+
if (!handle) return [false, null]
128+
125129
try {
126-
await this.acquire()
127-
return await callback()
130+
const result = await callback()
131+
return [true, result]
128132
} finally {
129133
await this.release()
130134
}
@@ -134,12 +138,15 @@ export class Lock {
134138
* Same as `run` but try to acquire the lock immediately
135139
* Or throw an error if the lock is already acquired
136140
*/
137-
async runImmediately<T>(callback: () => Promise<T>): Promise<T> {
141+
async runImmediately<T>(callback: () => Promise<T>): Promise<[true, T] | [false, null]> {
142+
const handle = await this.acquireImmediately()
143+
if (!handle) return [false, null]
144+
138145
try {
139-
await this.acquireImmediately()
140-
return await callback()
146+
const result = await callback()
147+
return [true, result]
141148
} finally {
142-
await this.release()
149+
if (handle) await this.release()
143150
}
144151
}
145152

packages/verrou/src/test_suite.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import type { Group } from '@japa/runner/core'
44
import type { test as JapaTest } from '@japa/runner'
55
import { setTimeout as sleep } from 'node:timers/promises'
66

7+
import { E_LOCK_NOT_OWNED } from '../index.js'
78
import { LockFactory } from './lock_factory.js'
89
import type { LockStore } from './types/main.js'
9-
import { E_LOCK_NOT_OWNED, E_LOCK_TIMEOUT } from '../index.js'
1010

1111
export function registerStoreTestSuite(options: {
1212
test: typeof JapaTest
@@ -73,32 +73,33 @@ export function registerStoreTestSuite(options: {
7373
// @ts-expect-error poppinss/utils typing bug
7474
}).throws(E_LOCK_NOT_OWNED.message, E_LOCK_NOT_OWNED)
7575

76-
test('throws timeout error when lock is not acquired in time', async () => {
76+
test('acquire returns false when lock is not acquired in time', async ({ assert }) => {
7777
const provider = new LockFactory(options.createStore(), {
7878
retry: { timeout: 500 },
7979
})
8080
const lock = provider.createLock('foo')
8181

8282
await lock.acquire()
8383

84-
await lock.acquire()
85-
// @ts-expect-error poppinss/utils typing bug
86-
}).throws(E_LOCK_TIMEOUT.message, E_LOCK_TIMEOUT)
84+
const handle = await lock.acquire()
85+
assert.isFalse(handle)
86+
})
8787

8888
test('run passes result', async ({ assert }) => {
8989
const provider = new LockFactory(options.createStore())
9090
const lock = provider.createLock('foo')
9191

92-
const result = await lock.run(async () => 'hello world')
92+
const [executed, result] = await lock.run(async () => 'hello world')
9393

94-
assert.equal(result, 'hello world')
94+
assert.deepEqual(executed, true)
95+
assert.deepEqual(result, 'hello world')
9596
})
9697

9798
test('run passes result from a promise', async ({ assert }) => {
9899
const provider = new LockFactory(options.createStore())
99100
const lock = provider.createLock('foo')
100101

101-
const result = await lock.run(async () => Promise.resolve('hello world'))
102+
const [, result] = await lock.run(async () => Promise.resolve('hello world'))
102103

103104
assert.equal(result, 'hello world')
104105
})
@@ -139,13 +140,13 @@ export function registerStoreTestSuite(options: {
139140

140141
assert.isFalse(flag)
141142

142-
const result = await lock.run(async () => {
143+
const [, result] = await lock.run(async () => {
143144
assert.isTrue(flag)
144145
return '42'
145146
})
146147

147148
assert.isTrue(flag)
148-
assert.equal(result, '42')
149+
assert.deepEqual(result, '42')
149150
})
150151

151152
test('exceptions during run do not leave mutex in locked state', async ({ assert }) => {

0 commit comments

Comments
 (0)