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
java.lang.Iterable object has Enumerable ancestor but map cannot be called (jruby-9.3.x) #6924
Comments
I still don't have a small example but the problem can be easily reproduced:
and then in
If the last line is additionally copied in between the 2 requires (in fresh irb) there is no exception for the remainder of the irb session and on jruby-9.2.19.0 there is no exception in any case. The anonymous Iterable class can be found here: https://github.com/neo4j/neo4j-java-driver/blob/4.3/driver/src/main/java/org/neo4j/driver/internal/value/ListValue.java#L86-L113 |
I've been struggling with what seems to be the same issue, and finally managed to reduce it down to a simple spec (loosely based on interfaces/iterable_spec.rb) that fails on JRuby 9.3.1.0: describe "Classes that implement Iterable" do
it "should provide #map even if Enumerable has been prepended to" do
java.util.ArrayList.new # ArrayList implements Iterable
Enumerable.prepend(Module.new)
# SQLException also implements Iterable, and probably has no Ruby class wrapper defined yet
iterable = java.sql.SQLException.new
expect { iterable.map(&:to_s) }.not_to raise_exception
end
end (In my case, the actual trigger for this bug was the Ps. I suspect that this regression might be related to this comment in the IncludedModuleWrapper constructor: // FIXME: Is this ok if we are including a prepended module?
methods = origin.getMethodsForWrite(); Pps. For the time being, the following work-around seems to restore the missing methods adequately: # Klugy work-around for https://github.com/jruby/jruby/issues/6924
[Java::JavaLang::Iterable, Java::JavaUtil::Enumeration, Java::JavaUtil::Iterator, Java::JavaUtil::Collection].each do |mod|
mod.include(Enumerable)
end (FWIW, those are all the Java interfaces whose Ruby wrappers are defined, in JavaLang.java and JavaUtil.Java, to include Enumerable. Just patching Iterable alone is not sufficient — it leaves e.g. |
@ikaronen-relex Good investigation! So is something prepending on to Enumerable and breaking our inclusion logic? @enebo Could this be related to changes you were making for looking up prepends and includes? |
@headius Yes, at least ActiveSupport does that, see link above. And apparently active_graph depends on ActiveSupport, so that's probably the trigger in @klobuczek's example, too. |
@ikaronen-relex Ok, great... so we have a cause and can dig into a solution. |
Some start of notes on this I am using @ikaronen-relex example to work from. The FIXME probably is not the issue (I should have removed it) but that commit and a couple around it is definitely where the issue came from. The idea of swapping out methods is one of the bigger changes. It could be the problem but I will dig more. My first observation is that ArrayList if it is moved after the prepend it works fine. An ArrayList before will break SQLException because of the prepend. Both SQLException and ArrayList have Iterable in its inheritance so I guess the prepend has to break Enumerable in Iterable in some way? What is strange is without the ArrayList the SQLException case will work again. What is even stranger is that if I reverse ArrayList and SQLException it works again as well. |
More notes. When prepend(Module.new) it uses the literal Enumerable instance to modify. This isn't wrong but this will for sure muck with older things which still reference the method table of the original Enumerable. That is it will only muck with the users if they are actually using the actual method table of the original Enumerable. So a couple of things worth pointing out: module A
def one
end
end
class B
include A
end
p B.instance_methods(true)
module A
def two
end
end
p B.instance_methods(true) Ordinary method updates will be seen. Second call to instance_methods will see two. If I change two to be in another module and include that into A then we will not see it: module A
def one
end
end
class B
include A
end
p B.instance_methods(true)
module C
def beh
end
end
module A
include C
end
p B.instance_methods(true) This is because when B did the include it resolves the inheritance chain at that point (note: this is not true in 3.0...well it is but 3.0 will revisit all previous includes and fix things up). So the big problem here is that a prepend changes hierarchy and the current implementation will change the module to no methods then set the prependedmodule with the original methods. Since the older includes no nothing of this prepend they do not have the same inheritance so they do not see the methods in the new location. EXCEPT we have methodLocation which for Enumerable will be the prependedModule which contains the original methods. If we are lucky then we are missing one piece of logic within Java integration which is somehow looking for a method and bypassing methodLocation. Otherwise I am off the map here and it is something else. The reason I think this is likely the issue is we have only seen this with Java classes we pull in. |
Amusing but not done yet. That FIXME comment eerily did predict the issue. I made the comment and it was for a different concern yet it was the main cause of this regression: methods = origin.getMethodsForWrite(); should be methods = origin.getMethodLocation().getMethodsForWrite(); This makes sense because after a preperd all the methods have left Enumerable and went into a PrependedModule (horrible name for this). That prepended module is the method location for Enumerable. Simple....yet....
So there is a sole failure in both MRI and ruby/spec test suites from this fix invovling refinements. I can say with certainty the fix is the right one but refinements is a beast. So I will try and see why this no longer works (how many people prepend within refinements?). |
When we want to include a given module, we should be retrieving its method location. This avoids prepends, which we will mine out during the normal process of walking all modules included or prepended into the source module. Fixes jruby#6924
@klobuczek @ikaronen-relex we worked on this from the perspective of what @ikaronen-relex submitted with Iterable/Enumerable. The cause was fairly clear that once something was prepended we were still trying to look to the original module instead of the place where we store the methods after a prepend takes place. |
The anomaly described below happens on jruby-9.3.x. On jruby-9.2.19.0 everything works as expected.
I have a case where an anonymous object implementing java.lang.Iterable is returned from java, but under some circumstances, the Enumerable methods cannot be called on it although Enumerable is one of the ancestors of that object.
Example:
There is quite a bit of prepending and including of ruby modules into java classes going on in both gems. If the
1st call
is made everything looks good. But if the1st call
is commented out the error is:the caller side looks like this:
values(&:itself)
returns an object of an anonymous class implementingjava.lang.Iterable
which hasEnumerable
as ancestor.You can call the
each
method from Iterable and then chainmap
onto it and this works but that's just a workaround.I am sorry for this vague description but this is a close as I can get after multiple hours of investigation.
I believe there are 2 problems:
I'm happy to do further debugging with your guidance, but right now I'm out of ideas. I was not able to create a minimal example to reproduce the issue.
The text was updated successfully, but these errors were encountered: