Disposable identityHashCode Collision causing wrong behavior

Answered

One of our users bumped into that error:

Text:

com.intellij.util.IncorrectOperationException: Sorry but parent: com.tabnine.inline.CompletionPreview@3e2d7098 (class com.tabnine.inline.CompletionPreview) has already been disposed (see the cause for stacktrace) so the child: com.tabnine.inline.render.DefaultTabnineInlay@215e257b (class com.tabnine.inline.render.DefaultTabnineInlay) will never be disposed
	at com.intellij.openapi.util.ObjectTree.register(ObjectTree.java:53)
	at com.intellij.openapi.util.Disposer.register(Disposer.java:118)
	at com.tabnine.inline.render.DefaultTabnineInlay.<init>(DefaultTabnineInlay.kt:20)
	at com.tabnine.inline.render.TabnineInlay$Companion.create(TabnineInlay.kt:18)
	at com.tabnine.inline.render.TabnineInlay.create(TabnineInlay.kt)
	at com.tabnine.inline.CompletionPreview.<init>(CompletionPreview.java:52)
	at com.tabnine.inline.CompletionPreview.createInstance(CompletionPreview.java:65)
	at com.tabnine.inline.InlineCompletionHandler.showInlineCompletion(InlineCompletionHandler.java:95)
	at com.tabnine.inline.InlineCompletionHandler.lambda$rerenderCompletion$3(InlineCompletionHandler.java:66)
	at com.intellij.openapi.application.TransactionGuardImpl.runWithWritingAllowed(TransactionGuardImpl.java:215)
	at com.intellij.openapi.application.TransactionGuardImpl.access$100(TransactionGuardImpl.java:22)
	at com.intellij.openapi.application.TransactionGuardImpl$1.run(TransactionGuardImpl.java:197)
	at com.intellij.openapi.application.impl.ApplicationImpl.runIntendedWriteActionOnCurrentThread(ApplicationImpl.java:873)
	at com.intellij.openapi.application.impl.ApplicationImpl$3.run(ApplicationImpl.java:511)
	at com.intellij.openapi.application.impl.FlushQueue.doRun(FlushQueue.java:69)
	at com.intellij.openapi.application.impl.FlushQueue.runNextEvent(FlushQueue.java:112)
	at com.intellij.openapi.application.impl.FlushQueue.flushNow(FlushQueue.java:42)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:313)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:776)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:727)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:746)
	at com.intellij.ide.IdeEventQueue.defaultDispatchEvent(IdeEventQueue.java:898)
	at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.java:746)
	at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$6(IdeEventQueue.java:439)
	at com.intellij.openapi.progress.impl.CoreProgressManager.computePrioritized(CoreProgressManager.java:803)
	at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$7(IdeEventQueue.java:438)
	at com.intellij.openapi.application.TransactionGuardImpl.performActivity(TransactionGuardImpl.java:106)
	at com.intellij.ide.IdeEventQueue.performActivity(IdeEventQueue.java:604)
	at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$8(IdeEventQueue.java:436)
	at com.intellij.openapi.application.impl.ApplicationImpl.runIntendedWriteActionOnCurrentThread(ApplicationImpl.java:873)
	at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.java:484)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:207)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:128)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:117)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:113)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:105)
	at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:92)

As you can see from the code: https://github.com/codota/tabnine-intellij/blob/master/src/main/java/com/tabnine/inline/CompletionPreview.java#L52

The object is regarded as disposed of, although the creation of TabnineInlay was called from the constructor of CompletionPreview... This means that it is not really feasible that the CompletionPreview instance was disposed of before it was even created.

That led me to look at what is throwing the exception:

Object wasDisposed = getDisposalInfo(parent);
if (wasDisposed != null) {
throw new IncorrectOperationException("Sorry but parent: " + parent + " has already been disposed " +
"(see the cause for stacktrace) so the child: "+child+" will never be disposed",
wasDisposed instanceof Throwable ? (Throwable)wasDisposed : null);
}

From which I found that you use the following identity strategy:

TObjectIdentityHashingStrategy

Which computes hashcode as such:

public final int computeHashCode(T object) {
return System.identityHashCode(object);
}

This is the cause of the bug. As this is what the docs are stating: ״As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the JavaTM programming language.)״.

This means that it is guaranteed that an object will have the same identityHashCode for its entire lifetime, but it is not guaranteed to be unique throughout the program life. Since we are creating and disposing of a big amount of CompletionPreviews, it seems that a collision had occurred, which caused the exception.

(see https://stackoverflow.com/a/4933258/2420354)

1 comment
Comment actions Permalink

Boaz Thank you for the analysis. I think things are a bit more complicated though. Even if you managed to bump into an identityHash collision (not a small feat btw, you should definitely apply to https://en.wikipedia.org/wiki/NIST_hash_function_competition :) ),

it's not enough to cause the bug, because this identityHashCode is used for the Map implementation. And maps are written specifically to survive their key hash collisions (in this case, using an open addressing strategy). 
So the truth I think is out there somewhere else. Maybe TabnineInlay.create(this) somehow disposes its argument? Or it's the other thread who disposes it?

0

Please sign in to leave a comment.