/*
* ModeShape (http://www.modeshape.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modeshape.persistence.relational;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.modeshape.schematic.AbstractSchematicDBTest;
import org.modeshape.schematic.Schematic;
import org.modeshape.schematic.SchematicDb;
import org.modeshape.schematic.SchematicEntry;
import org.modeshape.schematic.internal.annotation.FixFor;
/**
* Integration test for {@link RelationalDb}. The configuration used for this test is using system properties set by Maven
* together with Docker.
*
* @author Horia Chiorean (hchiorea@redhat.com)
*/
public class RelationalDbIT extends AbstractSchematicDBTest {
@Override
protected SchematicDb getDb() throws Exception {
return Schematic.getDb(RelationalDbIT.class.getClassLoader().getResourceAsStream("db-config.json"));
}
@Before
public void before() throws Exception {
super.before();
// run a query to validate that the table has been created and is empty.
assertEquals(0, db.keys().size());
}
@After
public void after() throws Exception {
super.after();
// run a query to check that the table has been removed
try {
db.keys();
fail("The DB table should have been dropped...");
} catch (RelationalProviderException e) {
//expected
}
}
@Test
public void shouldLockEntriesExclusively() throws Exception {
insertAndLock(100);
}
protected void insertAndLock(int entriesCount) throws Exception {
List<String> ids = insertMultipleEntries(entriesCount, Executors.newSingleThreadExecutor()).get();
// run and commit tx 1
boolean result = simulateTransaction(() -> db.lockForWriting(ids));
assertTrue("Locks should have been obtained", result);
// run and commit tx 2
result = simulateTransaction(() -> db.lockForWriting(ids));
assertTrue("Locks should have been obtained", result);
}
@Test
public void shouldReportNonExistentEntryAsLocked() throws Exception {
simulateTransaction(() -> {
Assert.assertTrue(db.lockForWriting("non_existant"));
return null;
});
}
@Test(expected = RelationalProviderException.class)
public void shouldNotLockEntriesWithoutTransaction() throws Exception {
String id = writeSingleEntry().id();
db.lockForWriting(id);
}
@Test
@Ignore("ignored by default because on most DBs running in Docker it takes a long time for the lock timeout message")
public void concurrentThreadsShouldNotGetSameLock() throws Exception {
String id = writeSingleEntry().id();
CyclicBarrier barrier = new CyclicBarrier(2);
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Boolean> t1 = executorService.submit(() -> {
db.txStarted("1");
boolean result = db.lockForWriting(id);
barrier.await();
db.txCommitted("1");
return result;
});
Future<Boolean> t2 = executorService.submit(() -> {
db.txStarted("2");
boolean result = db.lockForWriting(id);
barrier.await();
db.txCommitted("2");
return result;
});
boolean t1Success = t1.get();
boolean t2Success = t2.get();
assertTrue("Only one of the threads should have been able to lock" , (t1Success && !t2Success) || (!t1Success && t2Success));
}
@Test
@FixFor( "MODE-2629" )
public void shouldReadWithDifferentBatches() throws Exception {
ExecutorService executorService = Executors.newSingleThreadExecutor();
int maxStatementParamCount = DefaultStatements.DEFAULT_MAX_STATEMENT_PARAM_COUNT;
try {
List<String> ids = insertMultipleEntries(2000, executorService).get(10, TimeUnit.SECONDS);
loadAndAssertIds(ids, 0);
loadAndAssertIds(ids, 1);
loadAndAssertIds(ids, maxStatementParamCount / 2);
loadAndAssertIds(ids, (maxStatementParamCount / 2) + 1);
loadAndAssertIds(ids, (maxStatementParamCount /2) + (maxStatementParamCount / 4));
loadAndAssertIds(ids, maxStatementParamCount);
} finally {
executorService.shutdownNow();
}
}
private void loadAndAssertIds(List<String> insertedIds, int batchSize) {
List<String> expectedIds = insertedIds.subList(0, batchSize);
List<SchematicEntry> entries = db.load(expectedIds);
List<String> actualIds = entries.stream().map(SchematicEntry::id).collect(Collectors.toList());
Collections.sort(expectedIds);
Collections.sort(actualIds);
assertEquals("The same entries should have been read back", expectedIds, actualIds);
}
@Test
@FixFor( "MODE-2629" )
public void shouldLockWithDifferentBatches() throws Exception {
ExecutorService executorService = Executors.newSingleThreadExecutor();
int maxStatementParamCount = DefaultStatements.DEFAULT_MAX_STATEMENT_PARAM_COUNT;
try {
List<String> ids = insertMultipleEntries(2000, executorService).get(10, TimeUnit.SECONDS);
lockIds(ids, 1);
lockIds(ids, maxStatementParamCount / 2);
lockIds(ids, (maxStatementParamCount / 2) + 1);
lockIds(ids, (maxStatementParamCount /2) + (maxStatementParamCount / 4));
lockIds(ids, maxStatementParamCount);
} finally {
executorService.shutdownNow();
}
}
private void lockIds(List<String> insertedIds, int batchSize) throws Exception {
List<String> subList = insertedIds.subList(0, batchSize);
simulateTransaction(() -> {
assertTrue("Entries could not be locked", db.lockForWriting(subList));
return null;
});
}
@Test
@FixFor( "MODE-2629" )
public void shouldRemoveInBatches() throws Exception {
ExecutorService executorService = Executors.newSingleThreadExecutor();
int maxStatementParamCount = DefaultStatements.DEFAULT_MAX_STATEMENT_PARAM_COUNT;
try {
int count = 2000;
List<String> ids = insertMultipleEntries(count, executorService).get(10, TimeUnit.SECONDS);
removeBatch(ids, 0, 1);
removeBatch(ids, 1, maxStatementParamCount / 2 + 1);
int start = maxStatementParamCount / 2 + 1;
while (start < count) {
int end = start + maxStatementParamCount > count ? count : start + maxStatementParamCount;
removeBatch(ids, start, end);
start = end;
}
assertTrue("Not all keys were deleted", db.keys().isEmpty());
} finally {
executorService.shutdownNow();
}
}
private void removeBatch(List<String> ids, int start, int end) throws Exception {
simulateTransaction(() -> {
List<String> toRemove = ids.subList(start, end);
toRemove.forEach(id -> db.remove(id));
return null;
});
}
}