fix: последните 4 критични/средни бъга — LSM-Tree, MVCC, DistTxn, Raft
Поправени проблеми: - LSM-Tree: WAL write вече е извън db.lock (отделен walLock) → по-добра concurrency - MVCC: добавен compactVersions() който се изпълнява на всеки 100 commits Премахва стари overwritten версии, които не са видими за active транзакции - DistTxn: commit() вече НЕ rollback-ва commit-нали participants Ако някой participant е commit-нал, транзакцията се маркира като committed - Raft: добавен disk persistence за currentTerm, votedFor, log saveState() при всяка промяна; loadState() при стартиране 292 теста, 0 failure-а.
This commit is contained in:
@@ -166,16 +166,16 @@ proc commit*(txn: DistributedTransaction): bool =
|
||||
if allOk:
|
||||
txn.state = dtsCommitted
|
||||
else:
|
||||
# Rollback committed participants on commit failure
|
||||
for nodeId in committedNodes:
|
||||
var p = txn.participants[nodeId]
|
||||
if p.host.len > 0 and p.port > 0:
|
||||
discard sendDistTxnRpc(p.host, p.port, txn.id, "ROLLBACK")
|
||||
p.committed = false
|
||||
p.aborted = true
|
||||
if committedNodes.len > 0:
|
||||
# Some participants already committed — cannot rollback committed nodes
|
||||
# without violating atomicity. Mark as committed and rely on
|
||||
# reconciliation/retry for the remaining participants.
|
||||
txn.state = dtsCommitted
|
||||
else:
|
||||
# No participant committed yet — safe to abort
|
||||
txn.state = dtsAborted
|
||||
release(txn.lock)
|
||||
return allOk
|
||||
return allOk or committedNodes.len > 0
|
||||
|
||||
proc rollback*(txn: DistributedTransaction): bool =
|
||||
acquire(txn.lock)
|
||||
|
||||
@@ -229,6 +229,8 @@ proc write*(tm: TxnManager, txn: Transaction, key: string, value: seq[byte]): bo
|
||||
proc delete*(tm: TxnManager, txn: Transaction, key: string): bool =
|
||||
return tm.write(txn, key, @[])
|
||||
|
||||
proc compactVersions*(tm: TxnManager)
|
||||
|
||||
proc commit*(tm: TxnManager, txn: Transaction): bool =
|
||||
acquire(tm.lock)
|
||||
if txn.state != tsActive:
|
||||
@@ -258,9 +260,41 @@ proc commit*(tm: TxnManager, txn: Transaction): bool =
|
||||
tm.committedTxnsSet.incl(txn.id)
|
||||
tm.activeTxns.del(txn.id)
|
||||
tm.deadlockDetector.removeTxn(uint64(txn.id))
|
||||
|
||||
# Compact version chains periodically (every 100 commits)
|
||||
if tm.committedTxns.len mod 100 == 0:
|
||||
tm.compactVersions()
|
||||
|
||||
release(tm.lock)
|
||||
return true
|
||||
|
||||
proc compactVersions*(tm: TxnManager) =
|
||||
## Remove old overwritten versions that are no longer visible to any active transaction.
|
||||
for key, versions in tm.globalVersions.mpairs:
|
||||
if versions.len <= 3:
|
||||
continue
|
||||
var newVersions: seq[VersionedRecord] = @[]
|
||||
for i in 0..<versions.len:
|
||||
let v = versions[i]
|
||||
# Always keep the latest (non-deleted) version
|
||||
if v.xmax == TxnId(0):
|
||||
newVersions.add(v)
|
||||
continue
|
||||
# For old overwritten versions, check if any active txn might still see them
|
||||
var mightBeVisible = false
|
||||
for txnId, txn in tm.activeTxns:
|
||||
if txn.state == tsActive:
|
||||
# A version is visible if its creator is not newer than snapshotMaxTxn
|
||||
# and the creator was committed at snapshot time
|
||||
if uint64(v.xmin) <= uint64(txn.snapshotMaxTxn) and
|
||||
v.xmin notin txn.snapshotTxns and
|
||||
v.xmin notin tm.abortedTxns:
|
||||
mightBeVisible = true
|
||||
break
|
||||
if mightBeVisible:
|
||||
newVersions.add(v)
|
||||
versions = newVersions
|
||||
|
||||
proc abortTxn*(tm: TxnManager, txn: Transaction): bool =
|
||||
acquire(tm.lock)
|
||||
if txn.state != tsActive:
|
||||
|
||||
@@ -10,6 +10,7 @@ import std/asyncnet
|
||||
import std/streams
|
||||
import std/strutils
|
||||
import std/endians
|
||||
import std/os
|
||||
import ../protocol/wire
|
||||
|
||||
type
|
||||
@@ -46,6 +47,7 @@ type
|
||||
votesReceived*: HashSet[string]
|
||||
peerAddrs*: Table[string, tuple[host: string, port: int]]
|
||||
raftPort*: int
|
||||
dataDir*: string
|
||||
|
||||
RaftMessageKind* = enum
|
||||
rmkRequestVote
|
||||
@@ -73,9 +75,61 @@ type
|
||||
nodes*: Table[string, RaftNode]
|
||||
messageQueue*: Deque[RaftMessage]
|
||||
|
||||
proc newRaftNode*(id: string, peers: seq[string], raftPort: int = 0): RaftNode =
|
||||
const RaftStateFile = "raft_state.bin"
|
||||
|
||||
proc saveState(node: RaftNode) =
|
||||
if node.dataDir.len == 0: return
|
||||
createDir(node.dataDir)
|
||||
let path = node.dataDir / RaftStateFile
|
||||
let tmpPath = path & ".tmp"
|
||||
var s = newFileStream(tmpPath, fmWrite)
|
||||
if s == nil: return
|
||||
s.write(node.currentTerm)
|
||||
s.write(uint32(node.votedFor.len))
|
||||
s.write(node.votedFor)
|
||||
s.write(uint32(node.log.len))
|
||||
for entry in node.log:
|
||||
s.write(entry.term)
|
||||
s.write(entry.index)
|
||||
s.write(uint32(entry.command.len))
|
||||
s.write(entry.command)
|
||||
s.write(uint32(entry.data.len))
|
||||
if entry.data.len > 0:
|
||||
s.writeData(addr entry.data[0], entry.data.len)
|
||||
s.close()
|
||||
moveFile(tmpPath, path)
|
||||
|
||||
proc loadState(node: RaftNode) =
|
||||
if node.dataDir.len == 0: return
|
||||
let path = node.dataDir / RaftStateFile
|
||||
if not fileExists(path): return
|
||||
var s = newFileStream(path, fmRead)
|
||||
if s == nil: return
|
||||
try:
|
||||
node.currentTerm = s.readUint64()
|
||||
let votedForLen = int(s.readUint32())
|
||||
if votedForLen > 0:
|
||||
node.votedFor = s.readStr(votedForLen)
|
||||
let logLen = int(s.readUint32())
|
||||
node.log = newSeq[LogEntry](logLen)
|
||||
for i in 0..<logLen:
|
||||
let term = s.readUint64()
|
||||
let index = s.readUint64()
|
||||
let cmdLen = int(s.readUint32())
|
||||
let cmd = s.readStr(cmdLen)
|
||||
let dataLen = int(s.readUint32())
|
||||
var data = newSeq[byte](dataLen)
|
||||
if dataLen > 0:
|
||||
discard s.readData(addr data[0], dataLen)
|
||||
node.log[i] = LogEntry(term: term, index: index, command: cmd, data: data)
|
||||
except:
|
||||
discard
|
||||
s.close()
|
||||
|
||||
proc newRaftNode*(id: string, peers: seq[string], raftPort: int = 0,
|
||||
dataDir: string = ""): RaftNode =
|
||||
randomize()
|
||||
RaftNode(
|
||||
result = RaftNode(
|
||||
id: id,
|
||||
state: rsFollower,
|
||||
currentTerm: 0,
|
||||
@@ -92,7 +146,9 @@ proc newRaftNode*(id: string, peers: seq[string], raftPort: int = 0): RaftNode =
|
||||
votesReceived: initHashSet[string](),
|
||||
peerAddrs: initTable[string, tuple[host: string, port: int]](),
|
||||
raftPort: raftPort,
|
||||
dataDir: dataDir,
|
||||
)
|
||||
result.loadState()
|
||||
|
||||
proc newRaftCluster*(): RaftCluster =
|
||||
RaftCluster(
|
||||
@@ -131,6 +187,7 @@ proc becomeFollower*(node: RaftNode, term: uint64) =
|
||||
node.currentTerm = term
|
||||
node.votedFor = ""
|
||||
node.votesReceived.clear()
|
||||
node.saveState()
|
||||
|
||||
proc becomeCandidate*(node: RaftNode) =
|
||||
node.state = rsCandidate
|
||||
@@ -138,6 +195,7 @@ proc becomeCandidate*(node: RaftNode) =
|
||||
node.votedFor = node.id
|
||||
node.votesReceived.clear()
|
||||
node.votesReceived.incl(node.id)
|
||||
node.saveState()
|
||||
|
||||
proc becomeLeader*(node: RaftNode) =
|
||||
node.state = rsLeader
|
||||
@@ -166,6 +224,7 @@ proc handleRequestVote*(node: RaftNode, msg: RaftMessage): RaftMessage =
|
||||
|
||||
if canVote and logOk:
|
||||
node.votedFor = msg.senderId
|
||||
node.saveState()
|
||||
reply.success = true
|
||||
reply.term = node.currentTerm
|
||||
|
||||
@@ -197,14 +256,20 @@ proc handleAppendEntries*(node: RaftNode, msg: RaftMessage): RaftMessage =
|
||||
return reply
|
||||
|
||||
# Append new entries
|
||||
var logChanged = false
|
||||
for entry in msg.entries:
|
||||
let idx = int(entry.index - 1)
|
||||
if idx < node.log.len:
|
||||
if node.log[idx].term != entry.term:
|
||||
node.log.setLen(idx)
|
||||
node.log.add(entry)
|
||||
logChanged = true
|
||||
else:
|
||||
node.log.add(entry)
|
||||
logChanged = true
|
||||
|
||||
if logChanged:
|
||||
node.saveState()
|
||||
|
||||
# Update commit index
|
||||
if msg.leaderCommit > node.commitIndex:
|
||||
@@ -258,6 +323,7 @@ proc appendLog*(node: RaftNode, command: string, data: seq[byte] = @[]): LogEntr
|
||||
data: data,
|
||||
)
|
||||
node.log.add(result)
|
||||
node.saveState()
|
||||
|
||||
proc handleVoteReply*(node: RaftNode, reply: RaftMessage) =
|
||||
if reply.term > node.currentTerm:
|
||||
|
||||
@@ -51,6 +51,7 @@ type
|
||||
currentSeq: uint64
|
||||
nextSSTableId: int
|
||||
lock*: Lock
|
||||
walLock*: Lock
|
||||
|
||||
proc newMemTable(maxSize: int = DefaultMemTableSize): MemTable =
|
||||
MemTable(entries: @[], size: 0, maxSize: maxSize)
|
||||
@@ -322,6 +323,7 @@ proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree =
|
||||
|
||||
new(result)
|
||||
initLock(result.lock)
|
||||
initLock(result.walLock)
|
||||
result.dir = dir
|
||||
result.memTable = newMemTable(memMaxSize)
|
||||
result.immutableMem = newMemTable(0)
|
||||
@@ -375,10 +377,13 @@ proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree =
|
||||
stream.close()
|
||||
|
||||
proc put*(db: LSMTree, key: string, value: seq[byte]) =
|
||||
let ts = uint64(getMonoTime().ticks())
|
||||
acquire(db.walLock)
|
||||
db.wal.writePut(cast[seq[byte]](key), value, ts)
|
||||
release(db.walLock)
|
||||
|
||||
acquire(db.lock)
|
||||
defer: release(db.lock)
|
||||
let ts = uint64(getMonoTime().ticks())
|
||||
db.wal.writePut(cast[seq[byte]](key), value, ts)
|
||||
if not db.memTable.put(key, value, ts):
|
||||
if db.immutableMem.len > 0:
|
||||
db.flushUnsafe()
|
||||
@@ -387,10 +392,13 @@ proc put*(db: LSMTree, key: string, value: seq[byte]) =
|
||||
discard db.memTable.put(key, value, ts)
|
||||
|
||||
proc delete*(db: LSMTree, key: string) =
|
||||
let ts = uint64(getMonoTime().ticks())
|
||||
acquire(db.walLock)
|
||||
db.wal.writeDelete(cast[seq[byte]](key), ts)
|
||||
release(db.walLock)
|
||||
|
||||
acquire(db.lock)
|
||||
defer: release(db.lock)
|
||||
let ts = uint64(getMonoTime().ticks())
|
||||
db.wal.writeDelete(cast[seq[byte]](key), ts)
|
||||
if not db.memTable.put(key, @[], ts, deleted = true):
|
||||
if db.immutableMem.len > 0:
|
||||
db.flushUnsafe()
|
||||
|
||||
Reference in New Issue
Block a user