Skip to content
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

Synchronize the lookup-and-define-class logic in blackbird #172

Merged
merged 1 commit into from
May 4, 2022

Conversation

josephlbarnett
Copy link
Contributor

If blackbird attempts to access/define a new class concurrently,
it can hit a race condition between attempting to access it and
defining it when that fails. If it does fail, enter a synchronized
block and try again before defining the access class.

Should fix #169

throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
synchronized(CrossLoaderAccess.class) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there may be a narrower thing to synchronize on ? (pkg.getName().intern() perhaps?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was thinking the same thing but intern() itself can be costly enough. So maybe start with safest approach like so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this only happens once per class during warmup, I think a coarse lock is fine.
Tiny nit: the other keyword blocks in this class have a space between e.g. catch ( so matching style here would have one more space synchronized (...)

Comment on lines 113 to 117
try {
return Class.forName(pkg.getName() + "." + CLASS_NAME, true, lookup.lookupClass().getClassLoader());
} catch (ClassNotFoundException ign) { }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"try again" logic is attempting to do double checked locking -- I think that's ok in this case but could always use more eyes on those attempts. Could also remove the unsynchronized reflection and just do everything in a sync block?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's nice to avoid unnecessary synchronization in the common path - where the class is already loaded.
It's a little unfortunate that this Class.forName expression is repeated, but factoring it out might not be much clearer.

What about computing the className and fetching classLoader once, stash them in local vars - then at least we don't do the string concatenation twice unnecessarily.

return (Class<?>) DEFINE_CLASS.invokeExact(lookup, classBytes.toByteArray());
} catch (RuntimeException | Error | IOException | ReflectiveOperationException e) {
throw e;
} catch (Throwable e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be Exception although I suppose does not make much difference as preceding catch block already handles RuntimeException and Error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possibly. I didn't want to mess with the existing code much so left it as is other than surrounding with synchronization....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invokeExact declares throws Throwable and since it's possible to declare your own subtype of Throwable, the RuntimeException | Error doesn't actually cover all possible types. So I think making this change would result in a compiler error. Feel free to play around with it though and see if you can do better!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! Did not realize that someone would mark Throwable to be thrown -- don't recall ever seeing that -- but if so, yes, that makes sense.
(also don't remember ever seeing custom Throwable sub-class but now I know :) ).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because invokeExact represents an arbitrary method call, with unknown throws declared. So since you can call a method that might throws Throwable, so too invokeExact must handle this possibility.

This was not needed in classic reflection, since any thrown exception was wrapped in an InvocationTargetException. But, this didn't end up always being good - for example if you wrapped an unexpected Error often the exception handling would get very confusing.

@cowtowncoder
Copy link
Member

Can't find anything wrong but I'll let @stevenschlansker review as he's the author of Blackbird module should know best.

@cowtowncoder cowtowncoder added the blackbird Issue related to Blackbird module label May 4, 2022
Copy link
Contributor

@stevenschlansker stevenschlansker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. One minor potential change suggested.

throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
synchronized(CrossLoaderAccess.class) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this only happens once per class during warmup, I think a coarse lock is fine.
Tiny nit: the other keyword blocks in this class have a space between e.g. catch ( so matching style here would have one more space synchronized (...)

Comment on lines 113 to 117
try {
return Class.forName(pkg.getName() + "." + CLASS_NAME, true, lookup.lookupClass().getClassLoader());
} catch (ClassNotFoundException ign) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's nice to avoid unnecessary synchronization in the common path - where the class is already loaded.
It's a little unfortunate that this Class.forName expression is repeated, but factoring it out might not be much clearer.

What about computing the className and fetching classLoader once, stash them in local vars - then at least we don't do the string concatenation twice unnecessarily.

If blackbird attempts to access/define a new class concurrently,
it can hit a race condition between attempting to access it and
defining it when that fails.  If it does fail, enter a synchronized
block and try again before defining the access class.

Should fix FasterXML#169
@josephlbarnett
Copy link
Contributor Author

updated per @stevenschlansker's suggestions. would be good to get this in 2.13 as well once accepted, anything I can do to help with that?

@cowtowncoder
Copy link
Member

@josephlbarnett For 2.13, it'd be easier if PR was based on 2.13. I can try cherry-picking if that is too much work.

My only concern is wrt safety of the fix, but it seems low-risk enough. It'd go in 2.13.3 which should get out quite soon, as well.

@josephlbarnett
Copy link
Contributor Author

#173 is a 2.13 cherry-pick of this commit

@cowtowncoder
Copy link
Member

Not sure what gives, but JDK 11 fails with one test.

@josephlbarnett
Copy link
Contributor Author

Not sure what gives, but JDK 11 fails with one test.

I see the test failing both before and after this change? not sure what's happened there

@cowtowncoder
Copy link
Member

cowtowncoder commented May 4, 2022

@josephlbarnett that makes sense. And yes, I can reproduce the fail locally.

I think this was hidden by having JDKs 8 and 14 tested on CI (not 11). But since 14 was dropped by Github it seems (not sure why), changed to 8 and 11 exposing the problem.

And I think we'll get plenty more fails with JDK 17 so it's not clear what to do right now.

@cowtowncoder cowtowncoder merged commit a0b9ef9 into FasterXML:2.14 May 4, 2022
cowtowncoder added a commit that referenced this pull request May 4, 2022
Synchronize the lookup-and-define-class logic in blackbird (2.13 cherry pick of #172)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blackbird Issue related to Blackbird module
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Blackbird fails with LinkageError when the same class is used across two separate classloaders
3 participants