001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.schematic;
017
018import static junit.framework.Assert.assertEquals;
019import static junit.framework.Assert.assertFalse;
020import static junit.framework.Assert.assertNotNull;
021import static junit.framework.Assert.assertTrue;
022import static org.junit.Assert.assertNull;
023import java.util.List;
024import java.util.Set;
025import java.util.TreeSet;
026import java.util.UUID;
027import java.util.concurrent.Callable;
028import java.util.concurrent.CompletableFuture;
029import java.util.concurrent.CyclicBarrier;
030import java.util.concurrent.ExecutorService;
031import java.util.concurrent.Executors;
032import java.util.concurrent.Future;
033import java.util.concurrent.TimeUnit;
034import java.util.stream.Collectors;
035import java.util.stream.IntStream;
036import org.junit.After;
037import org.junit.Assert;
038import org.junit.Before;
039import org.junit.Test;
040import org.modeshape.schematic.document.Document;
041import org.modeshape.schematic.document.EditableDocument;
042import org.modeshape.schematic.document.Json;
043import org.modeshape.schematic.document.ParsingException;
044import org.modeshape.schematic.internal.document.BasicDocument;
045
046/**
047 * Base class for the different {@link SchematicDb} implementation.
048 * 
049 * @author Horia Chiorean (hchiorea@redhat.com)
050 */
051public abstract class AbstractSchematicDBTest {
052
053    protected static final Document DEFAULT_CONTENT ;
054    
055    private static final String VALUE_FIELD = "value";
056    
057    protected SchematicDb db;
058    protected boolean print = false;
059
060    static {
061        try {
062            DEFAULT_CONTENT = Json.read(AbstractSchematicDBTest.class.getClassLoader().getResourceAsStream("document.json"));
063        } catch (ParsingException e) {
064            throw new RuntimeException(e);
065        }
066    }
067    
068    protected abstract SchematicDb getDb() throws Exception ;
069
070    @Before
071    public void before() throws Exception {
072        db = getDb();
073        db.start();
074    }
075
076    @After
077    public void after() throws Exception {
078        db.stop();
079    }
080
081    @Test
082    public void shouldGetAndPut() throws Exception {
083        List<SchematicEntry> dbEntries = randomEntries(3);
084        //simulate the start of a transaction
085        db.txStarted("0");
086
087        //write some entries without committing
088        dbEntries.forEach(dbEntry -> db.put(dbEntry.id(), dbEntry.content()));
089        Set<String> expectedIds = dbEntries.stream().map(SchematicEntry::id).collect(Collectors.toCollection(TreeSet::new));
090        // check that the same connection is used and the entries are still there
091        assertTrue(db.keys().containsAll(expectedIds));
092        // simulate a commit for the write
093        db.txCommitted("0");
094        // check that the entries are still there
095        assertTrue(db.keys().containsAll(expectedIds));
096        // check that for each entry the content is correctly stored
097        dbEntries.stream().forEach(entry -> assertEquals(entry.content(), db.getEntry(entry.id()).content()));
098
099        // update one of the documents and check the update is correct
100        SchematicEntry firstEntry = dbEntries.get(0);
101        String idToUpdate = firstEntry.id();
102        EditableDocument updatedDocument = firstEntry.content().edit(true);
103        updatedDocument.setNumber(VALUE_FIELD, 2);
104
105        //simulate a new transaction
106        db.txStarted("1");
107        db.get(idToUpdate);
108        db.put(idToUpdate, updatedDocument);
109        assertEquals(updatedDocument, db.getEntry(idToUpdate).content());
110        db.txCommitted("1");
111        assertEquals(updatedDocument, db.getEntry(idToUpdate).content());
112    }
113
114    @Test
115    public void shouldReadSchematicEntry() throws Exception {
116        SchematicEntry entry = writeSingleEntry();
117        SchematicEntry schematicEntry = db.getEntry(entry.id());
118        assertNotNull(schematicEntry);
119        assertTrue(db.containsKey(entry.id()));
120    }
121
122    @Test
123    public void shouldEditContentDirectly() throws Exception {
124        // test the editing of content for an existing entry
125        SchematicEntry entry = writeSingleEntry();
126        EditableDocument editableDocument = simulateTransaction(() -> db.editContent(entry.id(), false));
127        assertNotNull(editableDocument);
128        assertEquals(entry.content(), editableDocument);
129        simulateTransaction(() -> {
130            EditableDocument document = db.editContent(entry.id(), false);
131            document.setNumber(VALUE_FIELD, 2);
132            return null;
133        });
134        Document doc = db.getEntry(entry.id()).content();
135        assertEquals(2, (int) doc.getInteger(VALUE_FIELD));
136
137        // test the editing of content for a new entry which should be create
138        String newId = UUID.randomUUID().toString();
139        EditableDocument newDocument = simulateTransaction(() -> db.editContent(newId, true));
140        assertNotNull(newDocument);
141        // the content in the DB should be an empty schematic entry...
142        SchematicEntry schematicEntry = db.getEntry(newId);
143        assertEquals(newId, schematicEntry.id());
144        assertEquals(new BasicDocument(), schematicEntry.content());
145
146        // test the editing of a non-existing id without creating a new entry for it
147        newDocument = simulateTransaction(() -> db.editContent(UUID.randomUUID().toString(), false));
148        assertNull(newDocument);
149    }
150
151    @Test
152    public void shouldPutIfAbsent() throws Exception {
153        SchematicEntry entry = writeSingleEntry();
154        EditableDocument editableDocument = entry.content().edit(true);
155        editableDocument.setNumber(VALUE_FIELD, 100);
156        SchematicEntry updatedEntry = simulateTransaction(() -> db.putIfAbsent(entry.id(), entry.content()));
157        assertNotNull(updatedEntry);
158        assertEquals(1, (int) updatedEntry.content().getInteger(VALUE_FIELD));
159
160        SchematicEntry newEntry = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
161        assertNull(simulateTransaction(() -> db.putIfAbsent(newEntry.id(), newEntry.content())));
162        updatedEntry = db.getEntry(newEntry.id());
163        assertNotNull(updatedEntry);
164    }
165
166    @Test
167    public void shouldPutSchematicEntry() throws Exception {
168        SchematicEntry originalEntry = randomEntries(1).get(0);
169        simulateTransaction(() -> {
170            db.putEntry(originalEntry.source());
171            return null;
172        });
173
174        SchematicEntry actualEntry = db.getEntry(originalEntry.id());
175        assertNotNull(actualEntry);
176        assertEquals(originalEntry.getMetadata(), actualEntry.getMetadata());
177        assertEquals(originalEntry.content(), actualEntry.content());
178        assertEquals(DEFAULT_CONTENT, actualEntry.content());
179    }
180
181    @Test
182    public void shouldRemoveDocument() throws Exception {
183        SchematicEntry entry = writeSingleEntry();
184        simulateTransaction(() -> db.remove(entry.id()));
185        assertFalse(db.containsKey(entry.id()));
186    }
187
188    @Test
189    public void shouldRemoveAllDocuments() throws Exception {
190        int count = 3;
191        simulateTransaction(() -> {
192            randomEntries(count).forEach(entry -> db.put(entry.id(), entry.content()));
193            return null;
194        });
195        assertFalse(db.keys().isEmpty());
196        simulateTransaction(() -> {
197            db.removeAll();
198            return null;
199        });
200        assertTrue(db.keys().isEmpty());
201    }
202
203    @Test
204    public void shouldIsolateChangesWithinTransaction() throws Exception {
205        SchematicEntry entry1 = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
206        SchematicEntry entry2 = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
207        CyclicBarrier syncBarrier = new CyclicBarrier(2);
208        CompletableFuture<Void> thread1 = CompletableFuture.runAsync(() -> changeAndCommit(entry1, entry2, syncBarrier));
209        CompletableFuture<Void> thread2 = CompletableFuture.runAsync(() -> changeAndCommit(entry2, entry1, syncBarrier));
210        thread1.get(3, TimeUnit.SECONDS);
211        thread2.get(3, TimeUnit.SECONDS);
212
213        // both transactions should've removed the entries in the end
214        Assert.assertFalse(db.containsKey(entry1.id()));
215        Assert.assertFalse(db.containsKey(entry2.id()));
216    }
217
218    @Test
219    public void shouldRollbackChangesWithinTransaction() throws Exception {
220        SchematicEntry entry1 = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
221        SchematicEntry entry2 = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
222        CyclicBarrier syncBarrier = new CyclicBarrier(2);
223
224        CompletableFuture<Void> thread1 = CompletableFuture.runAsync(() -> changeAndRollback(entry1, entry2, syncBarrier));
225        CompletableFuture<Void> thread2 = CompletableFuture.runAsync(() -> changeAndRollback(entry2, entry1, syncBarrier));
226
227        thread1.get(2, TimeUnit.SECONDS);
228        thread2.get(2, TimeUnit.SECONDS);
229
230        // both transactions should've rolledback their original changes
231        Assert.assertEquals(entry1.content(), db.getEntry(entry1.id()).content());
232        Assert.assertEquals(entry2.content(), db.getEntry(entry2.id()).content());
233    }
234
235    @Test
236    public void shouldInsertAndUpdateEntriesConcurrentlyWithMultipleWriters() throws Exception {
237        int threadsCount = 100;
238        int entriesPerThread = 100;
239        ExecutorService executors = Executors.newFixedThreadPool(threadsCount);
240        print = false;
241        print("Starting the run of " + threadsCount + " threads with " + entriesPerThread + " insertions per thread...");
242        long startTime = System.nanoTime();
243        List<Future<List<String>>> results = IntStream.range(0, threadsCount)
244                                                      .mapToObj(value -> insertMultipleEntries(entriesPerThread, executors))
245                                                      .collect(Collectors.toList());
246
247        results.stream()
248               .map(future -> {
249                   try {
250                       return future.get(2, TimeUnit.MINUTES);
251                   } catch (Exception e) {
252                       throw new RuntimeException(e);
253                   }
254               })
255               .flatMap(List::stream)
256               .forEach(id -> Assert.assertTrue(db.containsKey(id)));
257        long durationMillis = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
258        if (print) {
259            System.out.printf("Total duration to insert " + threadsCount * entriesPerThread + " entries : " + durationMillis / 1000d + " seconds");
260        }
261    }
262
263    protected CompletableFuture<List<String>> insertMultipleEntries(int entriesPerThread, ExecutorService executors) {
264        return CompletableFuture.supplyAsync(() -> {
265            if (print) {
266                System.out.println(Thread.currentThread().getName() + " inserting " + entriesPerThread + " entries...");
267            }
268            String txId = UUID.randomUUID().toString();
269            db.txStarted(txId);
270            List<String> ids = null;
271            try {
272                ids = randomEntries(entriesPerThread)
273                        .stream()
274                        .map(dbEntry -> {
275                            db.put(dbEntry.id(), dbEntry.content());
276                            return dbEntry.id();
277                        })
278                        .collect(Collectors.toList());
279            } catch (Exception e) {
280                throw new RuntimeException(e);
281            }
282            db.txCommitted(txId);
283            return ids;
284        }, executors);
285    }
286
287    private void changeAndRollback(SchematicEntry ourEntry, SchematicEntry otherEntry, CyclicBarrier syncBarrier) {
288        try {
289            String txId = UUID.randomUUID().toString();
290
291            // start a tx and write the first entry
292            db.txStarted(txId);
293            db.put(ourEntry.id(), ourEntry.content());
294            db.txCommitted(txId);
295            syncBarrier.await();
296
297            Document ourDocument = db.getEntry(ourEntry.id()).content();
298            Document otherDocument = db.getEntry(otherEntry.id()).content();
299            Assert.assertEquals(ourDocument, otherDocument);
300
301            // start a new tx, make some changes and rollback
302            txId = UUID.randomUUID().toString();
303            db.txStarted(txId);
304            db.put(ourEntry.id(), new BasicDocument());
305            // rollback the tx
306            db.txRolledback(txId);
307            syncBarrier.await();
308
309            // and check that the visible documents are unchanged
310            Assert.assertEquals(ourDocument, db.getEntry(ourEntry.id()).content());
311            Assert.assertEquals(otherDocument, db.getEntry(otherEntry.id()).content());
312        } catch (RuntimeException re) {
313            syncBarrier.reset();
314            throw re;
315        } catch (Throwable t) {
316            t.printStackTrace();
317            syncBarrier.reset();
318            throw new RuntimeException(t);
319        }
320    }
321
322    protected void changeAndCommit(SchematicEntry ourEntry, SchematicEntry otherEntry, CyclicBarrier syncBarrier) {
323        try {
324            String txId = UUID.randomUUID().toString();
325
326            // start a tx and write the first entry
327            db.txStarted(txId);
328            db.put(ourEntry.id(), ourEntry.content());
329
330            // now both transactions should've written something without committing so test changes are not visible
331            Assert.assertTrue(db.containsKey(ourEntry.id()));
332            Assert.assertFalse(db.containsKey(otherEntry.id()));
333
334            // make some changes to ourEntry
335            BasicDocument updatedDoc = new BasicDocument();
336            db.put(ourEntry.id(), updatedDoc);
337
338            // check that the changes are only visible to ourselves... 
339            Document actualDocument = db.getEntry(ourEntry.id()).content();
340            Assert.assertTrue(db.containsKey(ourEntry.id()));
341            Assert.assertFalse(db.containsKey(otherEntry.id()));
342            Assert.assertEquals(updatedDoc, actualDocument);
343            syncBarrier.await();
344            // and wait for the other tx to make its own changes....
345            // now commit
346            db.txCommitted(txId);
347            syncBarrier.await();
348
349            // check that outside changes are visible...
350            Assert.assertTrue(db.containsKey(otherEntry.id()));
351            Document otherDocument = db.getEntry(otherEntry.id()).content();
352            Assert.assertEquals(updatedDoc, otherDocument);
353
354            // start a new tx
355            txId = UUID.randomUUID().toString();
356
357            db.txStarted(txId);
358            // remove entry entry
359            db.remove(ourEntry.id());
360            // and wait for the other tx to remove
361            syncBarrier.await();
362
363            // check that changes are not yet visible...            
364            Assert.assertFalse(db.containsKey(ourEntry.id()));
365            Assert.assertTrue(db.containsKey(otherEntry.id()));
366
367            // and wait for the other tx to remove
368            syncBarrier.await();
369
370            // commit the new tx
371            db.txCommitted(txId);
372            // and wait for the other tx to remove
373            syncBarrier.await();
374
375            // check that changes are not now visible...            
376            Assert.assertFalse(db.containsKey(ourEntry.id()));
377            Assert.assertFalse(db.containsKey(otherEntry.id()));
378        } catch (RuntimeException re) {
379            syncBarrier.reset();
380            throw re;
381        } catch (Throwable t) {
382            t.printStackTrace();
383            syncBarrier.reset();
384            throw new RuntimeException(t);
385        }
386    }
387
388    protected  <T> T simulateTransaction(Callable<T> operation) throws Exception {
389        db.txStarted("0");
390        T result = operation.call();
391        db.txCommitted("0");
392        return result;
393    }
394
395    protected SchematicEntry writeSingleEntry() throws Exception {
396        return simulateTransaction(() -> {
397            SchematicEntry entry = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
398            db.putEntry(entry.source());
399            return entry;
400        });
401    }
402
403
404    protected List<SchematicEntry> randomEntries(int sampleSize) throws Exception {
405        return IntStream.range(0, sampleSize).mapToObj(i -> SchematicEntry.create(
406                UUID.randomUUID().toString(), DEFAULT_CONTENT)).collect(Collectors.toList());
407    }
408
409    protected void print(String s) {
410        if (print) {
411            System.out.println(Thread.currentThread().getName() + ": " + s);
412        }
413    }
414}