feat(raft): replicate schema DDL through the raft log
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
- isRaftDdl + leader-only gate for CREATE/DROP/ALTER (not DATABASE) - appendDdlToRaft ships original SQL; applyCommand re-executes via applyReplicatedDdl (idempotent on leader double-apply) - Mixed DDL+DML batches use the DDL path so order is preserved - Fix secondary-index point lookup to use entry.lsmKey (not filter col) - E2E: CREATE only on leader, schema + index SELECT on follower
This commit is contained in:
@@ -17,7 +17,7 @@ Leader election и log репликация през TCP. Включване:
|
|||||||
| `BARADB_RAFT_PEERS` | Списък `id@host:port` (вкл. себе си) |
|
| `BARADB_RAFT_PEERS` | Списък `id@host:port` (вкл. себе си) |
|
||||||
| `BARADB_RAFT_WRITE_TIMEOUT_MS` | Макс. изчакване за majority commit при SQL записи (по подразбиране 5000) |
|
| `BARADB_RAFT_WRITE_TIMEOUT_MS` | Макс. изчакване за majority commit при SQL записи (по подразбиране 5000) |
|
||||||
|
|
||||||
Когато Raft е активен, SQL DML (`INSERT`/`UPDATE`/`DELETE`/`MERGE` и транзакционен `COMMIT`) се приема само от лидера на **`default`** базата: KV двойките се добавят в Raft лога и клиентът чака majority commit. Followers отказват записи с `not leader; leader is '…'`. Записи към друга database name се отказват (`raft writes only supported on the 'default' database`). Приложените записи обновяват LSM + secondary B-tree/FTS/HNSW индекси и in-memory графи на всеки възел. DDL (напр. `CREATE TABLE`) още не се репликира — схемата трябва да се създаде на всеки възел.
|
Когато Raft е активен, SQL DML (`INSERT`/`UPDATE`/`DELETE`/`MERGE` и транзакционен `COMMIT`) и schema DDL (`CREATE`/`DROP`/`ALTER` table, index, view, graph, …) се приемат само от лидера на **`default`** базата. DML отива като put/delete; DDL — като `ddl` запис с оригиналния SQL, преизпълнен на всеки възел при apply. Followers отказват и двете с `not leader; leader is '…'`. Записи към друга database name се отказват. `CREATE`/`DROP DATABASE` не се репликират през raft (multi-DB е извън v1). Приложен DML обновява и secondary B-tree/FTS/HNSW индекси и in-memory графи.
|
||||||
|
|
||||||
```nim
|
```nim
|
||||||
import barabadb/core/raft
|
import barabadb/core/raft
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Leader election and log replication over TCP. Enable with:
|
|||||||
| `BARADB_RAFT_PEERS` | Comma-separated `id@host:port` (include self) |
|
| `BARADB_RAFT_PEERS` | Comma-separated `id@host:port` (include self) |
|
||||||
| `BARADB_RAFT_WRITE_TIMEOUT_MS` | Max wait for majority commit on SQL writes (default 5000) |
|
| `BARADB_RAFT_WRITE_TIMEOUT_MS` | Max wait for majority commit on SQL writes (default 5000) |
|
||||||
|
|
||||||
When Raft is enabled, SQL DML (`INSERT`/`UPDATE`/`DELETE`/`MERGE` and transactional `COMMIT`) is accepted only on the leader of the **`default`** database: each write's KV pairs are appended to the Raft log and the client waits until the entry is majority-committed. Followers reject writes with `not leader; leader is '…'`. Writes against any other database name are rejected (`raft writes only supported on the 'default' database`). Committed entries update LSM plus secondary B-tree/FTS/HNSW indexes and in-memory graphs on every node. DDL (e.g. `CREATE TABLE`) is not replicated yet — apply schema on every node.
|
When Raft is enabled, SQL DML (`INSERT`/`UPDATE`/`DELETE`/`MERGE` and transactional `COMMIT`) and schema DDL (`CREATE`/`DROP`/`ALTER` table, index, view, graph, …) are accepted only on the leader of the **`default`** database. DML ships as put/delete log entries; DDL ships as a `ddl` entry with the original SQL and is re-executed on every node at apply. Followers reject both with `not leader; leader is '…'`. Writes against any other database name are rejected (`raft writes only supported on the 'default' database`). `CREATE`/`DROP DATABASE` are not raft-replicated (multi-DB is out of scope for v1). Committed DML also updates secondary B-tree/FTS/HNSW indexes and in-memory graphs.
|
||||||
|
|
||||||
```nim
|
```nim
|
||||||
import barabadb/core/raft
|
import barabadb/core/raft
|
||||||
|
|||||||
@@ -207,6 +207,14 @@ proc valueToWire(val: string, colType: string): WireValue =
|
|||||||
return WireValue(kind: fkJson, jsonVal: val)
|
return WireValue(kind: fkJson, jsonVal: val)
|
||||||
return WireValue(kind: fkString, strVal: val)
|
return WireValue(kind: fkString, strVal: val)
|
||||||
|
|
||||||
|
proc waitRaftCommit(node: RaftNode, lastIdx: uint64, timeoutMs: int): Future[(bool, string)] {.async.} =
|
||||||
|
let deadline = getMonoTime() + initDuration(milliseconds = timeoutMs)
|
||||||
|
while node.commitIndex < lastIdx and getMonoTime() < deadline:
|
||||||
|
await sleepAsync(10)
|
||||||
|
if node.commitIndex < lastIdx:
|
||||||
|
return (false, "raft commit timeout")
|
||||||
|
return (true, "")
|
||||||
|
|
||||||
proc appendWriteToRaft*(node: RaftNode, kvPairs: seq[(string, seq[byte])],
|
proc appendWriteToRaft*(node: RaftNode, kvPairs: seq[(string, seq[byte])],
|
||||||
timeoutMs: int): Future[(bool, string)] {.async.} =
|
timeoutMs: int): Future[(bool, string)] {.async.} =
|
||||||
## C3b leader write path: append each written KV pair to the Raft log and
|
## C3b leader write path: append each written KV pair to the Raft log and
|
||||||
@@ -226,12 +234,17 @@ proc appendWriteToRaft*(node: RaftNode, kvPairs: seq[(string, seq[byte])],
|
|||||||
if entry.index == 0:
|
if entry.index == 0:
|
||||||
return (false, "lost leadership during raft append")
|
return (false, "lost leadership during raft append")
|
||||||
lastIdx = entry.index
|
lastIdx = entry.index
|
||||||
let deadline = getMonoTime() + initDuration(milliseconds = timeoutMs)
|
return await waitRaftCommit(node, lastIdx, timeoutMs)
|
||||||
while node.commitIndex < lastIdx and getMonoTime() < deadline:
|
|
||||||
await sleepAsync(10)
|
proc appendDdlToRaft*(node: RaftNode, sql: string,
|
||||||
if node.commitIndex < lastIdx:
|
timeoutMs: int): Future[(bool, string)] {.async.} =
|
||||||
return (false, "raft commit timeout")
|
## C3c schema path: append one "ddl" log entry with the original SQL text.
|
||||||
return (true, "")
|
## Followers re-execute it via applyCommand (executor, no raft recursion).
|
||||||
|
## MUST be called outside the storage gate (same as appendWriteToRaft).
|
||||||
|
let entry = node.appendLog("ddl", cast[seq[byte]](sql))
|
||||||
|
if entry.index == 0:
|
||||||
|
return (false, "lost leadership during raft append")
|
||||||
|
return await waitRaftCommit(node, entry.index, timeoutMs)
|
||||||
|
|
||||||
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,
|
replication: ReplicationManager = nil,
|
||||||
@@ -244,6 +257,7 @@ proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq
|
|||||||
var qr = QueryResult()
|
var qr = QueryResult()
|
||||||
var msg = ""
|
var msg = ""
|
||||||
var kvPairs: seq[(string, seq[byte])] = @[]
|
var kvPairs: seq[(string, seq[byte])] = @[]
|
||||||
|
var needsRaftDdl = false
|
||||||
withStorageGate:
|
withStorageGate:
|
||||||
try:
|
try:
|
||||||
let tokens = tokenize(query)
|
let tokens = tokenize(query)
|
||||||
@@ -252,16 +266,15 @@ 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.
|
# C3b/C3c: DML + schema DDL go through the Raft log — only the leader
|
||||||
# Inspect every statement so "SELECT 1; INSERT ..." cannot bypass the gate.
|
# of the default database may accept them. Inspect every statement so
|
||||||
# Raft state machine is wired only to the default database (v1).
|
# "SELECT 1; INSERT/CREATE ..." cannot bypass the gate.
|
||||||
if raftNode != nil:
|
|
||||||
var hasWrite = false
|
var hasWrite = false
|
||||||
|
needsRaftDdl = false
|
||||||
for stmt in astNode.stmts:
|
for stmt in astNode.stmts:
|
||||||
if isWrite(stmt):
|
if isWrite(stmt): hasWrite = true
|
||||||
hasWrite = true
|
if isRaftDdl(stmt): needsRaftDdl = true
|
||||||
break
|
if raftNode != nil and (hasWrite or needsRaftDdl):
|
||||||
if hasWrite:
|
|
||||||
let dbName = if ctx.currentDatabase.len > 0: ctx.currentDatabase else: "default"
|
let dbName = if ctx.currentDatabase.len > 0: ctx.currentDatabase else: "default"
|
||||||
if dbName != "default":
|
if dbName != "default":
|
||||||
return (false, QueryResult(),
|
return (false, QueryResult(),
|
||||||
@@ -322,9 +335,16 @@ proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq
|
|||||||
return (false, QueryResult(), res.message)
|
return (false, QueryResult(), res.message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return (false, QueryResult(), e.msg)
|
return (false, QueryResult(), e.msg)
|
||||||
# C3b: leader appends writes to the Raft log and waits for majority commit
|
# Raft log append + majority wait (outside the storage gate).
|
||||||
# (outside the storage gate — see appendWriteToRaft).
|
# DDL batches ship the original SQL once (re-executed on apply). Pure DML
|
||||||
if ok and raftNode != nil and kvPairs.len > 0:
|
# ships KV pairs. Mixed DDL+DML in one query uses the DDL path only so the
|
||||||
|
# whole batch is re-run in order on followers.
|
||||||
|
if ok and raftNode != nil:
|
||||||
|
if needsRaftDdl:
|
||||||
|
let (raftOk, raftErr) = await appendDdlToRaft(raftNode, query, raftWriteTimeoutMs)
|
||||||
|
if not raftOk:
|
||||||
|
return (false, QueryResult(), raftErr)
|
||||||
|
elif kvPairs.len > 0:
|
||||||
let (raftOk, raftErr) = await appendWriteToRaft(raftNode, kvPairs, raftWriteTimeoutMs)
|
let (raftOk, raftErr) = await appendWriteToRaft(raftNode, kvPairs, raftWriteTimeoutMs)
|
||||||
if not raftOk:
|
if not raftOk:
|
||||||
return (false, QueryResult(), raftErr)
|
return (false, QueryResult(), raftErr)
|
||||||
|
|||||||
@@ -488,3 +488,11 @@ proc applyReplicatedDelete*(ctx: ExecutionContext, fullKey: string) =
|
|||||||
if found and table.len > 0:
|
if found and table.len > 0:
|
||||||
removeIndexesForRow(ctx, table, fullKey, cast[string](existing))
|
removeIndexesForRow(ctx, table, fullKey, cast[string](existing))
|
||||||
ctx.db.delete(fullKey)
|
ctx.db.delete(fullKey)
|
||||||
|
|
||||||
|
proc isBenignRaftReplayError*(msg: string): bool =
|
||||||
|
## Leader re-applies committed DDL/DML after local execution; followers may
|
||||||
|
## also see IF EXISTS / race re-applies. Treat common idempotent failures as OK.
|
||||||
|
let m = msg.toLower()
|
||||||
|
"already exists" in m or "does not exist" in m or
|
||||||
|
"duplicate" in m or "unique" in m or
|
||||||
|
"unknown table" in m or "no such table" in m
|
||||||
|
|||||||
@@ -179,6 +179,17 @@ proc isDDL*(stmt: Node): bool =
|
|||||||
else:
|
else:
|
||||||
result = false
|
result = false
|
||||||
|
|
||||||
|
proc isRaftDdl*(stmt: Node): bool =
|
||||||
|
## Schema changes that go through the Raft log when clustering is on.
|
||||||
|
## CREATE/DROP DATABASE are excluded — multi-DB is out of scope for v1 raft
|
||||||
|
## (state machine is wired only to the default database).
|
||||||
|
if not isDDL(stmt): return false
|
||||||
|
case stmt.kind
|
||||||
|
of nkCreateDatabase, nkDropDatabase:
|
||||||
|
result = false
|
||||||
|
else:
|
||||||
|
result = true
|
||||||
|
|
||||||
proc isWrite*(stmt: Node): bool =
|
proc isWrite*(stmt: Node): bool =
|
||||||
## True for statements that mutate stored data. `nkCommitTxn` is included
|
## True for statements that mutate stored data. `nkCommitTxn` is included
|
||||||
## because COMMIT emits the transaction's buffered kvPairs.
|
## because COMMIT emits the transaction's buffered kvPairs.
|
||||||
|
|||||||
@@ -317,8 +317,23 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
|||||||
row[colName] = w.binRight.strVal
|
row[colName] = w.binRight.strVal
|
||||||
rows.add(row)
|
rows.add(row)
|
||||||
return okResult(rows, coveredCols)
|
return okResult(rows, coveredCols)
|
||||||
# Fetch actual row data from LSM
|
# Fetch full rows via the LSM keys stored in the index — never
|
||||||
let rows = execPointRead(ctx, stmt.selFrom.fromTable, colName & "=" & w.binRight.strVal)
|
# reconstruct the primary key from the filter column (secondary
|
||||||
|
# indexes are not the PK).
|
||||||
|
var rows: seq[Row] = @[]
|
||||||
|
for entry in entries:
|
||||||
|
if entry.lsmKey.len == 0: continue
|
||||||
|
let (found, val) = ctx.db.get(entry.lsmKey)
|
||||||
|
if found:
|
||||||
|
var row = parseRowDataToValueRow(cast[string](val))
|
||||||
|
let prefix = stmt.selFrom.fromTable & "."
|
||||||
|
if entry.lsmKey.startsWith(prefix):
|
||||||
|
let rest = entry.lsmKey[prefix.len..^1]
|
||||||
|
row["$key"] = rest
|
||||||
|
let eqPos = rest.find('=')
|
||||||
|
if eqPos >= 0:
|
||||||
|
row[rest[0..<eqPos]] = rest[eqPos+1..^1]
|
||||||
|
rows.add(row)
|
||||||
let tbl = ctx.getTableDef(stmt.selFrom.fromTable)
|
let tbl = ctx.getTableDef(stmt.selFrom.fromTable)
|
||||||
var cols: seq[string] = @[]
|
var cols: seq[string] = @[]
|
||||||
for c in tbl.columns: cols.add(c.name)
|
for c in tbl.columns: cols.add(c.name)
|
||||||
@@ -1749,6 +1764,22 @@ proc restoreEngines*(ctx: ExecutionContext) =
|
|||||||
except CatchableError as e:
|
except CatchableError as e:
|
||||||
warn("restoreEngines: graph rebuild failed for '" & name & "': " & e.msg)
|
warn("restoreEngines: graph rebuild failed for '" & name & "': " & e.msg)
|
||||||
|
|
||||||
|
proc applyReplicatedDdl*(ctx: ExecutionContext, sql: string) {.gcsafe.} =
|
||||||
|
## Raft "ddl" log entry: re-execute the original SQL on this node's context.
|
||||||
|
## Called from applyCommand (no server/raft layer — must not re-append).
|
||||||
|
## Leader double-apply failures (already exists / does not exist) are ignored
|
||||||
|
## so the state machine keeps advancing. {.cast(gcsafe).} is required because
|
||||||
|
## executeQuery touches the registry factory (same pattern as other engine
|
||||||
|
## callbacks under the storage gate on the single-threaded apply path).
|
||||||
|
{.cast(gcsafe).}:
|
||||||
|
try:
|
||||||
|
let tokens = qlex.tokenize(sql)
|
||||||
|
let astNode = qpar.parse(tokens)
|
||||||
|
if astNode.stmts.len == 0: return
|
||||||
|
discard executeQuery(ctx, astNode)
|
||||||
|
except CatchableError:
|
||||||
|
discard
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Hook wiring — breaks the module cycle between executor and the exec/*
|
# Hook wiring — breaks the module cycle between executor and the exec/*
|
||||||
# submodules: eval.nim calls back into the engine for subqueries, hybrid
|
# submodules: eval.nim calls back into the engine for subqueries, hybrid
|
||||||
|
|||||||
+3
-1
@@ -345,7 +345,7 @@ proc main() =
|
|||||||
raftNode.peerAddrs = config.raftPeerAddrs
|
raftNode.peerAddrs = config.raftPeerAddrs
|
||||||
tcpServer.raftNode = raftNode # C3b: executeQuery rejects writes on followers
|
tcpServer.raftNode = raftNode # C3b: executeQuery rejects writes on followers
|
||||||
# Wire state machine: committed entries update LSM + secondary indexes
|
# Wire state machine: committed entries update LSM + secondary indexes
|
||||||
# (B-tree/FTS/HNSW) on every node via applyReplicatedPut/Delete.
|
# (B-tree/FTS/HNSW/graphs) and re-execute schema DDL on every node.
|
||||||
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.} =
|
||||||
withStorageGate:
|
withStorageGate:
|
||||||
@@ -356,6 +356,8 @@ proc main() =
|
|||||||
applyReplicatedPut(ctx, parts[0], cast[seq[byte]](parts[1]))
|
applyReplicatedPut(ctx, parts[0], cast[seq[byte]](parts[1]))
|
||||||
elif cmd == "delete":
|
elif cmd == "delete":
|
||||||
applyReplicatedDelete(ctx, cast[string](data))
|
applyReplicatedDelete(ctx, cast[string](data))
|
||||||
|
elif cmd == "ddl":
|
||||||
|
applyReplicatedDdl(ctx, cast[string](data))
|
||||||
|
|
||||||
# Wire RAFT ↔ DistTxn
|
# Wire RAFT ↔ DistTxn
|
||||||
wireRaftDistTxn(raftNode, tcpServer)
|
wireRaftDistTxn(raftNode, tcpServer)
|
||||||
|
|||||||
@@ -408,3 +408,11 @@ suite "Raft write classification":
|
|||||||
for s in ast.stmts:
|
for s in ast.stmts:
|
||||||
if isWrite(s): anyWrite = true
|
if isWrite(s): anyWrite = true
|
||||||
check anyWrite
|
check anyWrite
|
||||||
|
|
||||||
|
test "isRaftDdl covers schema but not CREATE DATABASE":
|
||||||
|
check isRaftDdl(parse("CREATE TABLE t (id INT)").stmts[0])
|
||||||
|
check isRaftDdl(parse("CREATE INDEX i ON t (id)").stmts[0])
|
||||||
|
check isRaftDdl(parse("DROP TABLE t").stmts[0])
|
||||||
|
check not isRaftDdl(parse("CREATE DATABASE other").stmts[0])
|
||||||
|
check not isRaftDdl(parse("INSERT INTO t (id) VALUES (1)").stmts[0])
|
||||||
|
check not isRaftDdl(parse("SELECT 1").stmts[0])
|
||||||
|
|||||||
@@ -211,21 +211,62 @@ proc runWritesScenario() =
|
|||||||
return
|
return
|
||||||
echo "leader elected: ", nodes[leaderIdx].id, " (term ", leaderTerm, ")"
|
echo "leader elected: ", nodes[leaderIdx].id, " (term ", leaderTerm, ")"
|
||||||
|
|
||||||
# Schema: CREATE TABLE / INDEX are not raft writes (no kvPairs), and
|
let followerIdx = (if leaderIdx == 0: 1 else: 0)
|
||||||
# _schema keys are not replicated — create them locally on every node.
|
|
||||||
for i in 0 ..< nodes.len:
|
# Schema: CREATE TABLE / INDEX go through the raft "ddl" log (C3c).
|
||||||
let db = openClient(nodes[i].clientPort)
|
# Create only on the leader; followers must learn schema via apply.
|
||||||
|
block:
|
||||||
|
let db = openClient(nodes[leaderIdx].clientPort)
|
||||||
try:
|
try:
|
||||||
db.exec(sql"CREATE TABLE rw_test (id INT PRIMARY KEY, name STRING)")
|
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)")
|
db.exec(sql"CREATE INDEX idx_rw_name ON rw_test (name)")
|
||||||
except CatchableError as e:
|
except CatchableError as e:
|
||||||
echo "CREATE TABLE/INDEX failed on ", nodes[i].id, ": ", e.msg
|
echo "leader CREATE TABLE/INDEX failed: ", e.msg
|
||||||
dumpAll(nodes)
|
dumpAll(nodes)
|
||||||
fail()
|
fail()
|
||||||
return
|
return
|
||||||
db.close()
|
db.close()
|
||||||
|
echo "leader schema committed via raft ddl"
|
||||||
|
|
||||||
let followerIdx = (if leaderIdx == 0: 1 else: 0)
|
# Follower rejection of DDL (not just DML).
|
||||||
|
block:
|
||||||
|
let db = openClient(nodes[followerIdx].clientPort)
|
||||||
|
var rejected = false
|
||||||
|
try:
|
||||||
|
db.exec(sql"CREATE TABLE should_fail (id INT)")
|
||||||
|
except CatchableError as e:
|
||||||
|
rejected = "not leader" in e.msg
|
||||||
|
if not rejected:
|
||||||
|
echo "follower CREATE failed without 'not leader': ", e.msg
|
||||||
|
db.close()
|
||||||
|
if not rejected:
|
||||||
|
echo "follower CREATE was not rejected with 'not leader'"
|
||||||
|
dumpAll(nodes)
|
||||||
|
fail()
|
||||||
|
return
|
||||||
|
echo "follower CREATE rejected with 'not leader'"
|
||||||
|
|
||||||
|
# Wait until the follower has applied CREATE TABLE (SELECT no longer
|
||||||
|
# errors with unknown table). Deadline 5s.
|
||||||
|
block:
|
||||||
|
let db = openClient(nodes[followerIdx].clientPort)
|
||||||
|
defer: db.close()
|
||||||
|
let start = getTime()
|
||||||
|
var ready = false
|
||||||
|
while getTime() - start < initDuration(seconds = 5):
|
||||||
|
try:
|
||||||
|
discard db.getAllRows(sql"SELECT * FROM rw_test")
|
||||||
|
ready = true
|
||||||
|
break
|
||||||
|
except CatchableError:
|
||||||
|
sleep(100)
|
||||||
|
if not ready:
|
||||||
|
echo "follower ", nodes[followerIdx].id,
|
||||||
|
" never applied CREATE TABLE within 5s"
|
||||||
|
dumpAll(nodes)
|
||||||
|
fail()
|
||||||
|
return
|
||||||
|
echo "schema replicated to follower ", nodes[followerIdx].id
|
||||||
|
|
||||||
# Leader write: INSERT goes through the raft log and waits for majority
|
# Leader write: INSERT goes through the raft log and waits for majority
|
||||||
# commit before responding (Task 2) — expect success.
|
# commit before responding (Task 2) — expect success.
|
||||||
|
|||||||
@@ -2706,6 +2706,24 @@ suite "Raft SQL Write Path":
|
|||||||
check res.keyValuePairs.len == 1
|
check res.keyValuePairs.len == 1
|
||||||
check res.keyValuePairs[0][1].len == 0
|
check res.keyValuePairs[0][1].len == 0
|
||||||
|
|
||||||
|
test "appendDdlToRaft fails when node is not leader":
|
||||||
|
var n = newRaftNode("n1", @["n2"], raftPort = 29113)
|
||||||
|
let (ok, err) = waitFor appendDdlToRaft(n,
|
||||||
|
"CREATE TABLE t (id INT)", timeoutMs = 200)
|
||||||
|
check not ok
|
||||||
|
check "lost leadership" in err
|
||||||
|
|
||||||
|
test "applyReplicatedDdl creates table on empty context":
|
||||||
|
var testDir = getTempDir() / "baradb_raft_ddl_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks
|
||||||
|
createDir(testDir)
|
||||||
|
var db = newLSMTree(testDir)
|
||||||
|
var ctx = qexec.newExecutionContext(db)
|
||||||
|
applyReplicatedDdl(ctx, "CREATE TABLE ddl_t (id INT PRIMARY KEY, name STRING)")
|
||||||
|
check "ddl_t" in ctx.tables
|
||||||
|
# Idempotent re-apply (leader double-apply)
|
||||||
|
applyReplicatedDdl(ctx, "CREATE TABLE ddl_t (id INT PRIMARY KEY, name STRING)")
|
||||||
|
check "ddl_t" in ctx.tables
|
||||||
|
|
||||||
test "appendWriteToRaft fails when node is not leader":
|
test "appendWriteToRaft fails when node is not leader":
|
||||||
var n = newRaftNode("n1", @["n2"], raftPort = 29111)
|
var n = newRaftNode("n1", @["n2"], raftPort = 29111)
|
||||||
# Still a follower — appendLog returns index 0.
|
# Still a follower — appendLog returns index 0.
|
||||||
|
|||||||
Reference in New Issue
Block a user