Ingest the RBAC graph in batches, not as a single CREATE - #534
Draft
marcinc wants to merge 2 commits into
Draft
Conversation
`index_rbac` sent the entire cluster graph as one `CREATE` statement. FalkorDB parses that into an AST and applies it in one transaction, and the transient cost is wildly out of proportion to the data: on a synthetic cluster of 600 Roles / 510 ClusterRoles / 600 RoleBindings / 510 ClusterRoleBindings, a 3.5 MB statement is enough to have FalkorDB killed under the 200Mi both `charts/krane/templates/deployment.yaml` and `docker-compose.yml` cap it at - against a graph that occupies ~9 MB once it exists. The same statement also takes 6s to apply, over the graph client's 1s read timeout, so the report fails outright at that size. Nodes and edges can no longer be created by the same statement, since an edge clause referencing `n1` needs `n1` bound in its own statement. Nodes now carry their label as an `_id` property instead of as a query scoped variable, and are created anonymously in batches. Edges are grouped by everything a single pattern fixes - the kind of node at either end, the relation, and its direction - and each batch becomes one `UNWIND` over the label pairs, matching both nodes back through an `_id` index created between the two phases. Both endpoints resolve by index scan. Batching gives up the single statement's atomicity, so `create_graph` discards the graph and re-raises if any statement fails, rather than leaving a partial graph for the report to read as a complete picture of the cluster. Verified against FalkorDB v4.20.3 on the cluster above: `krane report` completes under a 200Mi limit where master is OOM-killed, and the report JSON and all 488 generated tree view files are identical to master's.
Two things the batched ingest got wrong. An edge naming a node that was never built used to have Cypher create an unlabelled, propertyless node for it, since an undeclared variable in a CREATE pattern is a fresh node. Batching turned that into a silent drop. It does happen: three of the seven cached clusters here contain a ClusterRole granting `use` on a PodSecurityPolicy that the cluster does not define, which is a dangling reference in the RBAC rather than a fault in the builder. Dropping the edge is right - every query binds its nodes by label, so the node the old code created could never match one - but doing it quietly is not. The edges are collected and their count reported. `discard_graph` only rescued DeleteError, so a tidy up that failed any other way replaced the failure that caused it. Losing a Cypher syntax error and reporting a connection error in its place hides the only useful diagnostic. It now swallows everything, leaving the caller to re-raise the original. Verified against FalkorDB v4.20.3. A cache doctored to produce a Cypher syntax error part way through ingest leaves a 500 node partial graph with the discard removed, and no graph at all with it in place.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Work in progress. Closes #525.
The problem
Rbac::Ingest#index_rbacsent the whole cluster graph as one statement:FalkorDB parses that into a single tree and applies it in one transaction, and the transient cost is far out of proportion to the data it produces.
Why it was not a one-line change
Edge clauses reference nodes as variables bound in the same statement:
Split that across statements and the second one has no
n1. Sending nodes and edges separately needs a stable key on each node, and an index to match nodes back through.What this does
Node#to_snow carries the node's label as an_idproperty instead of binding it as a variable, so nodes are created anonymously:(:Role {..., _id:'n5'}).Builder#node_statementsbatches nodes intoCREATEstatements of at most 500.Builder#edge_statementsgroups edges by everything one pattern fixes — the kind of node at either end, the relation, and its direction — and emits oneUNWINDper batch:GRAPH.EXPLAINconfirms both ends resolve by index scan.Edge#to_sbecomesEdge#directions. A bidirectional edge still yields a relationship each way.Ingest#create_graphruns nodes, then the_idindexes, then edges. Batching gives up the single statement's atomicity, so if any statement fails it discards the graph and re-raises, rather than leaving a partial graph for the report to read as a complete picture of the cluster.Builder#bodyis removed, since nothing calls it any more.Testing
Unit suite: 292 examples, 0 failures.
End to end against FalkorDB v4.20.3 — the version the chart and the compose file both pin — configured the way they configure it (
RESULTSET_SIZE -1,BROWSER 0). Cache: 600 Roles, 510 ClusterRoles, 600 RoleBindings, 510 ClusterRoleBindings across 40 namespaces.Peak is read from the cgroup's
memory.peakwith no limit applied, so nothing is capped or reclaimed. The chart and the compose file both cap FalkorDB at 200Mi: master's peak is over that, this branch's fits under it.To be clear about what that does and does not show: master is not reliably killed at 200Mi. Much of its peak is page cache from writing the tree files, which the kernel can reclaim, so it often survives while sitting right on the limit. It is killed when the bundled browser is left enabled, which raises the idle footprint. The defensible claim is the peak demand, not the kill.
Output is unchanged. The report JSON is identical (3,484 normalised lines; 15 danger, 20 warning, 2 info, 18 success), as are all 2,821 generated dashboard files apart from the tree manifest's timestamp. Also checked against
cache/default, which exercises PodSecurityPolicy nodes and:SECURITYedges and contains a dangling reference — all 100 files identical there too.Worth a look in review
_idadds a property to every node. The resident graph grew from 8.6 MB to 9.3 MB on this cluster.CREATEtakes 6 s and the report fails on that timeout before memory even becomes the question. Batching happens to avoid it, because no single statement is slow enough to matter, but the default itself is still fragile and is left for a separate change.Edges naming an entity that is never defined
Worth its own note, as it is a real behaviour change.
An edge can name a node the builder never created. In a single
CREATE, an undeclared variable is simply a fresh node, so the old code silently made an unlabelled, propertyless one for it. Batching cannot do that, and theMATCHfinds nothing instead.It is not hypothetical. Three of the seven cached clusters here have one: a ClusterRole granting
useon a PodSecurityPolicy namedkindnetthat the cluster does not define. That is a dangling reference in the cluster's own RBAC, not a fault in the builder.Dropping the edge is the better behaviour — every Cypher query in the codebase binds its nodes by label, so the node the old code created could never match one, which is why the reports come out identical. Measured on
cache/default:What was wrong was doing it quietly. The edges are now collected and reported:
Surfacing this as a report finding, alongside the existing "Dangling roles", would be a reasonable follow up. Out of scope here.
The failure path, exercised for real
No longer only unit tested. A copy of
cache/defaultwas doctored so a ClusterRole name contains a single quote, which the unescaped value interpolation turns into a Cypher syntax error part way through ingest:rbac-failtest, 500 nodes — one node batchThat partial graph is exactly what the report would otherwise have read as a complete picture of the cluster.
Doing this also turned up a fault in the tidy up.
discard_graphonly rescuedDeleteError, so if the tidy up failed any other way — the graph having gone with the connection, say — that error replaced the one that caused it, losing the only useful diagnostic. It now swallows everything and lets the caller re-raise the original. Covered by a test that fails without the fix.Note the unescaped interpolation used to provoke this is a pre-existing weakness, untouched here.
Related
The two secondary notes in #525 are split out as #532 and #533.