[
https://issues.apache.org/jira/browse/IGNITE-29241?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18140873#comment-18140873
]
Reza Taroosheh commented on IGNITE-29241:
-----------------------------------------
Update on the investigation. The draft PR now has more tests
([c577ca96c2|https://github.com/apache/ignite/pull/13779/commits/c577ca96c2bd37db442d356ec1934cca27963bb8]).
No fix has been pushed yet.
*Cause.* An {{Externalizable}} value is marshalled inside
{{BPlusTree.invoke()}}. If its class isn't registered yet, the thread waits
there for registration. An interrupt ends the wait with a
{{BinaryObjectException}}, and {{BPlusTree.invoke()}} reports that
{{RuntimeException}} as {{CorruptedTreeException}}.
Each such class needs two registrations: its binary type ID, and its optimized
marshaller ID. The second one is done in
{{OptimizedMarshallerUtils.classDescriptor()}}, which always waits.
*put, putAll and put to a remote primary.* The value exists before the tree, so
its bytes can be made before the tree and before the entry locks. A failed
registration then fails the whole request before any key is written. With this
change in my local copy, the tests for these paths pass.
*invoke and the interceptor.* The new value does not exist before the tree,
because it is computed from the old value, which {{AtomicCacheUpdateClosure}}
gets inside {{BPlusTree.invoke()}}. The existing {{failIfUnregistered}} retry
does not cover the optimized marshaller. With the local change above, two tests
still fail with {{CorruptedTreeException}}:
* {{testInterruptedInvokeOfExternalizableClassWithRegisteredBinaryType}}: only
the optimized marshaller ID is left to register.
* {{testInterruptedInvokeOfNestedUnregisteredExternalizableClass}}: the
unregistered class is written inside {{writeExternal()}}.
The interceptor and read-through also make their values in
{{AtomicCacheUpdateClosure}}, so I expect the same problem there, but I have no
failing test for them yet.
*Next.* I am trying to run the entry processor before the tree, as
{{GridDhtAtomicCache.updateWithBatch()}} and {{TRANSACTIONAL}} caches already
do, so that no new value is marshaled inside the tree. The cost is one more key
read for each {{invoke}}.
> An interrupted ATOMIC put of a new Externalizable class is reported as a
> corrupted B+Tree and stops the node
> ------------------------------------------------------------------------------------------------------------
>
> Key: IGNITE-29241
> URL: https://issues.apache.org/jira/browse/IGNITE-29241
> Project: Ignite
> Issue Type: Bug
> Reporter: Reza Taroosheh
> Assignee: Reza Taroosheh
> Priority: Major
> Fix For: 2.20
>
> Time Spent: 10m
> Remaining Estimate: 0h
>
> If a thread is interrupted while an {{ATOMIC}} cache update writes a value of
> an {{Externalizable}} class (or a class with its own
> {{writeObject}}/{{readObject}}) that the node has not used yet, the update
> fails with {{CorruptedTreeException: B+Tree is corrupted}}. This is a
> {{CRITICAL_ERROR}}, and the default {{StopNodeOrHaltFailureHandler}} halts
> the JVM.
> Nothing is corrupted: the interrupt only ends the wait for the class name
> registration. An interrupt is an ordinary event on a pooled thread, for
> example at {{ExecutorService.shutdownNow()}}. It should fail the operation,
> not stop the node.
> Steps:
> # Start a server node with an {{ATOMIC}} cache.
> # Interrupt a thread, then call {{cache.put(1, value)}} from it, where
> {{value}} is of an {{Externalizable}} class not used in the cluster before.
> Actual: the put fails, and the failure handler is called with
> {{CorruptedTreeException: B+Tree is corrupted [... msg=Runtime failure on
> search row ...]}}, caused by {{BinaryObjectException: Failed to register
> class}}, caused by {{IgniteInterruptedCheckedException: Got interrupted while
> waiting for future to complete}}.
> Expected: the put fails with an exception, the failure handler is not called,
> and the node keeps running.
> The same interrupt fails the operation cleanly, without a failure handler
> call, when the value's class is handled by the binary marshaller, when the
> {{Externalizable}} class is nested in a binary value, or when the cache is
> {{TRANSACTIONAL}}, and the write is in a transaction.
> Workaround: write each such class once, on a thread that is not interrupted,
> before any thread that may be interrupted writes it.
> {{IgniteBinary.registerClass}} and {{IgniteBinary.toBinary}} do not help.
> Reproduced on 2.18.0 (reported on the dev list:
> https://lists.apache.org/thread.html/qqyp941vb5vmxlzz0d18yglzjyrw3t7r), and
> on master by a new test.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)