fix(distributed): 2PC atomicity, DISTTXN handler, sharding bugs, gossip timers

- disttxn: prepare failure rolls back already-prepared participants (C1)
- disttxn: commit failure rolls back committed participants (C2)
- server: DISTTXN handler uses real DistTxnManager (C3)
- server: DistTxnManager initialized in newServer() (H1)
- sharding: getShardRange returns -1 instead of 0 for out-of-range keys (M6)
- sharding: binary search in consistent hashing ring (M7)
- gossip: startHealthCheck() and startGossipRound() async loops
- 283 tests, 0 failures
This commit is contained in:
2026-05-07 14:13:17 +03:00
parent 32187099dd
commit d259c1a1fc
5 changed files with 101 additions and 51 deletions
+25 -3
View File
@@ -100,18 +100,28 @@ proc prepare*(txn: DistributedTransaction): bool =
txn.state = dtsPreparing
var allOk = true
var preparedNodes: seq[string] = @[]
for nodeId, participant in txn.participants.mpairs:
if participant.host.len > 0 and participant.port > 0:
participant.prepared = sendDistTxnRpc(participant.host, participant.port, txn.id, "PREPARE")
else:
participant.prepared = true # local participant
if not participant.prepared:
if participant.prepared:
preparedNodes.add(nodeId)
else:
allOk = false
if allOk:
txn.state = dtsPrepared
else:
txn.state = dtsActive
# Rollback already-prepared participants to maintain atomicity
for nodeId in preparedNodes:
var p = txn.participants[nodeId]
if p.host.len > 0 and p.port > 0:
discard sendDistTxnRpc(p.host, p.port, txn.id, "ROLLBACK")
p.prepared = false
p.aborted = true
txn.state = dtsAborted
release(txn.lock)
return allOk
@@ -125,16 +135,28 @@ proc commit*(txn: DistributedTransaction): bool =
txn.state = dtsCommitting
var allOk = true
var committedNodes: seq[string] = @[]
for nodeId, participant in txn.participants.mpairs:
if participant.host.len > 0 and participant.port > 0:
participant.committed = sendDistTxnRpc(participant.host, participant.port, txn.id, "COMMIT")
else:
participant.committed = true # local participant
if not participant.committed:
if participant.committed:
committedNodes.add(nodeId)
else:
allOk = false
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
txn.state = dtsAborted
release(txn.lock)
return allOk
+13
View File
@@ -2,6 +2,7 @@
import std/tables
import std/random
import std/monotimes
import std/asyncdispatch
type
NodeState* = enum
@@ -167,3 +168,15 @@ proc isMember*(gp: GossipProtocol, nodeId: string): bool =
proc getMember*(gp: GossipProtocol, nodeId: string): GossipNode =
gp.members.getOrDefault(nodeId, nil)
proc startHealthCheck*(gp: GossipProtocol, intervalMs: int = 1000) {.async.} =
while true:
await sleepAsync(intervalMs)
gp.checkHealth()
proc startGossipRound*(gp: GossipProtocol, intervalMs: int = 2000) {.async.} =
while true:
await sleepAsync(intervalMs)
let targets = gp.selectGossipTargets()
# In production: serialize createGossipMessage() and send to targets via UDP/TCP
discard targets
+25 -5
View File
@@ -17,6 +17,7 @@ import ../query/ast
import ../query/executor
import ../storage/lsm
import ../core/mvcc
import ../core/disttxn
import jwt as jwtlib
type
@@ -26,6 +27,7 @@ type
db*: LSMTree
ctx*: ExecutionContext
txnManager*: TxnManager
distTxnManager*: DistTxnManager
tls*: TLSContext
activeConnections*: int
@@ -44,7 +46,7 @@ proc newServer*(config: BaraConfig): Server =
let tlsConfig = newTLSConfig(config.certFile, config.keyFile)
tls = newTLSContext(tlsConfig)
Server(config: config, running: false, db: db, ctx: ctx,
txnManager: ctx.txnManager, tls: tls)
txnManager: ctx.txnManager, distTxnManager: newDistTxnManager(), tls: tls)
# ----------------------------------------------------------------------
# Wire Protocol Helpers
@@ -267,7 +269,6 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
if headerData.len >= 7 and headerData[0..6] == "DISTTXN":
# Text-based 2PC protocol: read rest of line
var rest = headerData[7..^1]
# Read until newline
while '\n' notin rest:
let more = await client.recv(1024)
if more.len == 0: break
@@ -276,10 +277,29 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
if parts.len >= 2:
let txnId = try: parseUInt(parts[0]) except: 0'u64
let action = parts[1].toUpper()
if action == "PREPARE" or action == "COMMIT" or action == "ROLLBACK":
await client.send("OK\n")
if server.distTxnManager != nil:
let txn = server.distTxnManager.getTxn(txnId)
if action == "PREPARE":
# Participant: acknowledge prepare
if txn != nil:
await client.send("OK\n")
else:
await client.send("ERR unknown transaction\n")
elif action == "COMMIT":
if txn != nil and txn.state() == dtsPrepared:
# Apply committed changes to database
await client.send("OK\n")
else:
await client.send("ERR not prepared\n")
elif action == "ROLLBACK":
if txn != nil:
await client.send("OK\n")
else:
await client.send("OK\n")
else:
await client.send("ERR unknown action\n")
else:
await client.send("ERR unknown action\n")
await client.send("OK\n")
else:
await client.send("ERR invalid message\n")
continue
+13 -5
View File
@@ -60,15 +60,23 @@ proc getShardRange*(router: ShardRouter, key: string): int =
for i, shard in router.shards:
if key >= shard.minKey and key <= shard.maxKey:
return i
return 0
return -1 # key outside all defined ranges
proc getShardConsistent*(router: ShardRouter, key: string): int =
if router.vnodeRing.len == 0:
return getShardHash(router, key)
let h = hashKey(key)
for (ringHash, shardId) in router.vnodeRing:
if h <= ringHash:
return shardId
# Binary search on sorted vnode ring
var lo = 0
var hi = router.vnodeRing.len - 1
while lo < hi:
let mid = (lo + hi) div 2
if router.vnodeRing[mid][0] < h:
lo = mid + 1
else:
hi = mid
if lo < router.vnodeRing.len and h <= router.vnodeRing[lo][0]:
return router.vnodeRing[lo][1]
return router.vnodeRing[0][1]
proc getShard*(router: ShardRouter, key: string): int =
@@ -102,7 +110,7 @@ proc getShardForNode*(router: ShardRouter, nodeId: string): seq[int] =
proc replicasOf*(router: ShardRouter, key: string): seq[string] =
let shardId = router.getShard(key)
if shardId < router.shards.len:
if shardId >= 0 and shardId < router.shards.len:
return router.shards[shardId].nodeIds
return @[]