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

Fix MyClassLoader idempotency - issue #49 #50

Merged
merged 8 commits into from
Feb 16, 2019
Merged

Fix MyClassLoader idempotency - issue #49 #50

merged 8 commits into from
Feb 16, 2019

Conversation

jbagdis
Copy link
Contributor

@jbagdis jbagdis commented Jul 10, 2018

Adds to failing unit tests that indicate the behavior described in issue #49, and a proposed solution that addresses the problem and resolves the tests.

@cowtowncoder
Copy link
Member

Sounds good. I have been (and am) swamped with non-OSS work and other things so there may be some delay in processing, but wanted to add a note to say thank you for submitting this. Even if gettng it in may take a while.

@jbagdis
Copy link
Contributor Author

jbagdis commented Oct 5, 2018

@cowtowncoder I just wanted to follow up and ask if there's anything else I can do to help get this change merged in soon. I'm happy to write additional unit tests or comments if you'd like. This issue has bitten me again a couple different times in the past few months, and I'm eager to get to a version with a fix.

@tjwilson90
Copy link

Pinging this ticket. I've hit this error several times as well.

* @param className The name of the to-be-loaded class
* @throws IllegalStateException if there is no parent ClassLoader or the lock object could not be retrieved
*/
private Object getParentClassLoadingLock(ClassLoader parentClassLoader, String className)
Copy link
Member

Choose a reason for hiding this comment

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

Is there particular reason it must be something internal to JDK, to lock on? Access to that object is potentially quite expensive, and with later JDK versions likely very fragile.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it needs to use the parent ClassLoader's internal lock for complete correctness. ClassLoader#loadClass acquires the lock, then (potentially) calls ClassLoader#findClass, which may ultimately call ClassLoader#defineClass. #defineClass will fail if it is called twice for the same class name (which is guarded against by the synchronization in #loadClass). Acquiring the same lock used by ClassLoader#loadClass guarantees that no other mechanism (outside of Afterburner) could load a class with the name we are using between the time we check to see whether it is defined and attempt to define it.

In our case, though, if we assume that MyClassLoader will be the only class loader implementation than attempts to load/define a class with a specific name (a name which always includes the 'Creator4JacksonDeserializer' suffix and the bytecode hash, so this seems like a safe assumption), then we should be safe in relaxing this requirement and just synchronizing on our own static lock.

Copy link
Member

Choose a reason for hiding this comment

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

I'd feel better about our own lock unless we can think of specific case where someone or something might try to load class with same name concurrently; at least outside of Afterburner.
If the concern is for concurrent Afterburner instances from same JVM, probably would be ok to use static singleton lock (... yeah, not a big fan of those things, who is, but here it seems inevitable...). And I guess that is a valid concern, so even though ideally one tries to avoid multiple instances of ObjectMapper -- and esp. so with Afterburner generating classes on the fly! -- there are some legit cases when 2 or 3 mappers are not for different configurations.

So, if this could be done that is great.

Also: just so I won't forget to follow up, I added this on:

https://github.com/FasterXML/jackson-future-ideas/wiki/Jackson-Work-in-Progress

as an urgent issue.

@cowtowncoder
Copy link
Member

cowtowncoder commented Jan 26, 2019

Ok so as per description of the problem this definitely seems worth fixing.
And I like that there is actual unit testing despite pretty challenging thing to test. Well done.

One question I have has to do with locking aspects; I am curious as to whether lock object needs to be an internal lock by JDK, or if something else could plausibly be used. I realize that it may be difficult to build locking at suitable scope (I think it'd need to be static synchronized map, keyed with ClassLoader + unique-class-name-hash-code combo?), but if that would work I'd prefer route where implementation details of JDK's class loading would not need to be relied on.

I added this issue/PR on my WIP list on:

https://github.com/FasterXML/jackson-future-ideas/wiki/Jackson-Work-in-Progress

@jbagdis
Copy link
Contributor Author

jbagdis commented Jan 28, 2019

Thanks for your feedback. I'll add a commit that switches to using locking internal to MyClassLoader (I agree something like "static synchronized map, keyed with ClassLoader + unique-class-name-hash-code combo" sounds right).

Just wanted to flag that MyClassLoader kind of already relies upon implementation details of the JDK's class loading mechanism insofar as it calls the protected #defineClass method directly on the parent class loader instance. I'm on board with keeping that surface area to a minimum, though.

String key = String.format("%s:%d:%s",
parentClassLoader.getClass().getCanonicalName(),
System.identityHashCode(parentClassLoader),
className);

Choose a reason for hiding this comment

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

nit: Might want to avoid the relatively expensive String.format when generating lock names and just concatenate instead.

Copy link
Member

Choose a reason for hiding this comment

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

Oh. Yes. String.format is HORRENDOUSLY slow, for what it does -- fine for exception logging (f.ex), but not for even generating not-in-busy-loop ids. Alas.

System.identityHashCode(parentClassLoader),
className);
Object newLock = new Object();
Object lock = parentParallelLockMap.putIfAbsent(key, newLock);

Choose a reason for hiding this comment

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

Do we need a corresponding parentParallelLockMap.remove(key) at the end of the critical section before releasing this lock to avoid this map growing unbounded?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it will work to simply expire entries from the map when the lock is released. Consider this sequence of operations contending for a lock for the same class name/loader pair (i.e. with the same map key: 'key'), beginning with an empty map:
[Thread A]: requests lock object for 'key'; map contains no entry for 'key' so Object-1 is created and added to map, then returned.
[Thread A]: attempts to acquire lock for Object-1; succeeds immediately.
[Thread B]: requests lock object for 'key'; receives Object-1 from map.
[Thread B]: attempts to acquire lock for Object-1; blocks.
[Thread A]: completes its work in the synchronized region and then (in either order) releases the lock on Object-1 and removes the 'key' -> Object-1 mapping from the map.
[Thread B]: immediately acquires lock for Object-1 and unblocks.
[Thread C]: requests lock object for 'key'; map contains no entry for 'key' so Object-2 is created and added to map, then returned.
[Thread C]: attempts to acquire lock for Object-2; succeeds immediately.

Now, Thread B and Thread C are both in the synchronized region together for what should be the same lock.

A Guava (or Caffeine) Cache with weak values would do the trick, but I doubt it's worth adding that dependency. For what it's worth, though, this is the same implementation as used by ClassLoader itself. Its lock map also grows without bound (but, since it only ever gains one entry per loaded class it can't really grow that big)

Choose a reason for hiding this comment

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

Good call @jbagdis , having a corresponding remove does allow for the race condition you mentioned.

@cowtowncoder
Copy link
Member

Ok -- apologies for delay here, but I will try to re-read this.

One last (I hope!) thing: @jbagdis unless I have asked for and gotten CLA (if so, apologies -- we have quite a few and sometimes I miss one we have on file), we would need it from

https://github.com/FasterXML/jackson/blob/master/contributor-agreement.pdf

(or corporate-CLA alternative)
and just once, before first merged contribution; one is good for all contributions for all projects.
Usual way is to print, fill & sign, scan, email to info at fasterxml dot com.

@cowtowncoder cowtowncoder merged commit 6f4eda7 into FasterXML:2.9 Feb 16, 2019
@cowtowncoder cowtowncoder added this to the 2.9.9 milestone Feb 16, 2019
@cowtowncoder
Copy link
Member

Ok, got CLA, merged!

One problem is merging to master (3.0) -- conversion from Asm to ByteBuddy means dependency is missing. I'll just add a test-dependency which should work, although ideally would be good to convert test case to use BB as well.

@jbagdis
Copy link
Contributor Author

jbagdis commented Feb 19, 2019

Thanks @cowtowncoder for getting this merged!

@danrwhitcomb
Copy link

Any idea what release this change will be a part of and when that will be made available?

@cowtowncoder
Copy link
Member

@danrwhitcomb Yes, as "milestone" indicates, will be part of 2.9.9 (and 2.10.0). When those get released I don't know at this point -- we are far enough with 2.9.x releases that it may take a while.
But I think it would probably make sense to get 2.9.9 out by end of March 2019, at latest. It will probably be released before 2.10.0, still, unless I get unexpected amount of free time to spend on Jackson.

@suchanlee
Copy link

@cowtowncoder are there plans to get 2.9.9 out soon? Would love to get the fix!

@cowtowncoder
Copy link
Member

Unfortunately I am rather busy at work so haven't had much time to spend on Jackson lately.
But if I can implement task I currently focus on (FasterXML/jackson-databind#2195), I'd probably consider releasing 2.9.9 as the next thing. That could happen by mid-May.

@suchanlee
Copy link

Got it thanks for the update.

stevenschlansker pushed a commit to stevenschlansker/jackson-modules-base that referenced this pull request Sep 5, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants