package com.constellio.data.dao.services.bigVault.solr; import static com.constellio.data.dao.dto.records.RecordsFlushing.NOW; import static java.util.Arrays.asList; import static junit.framework.Assert.fail; import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import junit.framework.Assert; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.params.ModifiableSolrParams; import org.joda.time.LocalDateTime; import org.junit.Before; import org.junit.Test; import com.constellio.data.dao.dto.records.RecordsFlushing; import com.constellio.data.dao.services.bigVault.solr.BigVaultException.CouldNotExecuteQuery; import com.constellio.data.dao.services.bigVault.solr.BigVaultException.OptimisticLocking; import com.constellio.data.dao.services.factories.DataLayerFactory; import com.constellio.data.dao.services.solr.ConstellioSolrInputDocument; import com.constellio.data.extensions.DataLayerSystemExtensions; import com.constellio.sdk.tests.ConstellioTest; import com.constellio.sdk.tests.annotations.SlowTest; public class BigVaultServerConcurrencyAcceptTest extends ConstellioTest { private static final String transaction1 = "transaction1"; private static final String transaction2 = "transaction2"; private static final String transaction3 = "transaction3"; BigVaultServer vaultServer; BigVaultServer anotherVaultServer; BigVaultServer aThirdVaultServer; private String gandalf = "gandalf"; private String edouard = "edouard"; private String dakota = "dakota"; private List emptyList = new ArrayList<>(); @Before public void setUp() throws Exception { givenDisabledAfterTestValidations(); // DataLayerFactory daosFactory = getDataLayerFactory(); setupSolrServers(); //vaultServer = daosFactory.getRecordsVaultServer(); //anotherVaultServer = new BigVaultServer(vaultServer.getNestedSolrServer(), BigVaultLogger.disabled()); } @Test public void givenTransactionWithOptimisticLockingExceptionWhenRetryingWithSameRecordsThenOK() throws Exception { add("Sam_Gamegie_ze_brave", "Frodon", "Gandalf"); updateExpectingAnOptimisticLocking(inCurrentVersion("Gandalf"), inCurrentVersion("Sam_Gamegie_ze_brave"), inVersion("Frodon", 42L)); updateWithCurrentVersionObtainedFromAQuery("Sam_Gamegie_ze_brave", "Gandalf"); } private void updateExpectingAnOptimisticLocking(SolrInputDocument... documents) { try { update(documents); Assert.fail("Expected : Optimistic Locking"); } catch (Exception e) { } } private void update(SolrInputDocument... documents) throws BigVaultException { vaultServer.addAll(new BigVaultServerTransaction(NOW).setUpdatedDocuments(asList(documents))); } private SolrInputDocument inCurrentVersion(String id) { Long version = getVersionOf(id); return updateDocument(id, "a", version); } private SolrInputDocument inVersion(String id, Long version) { return updateDocument(id, "a", version); } private void updateWithCurrentVersionObtainedFromAQuery(String... ids) { List<SolrInputDocument> inputDocuments = new ArrayList<>(); for (String id : ids) { Long version = getVersionOf(id); inputDocuments.add(updateDocument(id, "a", version)); } try { vaultServer.addAll(new BigVaultServerTransaction(NOW).setUpdatedDocuments(inputDocuments)); } catch (BigVaultException e) { throw new RuntimeException(e); } } private void add(String... ids) { List<SolrInputDocument> inputDocuments = new ArrayList<>(); for (String id : ids) { inputDocuments.add(addDocument(id, "a")); } try { vaultServer.addAll(new BigVaultServerTransaction(NOW).setNewDocuments(inputDocuments)); } catch (BigVaultException e) { throw new RuntimeException(e); } } private void setupSolrServers() { // DataLayerSystemExtensions extensions = new DataLayerSystemExtensions(); DataLayerFactory daosFactory = (DataLayerFactory) getDataLayerFactory(); BigVaultServer recordsVaultServer = daosFactory.getRecordsVaultServer(); vaultServer = recordsVaultServer.clone(); anotherVaultServer = recordsVaultServer.clone(); aThirdVaultServer = recordsVaultServer.clone(); } @Test public void testDeathStarInvulnerability() throws Exception { vaultServer.getNestedSolrServer().add(addDocument(dakota, "A")); vaultServer.getNestedSolrServer().add(addDocument(edouard, "A")); vaultServer.softCommit(); // assertThat(vaultServer.getNestedSolrServer()).isNotSameAs(anotherVaultServer.getNestedSolrServer()); SolrInputDocument firstServerUpdatedDocument = updateDocument(dakota, "B", getVersionOfDocumentOnServer(dakota, vaultServer)); vaultServer.verifyOptimisticLocking(-1, transaction1, asList(firstServerUpdatedDocument)); aThirdVaultServer.softCommit(); SolrInputDocument secondServerUpdatedDocument = updateDocument(dakota, "C", getVersionOfDocumentOnServer(dakota, anotherVaultServer)); SolrInputDocument secondServerUpdatedDocument2 = updateDocument(edouard, "C", getVersionOfDocumentOnServer(edouard, anotherVaultServer)); try { anotherVaultServer .verifyOptimisticLocking(-1, transaction2, asList(secondServerUpdatedDocument2, secondServerUpdatedDocument)); fail("Should throw an exception, since the first client has not finished the transaction"); } catch (Exception e) { //OK } vaultServer.processChanges(new BigVaultServerTransaction(transaction1, RecordsFlushing.LATER(), emptyList, asList(firstServerUpdatedDocument), emptyList, emptyList)); vaultServer.softCommit(); assertThat(getValueOf(dakota)).isEqualTo("B"); firstServerUpdatedDocument = updateDocument(dakota, "D", getVersionOfDocumentOnServer(dakota, vaultServer)); SolrInputDocument firstServerUpdatedDocument2 = updateDocument(edouard, "D", getVersionOfDocumentOnServer(edouard, vaultServer)); vaultServer.verifyOptimisticLocking(-1, transaction3, asList(firstServerUpdatedDocument, firstServerUpdatedDocument2)); vaultServer.processChanges(new BigVaultServerTransaction(transaction3, RecordsFlushing.LATER(), emptyList, asList(firstServerUpdatedDocument, firstServerUpdatedDocument2), emptyList, emptyList)); vaultServer.softCommit(); assertThat(getValueOf(dakota)).isEqualTo("D"); assertThat(getValueOf(edouard)).isEqualTo("D"); } @Test public void givenLockIsNotDeletedThenAutomaticallyDeletedAfterAGivenTime() throws Exception { int theDelayBeforeAutomaticRemoval = 66; LocalDateTime lockOClock = new LocalDateTime(); LocalDateTime lockRemovalOClock = lockOClock.plusSeconds(66); LocalDateTime oneSecondBeforeRemovalOClock = lockOClock.minusSeconds(1); givenTimeIs(lockOClock); vaultServer.getNestedSolrServer().add(addDocument(dakota, "A")); vaultServer.getNestedSolrServer().add(addDocument(edouard, "A")); vaultServer.softCommit(); // assertThat(vaultServer.getNestedSolrServer()).isNotSameAs(anotherVaultServer.getNestedSolrServer()); SolrInputDocument firstServerUpdatedDocument = updateDocument(dakota, "B", getVersionOfDocumentOnServer(dakota, vaultServer)); vaultServer.verifyOptimisticLocking(-1, transaction1, asList(firstServerUpdatedDocument)); vaultServer.softCommit(); assertThat(containsLockFor(dakota, vaultServer)).isTrue(); vaultServer.removeLockWithAgeGreaterThan(theDelayBeforeAutomaticRemoval); vaultServer.softCommit(); assertThat(containsLockFor(dakota, vaultServer)).isTrue(); givenTimeIs(oneSecondBeforeRemovalOClock); vaultServer.removeLockWithAgeGreaterThan(theDelayBeforeAutomaticRemoval); vaultServer.softCommit(); assertThat(containsLockFor(dakota, vaultServer)).isTrue(); givenTimeIs(lockRemovalOClock); vaultServer.removeLockWithAgeGreaterThan(theDelayBeforeAutomaticRemoval); vaultServer.softCommit(); assertThat(containsLockFor(dakota, vaultServer)).isFalse(); } private boolean containsLockFor(String id, BigVaultServer solrServer) throws CouldNotExecuteQuery { ModifiableSolrParams params = new ModifiableSolrParams(); params.set("q", "id:lock__" + id); return solrServer.query(params).getResults().size() == 1; } private SolrInputDocument addLock(String id) { SolrInputDocument doc = new SolrInputDocument(); doc.setField("id", lockId(id)); doc.setField("type_s", "lock"); doc.setField("_version_", "-1"); return doc; } private String lockId(String id) { return id + "_lock"; } @Test public void givenOptimisticLockingExceptionThenNoChangesToRecord() throws Exception { vaultServer.addAll(new BigVaultServerTransaction(NOW).setNewDocuments( asList(addDocument(gandalf, "A"), addDocument(edouard, "A"), addDocument(dakota, "A")))); Long gandalfVersion = getVersionOf(gandalf); Long edouardVersion = getVersionOf(edouard); try { vaultServer.addAll(new BigVaultServerTransaction(NOW).setUpdatedDocuments( asList(updateDocument(gandalf, "B", 42L), updateDocument(edouard, "A", edouardVersion)))); fail("Expected OptimisticLocking"); } catch (OptimisticLocking e) { } vaultServer.addAll(new BigVaultServerTransaction(NOW) .setUpdatedDocuments(asList(updateDocument(edouard, "B", edouardVersion)))); edouardVersion = getVersionOf(edouard); assertThat(getValueOf(gandalf)).isEqualTo("A"); assertThat(getValueOf(edouard)).isEqualTo("B"); assertThat(getVersionOf(gandalf)).isEqualTo(gandalfVersion); assertThat(getVersionOf(edouard)).isEqualTo(edouardVersion); Long dakotaVersion = getVersionOf(dakota); vaultServer.addAll(new BigVaultServerTransaction(NOW) .setUpdatedDocuments(asList(updateDocument(dakota, "B", dakotaVersion)))); Thread.sleep(5000); assertThat(getValueOf(gandalf)).isEqualTo("A"); assertThat(getValueOf(edouard)).isEqualTo("B"); assertThat(getValueOf(dakota)).isEqualTo("B"); assertThat(getVersionOf(gandalf)).isEqualTo(gandalfVersion); assertThat(getVersionOf(edouard)).isEqualTo(edouardVersion); assertThat(getVersionOf(dakota)).isNotEqualTo(dakotaVersion); } @SlowTest @Test public void givenAThreadCausingOptimisticLockingAndAnotherAddingWithoutProblemsThenOnlySecondRecordIsModified() throws Exception { vaultServer.addAll(new BigVaultServerTransaction(NOW).setNewDocuments( asList(addDocument(gandalf, "0"), addDocument(edouard, "0"), addDocument(dakota, "0")))); final int numberOfIterations = 1000; final AtomicBoolean expectedException = new AtomicBoolean(false); Thread thread1 = new Thread() { @Override public void run() { for (int i = 0; i < numberOfIterations; i++) { System.out.println("Iteration #" + i); try { vaultServer.addAll(new BigVaultServerTransaction(NOW).setUpdatedDocuments(asList( updateDocument(gandalf, "8888", 42L), updateDocument(edouard, "8888", 42L), updateDocument(dakota, "8888", 42L)))); expectedException.set(true); break; } catch (BigVaultException e) { //OK } } } }; Thread thread2 = new Thread() { @Override public void run() { for (int i = 0; i < numberOfIterations; i++) { boolean executed = false; while (!executed) { try { Long gandalfVersion = getVersionOf(gandalf); Integer gandalfValue = Integer.valueOf(getValueOf(gandalf)); Long edouardVersion = getVersionOf(edouard); Integer edouardValue = Integer.valueOf(getValueOf(edouard)); Long dakotaVersion = getVersionOf(dakota); Integer dakotaValue = Integer.valueOf(getValueOf(dakota)); SolrInputDocument gandalfUpdate = updateDocument(gandalf, "" + (gandalfValue + 1), gandalfVersion); SolrInputDocument edouardUpdate = updateDocument(edouard, "" + (edouardValue + 1), edouardVersion); SolrInputDocument dakotaUpdate = updateDocument(dakota, "" + (dakotaValue + 1), dakotaVersion); vaultServer.addAll(new BigVaultServerTransaction(NOW).setUpdatedDocuments( asList(gandalfUpdate, edouardUpdate, dakotaUpdate))); executed = true; } catch (BigVaultException e) { vaultServer.removeLockWithAgeGreaterThan(2); e.printStackTrace(); } } } } }; thread1.start(); thread2.start(); thread1.join(); thread2.join(); assertThat(expectedException.get()).isFalse(); assertThat(getValueOf(gandalf)).isEqualTo("" + numberOfIterations); assertThat(getValueOf(edouard)).isEqualTo("" + numberOfIterations); assertThat(getValueOf(dakota)).isEqualTo("" + numberOfIterations); } @SlowTest @Test public void givenFlushIsNowWhenTwoThreadsAreModifyingTheSameDocumentThenNoConflict() throws Exception { givenTwoThreadsAreModifyingTheSameDocumentThenNoConflict(NOW()); } @SlowTest @Test public void givenFlushIsWithin2SecWhenTwoThreadsAreModifyingTheSameDocumentThenNoConflict() throws Exception { givenTwoThreadsAreModifyingTheSameDocumentThenNoConflict(RecordsFlushing.LATER); } private void givenTwoThreadsAreModifyingTheSameDocumentThenNoConflict(final RecordsFlushing recordsFlushing) throws BigVaultException, InterruptedException, IOException, SolrServerException { vaultServer.addAll(new BigVaultServerTransaction(NOW).setNewDocuments( asList(addDocument(gandalf, "0"), addDocument(edouard, "0"), addDocument(dakota, "0")))); final int numberOfIterations = 100; final AtomicInteger problems = new AtomicInteger(); Thread thread1 = new Thread() { @Override public void run() { for (int i = 0; i < numberOfIterations; i++) { System.out.println(i); boolean retry = true; while (retry) { try { Long gandalfVersion = getVersionOf(gandalf); Integer gandalfValue = Integer.valueOf(getValueOf(gandalf)); Long edouardVersion = getVersionOf(edouard); Integer edouardValue = Integer.valueOf(getValueOf(edouard)); Long dakotaVersion = getVersionOf(dakota); Integer dakotaValue = Integer.valueOf(getValueOf(dakota)); SolrInputDocument gandalfUpdate = updateDocument(gandalf, "" + (gandalfValue + 1), gandalfVersion); SolrInputDocument edouardUpdate = updateDocument(edouard, "" + (edouardValue + 1), edouardVersion); SolrInputDocument dakotaUpdate = updateDocument(dakota, "" + (dakotaValue + 1), dakotaVersion); vaultServer.addAll(new BigVaultServerTransaction(recordsFlushing).setUpdatedDocuments( asList(gandalfUpdate, edouardUpdate, dakotaUpdate))); retry = false; } catch (BigVaultException e) { problems.addAndGet(1); retry = true; } } } } }; Thread thread2 = new Thread() { @Override public void run() { for (int i = 0; i < numberOfIterations; i++) { System.out.println(i); boolean retry = true; while (retry) { try { Long gandalfVersion = getVersionOf(gandalf); Integer gandalfValue = Integer.valueOf(getValueOf(gandalf)); Long edouardVersion = getVersionOf(edouard); Integer edouardValue = Integer.valueOf(getValueOf(edouard)); Long dakotaVersion = getVersionOf(dakota); Integer dakotaValue = Integer.valueOf(getValueOf(dakota)); SolrInputDocument gandalfUpdate = updateDocument(gandalf, "" + (gandalfValue + 1), gandalfVersion); SolrInputDocument edouardUpdate = updateDocument(edouard, "" + (edouardValue + 1), edouardVersion); SolrInputDocument dakotaUpdate = updateDocument(dakota, "" + (dakotaValue + 1), dakotaVersion); vaultServer.addAll(new BigVaultServerTransaction(recordsFlushing).setUpdatedDocuments( asList(gandalfUpdate, edouardUpdate, dakotaUpdate))); retry = false; } catch (BigVaultException e) { problems.addAndGet(1); retry = true; } } } } }; thread1.start(); thread2.start(); thread1.join(); thread2.join(); vaultServer.softCommit(); assertThat(getValueOf(gandalf)).isEqualTo("" + (numberOfIterations * 2)); assertThat(getValueOf(edouard)).isEqualTo("" + (numberOfIterations * 2)); assertThat(getValueOf(dakota)).isEqualTo("" + (numberOfIterations * 2)); assertThat(problems.get()).isGreaterThan(100) .describedAs("The test passed, but there wasn't enought conflict to prove it is correct"); } boolean isExistingOnServer(String id, BigVaultServer solrServer) throws SolrServerException, CouldNotExecuteQuery { ModifiableSolrParams params = new ModifiableSolrParams(); params.set("q", "id:" + id); return solrServer.query(params).getResults().size() == 1; } Long getVersionOfDocumentOnServer(String id, BigVaultServer solrServer) throws SolrServerException, CouldNotExecuteQuery { ModifiableSolrParams params = new ModifiableSolrParams(); params.set("q", "id:" + id); return (Long) solrServer.query(params).getResults().get(0).getFieldValue("_version_"); } String getValueOf(String id) { ModifiableSolrParams params = new ModifiableSolrParams(); params.set("q", "id:" + id); try { return (String) vaultServer.querySingleResult(params).getFieldValue("aField_s"); } catch (BigVaultException e) { throw new RuntimeException(e); } } Long getVersionOf(String id) { ModifiableSolrParams params = new ModifiableSolrParams(); params.set("q", "id:" + id); try { return (Long) vaultServer.querySingleResult(params).getFieldValue("_version_"); } catch (BigVaultException e) { throw new RuntimeException(e); } } SolrInputDocument addDocument(String id, String value) { SolrInputDocument doc = new ConstellioSolrInputDocument(); doc.setField("id", id); doc.setField("aField_s", value); return doc; } SolrInputDocument updateDocument(String id, String newValue, Long version) { SolrInputDocument doc = new ConstellioSolrInputDocument(); doc.setField("id", id); doc.setField("_version_", version); Map<String, String> maps = new HashMap<>(); maps.put("set", newValue); doc.setField("aField_s", maps); return doc; } SolrInputDocument updateDocument(String id, String newValue) { SolrInputDocument doc = new ConstellioSolrInputDocument(); doc.setField("id", id); Map<String, String> maps = new HashMap<>(); maps.put("set", newValue); doc.setField("aField_s", maps); return doc; } }