Neo4j Long-Running Transaction Killer Guard
Find Neo4j transactions over an elapsed-time threshold, alert Slack, terminate the offenders after approval, then re-list and fail if any targeted transaction is still open.
Find Neo4j indexes left FAILED or stuck POPULATING, alert Slack, drop and recreate each from its own definition, then verify every index is ONLINE.
A Neo4j schema index does not always come up ONLINE. A population error, such as a
property value too large to index, leaves it FAILED. A crash during the build can
leave it stuck POPULATING. Either way the index stays in the catalog and is still
maintained on every write, but it never serves a lookup. Queries that should use it
fall back to a full label scan and get slower, with nothing in an error log to say
why. Neo4j does not recover a FAILED index on its own, even once the cause is gone:
it stays FAILED until someone drops and recreates it.
This blueprint lists every index that is not ONLINE. It alerts Slack and can drop
then recreate each one from the index own createStatement, so the rebuild uses
Neo4j exact definition for any index type. It waits for population to finish, then
audits again and fails the run if an index it rebuilt is still not ONLINE.
This blueprint was created by zkasuran.
| State | What it means | What this guard does |
|---|---|---|
FAILED |
Population hit an error and gave up. Still written on every change, never read | Drop and recreate from its own definition, then verify ONLINE |
POPULATING |
Still building or stuck mid build after crash | Same path. A legitimate build in progress also shows this, so run the guard outside large index builds |
| owned by a constraint | A non-ONLINE index that backs a constraint | Reported, not touched. DROP INDEX refuses it, the constraint must be rebuilt instead |
audit (io.kestra.plugin.neo4j.Query, read-only, storeType: FETCH) runs SHOW INDEXES ... WHERE state <> 'ONLINE'. For each index it returns the state, the type, whether it is remediable (not owned by a constraint), the failure message, a DROP INDEX ... IF EXISTS statement with the name back-quoted and the index own createStatement.summary logs every finding with its exact definition.route (io.kestra.plugin.core.flow.If on outputs.audit.size > 0):clean logs that every index is ONLINE.alert posts the list to Slack, noting any constraint-owned indexes it will not touch and whether it is about to remediate.remediate_gate runs only when auto_remediate is true and dry_run is false.remediate (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) takes one index at a time. For a remediable index it runs the drop then the recreate. A constraint-owned index is skipped.settle calls db.awaitIndexes to wait for the recreated indexes to build.reaudit runs the same SHOW INDEXES check again. verify_result fails the run if any index this run tried to remediate is still not ONLINE.errors alerts Slack when the flow itself fails, for example an unreachable instance or bad credentials, so a failed audit is never read as "no problem".SHOW INDEXES state, not guessed from query plans.DROP INDEX refuses those and the flow filters them out first.auto_remediate, dry_run) between a finding and any schema change.Query driving schema introspection (SHOW INDEXES) and schema change (DROP INDEX, CREATE INDEX) in one flow.Spotting a non-ONLINE index is one query. Fixing it safely is a sequence: capture each index real definition, drop and recreate one at a time, wait for population, then check the result and fail if it did not take. Kestra runs that sequence on a schedule behind two gates, keeps constraint-backed indexes out of it, alerts Slack on both the finding and a failure, then keeps an execution history of what was rebuilt and where.
On Kestra 2.0.5 OSS against Neo4j 5.26.31 Community, with a local HTTP sink standing in for Slack. A FAILED index was produced for real by indexing an oversized property: a Doc node was given a 200000 character blob, then CREATE INDEX ... FOR (n:Doc) ON (n.blob) drove the index to FAILED (property value too large to index). cypher-shell confirmed the state before and after every run.
| Run | Inputs | Result |
|---|---|---|
| 1 | defaults, clean graph | 0 indexes not ONLINE, clean logged. No alert |
| 2 | defaults, one FAILED index | 1 index reported, alert sent. No change. cypher-shell shows it still FAILED |
| 3 | auto_remediate: true, dry_run: true |
Same report, remediation held. cypher-shell shows the index untouched, still FAILED |
| 4 | auto_remediate: true, dry_run: false, root cause fixed first |
Dropped and recreated. settle waited, reaudit clean, verify_result passed. cypher-shell shows the index ONLINE |
| 5 | auto_remediate: true, dry_run: false, oversized value still present |
Dropped and recreated, population FAILED again, verify_result failed the run with the index name and state. errors alert sent |
| 6 | unreachable URL | audit failed, errors alert sent |
Exact execution ids and the before and after state counts are in the pull request notes.
SHOW INDEXES and its createStatement column are available on Community and Enterprise.SHOW INDEXES. For remediation, a user allowed to DROP INDEX and CREATE INDEX.docker run -d --name neo4j -p 7687:7687 -e NEO4J_AUTH=neo4j/testpassword neo4j:5;CREATE (:Doc {blob: 'x'}); MATCH (n:Doc) SET n.blob = reduce(s='', i IN range(1,20000) | s + 'ABCDEFGHIJ'); CREATE INDEX doc_blob_idx FOR (n:Doc) ON (n.blob);.NEO4J_PASSWORD: password for the Neo4j user, the default for the password input.SLACK_WEBHOOK_URL: Slack incoming webhook used by alert and the errors block.SECRET_, for example SECRET_NEO4J_PASSWORD=$(echo -n 'testpassword' | base64).| Input | Default | Purpose |
|---|---|---|
url |
bolt://localhost:7687 |
Bolt endpoint |
user |
neo4j |
Neo4j user |
password |
{{ secret('NEO4J_PASSWORD') }} |
Password, from the secret by default |
await_timeout_seconds |
120 |
How long db.awaitIndexes waits for a rebuild |
auto_remediate |
false |
Allow drop and recreate |
dry_run |
true |
Must be false for any schema change to run |
NEO4J_PASSWORD and SLACK_WEBHOOK_URL secrets.url and user.auto_remediate: true and dry_run: false to drop and recreate the indexes, outside peak hours since a rebuild reads the whole label.daily trigger, which always runs in safe mode.Pause before remediate for human approval on production graphs.SHOW INDEXES run against a specific database in a multi-database setup.POPULATING as healthy by excluding indexes whose populationPercent is climbing, so only truly stuck ones alert.