414 lines
15 KiB
TypeScript
414 lines
15 KiB
TypeScript
import { assert, describe, it } from "@effect/vitest"
|
|
import { Effect, Exit, Fiber, TxReentrantLock } from "effect"
|
|
|
|
describe("TxReentrantLock", () => {
|
|
describe("constructors", () => {
|
|
it.effect("make creates an unlocked lock", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
assert.strictEqual(yield* TxReentrantLock.locked(lock), false)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocked(lock), false)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocked(lock), false)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 0)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 0)
|
|
})))
|
|
})
|
|
|
|
describe("read lock", () => {
|
|
it.effect("acquireRead and releaseRead work correctly", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
const count = yield* TxReentrantLock.acquireRead(lock)
|
|
assert.strictEqual(count, 1)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocked(lock), true)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 1)
|
|
|
|
const remaining = yield* TxReentrantLock.releaseRead(lock)
|
|
assert.strictEqual(remaining, 0)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocked(lock), false)
|
|
})))
|
|
|
|
it.effect("multiple read acquires from same fiber are reentrant", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
const c1 = yield* TxReentrantLock.acquireRead(lock)
|
|
assert.strictEqual(c1, 1)
|
|
const c2 = yield* TxReentrantLock.acquireRead(lock)
|
|
assert.strictEqual(c2, 2)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 2)
|
|
|
|
yield* TxReentrantLock.releaseRead(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 1)
|
|
yield* TxReentrantLock.releaseRead(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 0)
|
|
})))
|
|
})
|
|
|
|
describe("write lock", () => {
|
|
it.effect("acquireWrite and releaseWrite work correctly", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
const count = yield* TxReentrantLock.acquireWrite(lock)
|
|
assert.strictEqual(count, 1)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocked(lock), true)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 1)
|
|
|
|
const remaining = yield* TxReentrantLock.releaseWrite(lock)
|
|
assert.strictEqual(remaining, 0)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocked(lock), false)
|
|
})))
|
|
|
|
it.effect("multiple write acquires from same fiber are reentrant", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
const c1 = yield* TxReentrantLock.acquireWrite(lock)
|
|
assert.strictEqual(c1, 1)
|
|
const c2 = yield* TxReentrantLock.acquireWrite(lock)
|
|
assert.strictEqual(c2, 2)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 2)
|
|
|
|
yield* TxReentrantLock.releaseWrite(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 1)
|
|
yield* TxReentrantLock.releaseWrite(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 0)
|
|
})))
|
|
|
|
it.effect("write lock holder can acquire read lock (reentrancy)", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
yield* TxReentrantLock.acquireWrite(lock)
|
|
const readCount = yield* TxReentrantLock.acquireRead(lock)
|
|
assert.strictEqual(readCount, 1)
|
|
assert.strictEqual(yield* TxReentrantLock.locked(lock), true)
|
|
|
|
yield* TxReentrantLock.releaseRead(lock)
|
|
yield* TxReentrantLock.releaseWrite(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.locked(lock), false)
|
|
})))
|
|
})
|
|
|
|
describe("scoped operations", () => {
|
|
it.effect("readLock scoped acquires and releases", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
yield* Effect.scoped(
|
|
Effect.gen(function*() {
|
|
yield* TxReentrantLock.readLock(lock)
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), true)
|
|
})
|
|
)
|
|
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), false)
|
|
}))
|
|
|
|
it.effect("writeLock scoped acquires and releases", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
yield* Effect.scoped(
|
|
Effect.gen(function*() {
|
|
yield* TxReentrantLock.writeLock(lock)
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), true)
|
|
})
|
|
)
|
|
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), false)
|
|
}))
|
|
|
|
it.effect("writeLock can be acquired reentrantly in the same scope", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
yield* Effect.scoped(
|
|
Effect.gen(function*() {
|
|
yield* TxReentrantLock.writeLock(lock)
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), true)
|
|
})
|
|
)
|
|
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), false)
|
|
}))
|
|
})
|
|
|
|
describe("withLock operations", () => {
|
|
it.effect("withReadLock runs effect and releases", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
const result = yield* TxReentrantLock.withReadLock(
|
|
lock,
|
|
Effect.gen(function*() {
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), true)
|
|
return "read-result"
|
|
})
|
|
)
|
|
|
|
assert.strictEqual(result, "read-result")
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), false)
|
|
}))
|
|
|
|
it.effect("withWriteLock runs effect and releases", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
const result = yield* TxReentrantLock.withWriteLock(
|
|
lock,
|
|
Effect.gen(function*() {
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), true)
|
|
return "write-result"
|
|
})
|
|
)
|
|
|
|
assert.strictEqual(result, "write-result")
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), false)
|
|
}))
|
|
|
|
it.effect("withLock is alias for withWriteLock", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
const result = yield* TxReentrantLock.withLock(
|
|
lock,
|
|
Effect.gen(function*() {
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), true)
|
|
return "exclusive"
|
|
})
|
|
)
|
|
|
|
assert.strictEqual(result, "exclusive")
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.locked(lock)), false)
|
|
}))
|
|
|
|
it.effect("withReadLock releases on failure", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
const result = yield* Effect.exit(
|
|
TxReentrantLock.withReadLock(
|
|
lock,
|
|
Effect.fail("boom")
|
|
)
|
|
)
|
|
|
|
assert.isTrue(Exit.isFailure(result))
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), false)
|
|
}))
|
|
|
|
it.effect("withWriteLock releases on failure", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
const result = yield* Effect.exit(
|
|
TxReentrantLock.withWriteLock(
|
|
lock,
|
|
Effect.fail("boom")
|
|
)
|
|
)
|
|
|
|
assert.isTrue(Exit.isFailure(result))
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), false)
|
|
}))
|
|
|
|
it.effect("withReadLock data-last (pipe) style", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
const result = yield* lock.pipe(
|
|
TxReentrantLock.withReadLock(Effect.succeed("piped"))
|
|
)
|
|
|
|
assert.strictEqual(result, "piped")
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.readLocked(lock)), false)
|
|
}))
|
|
|
|
it.effect("withWriteLock data-last (pipe) style", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
const result = yield* lock.pipe(
|
|
TxReentrantLock.withWriteLock(Effect.succeed("piped-write"))
|
|
)
|
|
|
|
assert.strictEqual(result, "piped-write")
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.writeLocked(lock)), false)
|
|
}))
|
|
})
|
|
|
|
describe("concurrency", () => {
|
|
it.effect("multiple readers can proceed concurrently", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
|
|
const fiber1 = yield* Effect.forkChild(
|
|
TxReentrantLock.withReadLock(lock, Effect.succeed(1))
|
|
)
|
|
const fiber2 = yield* Effect.forkChild(
|
|
TxReentrantLock.withReadLock(lock, Effect.succeed(2))
|
|
)
|
|
const fiber3 = yield* Effect.forkChild(
|
|
TxReentrantLock.withReadLock(lock, Effect.succeed(3))
|
|
)
|
|
|
|
const [r1, r2, r3] = yield* Effect.all([
|
|
Fiber.join(fiber1),
|
|
Fiber.join(fiber2),
|
|
Fiber.join(fiber3)
|
|
])
|
|
|
|
assert.strictEqual(r1, 1)
|
|
assert.strictEqual(r2, 2)
|
|
assert.strictEqual(r3, 3)
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.locked(lock)), false)
|
|
}))
|
|
|
|
it.effect("writer blocks other writers", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
const order: Array<string> = []
|
|
|
|
yield* Effect.tx(TxReentrantLock.acquireWrite(lock))
|
|
|
|
const fiber = yield* Effect.forkChild(
|
|
TxReentrantLock.withWriteLock(
|
|
lock,
|
|
Effect.sync(() => {
|
|
order.push("child-wrote")
|
|
})
|
|
)
|
|
)
|
|
|
|
// Child should be blocked because we hold the write lock
|
|
order.push("parent-releasing")
|
|
yield* Effect.tx(TxReentrantLock.releaseWrite(lock))
|
|
|
|
yield* Fiber.join(fiber)
|
|
assert.deepStrictEqual(order, ["parent-releasing", "child-wrote"])
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.locked(lock)), false)
|
|
}))
|
|
|
|
it.effect("writer blocks readers from other fibers", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
const order: Array<string> = []
|
|
|
|
yield* Effect.tx(TxReentrantLock.acquireWrite(lock))
|
|
|
|
const fiber = yield* Effect.forkChild(
|
|
TxReentrantLock.withReadLock(
|
|
lock,
|
|
Effect.sync(() => {
|
|
order.push("child-read")
|
|
})
|
|
)
|
|
)
|
|
|
|
order.push("parent-releasing")
|
|
yield* Effect.tx(TxReentrantLock.releaseWrite(lock))
|
|
|
|
yield* Fiber.join(fiber)
|
|
assert.deepStrictEqual(order, ["parent-releasing", "child-read"])
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.locked(lock)), false)
|
|
}))
|
|
|
|
it.effect("reader blocks writer from another fiber", () =>
|
|
Effect.gen(function*() {
|
|
const lock = yield* Effect.tx(TxReentrantLock.make())
|
|
const order: Array<string> = []
|
|
|
|
yield* Effect.tx(TxReentrantLock.acquireRead(lock))
|
|
|
|
const fiber = yield* Effect.forkChild(
|
|
TxReentrantLock.withWriteLock(
|
|
lock,
|
|
Effect.sync(() => {
|
|
order.push("child-wrote")
|
|
})
|
|
)
|
|
)
|
|
|
|
order.push("parent-releasing")
|
|
yield* Effect.tx(TxReentrantLock.releaseRead(lock))
|
|
|
|
yield* Fiber.join(fiber)
|
|
assert.deepStrictEqual(order, ["parent-releasing", "child-wrote"])
|
|
assert.strictEqual(yield* Effect.tx(TxReentrantLock.locked(lock)), false)
|
|
}))
|
|
})
|
|
|
|
describe("getters", () => {
|
|
it.effect("readLocks counts total read locks", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
yield* TxReentrantLock.acquireRead(lock)
|
|
yield* TxReentrantLock.acquireRead(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 2)
|
|
|
|
yield* TxReentrantLock.releaseRead(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 1)
|
|
|
|
yield* TxReentrantLock.releaseRead(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.readLocks(lock), 0)
|
|
})))
|
|
|
|
it.effect("writeLocks counts write lock depth", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 0)
|
|
|
|
yield* TxReentrantLock.acquireWrite(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 1)
|
|
|
|
yield* TxReentrantLock.acquireWrite(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 2)
|
|
|
|
yield* TxReentrantLock.releaseWrite(lock)
|
|
yield* TxReentrantLock.releaseWrite(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.writeLocks(lock), 0)
|
|
})))
|
|
|
|
it.effect("locked returns true when any lock is held", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
assert.strictEqual(yield* TxReentrantLock.locked(lock), false)
|
|
|
|
yield* TxReentrantLock.acquireRead(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.locked(lock), true)
|
|
yield* TxReentrantLock.releaseRead(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.locked(lock), false)
|
|
|
|
yield* TxReentrantLock.acquireWrite(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.locked(lock), true)
|
|
yield* TxReentrantLock.releaseWrite(lock)
|
|
assert.strictEqual(yield* TxReentrantLock.locked(lock), false)
|
|
})))
|
|
})
|
|
|
|
describe("type guards", () => {
|
|
it.effect("isTxReentrantLock identifies TxReentrantLock instances", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
assert.isTrue(TxReentrantLock.isTxReentrantLock(lock))
|
|
assert.isFalse(TxReentrantLock.isTxReentrantLock({}))
|
|
assert.isFalse(TxReentrantLock.isTxReentrantLock(null))
|
|
assert.isFalse(TxReentrantLock.isTxReentrantLock(undefined))
|
|
})))
|
|
})
|
|
|
|
describe("release without acquire", () => {
|
|
it.effect("releaseRead without acquire returns 0", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
const result = yield* TxReentrantLock.releaseRead(lock)
|
|
assert.strictEqual(result, 0)
|
|
})))
|
|
|
|
it.effect("releaseWrite without acquire returns 0", () =>
|
|
Effect.tx(Effect.gen(function*() {
|
|
const lock = yield* TxReentrantLock.make()
|
|
const result = yield* TxReentrantLock.releaseWrite(lock)
|
|
assert.strictEqual(result, 0)
|
|
})))
|
|
})
|
|
})
|