fix(raft): multi-stmt write gate, rich apply, delete kv convention
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
- Reject follower writes if any statement in the batch is DML/COMMIT (not only stmts[0]). - COMMIT always emits empty-valued kvPairs for isDelete entries. - applyCommand updates LSM plus secondary B-tree/FTS/HNSW indexes (applyReplicatedPut/Delete) so follower index scans see replicated rows. - Tests: not-leader append, commit timeout, index apply unit, E2E index-backed SELECT on follower.
This commit is contained in:
@@ -396,3 +396,15 @@ suite "Raft write classification":
|
||||
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])
|
||||
|
||||
test "multi-statement queries with a trailing write are still writes":
|
||||
## Server rejection must not look only at stmts[0] — a SELECT first
|
||||
## would otherwise let a follower execute the INSERT.
|
||||
let ast = parse("SELECT 1; INSERT INTO t (id) VALUES (1)")
|
||||
check ast.stmts.len == 2
|
||||
check not isWrite(ast.stmts[0])
|
||||
check isWrite(ast.stmts[1])
|
||||
var anyWrite = false
|
||||
for s in ast.stmts:
|
||||
if isWrite(s): anyWrite = true
|
||||
check anyWrite
|
||||
|
||||
@@ -211,14 +211,15 @@ proc runWritesScenario() =
|
||||
return
|
||||
echo "leader elected: ", nodes[leaderIdx].id, " (term ", leaderTerm, ")"
|
||||
|
||||
# Schema: CREATE TABLE is not a raft write (DML only is), and its _schema
|
||||
# keys are not replicated — create the table locally on every node.
|
||||
# Schema: CREATE TABLE / INDEX are not raft writes (no kvPairs), and
|
||||
# _schema keys are not replicated — create them locally on every node.
|
||||
for i in 0 ..< nodes.len:
|
||||
let db = openClient(nodes[i].clientPort)
|
||||
try:
|
||||
db.exec(sql"CREATE TABLE rw_test (id INT PRIMARY KEY, name STRING)")
|
||||
db.exec(sql"CREATE INDEX idx_rw_name ON rw_test (name)")
|
||||
except CatchableError as e:
|
||||
echo "CREATE TABLE failed on ", nodes[i].id, ": ", e.msg
|
||||
echo "CREATE TABLE/INDEX failed on ", nodes[i].id, ": ", e.msg
|
||||
dumpAll(nodes)
|
||||
fail()
|
||||
return
|
||||
@@ -250,6 +251,31 @@ proc runWritesScenario() =
|
||||
return
|
||||
echo "row replicated to follower ", nodes[followerIdx].id
|
||||
|
||||
# Index path: rich apply must have updated the follower B-tree so a
|
||||
# filtered SELECT (planner prefers secondary index) still finds the row.
|
||||
block:
|
||||
let db = openClient(nodes[followerIdx].clientPort)
|
||||
var saw = false
|
||||
try:
|
||||
let rows = db.getAllRows(
|
||||
sql"SELECT id, name FROM rw_test WHERE name = 'raft-row'")
|
||||
for row in rows:
|
||||
if row.len >= 2 and row[1] == "raft-row":
|
||||
saw = true
|
||||
except CatchableError as e:
|
||||
echo "follower index SELECT failed: ", e.msg
|
||||
dumpAll(nodes)
|
||||
fail()
|
||||
return
|
||||
db.close()
|
||||
if not saw:
|
||||
echo "follower ", nodes[followerIdx].id,
|
||||
" index-backed SELECT missed the replicated row"
|
||||
dumpAll(nodes)
|
||||
fail()
|
||||
return
|
||||
echo "follower index-backed SELECT saw the row"
|
||||
|
||||
# Follower rejection: DML on a follower must fail with "not leader".
|
||||
block:
|
||||
let db = openClient(nodes[followerIdx].clientPort)
|
||||
|
||||
@@ -2706,6 +2706,44 @@ suite "Raft SQL Write Path":
|
||||
check res.keyValuePairs.len == 1
|
||||
check res.keyValuePairs[0][1].len == 0
|
||||
|
||||
test "appendWriteToRaft fails when node is not leader":
|
||||
var n = newRaftNode("n1", @["n2"], raftPort = 29111)
|
||||
# Still a follower — appendLog returns index 0.
|
||||
let (ok, err) = waitFor appendWriteToRaft(n,
|
||||
@[("k", cast[seq[byte]]("v"))], timeoutMs = 200)
|
||||
check not ok
|
||||
check "lost leadership" in err
|
||||
|
||||
test "appendWriteToRaft times out without majority replies":
|
||||
# Leader with peers but no network — commitIndex never advances.
|
||||
var n = newRaftNode("n1", @["n2", "n3"], raftPort = 29112)
|
||||
n.becomeLeader()
|
||||
let (ok, err) = waitFor appendWriteToRaft(n,
|
||||
@[("k", cast[seq[byte]]("v"))], timeoutMs = 300)
|
||||
check not ok
|
||||
check "raft commit timeout" in err
|
||||
|
||||
test "applyReplicatedPut updates secondary B-tree indexes":
|
||||
var testDir = getTempDir() / "baradb_raft_apply_idx_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks
|
||||
createDir(testDir)
|
||||
var db = newLSMTree(testDir)
|
||||
var ctx = qexec.newExecutionContext(db)
|
||||
discard qexec.executeQuery(ctx, parse(
|
||||
"CREATE TABLE t (id INT PRIMARY KEY, name STRING)"))
|
||||
discard qexec.executeQuery(ctx, parse(
|
||||
"CREATE INDEX idx_name ON t (name)"))
|
||||
# Simulate follower apply of a leader-replicated INSERT (no execInsert).
|
||||
applyReplicatedPut(ctx, "t.id=1", cast[seq[byte]]("name=alice"))
|
||||
check "t.name" in ctx.btrees
|
||||
let entries = ctx.btrees["t.name"].get("alice")
|
||||
check entries.len >= 1
|
||||
check entries[0].lsmKey == "t.id=1"
|
||||
# Delete must drop the index entry too.
|
||||
applyReplicatedDelete(ctx, "t.id=1")
|
||||
check ctx.btrees["t.name"].get("alice").len == 0
|
||||
let (found, _) = db.get("t.id=1")
|
||||
check not found
|
||||
|
||||
suite "CLI Autocomplete":
|
||||
test "Autocomplete commands":
|
||||
let res = autocomplete("he")
|
||||
|
||||
Reference in New Issue
Block a user