Skip to content

Commit 1906d3c

Browse files
authored
OAK-11720: Introduce tests for exclusive merge lock (#2351)
1 parent 1c85b3a commit 1906d3c

File tree

1 file changed

+125
-0
lines changed

1 file changed

+125
-0
lines changed

oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreBranchTest.java

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,33 @@
1616
*/
1717
package org.apache.jackrabbit.oak.plugins.document;
1818

19+
import org.apache.jackrabbit.oak.api.CommitFailedException;
1920
import org.apache.jackrabbit.oak.json.JsopDiff;
21+
import org.apache.jackrabbit.oak.spi.commit.CommitHook;
22+
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
2023
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
2124
import org.apache.jackrabbit.oak.spi.state.NodeState;
2225
import org.junit.Rule;
2326
import org.junit.Test;
2427

28+
import java.util.concurrent.atomic.AtomicInteger;
29+
import java.util.concurrent.locks.ReentrantReadWriteLock;
30+
31+
import static org.apache.jackrabbit.oak.api.CommitFailedException.MERGE;
2532
import static org.apache.jackrabbit.oak.plugins.document.TestUtils.merge;
2633
import static org.apache.jackrabbit.oak.plugins.document.TestUtils.persistToBranch;
2734
import static org.junit.Assert.assertFalse;
35+
import static org.junit.Assert.assertNotNull;
2836
import static org.junit.Assert.assertTrue;
37+
import static org.junit.Assert.assertEquals;
2938
import static org.junit.Assert.fail;
39+
import static org.mockito.ArgumentMatchers.anyInt;
40+
import static org.mockito.ArgumentMatchers.anyLong;
41+
import static org.mockito.ArgumentMatchers.anyBoolean;
42+
import static org.mockito.ArgumentMatchers.eq;
43+
import static org.mockito.Mockito.mock;
44+
import static org.mockito.Mockito.never;
45+
import static org.mockito.Mockito.verify;
3046

3147
public class DocumentNodeStoreBranchTest {
3248

@@ -120,4 +136,113 @@ public void noopChanges() throws Exception {
120136
}
121137
builder.getNodeState().compareAgainstBaseState(root, new JsopDiff());
122138
}
139+
140+
@Test // OAK-11720
141+
public void mergeRetriesWithExclusiveLock() throws Exception {
142+
// avoidMergeLock = false -> should retry with exclusive lock
143+
boolean AVOID_MERGE_LOCK = false;
144+
DocumentMK.Builder mkBuilder = builderProvider.newBuilder();
145+
DocumentNodeStoreStatsCollector statsCollector = mock(DocumentNodeStoreStatsCollector.class);
146+
mkBuilder.setNodeStoreStatsCollector(statsCollector);
147+
DocumentNodeStore store = mkBuilder.getNodeStore();
148+
// Max back-off time for retries.
149+
// It will retry with a waiting time of 50ms, 100ms, 200ms and 400ms (4 attempts in total).
150+
store.setMaxBackOffMillis(500);
151+
152+
// Best way to simulate a merge failure is to use a CommitHook that throws
153+
// an exception on the first 4 attempts and succeeds on the 5th attempt.
154+
AtomicInteger hookInvocations = new AtomicInteger();
155+
CommitHook hook = (before, after, info) -> {
156+
int count = hookInvocations.incrementAndGet();
157+
if (count <= 4) { // Force a merge failure for the first 4 attempts
158+
throw new CommitFailedException(MERGE, 1000 + count, "simulated failure");
159+
} else {
160+
// on the 5th attempt will succeed
161+
return after;
162+
}
163+
};
164+
165+
// create a test node to be merged
166+
NodeBuilder builder = store.getRoot().builder();
167+
builder.child("testNode").setProperty("testProperty", "testValue");
168+
169+
DocumentNodeStoreBranch branch = new DocumentNodeStoreBranch(store, store.getRoot(),
170+
new ReentrantReadWriteLock(), AVOID_MERGE_LOCK // avoidMergeLock set to false - must retry with exclusive lock
171+
);
172+
branch.setRoot(builder.getNodeState());
173+
174+
// Initially the test node must not exist
175+
assertFalse(store.getRoot().hasChildNode("testNode"));
176+
NodeState result = branch.merge(hook, CommitInfo.EMPTY);
177+
assertNotNull(result);
178+
// Check the CommitHook was invoked 5 times (4 failures + 1 success)
179+
assertEquals("CommitHook must be invoked 5 times", 5, hookInvocations.get());
180+
// The test node must now exist after the successful merge
181+
assertTrue("Node must be present after successful merge", store.getRoot().hasChildNode("testNode"));
182+
assertTrue("Property must be set after successful merge", store.getRoot().getChildNode("testNode").hasProperty("testProperty"));
183+
184+
// Verify that first 4 attempts failed with exclusive == false
185+
verify(statsCollector).failedMerge(anyInt(), anyLong(), anyLong(), eq(false));
186+
// Verify that the last attempt succeeded with exclusive == true
187+
verify(statsCollector).doneMerge(anyInt(), anyInt(), anyLong(), anyLong(), eq(true));
188+
// Verify that no attempt without exclusive lock failed
189+
verify(statsCollector, never()).doneMerge(anyInt(), anyInt(), anyLong(), anyLong(), eq(false));
190+
}
191+
192+
@Test // OAK-11720
193+
public void mergeRetriesWithoutExclusiveLock() {
194+
// avoidMergeLock = true -> should not retry with exclusive lock and fail immediately
195+
boolean AVOID_MERGE_LOCK = true;
196+
DocumentMK.Builder mkBuilder = builderProvider.newBuilder();
197+
DocumentNodeStoreStatsCollector statsCollector = mock(DocumentNodeStoreStatsCollector.class);
198+
mkBuilder.setNodeStoreStatsCollector(statsCollector);
199+
DocumentNodeStore store = mkBuilder.getNodeStore();
200+
// Max back-off time for retries.
201+
// It will retry with a waiting time of 50ms, 100ms, 200ms and 400ms (4 attempts in total)
202+
store.setMaxBackOffMillis(500);
203+
204+
AtomicInteger hookInvocations = new AtomicInteger();
205+
CommitHook hook = (before, after, info) -> {
206+
int count = hookInvocations.incrementAndGet();
207+
if (count <= 4) { // Force a merge failure for the first 4 attempts
208+
throw new CommitFailedException(MERGE, 1000 + count, "simulated failure");
209+
} else {
210+
// on the 5th attempt will succeed
211+
return after;
212+
}
213+
};
214+
215+
// create a test node to be merged
216+
NodeBuilder builder = store.getRoot().builder();
217+
builder.child("testNode").setProperty("testProperty", "testValue");
218+
219+
DocumentNodeStoreBranch branch = new DocumentNodeStoreBranch(store, store.getRoot(),
220+
new ReentrantReadWriteLock(), AVOID_MERGE_LOCK // avoidMergeLock set to true - must fail after retries without exclusive lock
221+
);
222+
branch.setRoot(builder.getNodeState());
223+
224+
// Initially the test node must not exist
225+
assertFalse(store.getRoot().hasChildNode("testNode"));
226+
try {
227+
branch.merge(hook, CommitInfo.EMPTY);
228+
fail("Merge must fail with CommitFailedException after all the attempts without exclusive lock");
229+
} catch (CommitFailedException e) {
230+
assertEquals(MERGE, e.getType());
231+
assertEquals(1004, e.getCode());
232+
}
233+
234+
// Check the CommitHook was invoked 4 times (4 failures)
235+
assertEquals("CommitHook must be invoked 4 times", 4, hookInvocations.get());
236+
// The test node must NOT exist after the successful merge
237+
assertFalse("Node must be present after successful merge", store.getRoot().hasChildNode("testNode"));
238+
239+
// Verify that first 4 attempts failed with exclusive == false
240+
verify(statsCollector).failedMerge(anyInt(), anyLong(), anyLong(), eq(false));
241+
// Verify that no attempt failed with exclusive == true
242+
verify(statsCollector, never()).failedMerge(anyInt(), anyLong(), anyLong(), eq(true));
243+
// Verify that no merge attempt happened with exclusive == true
244+
verify(statsCollector, never()).failedMerge(anyInt(), anyLong(), anyLong(), eq(true));
245+
// Verify that the merge never succeeded (with any value of exclusive lock)
246+
verify(statsCollector, never()).doneMerge(anyInt(), anyInt(), anyLong(), anyLong(), anyBoolean());
247+
}
123248
}

0 commit comments

Comments
 (0)