7b9f49794ef3ce7b37699f6b08249b7d6e3f7e06
[mdsal.git] / dom / mdsal-dom-spi / src / test / java / org / opendaylight / mdsal / dom / spi / PingPongTransactionChainTest.java
1 /*
2  * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.mdsal.dom.spi;
9
10 import static org.hamcrest.CoreMatchers.allOf;
11 import static org.hamcrest.CoreMatchers.containsString;
12 import static org.hamcrest.CoreMatchers.endsWith;
13 import static org.hamcrest.CoreMatchers.startsWith;
14 import static org.hamcrest.MatcherAssert.assertThat;
15 import static org.junit.Assert.assertEquals;
16 import static org.junit.Assert.assertFalse;
17 import static org.junit.Assert.assertNotNull;
18 import static org.junit.Assert.assertSame;
19 import static org.junit.Assert.assertThrows;
20 import static org.mockito.ArgumentMatchers.any;
21 import static org.mockito.Mockito.doAnswer;
22 import static org.mockito.Mockito.doNothing;
23 import static org.mockito.Mockito.doReturn;
24 import static org.mockito.Mockito.mock;
25 import static org.mockito.Mockito.never;
26 import static org.mockito.Mockito.verify;
27 import static org.mockito.Mockito.verifyNoMoreInteractions;
28
29 import com.google.common.util.concurrent.FluentFuture;
30 import com.google.common.util.concurrent.Futures;
31 import com.google.common.util.concurrent.SettableFuture;
32 import java.util.Optional;
33 import java.util.concurrent.ExecutionException;
34 import java.util.function.Function;
35 import org.junit.Before;
36 import org.junit.Test;
37 import org.junit.runner.RunWith;
38 import org.mockito.Mock;
39 import org.mockito.junit.MockitoJUnitRunner;
40 import org.opendaylight.mdsal.common.api.CommitInfo;
41 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
42 import org.opendaylight.mdsal.common.api.TransactionCommitFailedException;
43 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadOperations;
44 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
45 import org.opendaylight.mdsal.dom.api.DOMDataTreeTransaction;
46 import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteOperations;
47 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
48 import org.opendaylight.mdsal.dom.api.DOMTransactionChainListener;
49 import org.opendaylight.yangtools.util.concurrent.FluentFutures;
50 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
51 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
52
53 @RunWith(MockitoJUnitRunner.StrictStubs.class)
54 public class PingPongTransactionChainTest {
55     @Mock
56     public Function<DOMTransactionChainListener, DOMTransactionChain> delegateFactory;
57     @Mock
58     public DOMTransactionChainListener listener;
59     @Mock
60     public DOMTransactionChain chain;
61     @Mock
62     public DOMDataTreeReadWriteTransaction rwTx;
63     @Mock
64     public DOMDataTreeReadWriteTransaction rwTx1;
65     @Mock
66     public DOMDataTreeReadWriteTransaction rwTx2;
67
68     public DOMTransactionChainListener pingPongListener;
69     public PingPongTransactionChain pingPong;
70
71     @Before
72     public void before() {
73         // Slightly complicated bootstrap
74         doAnswer(invocation -> {
75             pingPongListener = invocation.getArgument(0);
76             return chain;
77         }).when(delegateFactory).apply(any());
78         pingPong = new PingPongTransactionChain(delegateFactory, listener);
79         verify(delegateFactory).apply(any());
80
81         doReturn(rwTx).when(chain).newReadWriteTransaction();
82     }
83
84     @Test
85     public void testIdleClose() {
86         doNothing().when(chain).close();
87         pingPong.close();
88         verify(chain).close();
89
90         doNothing().when(listener).onTransactionChainSuccessful(pingPong);
91         pingPongListener.onTransactionChainSuccessful(chain);
92         verify(listener).onTransactionChainSuccessful(pingPong);
93     }
94
95     @Test
96     public void testIdleFailure() {
97         final var cause = new Throwable();
98         doNothing().when(listener).onTransactionChainFailed(pingPong, null, cause);
99         doReturn("mock").when(chain).toString();
100         pingPongListener.onTransactionChainFailed(chain, rwTx, cause);
101         verify(listener).onTransactionChainFailed(pingPong, null, cause);
102     }
103
104     @Test
105     public void testReadOnly() {
106         final var tx = pingPong.newReadOnlyTransaction();
107         assertGetIdentifier(tx);
108         assertReadOperations(tx);
109         assertCommit(tx::close);
110     }
111
112     @Test
113     public void testReadWrite() {
114         final var tx = pingPong.newReadWriteTransaction();
115         assertGetIdentifier(tx);
116         assertReadOperations(tx);
117         assertWriteOperations(tx);
118         assertCommit(tx::commit);
119     }
120
121     @Test
122     public void testWriteOnly() {
123         final var tx = pingPong.newWriteOnlyTransaction();
124         assertGetIdentifier(tx);
125         assertWriteOperations(tx);
126         assertCommit(tx::commit);
127     }
128
129     private void assertGetIdentifier(final DOMDataTreeTransaction tx) {
130         final var id = mock(Object.class);
131         doReturn(id).when(rwTx).getIdentifier();
132         assertSame(id, tx.getIdentifier());
133     }
134
135     private void assertReadOperations(final DOMDataTreeReadOperations tx) {
136         doReturn(FluentFutures.immediateTrueFluentFuture()).when(rwTx).exists(
137             LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
138         final var exists = tx.exists(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
139         verify(rwTx).exists(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
140         assertEquals(Boolean.TRUE, assertDone(exists));
141
142         doReturn(FluentFutures.immediateFluentFuture(Optional.empty())).when(rwTx).read(
143             LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
144         final var read = tx.read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
145         verify(rwTx).read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
146         assertEquals(Optional.empty(), assertDone(read));
147     }
148
149     private void assertWriteOperations(final DOMDataTreeWriteOperations tx) {
150         doNothing().when(rwTx).delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
151         tx.delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
152         verify(rwTx).delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
153
154         final var data = mock(NormalizedNode.class);
155         doNothing().when(rwTx).merge(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
156         tx.merge(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
157         verify(rwTx).merge(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
158
159         doNothing().when(rwTx).put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
160         tx.put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
161         verify(rwTx).put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
162     }
163
164     private void assertCommit(final Runnable commitMethod) {
165         doReturn(CommitInfo.emptyFluentFuture()).when(rwTx).commit();
166         commitMethod.run();
167         verify(rwTx).commit();
168     }
169
170     @Test
171     public void testCommitFailure() {
172         assertCommitFailure(() -> { });
173     }
174
175     @Test
176     public void testCommitFailureAfterClose() {
177         assertCommitFailure(() -> {
178             doNothing().when(chain).close();
179             pingPong.close();
180             verify(chain).close();
181         });
182     }
183
184     private void assertCommitFailure(final Runnable asyncAction) {
185         final var tx = pingPong.newWriteOnlyTransaction();
186
187         final var rwTxFuture = SettableFuture.<CommitInfo>create();
188         doReturn(FluentFuture.from(rwTxFuture)).when(rwTx).commit();
189
190         final var txFuture = tx.commit();
191         verify(rwTx).commit();
192         assertFalse(txFuture.isDone());
193
194         asyncAction.run();
195
196         final var cause = new TransactionCommitFailedException("cause");
197         rwTxFuture.setException(cause);
198         assertSame(cause, assertThrows(ExecutionException.class, () -> Futures.getDone(txFuture)).getCause());
199     }
200
201     @Test
202     public void testSimpleCancelFalse() {
203         assertSimpleCancel(false);
204     }
205
206     @Test
207     public void testSimpleCancelTrue() {
208         assertSimpleCancel(true);
209     }
210
211     private void assertSimpleCancel(final boolean result) {
212         final var tx = pingPong.newWriteOnlyTransaction();
213
214         doReturn(result).when(rwTx).cancel();
215         assertEquals(result, tx.cancel());
216         verify(rwTx).cancel();
217     }
218
219     @Test
220     public void testNewAfterSuccessfulCancel() {
221         doReturn(true).when(rwTx).cancel();
222         pingPong.newWriteOnlyTransaction().cancel();
223         assertNotNull(pingPong.newWriteOnlyTransaction());
224     }
225
226     @Test
227     public void testNewAfterNew() {
228         assertNotNull(pingPong.newWriteOnlyTransaction());
229         doReturn(true).when(rwTx).cancel();
230         doReturn("mock").when(rwTx).toString();
231         final var ex = assertThrows(IllegalStateException.class, () -> pingPong.newWriteOnlyTransaction());
232         assertThat(ex.getMessage(), allOf(
233             startsWith("New transaction PingPongTransaction"),
234             containsString(" raced with transaction PingPongTransaction")));
235     }
236
237     @Test
238     public void testReadWriteReuse() {
239         final var tx = pingPong.newReadWriteTransaction();
240         final var rwTxFuture = SettableFuture.<CommitInfo>create();
241         doReturn(FluentFuture.from(rwTxFuture)).when(rwTx).commit();
242         // Now rwTx is inflight, but does not commit immediately
243         final var txFuture = tx.commit();
244         verify(rwTx).commit();
245
246         // Assert identity without delving into details
247         final var id = mock(Object.class);
248         doReturn(id).when(rwTx).getIdentifier();
249         assertSame(tx.getIdentifier(), id);
250
251         doReturn(rwTx1).when(chain).newReadWriteTransaction();
252         final var tx1 = pingPong.newWriteOnlyTransaction();
253         // now rwTx1 is ready, waiting for inflight to be completed
254         final var tx1Future = tx1.commit();
255
256         final var id1 = mock(Object.class);
257         doReturn(id1).when(rwTx1).getIdentifier();
258         assertSame(tx1.getIdentifier(), id1);
259
260         // Ready transaction is picked up by fast path allocation
261         final var tx2 = pingPong.newWriteOnlyTransaction();
262         assertSame(tx2.getIdentifier(), id1);
263
264         // Complete inflight transaction...
265         rwTxFuture.set(CommitInfo.empty());
266         assertDone(txFuture);
267         // ... but we are still holding the follow-up frontend transaction ...
268         assertFalse(tx1Future.isDone());
269         verify(rwTx1, never()).commit();
270
271         // ... and it will commit once we commit tx2 ...
272         doReturn(CommitInfo.emptyFluentFuture()).when(rwTx1).commit();
273         final var tx2Future = tx2.commit();
274         // ... at which point both complete
275         assertDone(tx1Future);
276         assertDone(tx2Future);
277     }
278
279     @Test
280     public void commitWhileInflight() {
281         final var tx = pingPong.newReadWriteTransaction();
282
283         final var rwTxFuture = SettableFuture.<CommitInfo>create();
284         doReturn(FluentFuture.from(rwTxFuture)).when(rwTx).commit();
285         // rwTxFuture is inflight
286         final var txFuture = tx.commit();
287         verify(rwTx).commit();
288         assertFalse(txFuture.isDone());
289
290         doReturn(rwTx1).when(chain).newReadWriteTransaction();
291         final var rwTxFuture1 = SettableFuture.<CommitInfo>create();
292         final var tx1 = pingPong.newWriteOnlyTransaction();
293         final var tx1Future = tx1.commit();
294
295         doReturn(FluentFuture.from(rwTxFuture1)).when(rwTx1).commit();
296         rwTxFuture.set(CommitInfo.empty());
297         assertDone(txFuture);
298         verify(rwTx1).commit();
299
300         rwTxFuture1.set(CommitInfo.empty());
301         assertDone(tx1Future);
302     }
303
304     @Test
305     public void testNewAfterAsyncShutdown() {
306         // Setup inflight transaction
307         final var tx = pingPong.newReadWriteTransaction();
308         final var rwTxFuture = SettableFuture.<CommitInfo>create();
309         doReturn(FluentFuture.from(rwTxFuture)).when(rwTx).commit();
310         final var txFuture = tx.commit();
311         assertFalse(txFuture.isDone());
312
313         // Setup ready transaction
314         doReturn(rwTx1).when(chain).newReadWriteTransaction();
315         final var rwTx1Future = SettableFuture.<CommitInfo>create();
316         doReturn(FluentFuture.from(rwTx1Future)).when(rwTx1).commit();
317
318         final var tx1Future = pingPong.newReadWriteTransaction().commit();
319         assertFalse(tx1Future.isDone());
320
321         pingPong.close();
322
323         final var ex = assertThrows(IllegalStateException.class, pingPong::newWriteOnlyTransaction);
324         assertThat(ex.getMessage(), allOf(startsWith("Transaction chain "), endsWith(" has been shut down")));
325         doNothing().when(chain).close();
326         rwTxFuture.set(CommitInfo.empty());
327         assertDone(txFuture);
328         verify(chain).close();
329
330         rwTx1Future.set(CommitInfo.empty());
331         assertDone(tx1Future);
332     }
333
334     @Test
335     public void testIdempotentClose() {
336         doNothing().when(chain).close();
337         pingPong.close();
338         verify(chain).close();
339         pingPong.close();
340         verifyNoMoreInteractions(chain);
341     }
342
343     private static <T> T assertDone(final FluentFuture<T> future) {
344         try {
345             return Futures.getDone(future);
346         } catch (ExecutionException e) {
347             throw new AssertionError(e);
348         }
349     }
350 }