Reza Taroosheh created IGNITE-29276:
---------------------------------------

             Summary: An interrupted ATOMIC update is reported as a corrupted 
B+Tree if the interceptor returns a value of a new Externalizable class
                 Key: IGNITE-29276
                 URL: https://issues.apache.org/jira/browse/IGNITE-29276
             Project: Ignite
          Issue Type: Bug
            Reporter: Reza Taroosheh
            Assignee: Reza Taroosheh
             Fix For: 2.20


h3. Problem
In an ATOMIC cache, {{CacheInterceptor.onBeforePut()}} is called inside 
{{BPlusTree.invoke()}}, from 
{{GridCacheMapEntry.AtomicCacheUpdateClosure.update()}}. If it returns a value 
of an {{Externalizable}} class that isn't registered yet, the value is 
marshaled there by {{prepareForCache()}}, and the thread waits for the class 
registration inside the tree. An interrupt ends the wait with a 
{{BinaryObjectException}}, {{BPlusTree.invoke()}} reports it as 
{{CorruptedTreeException}}, and the failure handler stops the node, although 
nothing is corrupted.

IGNITE-29241 fixes the same problem for {{put}}, {{putAll}}, {{invoke}} and 
{{invokeAll}} by making the bytes of the new value before the tree. It doesn't 
cover the interceptor, because on this path the interceptor's value only exists 
inside the tree.

h3. Reproducer
In {{AtomicPutInterruptTest}} (added by IGNITE-29241):
{code:java}
/** The interceptor replaces the new value with an Externalizable value inside 
the cache update. */
@Test
public void 
testInterruptedPutWithInterceptorReturningUnregisteredExternalizableClass() 
throws Exception {
    IgniteCache<Integer, Object> cache = grid(0).createCache(new 
CacheConfiguration<Integer, Object>("interceptor")
        .setAtomicityMode(ATOMIC)
        .setInterceptor(new ExternalizableInterceptor()));

    checkInterruptedUpdateFails(() -> cache.put(1, 1));

    cache.put(1, 2);

    assertEquals(2, ((ExternalizableValue)cache.get(1)).val);
}

/** Replaces the new value with an Externalizable value, so the value is 
created inside the cache update. */
private static class ExternalizableInterceptor extends 
CacheInterceptorAdapter<Integer, Object> {
    /** {@inheritDoc} */
    @Override public Object onBeforePut(Cache.Entry<Integer, Object> entry, 
Object newVal) {
        return new ExternalizableValue((Integer)newVal);
    }
}
{code}

Expected: the put fails, and the failure handler isn't called.
Actual: the failure handler is called with {{CorruptedTreeException}}, caused 
by {{BinaryObjectException: Failed to register class}} from 
{{AtomicCacheUpdateClosure.update()}} -> {{prepareForCache()}}, caused by 
{{IgniteInterruptedCheckedException}}.

h3. Notes
* Moving {{onBeforePut()}} before the tree isn't a simple change: inside the 
tree it runs after the checks that can still reject the update (version check, 
filter, conflict resolution, expiry), so calling it earlier would also call it 
for updates that are rejected later.
* In {{GridDhtAtomicCache.updateWithBatch()}} (multi-key update with a 
write-through store), the interceptor already runs before the tree, so the 
bytes of its value can be made right after it, as IGNITE-29241 does for the 
entry processor's value.




--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to