1import { assert, describe, it } from "@effect/vitest"
2import { Effect, Exit, Fiber, Scope, TxReentrantLock } from "effect"
3
4describe("TxReentrantLock", () => {
5 describe("constructors", () => {
6 it.effect("make creates an unlocked lock", () =>
7 Effect.tx(Effect.gen(function*() {
8 const lock = yield* TxReentrantLock.make()
9 assert.strictEqual(yield* TxReentrantLock.locked(lock), false)
10 assert.strictEqual(yield* TxReentrantLock.readLocked(lock), false)
11 assert.strictEqual(yield* TxReentrantLock.writeLocked(lock), false)
12 assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 0)
13 assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 0)
14 })))
15 })
16
17 describe("read lock", () => {
18 it.effect("acquireRead and releaseRead work correctly", () =>
19 Effect.tx(Effect.gen(function*() {
20 const lock = yield* TxReentrantLock.make()
21 const count = yield* TxReentrantLock.acquireRead(lock)
22 assert.strictEqual(count, 1)
23 assert.strictEqual(yield* TxReentrantLock.readLocked(lock), true)
24 assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 1)
25
26 const remaining = yield* TxReentrantLock.releaseRead(lock)
27 assert.strictEqual(remaining, 0)
28 assert.strictEqual(yield* TxReentrantLock.readLocked(lock), false)
29 })))
30
31 it.effect("multiple read acquires from same fiber are reentrant", () =>
32 Effect.tx(Effect.gen(function*() {
33 const lock = yield* TxReentrantLock.make()
34 const c1 = yield* TxReentrantLock.acquireRead(lock)
35 assert.strictEqual(c1, 1)
36 const c2 = yield* TxReentrantLock.acquireRead(lock)
37 assert.strictEqual(c2, 2)
38 assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 2)
39
40 yield* TxReentrantLock.releaseRead(lock)
41 assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 1)
42 yield* TxReentrantLock.releaseRead(lock)
43 assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 0)
44 })))
45 })
46
47 describe("write lock", () => {
48 it.effect("acquireWrite and releaseWrite work correctly", () =>
49 Effect.tx(Effect.gen(function*() {
50 const lock = yield* TxReentrantLock.make()
51 const count = yield* TxReentrantLock.acquireWrite(lock)
52 assert.strictEqual(count, 1)
53 assert.strictEqual(yield* TxReentrantLock.writeLocked(lock), true)
54 assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 1)
55
56 const remaining = yield* TxReentrantLock.releaseWrite(lock)
57 assert.strictEqual(remaining, 0)
58 assert.strictEqual(yield* TxReentrantLock.writeLocked(lock), false)
59 })))
60
61 it.effect("multiple write acquires from same fiber are reentrant", () =>
62 Effect.tx(Effect.gen(function*() {
63 const lock = yield* TxReentrantLock.make()
64 const c1 = yield* TxReentrantLock.acquireWrite(lock)
65 assert.strictEqual(c1, 1)
66 const c2 = yield* TxReentrantLock.acquireWrite(lock)
67 assert.strictEqual(c2, 2)
68 assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 2)
69
70 yield* TxReentrantLock.releaseWrite(lock)
71 assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 1)
72 yield* TxReentrantLock.releaseWrite(lock)
73 assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 0)
74 })))
75
76 it.effect("write lock holder can acquire read lock (reentrancy)", () =>
77 Effect.tx(Effect.gen(function*() {
78 const lock = yield* TxReentrantLock.make()
79 yield* TxReentrantLock.acquireWrite(lock)
80 const readCount = yield* TxReentrantLock.acquireRead(lock)
81 assert.strictEqual(readCount, 1)
82 assert.strictEqual(yield* TxReentrantLock.locked(lock), true)
83
84 yield* TxReentrantLock.releaseRead(lock)
85 yield* TxReentrantLock.releaseWrite(lock)
86 assert.strictEqual(yield* TxReentrantLock.locked(lock), false)
87 })))
88 })
89
90 describe("scoped operations", () => {
91 it.effect("readLock scoped acquires and releases", () =>
92 Effect.gen(function*() {
93 const lock = yield* Effect.tx(TxReentrantLock.make())
94
95 yield* Effect.scoped(
96 Effect.gen(function*() {
97 yield* TxReentrantLock.readLock(lock)
98 assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), true)
99 })
100 )
101
102 assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), false)
103 }))
104
105 it.effect("writeLock scoped acquires and releases", () =>
106 Effect.gen(function*() {
107 const lock = yield* Effect.tx(TxReentrantLock.make())
108
109 yield* Effect.scoped(
110 Effect.gen(function*() {
111 yield* TxReentrantLock.writeLock(lock)
112 assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), true)
113 })
114 )
115
116 assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), false)
117 }))
118
119 it.effect("writeLock can be acquired reentrantly in the same scope", () =>
120 Effect.gen(function*() {
121 const lock = yield* Effect.tx(TxReentrantLock.make())
122
123 yield* Effect.scoped(
124 Effect.gen(function*() {
125 yield* TxReentrantLock.writeLock(lock)
126 assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), true)
127 })
128 )
129
130 assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), false)
131 }))
132 })
133
134 describe("withLock operations", () => {
135 it.effect("withReadLock runs effect and releases", () =>
136 Effect.gen(function*() {
137 const lock = yield* Effect.tx(TxReentrantLock.make())
138
139 const result = yield* TxReentrantLock.withReadLock(
140 lock,
141 Effect.gen(function*() {
142 assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), true)
143 return "read-result"
144 })
145 )
146
147 assert.strictEqual(result, "read-result")
148 assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), false)
149 }))
150
151 it.effect("withWriteLock runs effect and releases", () =>
152 Effect.gen(function*() {
153 const lock = yield* Effect.tx(TxReentrantLock.make())
154
155 const result = yield* TxReentrantLock.withWriteLock(
156 lock,
157 Effect.gen(function*() {
158 assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), true)
159 return "write-result"
160 })
161 )
162
163 assert.strictEqual(result, "write-result")
164 assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), false)
165 }))
166
167 it.effect("withLock is alias for withWriteLock", () =>
168 Effect.gen(function*() {
169 const lock = yield* Effect.tx(TxReentrantLock.make())
170
171 const result = yield* TxReentrantLock.withLock(
172 lock,
173 Effect.gen(function*() {
174 assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), true)
175 return "exclusive"
176 })
177 )
178
179 assert.strictEqual(result, "exclusive")
180 assert.strictEqual(yield* Effect.tx(TxReentrantLock.locked(lock)), false)
181 }))
182
183 it.effect("withReadLock releases on failure", () =>
184 Effect.gen(function*() {
185 const lock = yield* Effect.tx(TxReentrantLock.make())
186
187 const result = yield* Effect.exit(
188 TxReentrantLock.withReadLock(
189 lock,
190 Effect.fail("boom")
191 )
192 )
193
194 assert.isTrue(Exit.isFailure(result))
195 assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), false)
196 }))
197
198 it.effect("withWriteLock releases on failure", () =>
199 Effect.gen(function*() {
200 const lock = yield* Effect.tx(TxReentrantLock.make())
201
202 const result = yield* Effect.exit(
203 TxReentrantLock.withWriteLock(
204 lock,
205 Effect.fail("boom")
206 )
207 )
208
209 assert.isTrue(Exit.isFailure(result))
210 assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), false)
211 }))
212
213 it.effect("withReadLock data-last (pipe) style", () =>
214 Effect.gen(function*() {
215 const lock = yield* Effect.tx(TxReentrantLock.make())
216
217 const result = yield* lock.pipe(
218 TxReentrantLock.withReadLock(Effect.succeed("piped"))
219 )
220
221 assert.strictEqual(result, "piped")
222 assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), false)
223 }))
224
225 it.effect("withWriteLock data-last (pipe) style", () =>
226 Effect.gen(function*() {
227 const lock = yield* Effect.tx(TxReentrantLock.make())
228
229 const result = yield* lock.pipe(
230 TxReentrantLock.withWriteLock(Effect.succeed("piped-write"))
231 )
232
233 assert.strictEqual(result, "piped-write")
234 assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), false)
235 }))
236 })
237
238 describe("concurrency", () => {
239 it.effect("multiple readers can proceed concurrently", () =>
240 Effect.gen(function*() {
241 const lock = yield* Effect.tx(TxReentrantLock.make())
242
243 const fiber1 = yield* Effect.forkChild(
244 TxReentrantLock.withReadLock(lock, Effect.succeed(1))
245 )
246 const fiber2 = yield* Effect.forkChild(
247 TxReentrantLock.withReadLock(lock, Effect.succeed(2))
248 )
249 const fiber3 = yield* Effect.forkChild(
250 TxReentrantLock.withReadLock(lock, Effect.succeed(3))
251 )
252
253 const [r1, r2, r3] = yield* Effect.all([
254 Fiber.join(fiber1),
255 Fiber.join(fiber2),
256 Fiber.join(fiber3)
257 ])
258
259 assert.strictEqual(r1, 1)
260 assert.strictEqual(r2, 2)
261 assert.strictEqual(r3, 3)
262 assert.strictEqual(yield* Effect.tx(TxReentrantLock.locked(lock)), false)
263 }))
264
265 it.effect("writer blocks other writers", () =>
266 Effect.gen(function*() {
267 const lock = yield* Effect.tx(TxReentrantLock.make())
268 const order: Array<string> = []
269
270 yield* Effect.tx(TxReentrantLock.acquireWrite(lock))
271
272 const fiber = yield* Effect.forkChild(
273 TxReentrantLock.withWriteLock(
274 lock,
275 Effect.sync(() => {
276 order.push("child-wrote")
277 })
278 )
279 )
280
281 // Child should be blocked because we hold the write lock
282 order.push("parent-releasing")
283 yield* Effect.tx(TxReentrantLock.releaseWrite(lock))
284
285 yield* Fiber.join(fiber)
286 assert.deepStrictEqual(order, ["parent-releasing", "child-wrote"])
287 assert.strictEqual(yield* Effect.tx(TxReentrantLock.locked(lock)), false)
288 }))
289
290 it.effect("writer blocks readers from other fibers", () =>
291 Effect.gen(function*() {
292 const lock = yield* Effect.tx(TxReentrantLock.make())
293 const order: Array<string> = []
294
295 yield* Effect.tx(TxReentrantLock.acquireWrite(lock))
296
297 const fiber = yield* Effect.forkChild(
298 TxReentrantLock.withReadLock(
299 lock,
300 Effect.sync(() => {
301 order.push("child-read")
302 })
303 )
304 )
305
306 order.push("parent-releasing")
307 yield* Effect.tx(TxReentrantLock.releaseWrite(lock))
308
309 yield* Fiber.join(fiber)
310 assert.deepStrictEqual(order, ["parent-releasing", "child-read"])
311 assert.strictEqual(yield* Effect.tx(TxReentrantLock.locked(lock)), false)
312 }))
313
314 it.effect("reader blocks writer from another fiber", () =>
315 Effect.gen(function*() {
316 const lock = yield* Effect.tx(TxReentrantLock.make())
317 const order: Array<string> = []
318
319 yield* Effect.tx(TxReentrantLock.acquireRead(lock))
320
321 const fiber = yield* Effect.forkChild(
322 TxReentrantLock.withWriteLock(
323 lock,
324 Effect.sync(() => {
325 order.push("child-wrote")
326 })
327 )
328 )
329
330 order.push("parent-releasing")
331 yield* Effect.tx(TxReentrantLock.releaseRead(lock))
332
333 yield* Fiber.join(fiber)
334 assert.deepStrictEqual(order, ["parent-releasing", "child-wrote"])
335 assert.strictEqual(yield* Effect.tx(TxReentrantLock.locked(lock)), false)
336 }))
337 })
338
339 describe("getters", () => {
340 it.effect("readLocks counts total read locks", () =>
341 Effect.tx(Effect.gen(function*() {
342 const lock = yield* TxReentrantLock.make()
343 yield* TxReentrantLock.acquireRead(lock)
344 yield* TxReentrantLock.acquireRead(lock)
345 assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 2)
346
347 yield* TxReentrantLock.releaseRead(lock)
348 assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 1)
349
350 yield* TxReentrantLock.releaseRead(lock)
351 assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 0)
352 })))
353
354 it.effect("writeLocks counts write lock depth", () =>
355 Effect.tx(Effect.gen(function*() {
356 const lock = yield* TxReentrantLock.make()
357 assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 0)
358
359 yield* TxReentrantLock.acquireWrite(lock)
360 assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 1)
361
362 yield* TxReentrantLock.acquireWrite(lock)
363 assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 2)
364
365 yield* TxReentrantLock.releaseWrite(lock)
366 yield* TxReentrantLock.releaseWrite(lock)
367 assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 0)
368 })))
369
370 it.effect("locked returns true when any lock is held", () =>
371 Effect.tx(Effect.gen(function*() {
372 const lock = yield* TxReentrantLock.make()
373 assert.strictEqual(yield* TxReentrantLock.locked(lock), false)
374
375 yield* TxReentrantLock.acquireRead(lock)
376 assert.strictEqual(yield* TxReentrantLock.locked(lock), true)
377 yield* TxReentrantLock.releaseRead(lock)
378 assert.strictEqual(yield* TxReentrantLock.locked(lock), false)
379
380 yield* TxReentrantLock.acquireWrite(lock)
381 assert.strictEqual(yield* TxReentrantLock.locked(lock), true)
382 yield* TxReentrantLock.releaseWrite(lock)
383 assert.strictEqual(yield* TxReentrantLock.locked(lock), false)
384 })))
385 })
386
387 describe("type guards", () => {
388 it.effect("isTxReentrantLock identifies TxReentrantLock instances", () =>
389 Effect.tx(Effect.gen(function*() {
390 const lock = yield* TxReentrantLock.make()
391 assert.isTrue(TxReentrantLock.isTxReentrantLock(lock))
392 assert.isFalse(TxReentrantLock.isTxReentrantLock({}))
393 assert.isFalse(TxReentrantLock.isTxReentrantLock(null))
394 assert.isFalse(TxReentrantLock.isTxReentrantLock(undefined))
395 })))
396 })
397
398 describe("release without acquire", () => {
399 it.effect("releaseRead without acquire returns 0", () =>
400 Effect.tx(Effect.gen(function*() {
401 const lock = yield* TxReentrantLock.make()
402 const result = yield* TxReentrantLock.releaseRead(lock)
403 assert.strictEqual(result, 0)
404 })))
405
406 it.effect("releaseWrite without acquire returns 0", () =>
407 Effect.tx(Effect.gen(function*() {
408 const lock = yield* TxReentrantLock.make()
409 const result = yield* TxReentrantLock.releaseWrite(lock)
410 assert.strictEqual(result, 0)
411 })))
412 })
413
414 it.effect("releases a scoped read lock when another fiber closes the scope", () =>
415 Effect.gen(function*() {
416 const lock = yield* TxReentrantLock.make()
417 const scope = yield* Scope.make()
418 const fiber = yield* TxReentrantLock.readLock(lock).pipe(
419 Effect.provideService(Scope.Scope, scope),
420 Effect.forkChild
421 )
422 yield* Fiber.join(fiber)
423 yield* Scope.close(scope, Exit.void)
424 assert.isFalse(yield* TxReentrantLock.readLocked(lock))
425 }))
426
427 it.effect("releases a scoped write lock when another fiber closes the scope", () =>
428 Effect.gen(function*() {
429 const lock = yield* TxReentrantLock.make()
430 const scope = yield* Scope.make()
431 const fiber = yield* TxReentrantLock.writeLock(lock).pipe(
432 Effect.provideService(Scope.Scope, scope),
433 Effect.forkChild
434 )
435 yield* Fiber.join(fiber)
436 yield* Scope.close(scope, Exit.void)
437 assert.isFalse(yield* TxReentrantLock.writeLocked(lock))
438 }))
439})