test(engine): cover LoggingPageStoreTest, tempFile and StorageEngineTest
This commit is contained in:
1 parent
4c6fb19610
commit
78150274a1
2 files changed
+196
No files matched your search
@@ -0,0 +1,74 @@
|
||||
package kestrel.store.engine
|
||||
|
||||
import kestrel.store.heap.HeapFile
|
||||
import kestrel.store.heap.HeapPageException
|
||||
import kestrel.store.page.BufferPool
|
||||
import kestrel.store.page.DiskManager
|
||||
import kestrel.store.txn.TransactionManager
|
||||
import kestrel.store.wal.RecoveryManager
|
||||
import kestrel.store.wal.WriteAheadLog
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class LoggingPageStoreTest {
|
||||
private fun tempFile(prefix: String): File = Files.createTempFile(prefix, ".db").toFile().also { it.deleteOnExit() }
|
||||
|
||||
private inner class Fixture {
|
||||
val diskManager = DiskManager(tempFile("kestrel-logstore-data"))
|
||||
val bufferPool = BufferPool(diskManager, 16)
|
||||
val wal = WriteAheadLog(tempFile("kestrel-logstore-wal"))
|
||||
val txnManager = TransactionManager(wal, diskManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertThroughTheLoggingStoreProducesAWalRecordForTheMutatedPage() {
|
||||
val fx = Fixture()
|
||||
val heapHeaderId = HeapFile.create(fx.bufferPool).headerPageId
|
||||
|
||||
val txnId = fx.txnManager.begin()
|
||||
val touched = mutableListOf<Long>()
|
||||
val logging = LoggingPageStore(fx.bufferPool, fx.txnManager, txnId) { touched.add(it) }
|
||||
val rid = HeapFile.open(logging, heapHeaderId).insert("hello".toByteArray())
|
||||
logging.finish()
|
||||
|
||||
assertTrue(rid.pageId in touched, "the page the record landed on must be reported to the touch callback")
|
||||
assertTrue(fx.wal.lastLsnOf(txnId) != WriteAheadLog.NO_LSN, "insert must produce at least one logged update")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readOnlyAccessProducesNoWalRecord() {
|
||||
val fx = Fixture()
|
||||
val heap = HeapFile.create(fx.bufferPool)
|
||||
val rid = heap.insert("already there".toByteArray())
|
||||
fx.bufferPool.flushAll()
|
||||
|
||||
val txnId = fx.txnManager.begin()
|
||||
val lsnAfterBegin = fx.wal.lastLsnOf(txnId)
|
||||
val logging = LoggingPageStore(fx.bufferPool, fx.txnManager, txnId)
|
||||
val bytes = HeapFile.open(logging, heap.headerPageId).get(rid)
|
||||
logging.finish()
|
||||
|
||||
assertEquals("already there", String(bytes))
|
||||
assertEquals(lsnAfterBegin, fx.wal.lastLsnOf(txnId), "a pure read must not append any update record")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rollingBackAnUnfinishedInsertErasesItFromThePage() {
|
||||
val fx = Fixture()
|
||||
val heapHeaderId = HeapFile.create(fx.bufferPool).headerPageId
|
||||
|
||||
val txnId = fx.txnManager.begin()
|
||||
val logging = LoggingPageStore(fx.bufferPool, fx.txnManager, txnId)
|
||||
val rid = HeapFile.open(logging, heapHeaderId).insert("temporary".toByteArray())
|
||||
logging.finish()
|
||||
|
||||
RecoveryManager(fx.wal, fx.diskManager).rollback(txnId)
|
||||
fx.bufferPool.invalidate(rid.pageId)
|
||||
|
||||
assertFailsWith<HeapPageException> { HeapFile.open(fx.bufferPool, heapHeaderId).get(rid) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package kestrel.store.engine
|
||||
|
||||
import kestrel.store.codec.KeyCodec
|
||||
import kestrel.store.codec.TupleCodec
|
||||
import kestrel.store.heap.RecordId
|
||||
import kestrel.storage.Value
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class StorageEngineTest {
|
||||
private fun tempFile(prefix: String): File = Files.createTempFile(prefix, ".db").toFile().also { it.deleteOnExit() }
|
||||
|
||||
@Test
|
||||
fun aCommittedInsertSurvivesACrashThatNeverFlushedTheBufferPool() {
|
||||
val dataFile = tempFile("kestrel-engine-data")
|
||||
val walFile = tempFile("kestrel-engine-wal")
|
||||
|
||||
val engine1 = StorageEngine.open(dataFile, walFile)
|
||||
val heap = engine1.createHeap()
|
||||
val txn = engine1.beginTransaction()
|
||||
val rid = heap.insert(txn, "durable across a crash".toByteArray())
|
||||
engine1.commit(txn)
|
||||
// No engine1.close() / flush(): the committed page may still be sitting only in
|
||||
// the buffer pool, never written to dataFile. Only the WAL commit record is durable.
|
||||
|
||||
val engine2 = StorageEngine.open(dataFile, walFile)
|
||||
val reopenedHeap = engine2.openHeap(heap.headerPageId)
|
||||
val reader = engine2.beginTransaction()
|
||||
assertEquals("durable across a crash", String(reopenedHeap.get(reader, rid)))
|
||||
assertEquals(listOf(rid), reopenedHeap.scan(reader).map { it.first })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun abortMakesAnInsertInvisibleWithoutRestartingTheEngine() {
|
||||
val engine = StorageEngine.open(tempFile("kestrel-engine-data"), tempFile("kestrel-engine-wal"))
|
||||
val heap = engine.createHeap()
|
||||
|
||||
val writer = engine.beginTransaction()
|
||||
heap.insert(writer, "temporary".toByteArray())
|
||||
engine.abort(writer)
|
||||
|
||||
val reader = engine.beginTransaction()
|
||||
assertTrue(heap.scan(reader).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aTableAndItsSecondaryIndexCommitAndAbortTogetherAsOneTransaction() {
|
||||
val engine = StorageEngine.open(tempFile("kestrel-engine-data"), tempFile("kestrel-engine-wal"))
|
||||
val table = engine.createHeap()
|
||||
val index = engine.createIndex()
|
||||
|
||||
fun rowFor(id: Long, name: String) = TupleCodec.encode(listOf(Value.IntegerValue(id), Value.TextValue(name)))
|
||||
fun keyFor(id: Long) = KeyCodec.encode(Value.IntegerValue(id))
|
||||
|
||||
val setup = engine.beginTransaction()
|
||||
val committedRid = table.insert(setup, rowFor(1, "alice"))
|
||||
index.insert(setup, keyFor(1), committedRid.encode())
|
||||
engine.commit(setup)
|
||||
|
||||
val doomed = engine.beginTransaction()
|
||||
val doomedRid = table.insert(doomed, rowFor(2, "bob"))
|
||||
index.insert(doomed, keyFor(2), doomedRid.encode())
|
||||
engine.abort(doomed)
|
||||
|
||||
val reader = engine.beginTransaction()
|
||||
assertNull(index.search(reader, keyFor(2)), "the aborted row's index entry must not survive")
|
||||
val foundRid = RecordId.decode(index.search(reader, keyFor(1))!!)
|
||||
assertEquals(committedRid, foundRid)
|
||||
val row = TupleCodec.decode(table.get(reader, foundRid))
|
||||
assertEquals(listOf(Value.IntegerValue(1), Value.TextValue("alice")), row)
|
||||
assertEquals(listOf(committedRid), table.scan(reader).map { it.first })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anAbortedInsertFromAPriorSessionStaysGoneAfterReopening() {
|
||||
val dataFile = tempFile("kestrel-engine-data")
|
||||
val walFile = tempFile("kestrel-engine-wal")
|
||||
|
||||
val engine1 = StorageEngine.open(dataFile, walFile)
|
||||
val heap = engine1.createHeap()
|
||||
|
||||
val committed = engine1.beginTransaction()
|
||||
heap.insert(committed, "kept".toByteArray())
|
||||
engine1.commit(committed)
|
||||
|
||||
val doomed = engine1.beginTransaction()
|
||||
heap.insert(doomed, "discarded".toByteArray())
|
||||
engine1.abort(doomed)
|
||||
engine1.close()
|
||||
|
||||
val engine2 = StorageEngine.open(dataFile, walFile)
|
||||
val reopenedHeap = engine2.openHeap(heap.headerPageId)
|
||||
val reader = engine2.beginTransaction()
|
||||
assertEquals(listOf("kept"), reopenedHeap.scan(reader).map { String(it.second) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun closingAndReopeningPreservesBothTableAndIndexData() {
|
||||
val dataFile = tempFile("kestrel-engine-data")
|
||||
val walFile = tempFile("kestrel-engine-wal")
|
||||
|
||||
val engine1 = StorageEngine.open(dataFile, walFile)
|
||||
val table = engine1.createHeap()
|
||||
val index = engine1.createIndex()
|
||||
val txn = engine1.beginTransaction()
|
||||
val rid = table.insert(txn, TupleCodec.encode(listOf(Value.IntegerValue(7))))
|
||||
index.insert(txn, KeyCodec.encode(Value.IntegerValue(7)), rid.encode())
|
||||
engine1.commit(txn)
|
||||
engine1.close()
|
||||
|
||||
val engine2 = StorageEngine.open(dataFile, walFile)
|
||||
val reopenedTable = engine2.openHeap(table.headerPageId)
|
||||
val reopenedIndex = engine2.openIndex(index.rootPageId)
|
||||
val reader = engine2.beginTransaction()
|
||||
val foundRid = RecordId.decode(reopenedIndex.search(reader, KeyCodec.encode(Value.IntegerValue(7)))!!)
|
||||
assertEquals(listOf(Value.IntegerValue(7)), TupleCodec.decode(reopenedTable.get(reader, foundRid)))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user