fix: non-blocking replication/disttxn timeouts, OTLP epoch time, test paths, compiler warnings

This commit is contained in:
2026-05-07 15:41:33 +03:00
parent d3b38251c3
commit c8633b9589
7 changed files with 87 additions and 62 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ task build_release, "Build release version":
exec "nim c -d:ssl -d:release --opt:speed -o:build/baradadb src/baradadb.nim" exec "nim c -d:ssl -d:release --opt:speed -o:build/baradadb src/baradadb.nim"
task test, "Run all tests": task test, "Run all tests":
exec "nim c -d:ssl -r tests/test_all.nim" exec "nim c --path:src -d:ssl -r tests/test_all.nim"
task bench, "Run benchmarks": task bench, "Run benchmarks":
exec "nim c -d:ssl -d:release -r benchmarks/bench_storage.nim" exec "nim c --path:src -d:ssl -d:release -r benchmarks/bench_storage.nim"
+18 -1
View File
@@ -4,6 +4,7 @@ import std/locks
import std/monotimes import std/monotimes
import std/net import std/net
import std/strutils import std/strutils
import std/nativesockets
type type
DistTxnState* = enum DistTxnState* = enum
@@ -75,13 +76,29 @@ proc addParticipant*(txn: DistributedTransaction, nodeId: string,
) )
release(txn.lock) release(txn.lock)
proc connectWithTimeout(sock: Socket, host: string, port: Port, timeoutMs: int): bool =
## Non-blocking connect with timeout to avoid hanging on unreachable hosts.
sock.getFd.setBlocking(false)
try:
sock.connect(host, port)
sock.getFd.setBlocking(true)
return true
except OSError:
var fds = @[sock.getFd]
if selectWrite(fds, timeoutMs) <= 0:
return false
sock.getFd.setBlocking(true)
return true
proc sendDistTxnRpc(host: string, port: int, txnId: uint64, action: string, timeoutMs: int = 5000): bool = proc sendDistTxnRpc(host: string, port: int, txnId: uint64, action: string, timeoutMs: int = 5000): bool =
## Send 2PC RPC to participant node via TCP text protocol. ## Send 2PC RPC to participant node via TCP text protocol.
## Protocol: "DISTTXN <txnId> <action>\n" where action = PREPARE|COMMIT|ROLLBACK ## Protocol: "DISTTXN <txnId> <action>\n" where action = PREPARE|COMMIT|ROLLBACK
## Response: "OK\n" or "ERR <msg>\n" ## Response: "OK\n" or "ERR <msg>\n"
try: try:
var sock = newSocket() var sock = newSocket()
sock.connect(host, Port(port)) if not connectWithTimeout(sock, host, Port(port), timeoutMs):
sock.close()
return false
let msg = "DISTTXN " & $txnId & " " & action & "\n" let msg = "DISTTXN " & $txnId & " " & action & "\n"
sock.send(msg) sock.send(msg)
var response = "" var response = ""
+8 -9
View File
@@ -16,7 +16,6 @@ import ../query/executor
import ../storage/lsm import ../storage/lsm
import ../core/mvcc import ../core/mvcc
import ../protocol/wire import ../protocol/wire
import ../protocol/ratelimit
import ../core/websocket import ../core/websocket
import jwt as jwtlib import jwt as jwtlib
@@ -147,30 +146,30 @@ proc queryHandler(server: HttpServer): RequestHandler =
of JString: params.add(WireValue(kind: fkString, strVal: p.getStr())) of JString: params.add(WireValue(kind: fkString, strVal: p.getStr()))
else: params.add(WireValue(kind: fkString, strVal: $p)) else: params.add(WireValue(kind: fkString, strVal: $p))
let result = executor.executeQuery(reqCtx, astNode, params) let res = executor.executeQuery(reqCtx, astNode, params)
if result.success: if res.success:
var jsonRows = newJArray() var jsonRows = newJArray()
for row in result.rows: for row in res.rows:
var jsonRow = newJObject() var jsonRow = newJObject()
for col in result.columns: for col in res.columns:
let key = col let key = col
var val = "" var val = ""
if key in row: val = row[key] if key in row: val = row[key]
jsonRow[key] = %val jsonRow[key] = %val
jsonRows.add(jsonRow) jsonRows.add(jsonRow)
var jsonCols = newJArray() var jsonCols = newJArray()
for c in result.columns: for c in res.columns:
jsonCols.add(%c) jsonCols.add(%c)
ctx.json(%*{ ctx.json(%*{
"rows": jsonRows, "rows": jsonRows,
"affectedRows": result.affectedRows, "affectedRows": res.affectedRows,
"columns": jsonCols, "columns": jsonCols,
"message": if result.message.len > 0: %result.message else: newJNull() "message": if res.message.len > 0: %res.message else: newJNull()
}) })
else: else:
server.metrics.queryErrors += 1 server.metrics.queryErrors += 1
ctx.json(%*{"error": result.message}, 400) ctx.json(%*{"error": res.message}, 400)
proc healthHandler(): RequestHandler = proc healthHandler(): RequestHandler =
return proc(request: Request) {.gcsafe.} = return proc(request: Request) {.gcsafe.} =
+20 -1
View File
@@ -4,6 +4,7 @@ import std/sets
import std/locks import std/locks
import std/net import std/net
import std/strutils import std/strutils
import std/nativesockets
type type
@@ -72,13 +73,29 @@ proc connectReplica*(rm: ReplicationManager, id: string) =
rm.replicas[id].connected = true rm.replicas[id].connected = true
release(rm.lock) release(rm.lock)
proc connectWithTimeout(sock: Socket, host: string, port: Port, timeoutMs: int): bool =
## Non-blocking connect with timeout to avoid hanging on unreachable hosts.
sock.getFd.setBlocking(false)
try:
sock.connect(host, port)
sock.getFd.setBlocking(true)
return true
except OSError:
var fds = @[sock.getFd]
if selectWrite(fds, timeoutMs) <= 0:
return false
sock.getFd.setBlocking(true)
return true
proc shipToReplica(replica: Replica, lsn: uint64, data: seq[byte]): bool = proc shipToReplica(replica: Replica, lsn: uint64, data: seq[byte]): bool =
## Send replication data to a replica via TCP. ## Send replication data to a replica via TCP.
## Protocol: "REP <lsn> <dataLen>\n<data>" ## Protocol: "REP <lsn> <dataLen>\n<data>"
## Response: "ACK <lsn>\n" on success ## Response: "ACK <lsn>\n" on success
try: try:
var sock = newSocket() var sock = newSocket()
sock.connect(replica.host, Port(replica.port)) if not connectWithTimeout(sock, replica.host, Port(replica.port), 500):
sock.close()
return false
let header = "REP " & $lsn & " " & $data.len & "\n" let header = "REP " & $lsn & " " & $data.len & "\n"
sock.send(header) sock.send(header)
if data.len > 0: if data.len > 0:
@@ -108,6 +125,7 @@ proc writeLsn*(rm: ReplicationManager, data: seq[byte]): uint64 =
discard shipToReplica(replica, lsn, data) discard shipToReplica(replica, lsn, data)
return lsn return lsn
of rmSync: of rmSync:
if replicasToShip.len > 0:
rm.pendingAcks[lsn] = initHashSet[string]() rm.pendingAcks[lsn] = initHashSet[string]()
for replica in replicasToShip: for replica in replicasToShip:
rm.pendingAcks[lsn].incl(replica.id) rm.pendingAcks[lsn].incl(replica.id)
@@ -116,6 +134,7 @@ proc writeLsn*(rm: ReplicationManager, data: seq[byte]): uint64 =
discard shipToReplica(replica, lsn, data) discard shipToReplica(replica, lsn, data)
return lsn return lsn
of rmSemiSync: of rmSemiSync:
if replicasToShip.len > 0:
rm.pendingAcks[lsn] = initHashSet[string]() rm.pendingAcks[lsn] = initHashSet[string]()
var count = 0 var count = 0
for replica in replicasToShip: for replica in replicasToShip:
+13 -26
View File
@@ -33,11 +33,6 @@ type
tls*: TLSContext tls*: TLSContext
activeConnections*: int activeConnections*: int
ClientConnection = ref object
socket: AsyncSocket
id: int
currentTxn: Transaction
proc newServer*(config: BaraConfig): Server = proc newServer*(config: BaraConfig): Server =
let dataDir = config.dataDir / "server" let dataDir = config.dataDir / "server"
let db = newLSMTree(dataDir) let db = newLSMTree(dataDir)
@@ -61,12 +56,6 @@ proc readUint32BE(data: string, pos: int): uint32 =
bytes[i] = byte(data[pos + i]) bytes[i] = byte(data[pos + i])
bigEndian32(addr result, unsafeAddr bytes) bigEndian32(addr result, unsafeAddr bytes)
proc writeUint32BE(val: uint32): array[4, byte] =
bigEndian32(addr result, unsafeAddr val)
proc writeUint64BE(val: uint64): array[8, byte] =
bigEndian64(addr result, unsafeAddr val)
proc parseHeader(data: string): (bool, MessageHeader) = proc parseHeader(data: string): (bool, MessageHeader) =
if data.len < 12: if data.len < 12:
return (false, MessageHeader()) return (false, MessageHeader())
@@ -123,18 +112,18 @@ 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(), "")
let result = executor.executeQuery(ctx, astNode, params) let res = executor.executeQuery(ctx, astNode, params)
if result.success: if res.success:
# Ship written key-value pairs to replicas # Ship written key-value pairs to replicas
if replication != nil and result.keyValuePairs.len > 0: if replication != nil and res.keyValuePairs.len > 0:
for (key, value) in result.keyValuePairs: for (key, value) in res.keyValuePairs:
var data = newSeq[byte](key.len + 1 + value.len) var data = newSeq[byte](key.len + 1 + value.len)
for i, c in key: data[i] = byte(c) for i, c in key: data[i] = byte(c)
data[key.len] = byte(0) data[key.len] = byte(0)
for i, c in value: data[key.len + 1 + i] = c for i, c in value: data[key.len + 1 + i] = c
discard replication.writeLsn(data) discard replication.writeLsn(data)
var qr = QueryResult(affectedRows: result.affectedRows, rowCount: result.rows.len) var qr = QueryResult(affectedRows: res.affectedRows, rowCount: res.rows.len)
qr.columns = result.columns qr.columns = res.columns
var colTypes: seq[string] = @[] var colTypes: seq[string] = @[]
var tableName = "" var tableName = ""
@@ -147,7 +136,7 @@ proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq
if tableName.len > 0 and tableName in ctx.tables: if tableName.len > 0 and tableName in ctx.tables:
let tbl = ctx.tables[tableName] let tbl = ctx.tables[tableName]
for col in result.columns: for col in res.columns:
var found = "" var found = ""
for c in tbl.columns: for c in tbl.columns:
if c.name.toLower() == col.toLower(): if c.name.toLower() == col.toLower():
@@ -155,20 +144,20 @@ proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq
break break
colTypes.add(found) colTypes.add(found)
else: else:
colTypes = newSeq[string](result.columns.len) colTypes = newSeq[string](res.columns.len)
qr.columnTypes = colTypes.mapIt(typeToFieldKind(it)) qr.columnTypes = colTypes.mapIt(typeToFieldKind(it))
qr.rows = @[] qr.rows = @[]
for row in result.rows: for row in res.rows:
var wireRow: seq[WireValue] = @[] var wireRow: seq[WireValue] = @[]
for i, col in result.columns: for i, col in res.columns:
let val = if col in row: row[col] else: "" let val = if col in row: row[col] else: ""
let cType = if i < colTypes.len: colTypes[i] else: "" let cType = if i < colTypes.len: colTypes[i] else: ""
wireRow.add(valueToWire(val, cType)) wireRow.add(valueToWire(val, cType))
qr.rows.add(wireRow) qr.rows.add(wireRow)
return (true, qr, result.message) return (true, qr, res.message)
else: else:
return (false, QueryResult(), result.message) return (false, QueryResult(), res.message)
except Exception as e: except Exception as e:
return (false, QueryResult(), e.msg) return (false, QueryResult(), e.msg)
@@ -265,7 +254,6 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
info("Client " & $clientId & " connected") info("Client " & $clientId & " connected")
var connCtx = cloneForConnection(server.ctx) var connCtx = cloneForConnection(server.ctx)
let idleTimeout = server.config.idleTimeoutMs let idleTimeout = server.config.idleTimeoutMs
let queryTimeout = server.config.queryTimeoutMs
let slowThreshold = server.config.slowQueryThresholdMs let slowThreshold = server.config.slowQueryThresholdMs
let slowLog = server.config.slowQueryLogPath let slowLog = server.config.slowQueryLogPath
var authenticated = not server.config.authEnabled var authenticated = not server.config.authEnabled
@@ -366,7 +354,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
case header.kind case header.kind
of mkAuth: of mkAuth:
let tokenStr = parseAuthMessage(cast[seq[byte]](payload)) let tokenStr = parseAuthMessage(cast[seq[byte]](payload))
let (valid, userId, role) = verifyToken(secret, tokenStr) let (valid, userId, _) = verifyToken(secret, tokenStr)
if valid: if valid:
authenticated = true authenticated = true
let okMsg = makeAuthOkMessage(header.requestId) let okMsg = makeAuthOkMessage(header.requestId)
@@ -420,7 +408,6 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
await client.send(cast[string](errorMsg)) await client.send(cast[string](errorMsg))
of mkPing: of mkPing:
var pongPayload: seq[byte] = @[]
var pongMsg = WireMessage( var pongMsg = WireMessage(
header: MessageHeader(kind: mkPong, length: 0, requestId: header.requestId), header: MessageHeader(kind: mkPong, length: 0, requestId: header.requestId),
payload: @[], payload: @[],
+8 -3
View File
@@ -16,8 +16,10 @@ type
spanId*: string spanId*: string
parentSpanId*: string parentSpanId*: string
name*: string name*: string
startTime*: int64 # monotonic ticks startTime*: int64 # monotonic ticks (for duration)
startTimeEpochNs*: int64 # Unix epoch nanoseconds (for OTLP)
endTime*: int64 endTime*: int64
endTimeEpochNs*: int64
durationMs*: float64 durationMs*: float64
status*: SpanStatus status*: SpanStatus
attributes*: Table[string, string] attributes*: Table[string, string]
@@ -36,12 +38,14 @@ proc genId(tracer: Tracer): string =
proc beginSpan*(tracer: Tracer, name: string, attributes: Table[string, string] = initTable[string, string]()): Span = proc beginSpan*(tracer: Tracer, name: string, attributes: Table[string, string] = initTable[string, string]()): Span =
if not tracer.enabled: return nil if not tracer.enabled: return nil
let nowNs = int64(epochTime() * 1_000_000_000)
let span = Span( let span = Span(
traceId: if tracer.activeSpan != nil: tracer.activeSpan.traceId else: genId(tracer), traceId: if tracer.activeSpan != nil: tracer.activeSpan.traceId else: genId(tracer),
spanId: genId(tracer), spanId: genId(tracer),
parentSpanId: if tracer.activeSpan != nil: tracer.activeSpan.spanId else: "", parentSpanId: if tracer.activeSpan != nil: tracer.activeSpan.spanId else: "",
name: name, name: name,
startTime: getMonoTime().ticks(), startTime: getMonoTime().ticks(),
startTimeEpochNs: nowNs,
attributes: attributes, attributes: attributes,
) )
tracer.spans.add(span) tracer.spans.add(span)
@@ -51,6 +55,7 @@ proc beginSpan*(tracer: Tracer, name: string, attributes: Table[string, string]
proc endSpan*(tracer: Tracer, span: Span, status: SpanStatus = ssOk) = proc endSpan*(tracer: Tracer, span: Span, status: SpanStatus = ssOk) =
if span == nil or not tracer.enabled: return if span == nil or not tracer.enabled: return
span.endTime = getMonoTime().ticks() span.endTime = getMonoTime().ticks()
span.endTimeEpochNs = int64(epochTime() * 1_000_000_000)
span.durationMs = float64(span.endTime - span.startTime) / 1_000_000.0 span.durationMs = float64(span.endTime - span.startTime) / 1_000_000.0
span.status = status span.status = status
if span.parentSpanId.len > 0: if span.parentSpanId.len > 0:
@@ -100,8 +105,8 @@ proc exportOtlp*(tracer: Tracer, endpoint: string = "http://localhost:4318/v1/tr
"parentSpanId": span.parentSpanId, "parentSpanId": span.parentSpanId,
"name": span.name, "name": span.name,
"kind": "SPAN_KIND_INTERNAL", "kind": "SPAN_KIND_INTERNAL",
"startTimeUnixNano": $span.startTime, "startTimeUnixNano": $span.startTimeEpochNs,
"endTimeUnixNano": $span.endTime, "endTimeUnixNano": $span.endTimeEpochNs,
"status": {"code": if span.status == ssOk: "STATUS_CODE_OK" else: "STATUS_CODE_ERROR"}, "status": {"code": if span.status == ssOk: "STATUS_CODE_OK" else: "STATUS_CODE_ERROR"},
"attributes": attrs, "attributes": attrs,
}) })
+9 -11
View File
@@ -1246,8 +1246,6 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
let v = evalExpr(expr.aggArgs[0], row) let v = evalExpr(expr.aggArgs[0], row)
if maxVal == "" or v > maxVal: maxVal = v if maxVal == "" or v > maxVal: maxVal = v
newRow[alias] = maxVal newRow[alias] = maxVal
else:
newRow[alias] = ""
else: else:
let val = evalExpr(expr, if sourceRows.len > 0: sourceRows[0] else: initTable[string, string]()) let val = evalExpr(expr, if sourceRows.len > 0: sourceRows[0] else: initTable[string, string]())
if alias.len > 0: newRow[alias] = val if alias.len > 0: newRow[alias] = val
@@ -2275,11 +2273,11 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
storedRec.checksum[0..<16] & ", Expected: " & expectedChecksum[0..<16]) storedRec.checksum[0..<16] & ", Expected: " & expectedChecksum[0..<16])
let startTime = epochTime() let startTime = epochTime()
let result = executeMigrationSql(ctx, upBody) let res = executeMigrationSql(ctx, upBody)
let durationMs = int((epochTime() - startTime) * 1000) let durationMs = int((epochTime() - startTime) * 1000)
if not result.success: if not res.success:
return errResult("Migration '" & stmt.amName & "' failed: " & result.message) return errResult("Migration '" & stmt.amName & "' failed: " & res.message)
ctx.db.put(migrationAppliedKey(stmt.amName), cast[seq[byte]]("applied")) ctx.db.put(migrationAppliedKey(stmt.amName), cast[seq[byte]]("applied"))
setMigrationRecord(ctx, MigrationRecord( setMigrationRecord(ctx, MigrationRecord(
@@ -2332,10 +2330,10 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
if not found: if not found:
return errResult("Migration '" & name & "' not found during batch apply") return errResult("Migration '" & name & "' not found during batch apply")
let startTime = epochTime() let startTime = epochTime()
let result = executeMigrationSql(ctx, upBody) let res = executeMigrationSql(ctx, upBody)
let durationMs = int((epochTime() - startTime) * 1000) let durationMs = int((epochTime() - startTime) * 1000)
if not result.success: if not res.success:
return errResult("Migration '" & name & "' failed: " & result.message & return errResult("Migration '" & name & "' failed: " & res.message &
" (" & $appliedCount & " migrations applied before failure)") " (" & $appliedCount & " migrations applied before failure)")
ctx.db.put(migrationAppliedKey(name), cast[seq[byte]]("applied")) ctx.db.put(migrationAppliedKey(name), cast[seq[byte]]("applied"))
setMigrationRecord(ctx, MigrationRecord( setMigrationRecord(ctx, MigrationRecord(
@@ -2375,9 +2373,9 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
return errResult("Migration '" & name & "' not found during rollback") return errResult("Migration '" & name & "' not found during rollback")
if downBody.len == 0: if downBody.len == 0:
return errResult("Migration '" & name & "' has no DOWN script") return errResult("Migration '" & name & "' has no DOWN script")
let result = executeMigrationSql(ctx, downBody) let res = executeMigrationSql(ctx, downBody)
if not result.success: if not res.success:
return errResult("Rollback of '" & name & "' failed: " & result.message) return errResult("Rollback of '" & name & "' failed: " & res.message)
ctx.db.delete(migrationAppliedKey(name)) ctx.db.delete(migrationAppliedKey(name))
var rec = getMigrationRecord(ctx, name) var rec = getMigrationRecord(ctx, name)
rec.rolledBack = true rec.rolledBack = true