fix: rate limiter, graph/vector/query/server бъгове
Поправени проблеми: - Rate limiter: globalRate вече се enforce-ва; cleanupStaleClients() за memory leak - Graph engine: addEdge проверява дали src/dst nodes съществуват - Vector engine: distance() хвърля ValueError при dimension mismatch - Query: aggregate + * на empty result вече не crash-ва - Query: exprToSql funcCall вече работи с 0 аргумента - Query: INSERT trigger вече не crash-ва при празни VALUES - Server: recvWithTimeout() за всички network reads - Server: защита от negative activeConnections 292 теста, 0 failure-а.
This commit is contained in:
@@ -250,6 +250,16 @@ proc verifyToken(secret, tokenStr: string): (bool, string, string) =
|
|||||||
except:
|
except:
|
||||||
return (false, "", "")
|
return (false, "", "")
|
||||||
|
|
||||||
|
proc recvWithTimeout(client: AsyncSocket, size: int, timeoutMs: int): Future[string] {.async.} =
|
||||||
|
if timeoutMs <= 0:
|
||||||
|
return await client.recv(size)
|
||||||
|
let fut = client.recv(size)
|
||||||
|
let timeoutFut = sleepAsync(timeoutMs)
|
||||||
|
await fut or timeoutFut
|
||||||
|
if fut.finished:
|
||||||
|
return fut.read()
|
||||||
|
return ""
|
||||||
|
|
||||||
proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.} =
|
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)
|
||||||
@@ -270,7 +280,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
# Text-based 2PC protocol: read rest of line
|
# Text-based 2PC protocol: read rest of line
|
||||||
var rest = headerData[7..^1]
|
var rest = headerData[7..^1]
|
||||||
while '\n' notin rest:
|
while '\n' notin rest:
|
||||||
let more = await client.recv(1024)
|
let more = await client.recvWithTimeout(1024, idleTimeout)
|
||||||
if more.len == 0: break
|
if more.len == 0: break
|
||||||
rest.add(more)
|
rest.add(more)
|
||||||
let parts = rest.strip().split(" ")
|
let parts = rest.strip().split(" ")
|
||||||
@@ -308,7 +318,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
if headerData.len >= 4 and headerData[0..3] == "REP ":
|
if headerData.len >= 4 and headerData[0..3] == "REP ":
|
||||||
var rest = headerData[4..^1]
|
var rest = headerData[4..^1]
|
||||||
while '\n' notin rest:
|
while '\n' notin rest:
|
||||||
let more = await client.recv(1024)
|
let more = await client.recvWithTimeout(1024, idleTimeout)
|
||||||
if more.len == 0: break
|
if more.len == 0: break
|
||||||
rest.add(more)
|
rest.add(more)
|
||||||
let parts = rest.strip().split(" ")
|
let parts = rest.strip().split(" ")
|
||||||
@@ -318,7 +328,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
if dataLen > 0:
|
if dataLen > 0:
|
||||||
var data = ""
|
var data = ""
|
||||||
while data.len < dataLen:
|
while data.len < dataLen:
|
||||||
let chunk = await client.recv(dataLen - data.len)
|
let chunk = await client.recvWithTimeout(dataLen - data.len, idleTimeout)
|
||||||
if chunk.len == 0: break
|
if chunk.len == 0: break
|
||||||
data.add(chunk)
|
data.add(chunk)
|
||||||
# Apply replicated data to database
|
# Apply replicated data to database
|
||||||
@@ -424,7 +434,8 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
errorMsg("Client " & $clientId & " error: " & e.msg)
|
errorMsg("Client " & $clientId & " error: " & e.msg)
|
||||||
finally:
|
finally:
|
||||||
dec server.activeConnections
|
if server.activeConnections > 0:
|
||||||
|
dec server.activeConnections
|
||||||
info("Client " & $clientId & " disconnected")
|
info("Client " & $clientId & " disconnected")
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ proc addEdge*(g: Graph, src, dst: NodeId, label: string = "",
|
|||||||
weight: float64 = 1.0): EdgeId =
|
weight: float64 = 1.0): EdgeId =
|
||||||
acquire(g.lock)
|
acquire(g.lock)
|
||||||
defer: release(g.lock)
|
defer: release(g.lock)
|
||||||
|
if src notin g.nodes:
|
||||||
|
raise newException(KeyError, "Source node does not exist: " & $uint64(src))
|
||||||
|
if dst notin g.nodes:
|
||||||
|
raise newException(KeyError, "Destination node does not exist: " & $uint64(dst))
|
||||||
let id = EdgeId(g.nextEdgeId)
|
let id = EdgeId(g.nextEdgeId)
|
||||||
inc g.nextEdgeId
|
inc g.nextEdgeId
|
||||||
g.edges[id] = Edge(id: id, src: src, dst: dst, label: label,
|
g.edges[id] = Edge(id: id, src: src, dst: dst, label: label,
|
||||||
|
|||||||
@@ -90,6 +90,26 @@ proc newRateLimiter*(algo: RateLimitAlgo = rlaTokenBucket,
|
|||||||
proc allowRequest*(rl: RateLimiter, clientId: string): bool =
|
proc allowRequest*(rl: RateLimiter, clientId: string): bool =
|
||||||
acquire(rl.lock)
|
acquire(rl.lock)
|
||||||
|
|
||||||
|
# Global rate enforcement
|
||||||
|
var globalOk = true
|
||||||
|
if rl.globalRate > 0:
|
||||||
|
let globalKey = "__global__"
|
||||||
|
case rl.algo
|
||||||
|
of rlaTokenBucket:
|
||||||
|
if globalKey notin rl.buckets:
|
||||||
|
rl.buckets[globalKey] = newTokenBucket(float64(rl.globalRate),
|
||||||
|
float64(rl.globalRate) / 60.0)
|
||||||
|
globalOk = rl.buckets[globalKey].consume()
|
||||||
|
of rlaSlidingWindow, rlaFixedWindow:
|
||||||
|
if globalKey notin rl.windows:
|
||||||
|
rl.windows[globalKey] = newSlidingWindow(60_000_000_000, rl.globalRate)
|
||||||
|
globalOk = rl.windows[globalKey].allow()
|
||||||
|
|
||||||
|
if not globalOk:
|
||||||
|
release(rl.lock)
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Per-client rate enforcement
|
||||||
case rl.algo
|
case rl.algo
|
||||||
of rlaTokenBucket:
|
of rlaTokenBucket:
|
||||||
if clientId notin rl.buckets:
|
if clientId notin rl.buckets:
|
||||||
@@ -122,6 +142,25 @@ proc remainingQuota*(rl: RateLimiter, clientId: string): int =
|
|||||||
result = rl.perClientRate
|
result = rl.perClientRate
|
||||||
release(rl.lock)
|
release(rl.lock)
|
||||||
|
|
||||||
|
proc cleanupStaleClients*(rl: RateLimiter, maxClients: int = 10000) =
|
||||||
|
acquire(rl.lock)
|
||||||
|
# Remove oldest clients if exceeding max
|
||||||
|
if rl.buckets.len > maxClients:
|
||||||
|
var toRemove = rl.buckets.len - maxClients
|
||||||
|
for key in rl.buckets.keys:
|
||||||
|
if key != "__global__":
|
||||||
|
rl.buckets.del(key)
|
||||||
|
dec toRemove
|
||||||
|
if toRemove <= 0: break
|
||||||
|
if rl.windows.len > maxClients:
|
||||||
|
var toRemove = rl.windows.len - maxClients
|
||||||
|
for key in rl.windows.keys:
|
||||||
|
if key != "__global__":
|
||||||
|
rl.windows.del(key)
|
||||||
|
dec toRemove
|
||||||
|
if toRemove <= 0: break
|
||||||
|
release(rl.lock)
|
||||||
|
|
||||||
proc resetClient*(rl: RateLimiter, clientId: string) =
|
proc resetClient*(rl: RateLimiter, clientId: string) =
|
||||||
acquire(rl.lock)
|
acquire(rl.lock)
|
||||||
rl.buckets.del(clientId)
|
rl.buckets.del(clientId)
|
||||||
|
|||||||
@@ -188,7 +188,10 @@ proc exprToSql(node: Node): string =
|
|||||||
else: " " & $node.binOp & " "
|
else: " " & $node.binOp & " "
|
||||||
return exprToSql(node.binLeft) & opStr & exprToSql(node.binRight)
|
return exprToSql(node.binLeft) & opStr & exprToSql(node.binRight)
|
||||||
of nkFuncCall:
|
of nkFuncCall:
|
||||||
return node.funcName & "(" & exprToSql(node.funcArgs[0]) & ")"
|
if node.funcArgs.len > 0:
|
||||||
|
return node.funcName & "(" & exprToSql(node.funcArgs[0]) & ")"
|
||||||
|
else:
|
||||||
|
return node.funcName & "()"
|
||||||
of nkUnaryOp:
|
of nkUnaryOp:
|
||||||
return $node.unOp & " " & exprToSql(node.unOperand)
|
return $node.unOp & " " & exprToSql(node.unOperand)
|
||||||
else:
|
else:
|
||||||
@@ -1256,9 +1259,10 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
|
|||||||
if i < plan.projectExprs.len:
|
if i < plan.projectExprs.len:
|
||||||
let expr = plan.projectExprs[i]
|
let expr = plan.projectExprs[i]
|
||||||
if expr.kind == irekStar:
|
if expr.kind == irekStar:
|
||||||
for k, v in sourceRows[0]:
|
if sourceRows.len > 0:
|
||||||
if not k.startsWith("$") and not k.contains("."):
|
for k, v in sourceRows[0]:
|
||||||
newRow[k] = v
|
if not k.startsWith("$") and not k.contains("."):
|
||||||
|
newRow[k] = v
|
||||||
elif expr.kind == irekAggregate:
|
elif expr.kind == irekAggregate:
|
||||||
case expr.aggOp
|
case expr.aggOp
|
||||||
of irCount:
|
of irCount:
|
||||||
@@ -2004,9 +2008,10 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
|
|
||||||
# Fire BEFORE INSERT triggers
|
# Fire BEFORE INSERT triggers
|
||||||
var row = initTable[string, string]()
|
var row = initTable[string, string]()
|
||||||
for i, f in mutableFields:
|
if mutableValues.len > 0:
|
||||||
if i < mutableValues[0].len:
|
for i, f in mutableFields:
|
||||||
row[f] = mutableValues[0][i]
|
if i < mutableValues[0].len:
|
||||||
|
row[f] = mutableValues[0][i]
|
||||||
fireTriggers(ctx, stmt.insTarget, "before", "insert", row)
|
fireTriggers(ctx, stmt.insTarget, "before", "insert", row)
|
||||||
|
|
||||||
var kvPairs: seq[(string, seq[byte])]
|
var kvPairs: seq[(string, seq[byte])]
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ proc manhattanDistance*(a, b: Vector): float64 =
|
|||||||
return sum
|
return sum
|
||||||
|
|
||||||
proc distance*(a, b: Vector, metric: DistanceMetric): float64 =
|
proc distance*(a, b: Vector, metric: DistanceMetric): float64 =
|
||||||
|
if a.len != b.len:
|
||||||
|
raise newException(ValueError, "Vector dimension mismatch: " & $a.len & " != " & $b.len)
|
||||||
case metric
|
case metric
|
||||||
of dmCosine: cosineDistance(a, b)
|
of dmCosine: cosineDistance(a, b)
|
||||||
of dmEuclidean: euclideanDistance(a, b)
|
of dmEuclidean: euclideanDistance(a, b)
|
||||||
|
|||||||
Reference in New Issue
Block a user