feat(page): add BufferPoolExhaustedException and BufferPool
This commit is contained in:
1 parent
a1ae100c72
commit
5240cc6dee
1 file changed
+133
@@ -0,0 +1,133 @@
|
|||||||
|
package kestrel.store.page
|
||||||
|
|
||||||
|
import kestrel.KestrelException
|
||||||
|
|
||||||
|
class BufferPoolExhaustedException(message: String) : KestrelException(message)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed-capacity page cache in front of a [DiskManager], using clock
|
||||||
|
* (second-chance) replacement: each frame has a reference bit set on access,
|
||||||
|
* and eviction sweeps frames clearing bits before reclaiming an unpinned,
|
||||||
|
* unreferenced frame. Dirty pages are written back on eviction or explicit
|
||||||
|
* flush.
|
||||||
|
*/
|
||||||
|
class BufferPool(private val diskManager: DiskManager, private val capacity: Int) : PageStore {
|
||||||
|
init {
|
||||||
|
require(capacity > 0) { "buffer pool capacity must be positive" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val frames = arrayOfNulls<Page>(capacity)
|
||||||
|
private val referenceBit = BooleanArray(capacity)
|
||||||
|
private val pageTable = HashMap<Long, Int>()
|
||||||
|
private val freeFrames = ArrayDeque((0 until capacity).toList())
|
||||||
|
private var clockHand = 0
|
||||||
|
|
||||||
|
override fun fetchPage(pageId: Long): Page {
|
||||||
|
pageTable[pageId]?.let { idx ->
|
||||||
|
val page = frames[idx]!!
|
||||||
|
page.pinCount++
|
||||||
|
referenceBit[idx] = true
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
val idx = allocateFrame()
|
||||||
|
val page = diskManager.readPage(pageId)
|
||||||
|
page.pinCount = 1
|
||||||
|
frames[idx] = page
|
||||||
|
pageTable[pageId] = idx
|
||||||
|
referenceBit[idx] = true
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newPage(): Page {
|
||||||
|
val idx = allocateFrame()
|
||||||
|
val pageId = diskManager.allocatePage()
|
||||||
|
val page = Page(pageId)
|
||||||
|
page.pinCount = 1
|
||||||
|
page.isDirty = true
|
||||||
|
frames[idx] = page
|
||||||
|
pageTable[pageId] = idx
|
||||||
|
referenceBit[idx] = true
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unpinPage(pageId: Long, markDirty: Boolean) {
|
||||||
|
val idx = pageTable[pageId] ?: return
|
||||||
|
val page = frames[idx]!!
|
||||||
|
if (markDirty) page.isDirty = true
|
||||||
|
if (page.pinCount > 0) page.pinCount--
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flushPage(pageId: Long) {
|
||||||
|
val idx = pageTable[pageId] ?: return
|
||||||
|
flushFrame(idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flushAll() {
|
||||||
|
for (idx in frames.indices) {
|
||||||
|
if (frames[idx] != null) flushFrame(idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes a page from the cache and the backing file. Caller must not hold a pin on it. */
|
||||||
|
override fun deletePage(pageId: Long) {
|
||||||
|
dropFromCache(pageId)
|
||||||
|
diskManager.freePage(pageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops a page from the cache without writing it back or freeing it on
|
||||||
|
* disk, so the next [fetchPage] re-reads whatever is currently on disk.
|
||||||
|
* Used after [kestrel.store.wal.RecoveryManager] restores a before-image
|
||||||
|
* directly through the [DiskManager], bypassing this cache: without an
|
||||||
|
* invalidation the pool would keep serving the stale, rolled-back copy.
|
||||||
|
*/
|
||||||
|
fun invalidate(pageId: Long) {
|
||||||
|
dropFromCache(pageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dropFromCache(pageId: Long) {
|
||||||
|
val idx = pageTable[pageId] ?: return
|
||||||
|
val page = frames[idx]!!
|
||||||
|
check(page.pinCount == 0) { "cannot drop pinned page $pageId from the cache" }
|
||||||
|
pageTable.remove(pageId)
|
||||||
|
frames[idx] = null
|
||||||
|
referenceBit[idx] = false
|
||||||
|
freeFrames.addLast(idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPinned(pageId: Long): Boolean = pageTable[pageId]?.let { frames[it]?.pinCount ?: 0 > 0 } ?: false
|
||||||
|
|
||||||
|
private fun flushFrame(idx: Int) {
|
||||||
|
val page = frames[idx] ?: return
|
||||||
|
if (page.isDirty) {
|
||||||
|
diskManager.writePage(page)
|
||||||
|
page.isDirty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun allocateFrame(): Int {
|
||||||
|
if (freeFrames.isNotEmpty()) return freeFrames.removeFirst()
|
||||||
|
var sweeps = 0
|
||||||
|
val maxSweeps = 2 * capacity + 1
|
||||||
|
while (sweeps <= maxSweeps) {
|
||||||
|
val idx = clockHand
|
||||||
|
clockHand = (clockHand + 1) % capacity
|
||||||
|
val page = frames[idx]
|
||||||
|
if (page == null) return idx
|
||||||
|
if (page.pinCount > 0) {
|
||||||
|
sweeps++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (referenceBit[idx]) {
|
||||||
|
referenceBit[idx] = false
|
||||||
|
sweeps++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (page.isDirty) diskManager.writePage(page)
|
||||||
|
pageTable.remove(page.id)
|
||||||
|
frames[idx] = null
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
throw BufferPoolExhaustedException("no unpinned frame available to evict (capacity=$capacity)")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user