feat(engine): add TransactionalIndex and create
This commit is contained in:
1 parent
69148dda4b
commit
7afad5575e
1 file changed
+137
@@ -0,0 +1,137 @@
|
|||||||
|
package kestrel.store.engine
|
||||||
|
|
||||||
|
import kestrel.store.btree.BPlusTree
|
||||||
|
import kestrel.store.page.PageStore
|
||||||
|
import kestrel.store.txn.TransactionManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [BPlusTree] made crash-safe and transactional, following the same
|
||||||
|
* per-call [LoggingPageStore] pattern as [TransactionalHeap]. Locking uses
|
||||||
|
* [lockResourceId] rather than the tree's own root page id: [BPlusTree.delete]
|
||||||
|
* can shrink the tree and free its former root back to the shared page free
|
||||||
|
* list, so that id can later be reallocated to an unrelated page - using it
|
||||||
|
* as a lock resource would risk two unrelated structures serializing against
|
||||||
|
* each other by coincidence. [StorageEngine] hands out a disjoint id (see
|
||||||
|
* `nextResourceId`) precisely to avoid that.
|
||||||
|
*
|
||||||
|
* A root split or shrink also moves the tree's root pointer itself, which is
|
||||||
|
* plain in-memory engine state, not something [kestrel.store.wal.WriteAheadLog]
|
||||||
|
* records - rolling a transaction back restores page *bytes* but has no way
|
||||||
|
* to know it must also restore [rootPageId]. [rollback] does that: it
|
||||||
|
* remembers, on a transaction's first write, the root as of just before that
|
||||||
|
* transaction touched the tree, and restores it on abort.
|
||||||
|
*/
|
||||||
|
class TransactionalIndex private constructor(
|
||||||
|
private val bufferPool: PageStore,
|
||||||
|
private val txnManager: TransactionManager,
|
||||||
|
private val tracker: TouchedPageTracker,
|
||||||
|
private val lockResourceId: Long,
|
||||||
|
initialRootPageId: Long
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create(
|
||||||
|
bufferPool: PageStore,
|
||||||
|
txnManager: TransactionManager,
|
||||||
|
tracker: TouchedPageTracker,
|
||||||
|
lockResourceId: Long
|
||||||
|
): TransactionalIndex {
|
||||||
|
val tree = BPlusTree.create(bufferPool)
|
||||||
|
return TransactionalIndex(bufferPool, txnManager, tracker, lockResourceId, tree.rootPageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun open(
|
||||||
|
bufferPool: PageStore,
|
||||||
|
txnManager: TransactionManager,
|
||||||
|
tracker: TouchedPageTracker,
|
||||||
|
lockResourceId: Long,
|
||||||
|
rootPageId: Long
|
||||||
|
): TransactionalIndex = TransactionalIndex(bufferPool, txnManager, tracker, lockResourceId, rootPageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootPageId: Long = initialRootPageId
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val rootBeforeTxn = HashMap<Long, Long>()
|
||||||
|
private val rootAtSavepoint = HashMap<Long, Long>()
|
||||||
|
|
||||||
|
fun insert(txnId: Long, key: ByteArray, value: Long) {
|
||||||
|
txnManager.acquireExclusive(txnId, lockResourceId)
|
||||||
|
rootBeforeTxn.putIfAbsent(txnId, rootPageId)
|
||||||
|
val logging = loggingStoreFor(txnId)
|
||||||
|
val tree = BPlusTree.open(logging, rootPageId)
|
||||||
|
tree.insert(key, value)
|
||||||
|
rootPageId = tree.rootPageId
|
||||||
|
logging.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(txnId: Long, key: ByteArray): Boolean {
|
||||||
|
txnManager.acquireExclusive(txnId, lockResourceId)
|
||||||
|
rootBeforeTxn.putIfAbsent(txnId, rootPageId)
|
||||||
|
val logging = loggingStoreFor(txnId)
|
||||||
|
val tree = BPlusTree.open(logging, rootPageId)
|
||||||
|
val found = tree.delete(key)
|
||||||
|
rootPageId = tree.rootPageId
|
||||||
|
logging.finish()
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frees every page this tree owns back to the shared [kestrel.store.page.DiskManager] free
|
||||||
|
* list - used when the owning index (or its whole table) is dropped. See
|
||||||
|
* [TransactionalHeap.destroy]'s own doc for why this is deliberately not undo-safe: the same
|
||||||
|
* accepted tradeoff [kestrel.store.btree.BPlusTree] already makes for a merge or root-shrink's
|
||||||
|
* own page frees.
|
||||||
|
*/
|
||||||
|
fun destroy(txnId: Long) {
|
||||||
|
txnManager.acquireExclusive(txnId, lockResourceId)
|
||||||
|
for (pageId in BPlusTree.open(bufferPool, rootPageId).allPageIds()) bufferPool.deletePage(pageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(txnId: Long, key: ByteArray): Long? {
|
||||||
|
txnManager.acquireShared(txnId, lockResourceId)
|
||||||
|
return BPlusTree.open(bufferPool, rootPageId).search(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rangeScan(
|
||||||
|
txnId: Long,
|
||||||
|
from: ByteArray? = null,
|
||||||
|
to: ByteArray? = null,
|
||||||
|
toInclusive: Boolean = false
|
||||||
|
): List<Pair<ByteArray, Long>> {
|
||||||
|
txnManager.acquireShared(txnId, lockResourceId)
|
||||||
|
return BPlusTree.open(bufferPool, rootPageId).rangeScan(from, to, toInclusive).asSequence().toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restores the root pointer as of before [txnId]'s first write. Called by [StorageEngine.abort]. */
|
||||||
|
fun rollback(txnId: Long) {
|
||||||
|
rootBeforeTxn.remove(txnId)?.let { rootPageId = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drops the pending rollback snapshot once [txnId] is durably resolved. Called by [StorageEngine.commit]. */
|
||||||
|
fun forgetTransaction(txnId: Long) {
|
||||||
|
rootBeforeTxn.remove(txnId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snapshots the root pointer under [savepointLsn] (a transaction's [TransactionManager.currentLsn]
|
||||||
|
* at the moment a SAVEPOINT is established), so a later [restoreRootAt] can undo whatever root
|
||||||
|
* moves a split, merge or shrink made afterward - the same problem [rootBeforeTxn] solves for a
|
||||||
|
* whole transaction, scoped to a savepoint instead. Called by [StorageEngine.savepoint].
|
||||||
|
*/
|
||||||
|
fun snapshotRootAt(savepointLsn: Long) {
|
||||||
|
rootAtSavepoint[savepointLsn] = rootPageId
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restores the root pointer captured by [snapshotRootAt]. Called by [StorageEngine.rollbackToSavepoint]. */
|
||||||
|
fun restoreRootAt(savepointLsn: Long) {
|
||||||
|
rootPageId = rootAtSavepoint.getValue(savepointLsn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drops a snapshot once its savepoint can no longer be rolled back to. Called by [StorageEngine.discardSavepoint]. */
|
||||||
|
fun discardSavepoint(savepointLsn: Long) {
|
||||||
|
rootAtSavepoint.remove(savepointLsn)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loggingStoreFor(txnId: Long): LoggingPageStore =
|
||||||
|
LoggingPageStore(bufferPool, txnManager, txnId) { pageId -> tracker.record(txnId, pageId) }
|
||||||
|
}
|
||||||
Reference in new issue
Block a user