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)