/* * ToroDB * Copyright © 2014 8Kdata Technology (www.8kdata.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.torodb.metainfo.cache.mvcc; import static com.google.common.truth.Truth.assertThat; import com.torodb.common.util.Sequencer; import com.torodb.core.transaction.metainf.ImmutableMetaSnapshot; import com.torodb.core.transaction.metainf.MetainfoRepository.MergerStage; import com.torodb.core.transaction.metainf.MetainfoRepository.SnapshotStage; import com.torodb.core.transaction.metainf.MutableMetaSnapshot; import com.torodb.core.transaction.metainf.UnmergeableException; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; /** * * @author gortiz */ public class MvccMetainfoRepositoryTest { private MvccMetainfoRepository repository; private final String dbName = "dbName"; private final String dbId = "dbId"; private final String colName = "colName"; private final String colId = "colId"; private static final long MILLIS_TO_WAIT = 1_000; public MvccMetainfoRepositoryTest() { } @Before public void setUp() throws Exception { repository = new MvccMetainfoRepository(); } @Test public void testSingleThread() throws UnmergeableException { MutableMetaSnapshot mutableSnapshot; try (SnapshotStage snapshotStage = repository.startSnapshotStage()) { mutableSnapshot = snapshotStage.createMutableSnapshot(); } mutableSnapshot.addMetaDatabase(dbName, dbId) .addMetaCollection(colName, colId); Assert.assertNotNull(mutableSnapshot.getMetaDatabaseByName(dbName)); Assert.assertNotNull(mutableSnapshot.getMetaDatabaseByName(dbName) .getMetaCollectionByIdentifier(colId)); try (MergerStage mergeStage = repository.startMerge(mutableSnapshot)) { mergeStage.commit(); } ImmutableMetaSnapshot immutableSnapshot; try (SnapshotStage snapshotStage = repository.startSnapshotStage()) { immutableSnapshot = snapshotStage.createImmutableSnapshot(); } assertThat(immutableSnapshot.getMetaDatabaseByName(dbName)) .named("the database by name") .isNotNull(); Assert.assertNotNull(immutableSnapshot.getMetaDatabaseByName(dbName)); Assert.assertNotNull(immutableSnapshot.getMetaDatabaseByName(dbName) .getMetaCollectionByIdentifier(colId)); } /** * Tests if changes on a thread are seen by another thread after a merge phase. * * @throws Throwable */ @Test public void testReadCommited() throws Throwable { final Sequencer<ReaderRunnable.ReaderPhase> readerSequencer = new Sequencer<>( ReaderRunnable.ReaderPhase.class); final Sequencer<WriterRunnable.WriterPhase> writerSequencer = new Sequencer<>( WriterRunnable.WriterPhase.class); writerSequencer.notify(WriterRunnable.WriterPhase.values()); WriterRunnable writerRunnable = new WriterRunnable(repository, writerSequencer) { @Override protected void postSnapshot(MutableMetaSnapshot snapshot) { snapshot.addMetaDatabase(dbName, dbId) .addMetaCollection(colName, colId); } @Override protected void postMerge(MutableMetaSnapshot snapshot) { readerSequencer.notify(ReaderRunnable.ReaderPhase.values()); } }; ReaderRunnable readerRunnable = new ReaderRunnable(repository, readerSequencer) { @Override protected void callback(ImmutableMetaSnapshot snapshot) { Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName)); Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName).getMetaCollectionByIdentifier( colId)); } }; executeConcurrent(MILLIS_TO_WAIT, readerRunnable, writerRunnable); } /** * Test whether modifications are seen before merge finishes. * * @throws Throwable */ @Test public void testReadUncommited() throws Throwable { final Sequencer<ReaderRunnable.ReaderPhase> readerSequencer = new Sequencer<>( ReaderRunnable.ReaderPhase.class); final Sequencer<WriterRunnable.WriterPhase> writerSequencer = new Sequencer<>( WriterRunnable.WriterPhase.class); //Writer will wait until PRE_MERGE is sent writerSequencer.notify(WriterRunnable.WriterPhase.PRE_SNAPSHOT); writerSequencer.notify(WriterRunnable.WriterPhase.POST_SNAPSHOT); writerSequencer.notify(WriterRunnable.WriterPhase.POST_MERGE); //Reader will wait until writer modifies its snapshot WriterRunnable writerRunnable = new WriterRunnable(repository, writerSequencer) { @Override protected void postSnapshot(MutableMetaSnapshot snapshot) { snapshot.addMetaDatabase(dbName, dbId) .addMetaCollection(colName, colId); readerSequencer.notify(ReaderRunnable.ReaderPhase.PRE_SNAPSHOT); readerSequencer.notify(ReaderRunnable.ReaderPhase.PRE_CALLBACK); } }; ReaderRunnable readerRunnable = new ReaderRunnable(repository, readerSequencer) { @Override protected void callback(ImmutableMetaSnapshot snapshot) { Assert.assertNull(snapshot.getMetaDatabaseByName(dbName)); //after reader asserts, writer can merge writerSequencer.notify(WriterRunnable.WriterPhase.PRE_MERGE); } }; executeConcurrent(MILLIS_TO_WAIT, readerRunnable, writerRunnable); } /** * Test whether a transaction can see changes from a merge that happens after the read (it should * not). transaction starts. * * @throws Throwable */ @Test public void testRepeatableRead() throws Throwable { final Sequencer<ReaderRunnable.ReaderPhase> readerSequencer = new Sequencer<>( ReaderRunnable.ReaderPhase.class); final Sequencer<WriterRunnable.WriterPhase> writerSequencer = new Sequencer<>( WriterRunnable.WriterPhase.class); //Writer MERGE will wait reader get the snapshot writerSequencer.notify(WriterRunnable.WriterPhase.PRE_SNAPSHOT); writerSequencer.notify(WriterRunnable.WriterPhase.POST_SNAPSHOT); writerSequencer.notify(WriterRunnable.WriterPhase.POST_MERGE); //Reader will wait on an unorthodox way. It wont on its sequencer, but on the writer's one //and it will wait on the callback method readerSequencer.notify(ReaderRunnable.ReaderPhase.PRE_SNAPSHOT); readerSequencer.notify(ReaderRunnable.ReaderPhase.PRE_CALLBACK); WriterRunnable writerRunnable = new WriterRunnable(repository, writerSequencer) { @Override protected void postSnapshot(MutableMetaSnapshot snapshot) { snapshot.addMetaDatabase(dbName, dbId) .addMetaCollection(colName, colId); Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName)); Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName).getMetaCollectionByIdentifier( colId)); } @Override protected void postMerge(MutableMetaSnapshot snapshot) { //after merge, we should see the changes Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName)); Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName).getMetaCollectionByIdentifier( colId)); } }; ReaderRunnable readerRunnable = new ReaderRunnable(repository, readerSequencer) { @Override protected void callback(ImmutableMetaSnapshot snapshot) { //first we check the initial state Assert.assertNull(snapshot.getMetaDatabaseByName(dbName)); //after reader asserts, writer can merge writerSequencer.notify(WriterRunnable.WriterPhase.PRE_MERGE); //wait until writer merges writerSequencer.waitFor(WriterRunnable.WriterPhase.POST_MERGE); //after merge, we should not see the changes Assert.assertNull(snapshot.getMetaDatabaseByName(dbName)); } }; executeConcurrent(MILLIS_TO_WAIT, readerRunnable, writerRunnable); } /** * Tests whether two concurrent merges results on the union of changes. */ @Test public void testTwoMerges() throws Throwable { final String colName2 = colName + "2"; final String colId2 = colId + "2"; final Sequencer<WriterRunnable.WriterPhase> writerSequencer1 = new Sequencer<>( WriterRunnable.WriterPhase.class); final Sequencer<WriterRunnable.WriterPhase> writerSequencer2 = new Sequencer<>( WriterRunnable.WriterPhase.class); final Sequencer<ReaderRunnable.ReaderPhase> readerSequencer = new Sequencer<>( ReaderRunnable.ReaderPhase.class); //Writer1 will wait for merge until writer2 is ready to merge. Then writer1 will merge and //after that, writer2 will merge. writerSequencer1.notify(WriterRunnable.WriterPhase.PRE_SNAPSHOT); writerSequencer1.notify(WriterRunnable.WriterPhase.POST_SNAPSHOT); writerSequencer1.notify(WriterRunnable.WriterPhase.POST_MERGE); writerSequencer2.notify(WriterRunnable.WriterPhase.PRE_SNAPSHOT); writerSequencer2.notify(WriterRunnable.WriterPhase.POST_SNAPSHOT); writerSequencer2.notify(WriterRunnable.WriterPhase.POST_MERGE); readerSequencer.notify(ReaderRunnable.ReaderPhase.PRE_CALLBACK); WriterRunnable writerRunnable1 = new WriterRunnable(repository, writerSequencer1) { @Override protected void postSnapshot(MutableMetaSnapshot snapshot) { snapshot.addMetaDatabase(dbName, dbId) .addMetaCollection(colName, colId); //it can see its changes Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName)); Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName).getMetaCollectionByIdentifier( colId)); //but not changes executed by the other thread Assert.assertNull(snapshot.getMetaDatabaseByName(dbName).getMetaCollectionByIdentifier( colId2)); } @Override protected void postMerge(MutableMetaSnapshot snapshot) { //writer1 has finished its merge, so writer2 can start its own merge writerSequencer2.notify(WriterRunnable.WriterPhase.PRE_MERGE); } }; WriterRunnable writerRunnable2 = new WriterRunnable(repository, writerSequencer2) { @Override protected void postSnapshot(MutableMetaSnapshot snapshot) { snapshot.addMetaDatabase(dbName, dbId) .addMetaCollection(colName2, colId2); //it can see its changes Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName)); Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName).getMetaCollectionByIdentifier( colId2)); //but not changes executed by the other thread Assert.assertNull(snapshot.getMetaDatabaseByName(dbName) .getMetaCollectionByIdentifier(colId)); //writer2 has finished its modificiations, so writer1 can start its own merge writerSequencer1.notify(WriterRunnable.WriterPhase.PRE_MERGE); } @Override protected void postMerge(MutableMetaSnapshot snapshot) { //writer2 has finished his merge, so reader can start readerSequencer.notify(ReaderRunnable.ReaderPhase.PRE_SNAPSHOT); } }; ReaderRunnable readerRunnable = new ReaderRunnable(repository, readerSequencer) { @Override protected void callback(ImmutableMetaSnapshot snapshot) { //lets check that both changes are seen after merge Assert.assertNotNull(snapshot.getMetaDatabaseByName(dbName)); Assert.assertNotNull("Changes from writer1 are not seen", snapshot.getMetaDatabaseByName(dbName).getMetaCollectionByIdentifier(colId)); Assert.assertNotNull("Changes from writer2 are not seen", snapshot.getMetaDatabaseByName(dbName).getMetaCollectionByIdentifier(colId2)); } }; executeConcurrent(MILLIS_TO_WAIT, writerRunnable1, writerRunnable2, readerRunnable); } private void executeConcurrent(long maxMillis, Runnable... runnables) throws TimeoutException, Throwable { assert runnables.length > 0 : "at least one runnable must be sent"; ExecutorService es = Executors.newFixedThreadPool(runnables.length); List<Future<?>> futures = new ArrayList<>(runnables.length); for (Runnable runnable : runnables) { futures.add(es.submit(runnable)); } try { for (Future<?> future : futures) { future.get(maxMillis, TimeUnit.MILLISECONDS); } } catch (ExecutionException ex) { throw ex.getCause(); } } private static class ReaderRunnable implements Runnable { private final MvccMetainfoRepository repository; private final Sequencer<ReaderPhase> sequencer; public ReaderRunnable(MvccMetainfoRepository repository, Sequencer<ReaderPhase> sequencer) { this.repository = repository; this.sequencer = sequencer; } protected void preStart() { } protected void preSnapshot() { } protected void callback(ImmutableMetaSnapshot snapshot) { } @Override public void run() { ImmutableMetaSnapshot immutableSnapshot; preStart(); sequencer.waitFor(ReaderPhase.PRE_SNAPSHOT); preSnapshot(); try (SnapshotStage snapshotStage = repository.startSnapshotStage()) { immutableSnapshot = snapshotStage.createImmutableSnapshot(); } sequencer.waitFor(ReaderPhase.PRE_CALLBACK); callback(immutableSnapshot); } private static enum ReaderPhase { PRE_SNAPSHOT, PRE_CALLBACK; } } private static class WriterRunnable implements Runnable { private final MvccMetainfoRepository repository; private final Sequencer<WriterPhase> sequencer; public WriterRunnable(MvccMetainfoRepository repository, Sequencer<WriterPhase> sequencer) { this.repository = repository; this.sequencer = sequencer; } protected void preStart() { } protected void preSnapshot() { } protected void postSnapshot(MutableMetaSnapshot snapshot) { } protected void preMerge() { } protected void postMerge(MutableMetaSnapshot snapshot) { } @Override public void run() { MutableMetaSnapshot mutableSnapshot; preStart(); sequencer.waitFor(WriterPhase.PRE_SNAPSHOT); preSnapshot(); try (SnapshotStage snapshotStage = repository.startSnapshotStage()) { mutableSnapshot = snapshotStage.createMutableSnapshot(); } sequencer.waitFor(WriterPhase.POST_SNAPSHOT); postSnapshot(mutableSnapshot); sequencer.waitFor(WriterPhase.PRE_MERGE); preMerge(); try (MergerStage mergeStage = repository.startMerge(mutableSnapshot)) { mergeStage.commit(); } catch (UnmergeableException ex) { throw new AssertionError("Unmergeable changes", ex); } sequencer.waitFor(WriterPhase.POST_MERGE); postMerge(mutableSnapshot); } private static enum WriterPhase { PRE_SNAPSHOT, POST_SNAPSHOT, PRE_MERGE, POST_MERGE; } } }