New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Recursive hashing corrupts shared hash buffer #7866
Comments
So this is the only one that fails, yes? I have not found a cause yet but it seems like the problem must revolve around Node not hashing the same as you expect. I believe Set is working properly but some interaction with your overridden methods might be confusing the underlying Hash. |
Removing the memoization or calling |
Here's the smallest repro I have that still shows the issue: Edit: I pasted the wrong source at first. require 'set'
class Ptr
def hash
@hash ||= {x:1}.hash
end
end
class Node
def initialize
@ptr = Ptr.new
end
def hash
{ptr: @ptr}.hash
end
end
node = Node.new
set1 = Set[node]
set2 = Set[node]
puts set1 == set2
puts set2 == set1 There's some interaction between Set's logic for standing up the internal hash and the memoized |
I have tested whether something is wrong with our Set by forcing it to use the gem, which is the same pure-Ruby set.rb that CRuby uses. It did not change the result, so it seems the issue is in JRuby's Hash or in how we are compiling this code. |
New smaller reproduction that eliminates Set and Node. It's clearly not a problem with Set. class Ptr
def hash
@hash ||= { x: 1 }.hash
end
end
ptr = Ptr.new
set1 = { { ptr: ptr } => true}
set2 = { { ptr: ptr } => true}
puts set1 == set2
puts set2 == set1 |
Even smaller! class Ptr
def hash
@hash ||= { x: 1 }.hash
end
end
ptr = Ptr.new
puts({ ptr: ptr }.hash, { ptr: ptr }.hash) |
Set#==
returning false incorrectlyThe original optimization here moved the allocation of the buffer byte[16] into a thread-local to reuse it, but it kept the original inline modification of that buffer via ByteBuffer.putLong. This caused recursive Hash#hash calls to overwrite the buffer mid-hash, leading to issues like jruby#7866 where a lazily-calculated recursive hash produces a bogus value the first time through. The change here avoids accessing and modifying the shared buffer until after any recursive hash calls have completed.
OMG I found it. In JRuby 9.4.0.0, 60ae8ba introduced a cache for the byte[16] used to calculate an aggregate hash based on the key and value, but none of us noticed that the buffer was in-use during recursive hash calls. That led to those recursive calls wiping out the partial results already present. I've fixed it in #7901. |
The original optimization here moved the allocation of the buffer byte[16] into a thread-local to reuse it, but it kept the original inline modification of that buffer via ByteBuffer.putLong. This caused recursive Hash#hash calls to overwrite the buffer mid-hash, leading to issues like jruby#7866 where a lazily-calculated recursive hash produces a bogus value the first time through. The change here avoids accessing and modifying the shared buffer until after any recursive hash calls have completed.
I started memoizing the
#hash
method of a class (Ptr
, below), which seems to have broken comparison of a Set containing instances of a class (Node
) that has an ivar containing aPtr
, created inNode
's initializer.Expected: all
==
comparisons result intrue
Actual: first comparison is
false
(and the rest aretrue
)Some confluence of factors is at play here, I've simplified this down a lot, but simplifying further seems to no longer trigger the issue. Doing less in
Node#initialize
, or ifPtr#hash
orNode#hash
are hashing something simpler thanHash
instances, result in expected behavior.Output of the above, with notes:
Environment Information
The text was updated successfully, but these errors were encountered: