|
16 | 16 | */ |
17 | 17 | package org.apache.jackrabbit.oak.plugins.document; |
18 | 18 |
|
| 19 | +import org.apache.jackrabbit.oak.api.CommitFailedException; |
19 | 20 | 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; |
20 | 23 | import org.apache.jackrabbit.oak.spi.state.NodeBuilder; |
21 | 24 | import org.apache.jackrabbit.oak.spi.state.NodeState; |
22 | 25 | import org.junit.Rule; |
23 | 26 | import org.junit.Test; |
24 | 27 |
|
| 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; |
25 | 32 | import static org.apache.jackrabbit.oak.plugins.document.TestUtils.merge; |
26 | 33 | import static org.apache.jackrabbit.oak.plugins.document.TestUtils.persistToBranch; |
27 | 34 | import static org.junit.Assert.assertFalse; |
| 35 | +import static org.junit.Assert.assertNotNull; |
28 | 36 | import static org.junit.Assert.assertTrue; |
| 37 | +import static org.junit.Assert.assertEquals; |
29 | 38 | 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; |
30 | 46 |
|
31 | 47 | public class DocumentNodeStoreBranchTest { |
32 | 48 |
|
@@ -120,4 +136,113 @@ public void noopChanges() throws Exception { |
120 | 136 | } |
121 | 137 | builder.getNodeState().compareAgainstBaseState(root, new JsopDiff()); |
122 | 138 | } |
| 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 | + } |
123 | 248 | } |
0 commit comments