Repository navigation
Return an ElementIterable from the column index get and release its lock - #362
Merged
Merged
Conversation
Index.get locked the column index when called and unlocked it when the iterator was exhausted, so a get that was never iterated or a loop that broke early kept the lock, and setting an attribute inside the loop could deadlock with a setter on a view index. Non-indexed, degree and edge type columns held the graph read lock the same way. get and the entry iterator now copy the elements under the lock and return the copy. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… when iterating Index.get took the index lock when called and released it only when the iterator was exhausted, so a result that was never iterated or a loop that stopped early kept the lock with no way to release it. Non-indexed, degree and edge type columns held the graph read lock the same way. get now returns an ElementIterable, like Graph.getNodes: the lock is taken when iterator() is called and released when the iterator is exhausted or on doBreak(). Each iteration takes and releases its own hold, and doBreak after exhaustion or twice does nothing. The snapshot of the previous commit is reverted, the entry iterator is unchanged. Code compiled against the Iterable return type must be recompiled. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e graph lock doBreak only released the latest iterator of a result, so nested loops over the same result kept the outer lock. parallelStream unlocked the lock from a worker thread. A non-indexed column removed during an iteration of get, count, values, getMinValue or getMaxValue left the graph read lock held by the graph iterator. doBreak now releases every iteration of the result still holding the lock, parallelStream throws UnsupportedOperationException, and an exception during the iteration releases the graph iterator. The edge type column returns Graph.getEdges(type) as before. The documentation states that the lock is held from iterator() until the iterator is exhausted or doBreak is called, and that a loop body setting attributes or reading the graph can deadlock with threads writing to the index or the graph, in which case the result should be copied with toArray. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he same contract stream() opened an iterator that kept the lock when the stream stopped early or was never consumed; the result now streams over a copy. The edge type column returned Graph.getEdges(type), whose doBreak released a read hold even after the iteration ended; it is now wrapped like the other columns. doBreak walks the open iterations once instead of looping on the last one, and a column missing from the index gives an empty result with an array of the index element type. The documentation states that a result is used from a single thread, that a loop body calling the graph, model or index APIs can deadlock with writers, that doBreak is needed when the loop body throws, and that a value of the wrong type fails when iterating. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… narrow who waits for the lock The index of an indexed column removed after get() threw a NullPointerException on its destroyed map. Iterating, copying, counting and listing the values of that index now throw an IllegalArgumentException, like a column that isn't indexed, and release the lock. The documentation states that an open result of an indexed column only blocks the threads that set the column's value or add or remove elements of its table, and the graph writers otherwise. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.
Problem
ColumnStandardIndexImpl.get(value)took the lock of the column index when it was called and released it only when the returned iterator was exhausted, so the lock was held while the caller's code ran:index.get(column, value)and calling a setter in the loop, while another thread sets the same column or adds a node, deadlocked: the other thread held the table lock or the view index monitor and waited for the index lock, and the setter waited for them. This happens for a view index, for the elements of the view.getand never iterating, left the lock held for good, so every later setter oraddNodeon that column blocked.IllegalMonitorStateException.For the columns that are not indexed (
ColumnNoIndexImpl), and the degree and edge type columns,getheld the graph read lock the same way, andcount,values,getMinValueandgetMaxValueof a non-indexed column kept the graph read lock whengetAttributethrew (a removed column), which blocked every later write to the graph.Fix
Index.getandColumnIndex.getreturn anElementIterable<T>and follow the convention of the graph iterables: the lock is taken wheniterator()is called and held until the iterator is exhausted ordoBreak()is called by the iterating thread.ColumnStandardIndexImpl.get(value)returns aValueIterable. Iterators opened on one result are linked, so a singledoBreak()releases all of them, and it does nothing after exhaustion or when called twice.toArray,toCollectionandtoSetcopy under the lock and release it before returning.stream()iterates a copy and holds no lock,parallelStream()throwsUnsupportedOperationExceptionas inElementIterableWrapper.ColumnNoIndexImpl,DegreeNoIndexImplandEdgeTypeNoIndexImplreturn aFilteredElementIterable, a lazy filter over the graph iterable with the same contract. It releases the graph read lock if the filter or the source throws.count,values,getMinValueandgetMaxValueof a non-indexed column release the graph read lock when the loop does not finish.IndexImpl.getreturns an empty iterable typed with the element type for a column the index does not hold.Index.getandColumnIndex.getstates the rule and the limits below.Compatibility
The return type of
Index.getandColumnIndex.getchanges fromIterabletoElementIterable, which extendsIterable. Source code keeps compiling, but this is a binary incompatible change for code compiled against 0.8.10. Nothing in graphstore calls these methods, and Gephi usesvalues,count, the minimum and the maximum of the indexes but notget(column, value).Limits (documented)
toArray()first, it releases the lock before returning.doBreak()is called.doBreak()releases every open iteration of the same result, so it also ends an outer loop over that result.Tests
In
IndexStoreTest: a setter running while a view index is iterated, an unfinishedget, adoBreakafter a partial or complete iteration, nested iterations with onedoBreak,parallelStream(),stream().findFirst(), the edge type column,toArray()on the empty result of the edge index, andgetand the numeric index methods on a non-indexed column afterremoveColumn, on indexed and non-indexed columns (each fails on master with a timeout, an exception or a wrong result). I also comparedgetwith a model on 200 to 300 random sequences of 500 to 600 operations, with nodes and edges, indexed and non-indexed columns, degree and edge type columns, null,-0.0and NaN values and two views: no difference.An indexed
ColumnIndex, or a result obtained from it, used after its column was removed now throws theIllegalArgumentExceptionof the non-indexed path where it threw aNullPointerException(count,values,countValuesand the entry iterator included), and does not leave the lock held.getMinValueandgetMaxValuestill throw theirUnsupportedOperationException.🤖 Generated with Claude Code