You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I maintain a Gephi plugin (gephi-ai) that runs an HTTP API inside Gephi Desktop and does graph reads and writes from a non-EDT thread while the viz-engine renders. I should say up front that I have been troubleshooting this together with Claude (Anthropic's AI assistant), and the analysis and reproducer below came out of that joint debugging. I have tried to verify everything against the source and a live Gephi 0.11.2 session on macOS (Apple Silicon), but please read it as a good-faith diagnosis, and I am glad to re-check anything.
The symptom: my sessions would eventually wedge the whole application. A lock-free health endpoint kept answering, but every graph operation, reads included, blocked forever, and Gephi's own UI stopped processing graph work. Only a restart recovered. Thread dumps show no lock cycle, so deadlock detection sees nothing. This looks related to gephi/gephi#2546 and to gephi-plugins#250 / #150, though I think the mechanism here is in the lock API rather than in graph/table lock ordering.
I work around it today by reflecting into GraphLockImpl and polling timed tryLock for both locks. That works, but it is reflection into impl internals, which is why I am raising it here.
What the source seems to do
If I am reading it correctly: GraphLockImpl wraps an unfair new ReentrantReadWriteLock() with no fairness option (GraphLockImpl.java L30); readLock()/writeLock() are unbounded, non-interruptible lock() calls; and I could not find any tryLock, timed, or interruptible variant in the public GraphLock/Graph API, though I may have missed one. Iterators hold the read lock from construction (NodeStore.java L654) until exhaustion or doBreak(), and with auto-locking on by default a full-graph read section is proportional to graph size. During rendering, the viz-engine's world updaters appear to re-acquire read locks with essentially no idle gap.
Reproducer results
Standalone single file (collapsed below), depends only on org.gephi:graphstore:0.8.6, runs with mvn -q compile exec:java in about 40 seconds. It builds a 300k-node graph and runs 3 "render" threads that iterate all nodes under the read lock in a tight loop, which I hope is a fair stand-in for the world updaters. Numbers from macOS aarch64, JDK 25.
1. Blocking writeLock() works (the AQS queued-writer heuristic prevents starvation): 47 acquisitions, p50 wait 2.0 ms, max 3.1 ms. Every queued write also stalls new readers for the drain time of in-flight read sections, which grows with graph size.
2. Untimed tryLock() polling starves in my measurements. It barges and never enqueues, so with overlapping readers there may never be a zero-reader instant to succeed in:
untimed tryLock(): 0 / 4637 acquisitions in 6s
timed tryLock(120ms): 2116 / 2116 acquisitions in 6s
The timed variant enqueues and worked perfectly in the same test, but neither is reachable through the public API today.
3. One stalled reader plus one queued writer appears to wedge everything permanently. Reader A holds the read lock and waits on another thread; writer W enqueues; every new reader parks behind W (including trivial auto-locked reads like getNodes().toArray()); A never releases, W never acquires:
after 5s:
reader-A state: WAITING (holds read lock, awaiting B)
writer-W state: WAITING (queued for write lock)
reader-B state: WAITING (parked behind queued writer)
probe getNodes().toArray() returned: false
→ WEDGED: only killing the JVM recovers.
No lock cycle is visible to jstack. This matches what I saw in Gephi Desktop, including "reads hang too", though I cannot be certain every field wedge shares this root.
Field notes
Instrumenting a live session, I believe I caught a variant of scenario 3 in production, and it was my own bug: my endpoint iterated graph.getEdges() and broke out early at a result limit, which (if I understand the iterator contract correctly) leaked the iterator's read hold. The request thread then exited, making the hold permanently unreleasable, since ReentrantReadWriteLock read holds are a shared count with no owner tracking. From then on, one queued writer wedged everything while health kept answering, and a thread dump showed nothing at all, since the holder no longer existed. I confirmed it by reflecting getReadLockCount(), which read 1 while the app was idle. I have fixed this on my side, but a for plus break over an iterable is idiomatic Java, nothing in the API surfaces the consequence, and it may also catch first-party code (GraphSelectionImpl.setSelectedNodes(Graph, NodeIterable, ...) in Gephi's VisualizationImpl appears to take readLock() with no try/finally, so an exception in its consumer could potentially leak the same way).
Ideas, in case they are useful
Timed acquisition on GraphLock: tryReadLock(timeout, unit) / tryWriteLock(timeout, unit) delegating to the underlying lock. Looks non-breaking from the outside, removes the reflection workaround, and Add some API methods to check lock integrity (for tests) #145 already extended GraphLock once, if that precedent applies.
Queue diagnostics: perhaps hasQueuedWriter() or getQueueLength(), so a watchdog could turn scenario 3 into an actionable error instead of a force-quit.
Possibly a fairness flag in Configuration, though it would not fix 1 or 2 (tryLock() barges even on a fair lock) and fair mode usually costs read throughput, so I would benchmark before considering it.
A Javadoc note on readLock() and the auto-locked iterables: holding the lock across a cross-thread wait, or abandoning an iterator without exhaustion or doBreak(), can wedge the store once a writer queues.
I may be missing context on why the API is shaped this way, and if there is a recommended pattern for external integrations that I overlooked, I would appreciate the pointer. If the direction sounds reasonable, I am happy to try turning 1 and 2 into a PR with tests based on the reproducer.
Reproducer code
pom.xml + GraphStoreLockRepro.java (click to expand)
Problem
I maintain a Gephi plugin (gephi-ai) that runs an HTTP API inside Gephi Desktop and does graph reads and writes from a non-EDT thread while the viz-engine renders. I should say up front that I have been troubleshooting this together with Claude (Anthropic's AI assistant), and the analysis and reproducer below came out of that joint debugging. I have tried to verify everything against the source and a live Gephi 0.11.2 session on macOS (Apple Silicon), but please read it as a good-faith diagnosis, and I am glad to re-check anything.
The symptom: my sessions would eventually wedge the whole application. A lock-free health endpoint kept answering, but every graph operation, reads included, blocked forever, and Gephi's own UI stopped processing graph work. Only a restart recovered. Thread dumps show no lock cycle, so deadlock detection sees nothing. This looks related to gephi/gephi#2546 and to gephi-plugins#250 / #150, though I think the mechanism here is in the lock API rather than in graph/table lock ordering.
I work around it today by reflecting into
GraphLockImpland polling timedtryLockfor both locks. That works, but it is reflection into impl internals, which is why I am raising it here.What the source seems to do
If I am reading it correctly:
GraphLockImplwraps an unfairnew ReentrantReadWriteLock()with no fairness option (GraphLockImpl.java L30);readLock()/writeLock()are unbounded, non-interruptiblelock()calls; and I could not find anytryLock, timed, or interruptible variant in the publicGraphLock/GraphAPI, though I may have missed one. Iterators hold the read lock from construction (NodeStore.java L654) until exhaustion ordoBreak(), and with auto-locking on by default a full-graph read section is proportional to graph size. During rendering, the viz-engine's world updaters appear to re-acquire read locks with essentially no idle gap.Reproducer results
Standalone single file (collapsed below), depends only on
org.gephi:graphstore:0.8.6, runs withmvn -q compile exec:javain about 40 seconds. It builds a 300k-node graph and runs 3 "render" threads that iterate all nodes under the read lock in a tight loop, which I hope is a fair stand-in for the world updaters. Numbers from macOS aarch64, JDK 25.1. Blocking
writeLock()works (the AQS queued-writer heuristic prevents starvation): 47 acquisitions, p50 wait 2.0 ms, max 3.1 ms. Every queued write also stalls new readers for the drain time of in-flight read sections, which grows with graph size.2. Untimed
tryLock()polling starves in my measurements. It barges and never enqueues, so with overlapping readers there may never be a zero-reader instant to succeed in:The timed variant enqueues and worked perfectly in the same test, but neither is reachable through the public API today.
3. One stalled reader plus one queued writer appears to wedge everything permanently. Reader A holds the read lock and waits on another thread; writer W enqueues; every new reader parks behind W (including trivial auto-locked reads like
getNodes().toArray()); A never releases, W never acquires:No lock cycle is visible to
jstack. This matches what I saw in Gephi Desktop, including "reads hang too", though I cannot be certain every field wedge shares this root.Field notes
Instrumenting a live session, I believe I caught a variant of scenario 3 in production, and it was my own bug: my endpoint iterated
graph.getEdges()and broke out early at a result limit, which (if I understand the iterator contract correctly) leaked the iterator's read hold. The request thread then exited, making the hold permanently unreleasable, sinceReentrantReadWriteLockread holds are a shared count with no owner tracking. From then on, one queued writer wedged everything while health kept answering, and a thread dump showed nothing at all, since the holder no longer existed. I confirmed it by reflectinggetReadLockCount(), which read 1 while the app was idle. I have fixed this on my side, but aforplusbreakover an iterable is idiomatic Java, nothing in the API surfaces the consequence, and it may also catch first-party code (GraphSelectionImpl.setSelectedNodes(Graph, NodeIterable, ...)in Gephi's VisualizationImpl appears to takereadLock()with no try/finally, so an exception in its consumer could potentially leak the same way).Ideas, in case they are useful
GraphLock:tryReadLock(timeout, unit)/tryWriteLock(timeout, unit)delegating to the underlying lock. Looks non-breaking from the outside, removes the reflection workaround, and Add some API methods to check lock integrity (for tests) #145 already extendedGraphLockonce, if that precedent applies.hasQueuedWriter()orgetQueueLength(), so a watchdog could turn scenario 3 into an actionable error instead of a force-quit.Configuration, though it would not fix 1 or 2 (tryLock()barges even on a fair lock) and fair mode usually costs read throughput, so I would benchmark before considering it.readLock()and the auto-locked iterables: holding the lock across a cross-thread wait, or abandoning an iterator without exhaustion ordoBreak(), can wedge the store once a writer queues.I may be missing context on why the API is shaped this way, and if there is a recommended pattern for external integrations that I overlooked, I would appreciate the pointer. If the direction sounds reasonable, I am happy to try turning 1 and 2 into a PR with tests based on the reproducer.
Reproducer code
pom.xml + GraphStoreLockRepro.java (click to expand)
pom.xml
src/main/java/GraphStoreLockRepro.java