*/
package org.opendaylight.mdsal.dom.spi;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.opendaylight.mdsal.common.api.CommitInfo;
import org.opendaylight.mdsal.dom.api.DOMDataTreeTransaction;
import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteOperations;
import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
-import org.opendaylight.mdsal.dom.api.DOMTransactionChainListener;
import org.opendaylight.yangtools.util.concurrent.FluentFutures;
+import org.opendaylight.yangtools.yang.common.Empty;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class PingPongTransactionChainTest {
@Mock
- public Function<DOMTransactionChainListener, DOMTransactionChain> delegateFactory;
- @Mock
- public DOMTransactionChainListener listener;
- @Mock
+ public FutureCallback<Empty> listener;
+ @Mock(answer = Answers.CALLS_REAL_METHODS)
public DOMTransactionChain chain;
@Mock
public DOMDataTreeReadWriteTransaction rwTx;
+ @Mock
+ public DOMDataTreeReadWriteTransaction rwTx1;
+ @Mock
+ public DOMDataTreeReadWriteTransaction rwTx2;
+
+ private final SettableFuture<Empty> future = SettableFuture.create();
- public DOMTransactionChainListener pingPongListener;
public PingPongTransactionChain pingPong;
@Before
public void before() {
- // Slightly complicated bootstrap
- doAnswer(invocation -> {
- pingPongListener = invocation.getArgument(0);
- return chain;
- }).when(delegateFactory).apply(any());
- pingPong = new PingPongTransactionChain(delegateFactory, listener);
- verify(delegateFactory).apply(any());
-
+ doReturn(future).when(chain).future();
+ pingPong = new PingPongTransactionChain(chain);
doReturn(rwTx).when(chain).newReadWriteTransaction();
}
doNothing().when(chain).close();
pingPong.close();
verify(chain).close();
+ pingPong.addCallback(listener);
- doNothing().when(listener).onTransactionChainSuccessful(pingPong);
- pingPongListener.onTransactionChainSuccessful(chain);
- verify(listener).onTransactionChainSuccessful(pingPong);
+ future.set(Empty.value());
+ verify(listener).onSuccess(Empty.value());
}
@Test
public void testIdleFailure() {
final var cause = new Throwable();
- doNothing().when(listener).onTransactionChainFailed(pingPong, null, cause);
- pingPongListener.onTransactionChainFailed(chain, rwTx, cause);
- verify(listener).onTransactionChainFailed(pingPong, null, cause);
+ doNothing().when(listener).onFailure(cause);
+ doReturn("mock").when(chain).toString();
+
+ future.setException(cause);
+ pingPong.addCallback(listener);
+ verify(listener).onFailure(cause);
}
@Test
private void assertReadOperations(final DOMDataTreeReadOperations tx) {
doReturn(FluentFutures.immediateTrueFluentFuture()).when(rwTx).exists(
- LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty());
- final var exists = tx.exists(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty());
- verify(rwTx).exists(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty());
+ LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
+ final var exists = tx.exists(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
+ verify(rwTx).exists(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
assertEquals(Boolean.TRUE, assertDone(exists));
doReturn(FluentFutures.immediateFluentFuture(Optional.empty())).when(rwTx).read(
- LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty());
- final var read = tx.read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty());
- verify(rwTx).read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty());
+ LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
+ final var read = tx.read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
+ verify(rwTx).read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
assertEquals(Optional.empty(), assertDone(read));
}
private void assertWriteOperations(final DOMDataTreeWriteOperations tx) {
- doNothing().when(rwTx).delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty());
- tx.delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty());
- verify(rwTx).delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty());
-
- final var data = mock(NormalizedNode.class);
- doNothing().when(rwTx).merge(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty(), data);
- tx.merge(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty(), data);
- verify(rwTx).merge(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty(), data);
-
- doNothing().when(rwTx).put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty(), data);
- tx.put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty(), data);
- verify(rwTx).put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty(), data);
+ doNothing().when(rwTx).delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
+ tx.delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
+ verify(rwTx).delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
+
+ final var data = mock(ContainerNode.class);
+ doNothing().when(rwTx).merge(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
+ tx.merge(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
+ verify(rwTx).merge(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
+
+ doNothing().when(rwTx).put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
+ tx.put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
+ verify(rwTx).put(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of(), data);
}
private void assertCommit(final Runnable commitMethod) {
private void assertSimpleCancel(final boolean result) {
final var tx = pingPong.newWriteOnlyTransaction();
- doNothing().when(chain).close();
doReturn(result).when(rwTx).cancel();
+ assertEquals(result, tx.cancel());
+ verify(rwTx).cancel();
+ }
+
+ @Test
+ public void testNewAfterSuccessfulCancel() {
+ doReturn(true).when(rwTx).cancel();
+ pingPong.newWriteOnlyTransaction().cancel();
+ assertNotNull(pingPong.newWriteOnlyTransaction());
+ }
+
+ @Test
+ public void testNewAfterNew() {
+ assertNotNull(pingPong.newWriteOnlyTransaction());
+ doReturn(true).when(rwTx).cancel();
doReturn("mock").when(rwTx).toString();
+ final var ex = assertThrows(IllegalStateException.class, () -> pingPong.newWriteOnlyTransaction());
+ assertThat(ex.getMessage(), allOf(
+ startsWith("New transaction PingPongTransaction"),
+ containsString(" raced with transaction PingPongTransaction")));
+ }
- // FIXME: it seems we are doing the wrong, we should see 'result' returned here
- assertTrue(tx.cancel());
+ @Test
+ public void testReadWriteReuse() {
+ final var tx = pingPong.newReadWriteTransaction();
+ final var rwTxFuture = SettableFuture.<CommitInfo>create();
+ doReturn(FluentFuture.from(rwTxFuture)).when(rwTx).commit();
+ // Now rwTx is inflight, but does not commit immediately
+ final var txFuture = tx.commit();
+ verify(rwTx).commit();
- verify(rwTx).cancel();
+ // Assert identity without delving into details
+ final var id = mock(Object.class);
+ doReturn(id).when(rwTx).getIdentifier();
+ assertSame(tx.getIdentifier(), id);
+
+ doReturn(rwTx1).when(chain).newReadWriteTransaction();
+ final var tx1 = pingPong.newWriteOnlyTransaction();
+ // now rwTx1 is ready, waiting for inflight to be completed
+ final var tx1Future = tx1.commit();
+
+ final var id1 = mock(Object.class);
+ doReturn(id1).when(rwTx1).getIdentifier();
+ assertSame(tx1.getIdentifier(), id1);
+
+ // Ready transaction is picked up by fast path allocation
+ final var tx2 = pingPong.newWriteOnlyTransaction();
+ assertSame(tx2.getIdentifier(), id1);
+
+ // Complete inflight transaction...
+ rwTxFuture.set(CommitInfo.empty());
+ assertDone(txFuture);
+ // ... but we are still holding the follow-up frontend transaction ...
+ assertFalse(tx1Future.isDone());
+ verify(rwTx1, never()).commit();
+
+ // ... and it will commit once we commit tx2 ...
+ doReturn(CommitInfo.emptyFluentFuture()).when(rwTx1).commit();
+ final var tx2Future = tx2.commit();
+ // ... at which point both complete
+ assertDone(tx1Future);
+ assertDone(tx2Future);
+ }
+
+ @Test
+ public void commitWhileInflight() {
+ final var tx = pingPong.newReadWriteTransaction();
+
+ final var rwTxFuture = SettableFuture.<CommitInfo>create();
+ doReturn(FluentFuture.from(rwTxFuture)).when(rwTx).commit();
+ // rwTxFuture is inflight
+ final var txFuture = tx.commit();
+ verify(rwTx).commit();
+ assertFalse(txFuture.isDone());
+
+ doReturn(rwTx1).when(chain).newReadWriteTransaction();
+ final var rwTxFuture1 = SettableFuture.<CommitInfo>create();
+ final var tx1 = pingPong.newWriteOnlyTransaction();
+ final var tx1Future = tx1.commit();
+
+ doReturn(FluentFuture.from(rwTxFuture1)).when(rwTx1).commit();
+ rwTxFuture.set(CommitInfo.empty());
+ assertDone(txFuture);
+ verify(rwTx1).commit();
+
+ rwTxFuture1.set(CommitInfo.empty());
+ assertDone(tx1Future);
+ }
+
+ @Test
+ public void testNewAfterAsyncShutdown() {
+ // Setup inflight transaction
+ final var tx = pingPong.newReadWriteTransaction();
+ final var rwTxFuture = SettableFuture.<CommitInfo>create();
+ doReturn(FluentFuture.from(rwTxFuture)).when(rwTx).commit();
+ final var txFuture = tx.commit();
+ assertFalse(txFuture.isDone());
+
+ // Setup ready transaction
+ doReturn(rwTx1).when(chain).newReadWriteTransaction();
+ final var rwTx1Future = SettableFuture.<CommitInfo>create();
+ doReturn(FluentFuture.from(rwTx1Future)).when(rwTx1).commit();
+
+ final var tx1Future = pingPong.newReadWriteTransaction().commit();
+ assertFalse(tx1Future.isDone());
+
+ pingPong.close();
+
+ final var ex = assertThrows(IllegalStateException.class, pingPong::newWriteOnlyTransaction);
+ assertThat(ex.getMessage(), allOf(startsWith("Transaction chain "), endsWith(" has been shut down")));
+ doNothing().when(chain).close();
+ rwTxFuture.set(CommitInfo.empty());
+ assertDone(txFuture);
verify(chain).close();
+
+ rwTx1Future.set(CommitInfo.empty());
+ assertDone(tx1Future);
+ }
+
+ @Test
+ public void testIdempotentClose() {
+ doNothing().when(chain).close();
+ pingPong.close();
+ verify(chain).close();
+ pingPong.close();
+// verifyNoMoreInteractions(chain);
}
private static <T> T assertDone(final FluentFuture<T> future) {