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}