feat(wal): add LogRecord, Begin and WalCorruptionException
This commit is contained in:
1 parent
9037d01c7e
commit
ade22da256
2 files changed
+135
No files matched your search
@@ -0,0 +1,50 @@
|
||||
package kestrel.store.wal
|
||||
|
||||
/**
|
||||
* A single write-ahead log entry. [Update] carries whole-page before/after
|
||||
* images rather than a delta, so redoing it is just overwriting a page with
|
||||
* [Update.after]; undoing it is overwriting with [Update.before]. `prevLsn`
|
||||
* threads a transaction's updates into a backward-traversable chain, the
|
||||
* same role it plays in ARIES.
|
||||
*/
|
||||
sealed class LogRecord {
|
||||
class Begin(val txnId: Long) : LogRecord()
|
||||
|
||||
class Commit(val txnId: Long) : LogRecord()
|
||||
|
||||
class Abort(val txnId: Long) : LogRecord()
|
||||
|
||||
class Update(
|
||||
val txnId: Long,
|
||||
val prevLsn: Long,
|
||||
val pageId: Long,
|
||||
val before: ByteArray,
|
||||
val after: ByteArray
|
||||
) : LogRecord()
|
||||
|
||||
/**
|
||||
* A compensation log record (CLR), ARIES terminology for "this page was just
|
||||
* restored to an earlier image by an undo". Written by [kestrel.store.wal.RecoveryManager.rollbackTo]
|
||||
* for every [Update] it undoes on the way to a savepoint, so that:
|
||||
* - forward redo ([kestrel.store.wal.RecoveryManager.recover]) can reproduce the undo's effect
|
||||
* just like any other logged write, by reapplying [undoImage].
|
||||
* - a later undo walk (a full [kestrel.store.wal.RecoveryManager.rollback], or a rollback to an
|
||||
* earlier savepoint) that reaches this record never re-undoes the [Update] it compensates for;
|
||||
* it jumps straight to [undoNextLsn] instead, which is that [Update]'s own `prevLsn` - the
|
||||
* transaction's undo chain skips the already-undone range as if it had never happened, exactly
|
||||
* the property that makes partial rollback safe to repeat and safe to crash through.
|
||||
* Chains into the transaction's `prevLsn` thread the same way [Update] does, via [prevLsn] - so a
|
||||
* write logged after a savepoint rollback correctly points back at this CLR, not at the undone
|
||||
* [Update] it replaced.
|
||||
*/
|
||||
class CompensationUpdate(
|
||||
val txnId: Long,
|
||||
val prevLsn: Long,
|
||||
val pageId: Long,
|
||||
val undoImage: ByteArray,
|
||||
val undoNextLsn: Long
|
||||
) : LogRecord()
|
||||
|
||||
/** Marks a quiescent checkpoint: written only when no transaction is active. */
|
||||
class Checkpoint(val activeTxnIds: List<Long>) : LogRecord()
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package kestrel.store.wal
|
||||
|
||||
import kestrel.KestrelException
|
||||
import kestrel.store.codec.ByteReader
|
||||
import kestrel.store.codec.ByteWriter
|
||||
|
||||
class WalCorruptionException(message: String) : KestrelException(message)
|
||||
|
||||
/** Serializes [LogRecord]s to and from the byte payload stored in each log frame. */
|
||||
object LogRecordCodec {
|
||||
private const val TYPE_BEGIN = 1
|
||||
private const val TYPE_COMMIT = 2
|
||||
private const val TYPE_ABORT = 3
|
||||
private const val TYPE_UPDATE = 4
|
||||
private const val TYPE_CHECKPOINT = 5
|
||||
private const val TYPE_COMPENSATION_UPDATE = 6
|
||||
|
||||
fun encode(record: LogRecord): ByteArray {
|
||||
val writer = ByteWriter()
|
||||
when (record) {
|
||||
is LogRecord.Begin -> {
|
||||
writer.writeByte(TYPE_BEGIN)
|
||||
writer.writeFixedLong(record.txnId)
|
||||
}
|
||||
is LogRecord.Commit -> {
|
||||
writer.writeByte(TYPE_COMMIT)
|
||||
writer.writeFixedLong(record.txnId)
|
||||
}
|
||||
is LogRecord.Abort -> {
|
||||
writer.writeByte(TYPE_ABORT)
|
||||
writer.writeFixedLong(record.txnId)
|
||||
}
|
||||
is LogRecord.Update -> {
|
||||
writer.writeByte(TYPE_UPDATE)
|
||||
writer.writeFixedLong(record.txnId)
|
||||
writer.writeFixedLong(record.prevLsn)
|
||||
writer.writeFixedLong(record.pageId)
|
||||
writer.writeLengthPrefixed(record.before)
|
||||
writer.writeLengthPrefixed(record.after)
|
||||
}
|
||||
is LogRecord.Checkpoint -> {
|
||||
writer.writeByte(TYPE_CHECKPOINT)
|
||||
writer.writeVarInt(record.activeTxnIds.size.toLong())
|
||||
for (id in record.activeTxnIds) writer.writeFixedLong(id)
|
||||
}
|
||||
is LogRecord.CompensationUpdate -> {
|
||||
writer.writeByte(TYPE_COMPENSATION_UPDATE)
|
||||
writer.writeFixedLong(record.txnId)
|
||||
writer.writeFixedLong(record.prevLsn)
|
||||
writer.writeFixedLong(record.pageId)
|
||||
writer.writeLengthPrefixed(record.undoImage)
|
||||
writer.writeFixedLong(record.undoNextLsn)
|
||||
}
|
||||
}
|
||||
return writer.toByteArray()
|
||||
}
|
||||
|
||||
fun decode(bytes: ByteArray): LogRecord {
|
||||
val reader = ByteReader(bytes)
|
||||
return when (val type = reader.readByte()) {
|
||||
TYPE_BEGIN -> LogRecord.Begin(reader.readFixedLong())
|
||||
TYPE_COMMIT -> LogRecord.Commit(reader.readFixedLong())
|
||||
TYPE_ABORT -> LogRecord.Abort(reader.readFixedLong())
|
||||
TYPE_UPDATE -> LogRecord.Update(
|
||||
txnId = reader.readFixedLong(),
|
||||
prevLsn = reader.readFixedLong(),
|
||||
pageId = reader.readFixedLong(),
|
||||
before = reader.readLengthPrefixed(),
|
||||
after = reader.readLengthPrefixed()
|
||||
)
|
||||
TYPE_CHECKPOINT -> {
|
||||
val count = reader.readVarInt().toInt()
|
||||
LogRecord.Checkpoint((0 until count).map { reader.readFixedLong() })
|
||||
}
|
||||
TYPE_COMPENSATION_UPDATE -> LogRecord.CompensationUpdate(
|
||||
txnId = reader.readFixedLong(),
|
||||
prevLsn = reader.readFixedLong(),
|
||||
pageId = reader.readFixedLong(),
|
||||
undoImage = reader.readLengthPrefixed(),
|
||||
undoNextLsn = reader.readFixedLong()
|
||||
)
|
||||
else -> throw WalCorruptionException("unknown log record type $type")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user