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

Fixed deadlock when sealing objects with a LazyCtor #1476

Merged
merged 1 commit into from
May 30, 2024

Conversation

andreabergia
Copy link
Contributor

The reason is that ScriptableObject::sealObject will initialize eagerly any LazyLoadedCtor that are stored in the slot map - it needs to that because some of those will actually mutate the object (such as NativeJavaTopPackage), and that cannot work if the object is already sealed. However, it will also acquire a read lock on the slot map before iterating on it. But the mutations will try to acquire a write lock, causing a deadlock with just one thread.

This change fixes it by not holding the read lock while mutating, and also avoids marking the object as sealed until it is certainly correct to do so.

The reason is that ScriptableObject::sealObject will initialize eagerly
any LazyLoadedCtor that are stored in the slot map - it needs to that
because some of those will actually mutate the object (such as
NativeJavaTopPackage), and that cannot work if the object is already
sealed. However, it will also acquire a read lock on the slot map
before iterating on it. But the mutations will try to acquire a write
lock, causing a deadlock with just one thread.

This change fixes it by not holding the read lock while mutating, and
also avoids marking the object as sealed until it is certainly correct
to do so.
@gbrail
Copy link
Collaborator

gbrail commented May 17, 2024 via email

@andreabergia
Copy link
Contributor Author

The reason is that we are in process of upgrading our version of rhino, and we caught up 1.7.14 recently. In the (very) old version we used to use, objects had implicitly thread-safe, and so there are some code paths where they are shared after being created (like stored in queues, memory caches, that sort of things).
We would like to remove this, because it's not the best of patterns (we should share some proper domain object, not the raw JS objects), but for the moment we're going with the new thread-safe feature.

Copy link
Collaborator

@gbrail gbrail left a comment

Choose a reason for hiding this comment

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

Thanks -- I can see that this resolves the deadlock, but I'm concerned that it's still not thread-safe. In particular, in the new loop, we're modifying the values of the slot map without a lock. At the very least, I think that we should get a read lock in the finally clause in line 2000. Even then, I'm not sure that fixes everything.

I could also argue that since this is only for the specific case of a "lazy constructor" that happens only once per run, that this might all be academic, but it's another reason why I have been very wary to tell people that the "thread-safety" in Rhino is real.

for (Slot slot : toInitialize) {
// Need to check the type again, because initializing one slot _could_ have
// initialized another one
Object value = slot.value;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here, we're reading the "value" field of the slot without a lock being held.

try {
initializer.init();
} finally {
slot.value = initializer.getValue();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here, we're setting the value of the slot without the lock being held.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This would be a bit safer if it were instead:

long stamp = slotMap.writeLock()
slot.value = initializer.getValue();
slotMap.unlockWrite(stamp)

Copy link
Contributor

Choose a reason for hiding this comment

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

So this doesn't seem consistent with the way locks are used elsewhere in the code. Specifically the locks on slot maps are only for reading and mutation of the slot map, not the values of the slots. The reason we hit the deadlock isn't that we're setting the value for the slot being initialised, but rather because the lazy constructors for Java packages create additional slots.

Setting the value on a normal slot is thread safe, it's just setting a field, though I guess we could add var handle support if concurrent operations are needed with specified ordering, but since setting the value doesn't change the slot map no lock is required to ensure thread safety.

Lazy constructors themselves appear to be thread safe since they are synchronised, but there is an amusing possible ordering problem because a lazy constructor could set its own slot value to something, seal the scope, and then return a completely different value which would then be set on the slot. That's technically fine because the object is sealed rather than frozen, but Rhino seems to already be wrong in its treatment of sealed and the mutation of properties in several places.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, you're right and I misremembered what we were trying to do here. The lock protects the map and not the value, which means that this change doesn't change the purpose of what the lock is trying to do.

I still maintain that Rhino was never really designed properly for the case of multiple threads executing in the context of the same script in parallel, and if it did we'd have to make up lots of new rules because the standard doesn't contemplate that use case. But for projects that have done this in the past, this particular support at least helps a bit.

@andreabergia
Copy link
Contributor Author

Thanks -- I can see that this resolves the deadlock, but I'm concerned that it's still not thread-safe. In particular, in the new loop, we're modifying the values of the slot map without a lock. At the very least, I think that we should get a read lock in the finally clause in line 2000. Even then, I'm not sure that fixes everything.

Yep, we know that it is not technically not thread safe... but if you have a thread that is mutating the slots, and a thread that is trying to seal that object, you already have a race condition. Some of the mutations might go through, some might not, but you shouldn't even be trying. :-)

@gbrail
Copy link
Collaborator

gbrail commented May 30, 2024

Thanks -- I understand what we're fixing now!

@gbrail gbrail merged commit c6a873f into mozilla:master May 30, 2024
3 checks passed
@andreabergia andreabergia deleted the fix-deadlock-sealing-lazy-ctor branch November 28, 2024 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants