2 * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.mdsal.dom.spi;
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;
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;
53 @RunWith(MockitoJUnitRunner.StrictStubs.class)
54 public class PingPongTransactionChainTest {
56 public Function<DOMTransactionChainListener, DOMTransactionChain> delegateFactory;
58 public DOMTransactionChainListener listener;
60 public DOMTransactionChain chain;
62 public DOMDataTreeReadWriteTransaction rwTx;
64 public DOMDataTreeReadWriteTransaction rwTx1;
66 public DOMDataTreeReadWriteTransaction rwTx2;
68 public DOMTransactionChainListener pingPongListener;
69 public PingPongTransactionChain pingPong;
72 public void before() {
73 // Slightly complicated bootstrap
74 doAnswer(invocation -> {
75 pingPongListener = invocation.getArgument(0);
77 }).when(delegateFactory).apply(any());
78 pingPong = new PingPongTransactionChain(delegateFactory, listener);
79 verify(delegateFactory).apply(any());
81 doReturn(rwTx).when(chain).newReadWriteTransaction();
85 public void testIdleClose() {
86 doNothing().when(chain).close();
88 verify(chain).close();
90 doNothing().when(listener).onTransactionChainSuccessful(pingPong);
91 pingPongListener.onTransactionChainSuccessful(chain);
92 verify(listener).onTransactionChainSuccessful(pingPong);
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);
105 public void testReadOnly() {
106 final var tx = pingPong.newReadOnlyTransaction();
107 assertGetIdentifier(tx);
108 assertReadOperations(tx);
109 assertCommit(tx::close);
113 public void testReadWrite() {
114 final var tx = pingPong.newReadWriteTransaction();
115 assertGetIdentifier(tx);
116 assertReadOperations(tx);
117 assertWriteOperations(tx);
118 assertCommit(tx::commit);
122 public void testWriteOnly() {
123 final var tx = pingPong.newWriteOnlyTransaction();
124 assertGetIdentifier(tx);
125 assertWriteOperations(tx);
126 assertCommit(tx::commit);
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());
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));
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));
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());
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);
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);
164 private void assertCommit(final Runnable commitMethod) {
165 doReturn(CommitInfo.emptyFluentFuture()).when(rwTx).commit();
167 verify(rwTx).commit();
171 public void testCommitFailure() {
172 assertCommitFailure(() -> { });
176 public void testCommitFailureAfterClose() {
177 assertCommitFailure(() -> {
178 doNothing().when(chain).close();
180 verify(chain).close();
184 private void assertCommitFailure(final Runnable asyncAction) {
185 final var tx = pingPong.newWriteOnlyTransaction();
187 final var rwTxFuture = SettableFuture.<CommitInfo>create();
188 doReturn(FluentFuture.from(rwTxFuture)).when(rwTx).commit();
190 final var txFuture = tx.commit();
191 verify(rwTx).commit();
192 assertFalse(txFuture.isDone());
196 final var cause = new TransactionCommitFailedException("cause");
197 rwTxFuture.setException(cause);
198 assertSame(cause, assertThrows(ExecutionException.class, () -> Futures.getDone(txFuture)).getCause());
202 public void testSimpleCancelFalse() {
203 assertSimpleCancel(false);
207 public void testSimpleCancelTrue() {
208 assertSimpleCancel(true);
211 private void assertSimpleCancel(final boolean result) {
212 final var tx = pingPong.newWriteOnlyTransaction();
214 doReturn(result).when(rwTx).cancel();
215 assertEquals(result, tx.cancel());
216 verify(rwTx).cancel();
220 public void testNewAfterSuccessfulCancel() {
221 doReturn(true).when(rwTx).cancel();
222 pingPong.newWriteOnlyTransaction().cancel();
223 assertNotNull(pingPong.newWriteOnlyTransaction());
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")));
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();
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);
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();
256 final var id1 = mock(Object.class);
257 doReturn(id1).when(rwTx1).getIdentifier();
258 assertSame(tx1.getIdentifier(), id1);
260 // Ready transaction is picked up by fast path allocation
261 final var tx2 = pingPong.newWriteOnlyTransaction();
262 assertSame(tx2.getIdentifier(), id1);
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();
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);
280 public void commitWhileInflight() {
281 final var tx = pingPong.newReadWriteTransaction();
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());
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();
295 doReturn(FluentFuture.from(rwTxFuture1)).when(rwTx1).commit();
296 rwTxFuture.set(CommitInfo.empty());
297 assertDone(txFuture);
298 verify(rwTx1).commit();
300 rwTxFuture1.set(CommitInfo.empty());
301 assertDone(tx1Future);
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());
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();
318 final var tx1Future = pingPong.newReadWriteTransaction().commit();
319 assertFalse(tx1Future.isDone());
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();
330 rwTx1Future.set(CommitInfo.empty());
331 assertDone(tx1Future);
335 public void testIdempotentClose() {
336 doNothing().when(chain).close();
338 verify(chain).close();
340 verifyNoMoreInteractions(chain);
343 private static <T> T assertDone(final FluentFuture<T> future) {
345 return Futures.getDone(future);
346 } catch (ExecutionException e) {
347 throw new AssertionError(e);