feat(raft): classify writes, reject them on follower nodes
This commit is contained in:
@@ -20,11 +20,13 @@ import ../query/lexer
|
|||||||
import ../query/parser
|
import ../query/parser
|
||||||
import ../query/ast
|
import ../query/ast
|
||||||
import ../query/executor
|
import ../query/executor
|
||||||
|
import ../query/exec/params
|
||||||
import ../storage/lsm
|
import ../storage/lsm
|
||||||
import ../storage/gate
|
import ../storage/gate
|
||||||
import ../core/mvcc
|
import ../core/mvcc
|
||||||
import ../core/disttxn
|
import ../core/disttxn
|
||||||
import ../core/replication
|
import ../core/replication
|
||||||
|
import ../core/raft
|
||||||
import ../core/sharding
|
import ../core/sharding
|
||||||
import ../core/gossip
|
import ../core/gossip
|
||||||
import ../protocol/ratelimit
|
import ../protocol/ratelimit
|
||||||
@@ -41,6 +43,7 @@ type
|
|||||||
txnManager*: TxnManager
|
txnManager*: TxnManager
|
||||||
distTxnManager*: DistTxnManager
|
distTxnManager*: DistTxnManager
|
||||||
replicationManager*: ReplicationManager
|
replicationManager*: ReplicationManager
|
||||||
|
raftNode*: RaftNode
|
||||||
shardRouter*: ShardRouter
|
shardRouter*: ShardRouter
|
||||||
clusterMembership*: ClusterMembership
|
clusterMembership*: ClusterMembership
|
||||||
gossipProtocol*: GossipProtocol
|
gossipProtocol*: GossipProtocol
|
||||||
@@ -204,7 +207,8 @@ proc valueToWire(val: string, colType: string): WireValue =
|
|||||||
return WireValue(kind: fkString, strVal: val)
|
return WireValue(kind: fkString, strVal: val)
|
||||||
|
|
||||||
proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq[WireValue] = @[],
|
proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq[WireValue] = @[],
|
||||||
replication: ReplicationManager = nil): (bool, QueryResult, string) =
|
replication: ReplicationManager = nil,
|
||||||
|
raftNode: RaftNode = nil): (bool, QueryResult, string) =
|
||||||
## All storage access is under the global StorageGate so HTTP worker threads
|
## All storage access is under the global StorageGate so HTTP worker threads
|
||||||
## and the TCP event loop never touch ORC-managed LSM/executor state concurrently.
|
## and the TCP event loop never touch ORC-managed LSM/executor state concurrently.
|
||||||
withStorageGate:
|
withStorageGate:
|
||||||
@@ -215,6 +219,12 @@ proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq
|
|||||||
if astNode.stmts.len == 0:
|
if astNode.stmts.len == 0:
|
||||||
return (true, QueryResult(), "")
|
return (true, QueryResult(), "")
|
||||||
|
|
||||||
|
# C3b: writes go through the Raft log — only the leader may accept them.
|
||||||
|
if raftNode != nil and isWrite(astNode.stmts[0]):
|
||||||
|
if raftNode.state != rsLeader:
|
||||||
|
let who = if raftNode.leaderId.len > 0: raftNode.leaderId else: "none elected"
|
||||||
|
return (false, QueryResult(), "not leader; leader is '" & who & "'")
|
||||||
|
|
||||||
let res = executor.executeQuery(ctx, astNode, params)
|
let res = executor.executeQuery(ctx, astNode, params)
|
||||||
if res.success:
|
if res.success:
|
||||||
# Ship written key-value pairs to replicas
|
# Ship written key-value pairs to replicas
|
||||||
@@ -564,7 +574,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
|
|
||||||
if shardCheck:
|
if shardCheck:
|
||||||
let startTicks = getMonoTime().ticks()
|
let startTicks = getMonoTime().ticks()
|
||||||
let (success, result, errorMsg) = executeQuery(connCtx.db, connCtx, queryStr, replication=server.replicationManager)
|
let (success, result, errorMsg) = executeQuery(connCtx.db, connCtx, queryStr, replication=server.replicationManager, raftNode=server.raftNode)
|
||||||
let durationMs = int((getMonoTime().ticks() - startTicks) div 1_000_000)
|
let durationMs = int((getMonoTime().ticks() - startTicks) div 1_000_000)
|
||||||
|
|
||||||
if durationMs >= slowThreshold:
|
if durationMs >= slowThreshold:
|
||||||
@@ -584,7 +594,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
info("[" & $clientId & "] QueryParams: " & queryStr & " (" & $params.len & " params)")
|
info("[" & $clientId & "] QueryParams: " & queryStr & " (" & $params.len & " params)")
|
||||||
|
|
||||||
let startTicks = getMonoTime().ticks()
|
let startTicks = getMonoTime().ticks()
|
||||||
let (success, result, errorMsg) = executeQuery(connCtx.db, connCtx, queryStr, params, replication=server.replicationManager)
|
let (success, result, errorMsg) = executeQuery(connCtx.db, connCtx, queryStr, params, replication=server.replicationManager, raftNode=server.raftNode)
|
||||||
let durationMs = int((getMonoTime().ticks() - startTicks) div 1_000_000)
|
let durationMs = int((getMonoTime().ticks() - startTicks) div 1_000_000)
|
||||||
|
|
||||||
if durationMs >= slowThreshold:
|
if durationMs >= slowThreshold:
|
||||||
|
|||||||
@@ -178,3 +178,12 @@ proc isDDL*(stmt: Node): bool =
|
|||||||
result = true
|
result = true
|
||||||
else:
|
else:
|
||||||
result = false
|
result = false
|
||||||
|
|
||||||
|
proc isWrite*(stmt: Node): bool =
|
||||||
|
## True for statements that mutate stored data. `nkCommitTxn` is included
|
||||||
|
## because COMMIT emits the transaction's buffered kvPairs.
|
||||||
|
case stmt.kind
|
||||||
|
of nkInsert, nkUpdate, nkDelete, nkMerge, nkCommitTxn:
|
||||||
|
result = true
|
||||||
|
else:
|
||||||
|
result = false
|
||||||
|
|||||||
@@ -341,6 +341,7 @@ proc main() =
|
|||||||
var raftNode = newRaftNode(config.raftNodeId, raftPeers, config.raftPort,
|
var raftNode = newRaftNode(config.raftNodeId, raftPeers, config.raftPort,
|
||||||
dataDir = raftDataDir)
|
dataDir = raftDataDir)
|
||||||
raftNode.peerAddrs = config.raftPeerAddrs
|
raftNode.peerAddrs = config.raftPeerAddrs
|
||||||
|
tcpServer.raftNode = raftNode # C3b: executeQuery rejects writes on followers
|
||||||
# Wire state machine to apply committed entries to the default database
|
# Wire state machine to apply committed entries to the default database
|
||||||
let defaultDbInfo = getDatabaseInfo(registry, "default")
|
let defaultDbInfo = getDatabaseInfo(registry, "default")
|
||||||
raftNode.applyCommand = proc(cmd: string, data: seq[byte]) {.gcsafe.} =
|
raftNode.applyCommand = proc(cmd: string, data: seq[byte]) {.gcsafe.} =
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import std/strutils
|
|||||||
import std/os
|
import std/os
|
||||||
import std/tables
|
import std/tables
|
||||||
import ../src/barabadb/query/[parser, executor, lexer, ast]
|
import ../src/barabadb/query/[parser, executor, lexer, ast]
|
||||||
|
import ../src/barabadb/query/exec/params
|
||||||
import ../src/barabadb/core/types
|
import ../src/barabadb/core/types
|
||||||
import ../src/barabadb/core/config
|
import ../src/barabadb/core/config
|
||||||
import ../src/barabadb/storage/lsm
|
import ../src/barabadb/storage/lsm
|
||||||
@@ -383,3 +384,15 @@ suite "Raft peer address parsing":
|
|||||||
delEnv("BARADB_RAFT_PEERS")
|
delEnv("BARADB_RAFT_PEERS")
|
||||||
check msg.len > 0
|
check msg.len > 0
|
||||||
check bad in msg
|
check bad in msg
|
||||||
|
|
||||||
|
suite "Raft write classification":
|
||||||
|
|
||||||
|
test "isWrite classifies DML and COMMIT":
|
||||||
|
check isWrite(parse("INSERT INTO t (id) VALUES (1)").stmts[0])
|
||||||
|
check isWrite(parse("UPDATE t SET id = 2").stmts[0])
|
||||||
|
check isWrite(parse("DELETE FROM t WHERE id = 1").stmts[0])
|
||||||
|
check isWrite(parse("COMMIT").stmts[0])
|
||||||
|
check not isWrite(parse("SELECT * FROM t").stmts[0])
|
||||||
|
check not isWrite(parse("CREATE TABLE t (id INT)").stmts[0])
|
||||||
|
check not isWrite(parse("BEGIN").stmts[0])
|
||||||
|
check not isWrite(parse("ROLLBACK").stmts[0])
|
||||||
|
|||||||
Reference in New Issue
Block a user