test(txn): cover LockManagerTest, waitUntil and TransactionManagerTest

This commit is contained in:
dualfroz committed 2026-09-14 02:25:25 +02:00
1 parent 7a5b975887
commit dbdffe9b7b
2 files changed
+205

No files matched your search

@@ -0,0 +1,117 @@
package kestrel.store.txn
import java.util.concurrent.TimeUnit
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class LockManagerTest {
private fun waitUntil(timeoutMs: Long = 2000, predicate: () -> Boolean) {
val deadline = System.currentTimeMillis() + timeoutMs
while (!predicate()) {
if (System.currentTimeMillis() > deadline) throw AssertionError("condition never became true")
Thread.sleep(5)
}
}
@Test
fun sharedLocksAreCompatibleAcrossTransactions() {
val locks = LockManager()
locks.acquireShared(1L, 100L)
locks.acquireShared(2L, 100L)
assertEquals(mapOf(1L to LockManager.LockMode.SHARED, 2L to LockManager.LockMode.SHARED), locks.holders(100L))
}
@Test
fun reacquiringTheSameOrWeakerModeIsANoop() {
val locks = LockManager()
locks.acquireExclusive(1L, 100L)
locks.acquireShared(1L, 100L)
assertEquals(mapOf(1L to LockManager.LockMode.EXCLUSIVE), locks.holders(100L))
}
@Test
fun upgradeFromSharedToExclusiveSucceedsWhenSoleHolder() {
val locks = LockManager()
locks.acquireShared(1L, 100L)
locks.acquireExclusive(1L, 100L)
assertEquals(mapOf(1L to LockManager.LockMode.EXCLUSIVE), locks.holders(100L))
}
@Test
fun exclusiveLockBlocksAnotherTransactionUntilReleased() {
val locks = LockManager()
locks.acquireExclusive(1L, 100L)
val acquired = java.util.concurrent.atomic.AtomicBoolean(false)
val thread = Thread {
locks.acquireExclusive(2L, 100L)
acquired.set(true)
}
thread.start()
waitUntil { locks.waiters(100L).contains(2L) }
assertTrue(!acquired.get(), "txn 2 must not acquire while txn 1 holds the exclusive lock")
locks.release(1L, 100L)
thread.join(2000)
assertTrue(acquired.get())
assertEquals(mapOf(2L to LockManager.LockMode.EXCLUSIVE), locks.holders(100L))
}
@Test
fun releaseAllDropsEveryLockHeldByATransaction() {
val locks = LockManager()
locks.acquireExclusive(1L, 100L)
locks.acquireShared(1L, 200L)
locks.releaseAll(1L)
assertEquals(emptyMap(), locks.holders(100L))
assertEquals(emptyMap(), locks.holders(200L))
}
@Test
fun circularWaitIsDetectedAndRejectedWithoutBlocking() {
val locks = LockManager()
locks.acquireExclusive(1L, 100L) // txn 1 holds resource A
locks.acquireExclusive(2L, 200L) // txn 2 holds resource B
val blocked = java.util.concurrent.atomic.AtomicBoolean(false)
val thread = Thread {
locks.acquireExclusive(1L, 200L) // txn 1 wants B, held by txn 2: blocks
blocked.set(true)
}
thread.start()
waitUntil { locks.waiters(200L).contains(1L) }
// txn 2 now wants A, held by txn 1, which is already waiting on txn 2: a cycle.
assertFailsWith<LockManager.DeadlockException> { locks.acquireExclusive(2L, 100L) }
// Unblock the background waiter so the test does not leak a thread.
locks.release(2L, 200L)
thread.join(2000)
assertTrue(blocked.get())
}
@Test
fun unrelatedWaitersDoNotFalselyTriggerDeadlock() {
val locks = LockManager()
locks.acquireExclusive(1L, 100L)
// txn 2 requests an unrelated resource txn 1 does not hold: no cycle, should not throw.
locks.acquireExclusive(2L, 200L)
}
@Test
fun exclusiveWaiterEventuallyRunsToCompletionAfterHolderReleases() {
val locks = LockManager()
locks.acquireExclusive(1L, 100L)
val latch = java.util.concurrent.CountDownLatch(1)
val thread = Thread {
locks.acquireExclusive(2L, 100L)
latch.countDown()
}
thread.start()
waitUntil { locks.waiters(100L).contains(2L) }
locks.releaseAll(1L)
assertTrue(latch.await(2, TimeUnit.SECONDS))
}
}
@@ -0,0 +1,88 @@
package kestrel.store.txn
import kestrel.store.page.DiskManager
import kestrel.store.page.Page
import java.io.File
import java.nio.file.Files
import kestrel.store.wal.WriteAheadLog
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class TransactionManagerTest {
private fun tempFile(prefix: String): File = Files.createTempFile(prefix, ".tmp").toFile().also { it.deleteOnExit() }
private fun pageOf(fill: Int): ByteArray = ByteArray(Page.PAGE_SIZE) { fill.toByte() }
private class Fixture {
val diskManager = DiskManager(Files.createTempFile("kestrel-txnmgr-data", ".db").toFile().also { it.deleteOnExit() })
val wal = WriteAheadLog(Files.createTempFile("kestrel-txnmgr-wal", ".log").toFile().also { it.deleteOnExit() })
val manager = TransactionManager(wal, diskManager)
}
@Test
fun beginStartsATransactionInActiveStatus() {
val fixture = Fixture()
val txn = fixture.manager.begin()
assertEquals(TransactionManager.Status.ACTIVE, fixture.manager.statusOf(txn))
}
@Test
fun commitAppliesLoggedUpdateAndReleasesLocks() {
val fixture = Fixture()
val pageId = fixture.diskManager.allocatePage()
val before = pageOf(0)
fixture.diskManager.writePage(Page(pageId).also { System.arraycopy(before, 0, it.data, 0, before.size) })
val txn = fixture.manager.begin()
fixture.manager.acquireExclusive(txn, pageId)
val after = pageOf(3)
fixture.manager.logUpdate(txn, pageId, before, after)
fixture.diskManager.writePage(Page(pageId).also { System.arraycopy(after, 0, it.data, 0, after.size) })
fixture.manager.commit(txn)
assertEquals(TransactionManager.Status.COMMITTED, fixture.manager.statusOf(txn))
assertContentEquals(after, fixture.diskManager.readPage(pageId).data)
val otherTxn = fixture.manager.begin()
fixture.manager.acquireExclusive(otherTxn, pageId) // must not block: txn's locks were released on commit
}
@Test
fun abortRollsBackLoggedUpdatesAndReleasesLocks() {
val fixture = Fixture()
val pageId = fixture.diskManager.allocatePage()
val before = pageOf(1)
fixture.diskManager.writePage(Page(pageId).also { System.arraycopy(before, 0, it.data, 0, before.size) })
val txn = fixture.manager.begin()
fixture.manager.acquireExclusive(txn, pageId)
val after = pageOf(4)
fixture.manager.logUpdate(txn, pageId, before, after)
fixture.diskManager.writePage(Page(pageId).also { System.arraycopy(after, 0, it.data, 0, after.size) })
fixture.manager.abort(txn)
assertEquals(TransactionManager.Status.ABORTED, fixture.manager.statusOf(txn))
assertContentEquals(before, fixture.diskManager.readPage(pageId).data)
val otherTxn = fixture.manager.begin()
fixture.manager.acquireExclusive(otherTxn, pageId) // must not block: aborted txn released its locks
}
@Test
fun operationsOnAResolvedTransactionFail() {
val fixture = Fixture()
val txn = fixture.manager.begin()
fixture.manager.commit(txn)
assertFailsWith<TransactionException> { fixture.manager.commit(txn) }
assertFailsWith<TransactionException> { fixture.manager.abort(txn) }
assertFailsWith<TransactionException> { fixture.manager.acquireExclusive(txn, 1L) }
}
@Test
fun statusOfUnknownTransactionFails() {
val fixture = Fixture()
assertFailsWith<TransactionException> { fixture.manager.statusOf(999L) }
}
}