From aab1b23b2881a49a83468c835b3257a6e1e2dff2 Mon Sep 17 00:00:00 2001 From: Tom Page Date: Fri, 26 Feb 2021 12:34:22 +0000 Subject: [PATCH] MNT-22094 Add option to allow start up even if first ACL tx is missing from index. Add new unit test and refactor them to use a real TrackerState object. --- .../org/alfresco/solr/tracker/AclTracker.java | 27 ++++- .../templates/rerank/conf/solrcore.properties | 2 + .../alfresco/solr/tracker/AclTrackerTest.java | 114 ++++++++++++------ 3 files changed, 107 insertions(+), 36 deletions(-) diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/AclTracker.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/AclTracker.java index ee435f219..30d4c2557 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/AclTracker.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/AclTracker.java @@ -46,7 +46,6 @@ import java.util.stream.Collectors; import com.google.common.collect.Lists; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.httpclient.AuthenticationException; -import org.alfresco.repo.index.shard.ShardMethodEnum; import org.alfresco.solr.AclReport; import org.alfresco.solr.BoundedDeque; import org.alfresco.solr.InformationServer; @@ -84,6 +83,7 @@ public class AclTracker extends ActivatableTracker // Repository Remote API doesn't accept more than 512 aclChangeSetIds by invocation private static final int MAX_ACL_CHANGE_SET_BATCH_SIZE = 512; + private static final String ALLOW_MISSING_TRANSACTIONS_PROPERTY = "alfresco.aclTracker.allowMissingTransactions"; private int aclTrackerParallelism; @@ -105,6 +105,10 @@ public class AclTracker extends ActivatableTracker // Share run and write locks across all AclTracker threads private static Map RUN_LOCK_BY_CORE = new ConcurrentHashMap<>(); private static Map WRITE_LOCK_BY_CORE = new ConcurrentHashMap<>(); + + /** Allow starting the server even if the initial ACL transaction was not found. */ + private boolean allowMissingInitialAclTransaction = false; + @Override public Semaphore getWriteLock() { @@ -153,6 +157,9 @@ public class AclTracker extends ActivatableTracker RUN_LOCK_BY_CORE.put(coreName, new Semaphore(1, true)); WRITE_LOCK_BY_CORE.put(coreName, new Semaphore(1, true)); + + allowMissingInitialAclTransaction = Boolean.parseBoolean(p.getProperty(ALLOW_MISSING_TRANSACTIONS_PROPERTY, + Boolean.FALSE.toString())); } @Override @@ -413,6 +420,7 @@ public class AclTracker extends ActivatableTracker return; } + // Load the first ACL change sets from the Repository. AclChangeSets firstChangeSets = client.getAclChangeSets(null, 0L, null, INITIAL_MAX_ACL_CHANGE_SET_ID, 1); @@ -447,7 +455,17 @@ public class AclTracker extends ActivatableTracker LOGGER.error("If this is a new or rebuilt database your SOLR indexes " + "also need to be re-built to match the database."); LOGGER.error("You can also check your SOLR connection details in solrcore.properties."); - throw new AlfrescoRuntimeException("Initial acl transaction not found with correct timestamp"); + String exceptionMessage = "Initial ACL transaction from DB with Id=" + firstAclTxId + + " and Timestamp=" + firstAclTxCommitTime + " was not found in Solr core."; + if (allowMissingInitialAclTransaction) + { + LOGGER.error(exceptionMessage); + LOGGER.error("Ignoring missing ACL Transaction and continuing to start up as {} set to true.", ALLOW_MISSING_TRANSACTIONS_PROPERTY); + } + else + { + throw new AlfrescoRuntimeException(exceptionMessage); + } } else if (setSize == 1) { @@ -935,4 +953,9 @@ public class AclTracker extends ActivatableTracker super.invalidateState(); infoSrv.clearProcessedAclChangeSets(); } + + public void setAllowMissingInitialAclTransaction(boolean allowMissingInitialAclTransaction) + { + this.allowMissingInitialAclTransaction = allowMissingInitialAclTransaction; + } } diff --git a/search-services/alfresco-search/src/main/resources/solr/instance/templates/rerank/conf/solrcore.properties b/search-services/alfresco-search/src/main/resources/solr/instance/templates/rerank/conf/solrcore.properties index fad380b0c..10633186d 100644 --- a/search-services/alfresco-search/src/main/resources/solr/instance/templates/rerank/conf/solrcore.properties +++ b/search-services/alfresco-search/src/main/resources/solr/instance/templates/rerank/conf/solrcore.properties @@ -137,6 +137,8 @@ alfresco.aclBatchSize=100 alfresco.contentReadBatchSize=100 alfresco.contentUpdateBatchSize=1000 alfresco.cascadeNodeBatchSize=10 +# Allow starting Solr with an existing index even if it does not match the data in the DB (this should not be enabled for production systems). +alfresco.aclTracker.allowMissingTransactions=false # Trackers thread pools # Keep Content Tracker max threads to 1/4 of other values, diff --git a/search-services/alfresco-search/src/test/java/org/alfresco/solr/tracker/AclTrackerTest.java b/search-services/alfresco-search/src/test/java/org/alfresco/solr/tracker/AclTrackerTest.java index 1bec433b6..b7b32bd32 100644 --- a/search-services/alfresco-search/src/test/java/org/alfresco/solr/tracker/AclTrackerTest.java +++ b/search-services/alfresco-search/src/test/java/org/alfresco/solr/tracker/AclTrackerTest.java @@ -29,7 +29,8 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.alfresco.solr.tracker.AclTracker.INITIAL_MAX_ACL_CHANGE_SET_ID; -import static org.mockito.Mockito.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; @@ -48,104 +49,149 @@ import org.mockito.Mock; /** Unit tests for the {@link AclTracker}. */ public class AclTrackerTest { + /** The class under test. */ @InjectMocks - private AclTracker aclTracker = new AclTracker(); + private AclTracker aclTracker; + /** The class that gets information from the Repository. */ @Mock - private SOLRAPIClient solrAPIClient; + private SOLRAPIClient repositoryClient; + /** The class that gets information from Solr. */ @Mock - private InformationServer informationServer; - @Mock - private TrackerState trackerState; + private InformationServer solrInformationServer; @Before public void setUp() { + aclTracker = new AclTracker(); openMocks(this); } - /** Check that during the first run (with an empty index) then verification is successful. */ + /** Check that during the first run (with an empty repository and index) then verification is successful. */ @Test public void testCheckRepoAndIndexConsistency_firstRun_success() throws Exception { + TrackerState trackerState = new TrackerState(); AclChangeSets firstChangeSets = new AclChangeSets(emptyList()); - when(solrAPIClient.getAclChangeSets(null, 0L, + when(repositoryClient.getAclChangeSets(null, 0L, null, INITIAL_MAX_ACL_CHANGE_SET_ID, 1)).thenReturn(firstChangeSets); - when(informationServer.getAclTxDocsSize("1", "1000")).thenReturn(1); // Call the method under test. aclTracker.checkRepoAndIndexConsistency(trackerState); - verify(trackerState).setCheckedFirstAclTransactionTime(true); - verify(trackerState).setCheckedLastAclTransactionTime(true); + assertTrue("Expected first ACL transaction to have been checked.", trackerState.isCheckedFirstAclTransactionTime()); + assertTrue("Expected last ACL transaction to have been checked.", trackerState.isCheckedLastAclTransactionTime()); } /** Check that subsequent checks of a running index don't make expensive requests. */ @Test public void testCheckRepoAndIndexConsistency_alreadyInitialised_success() throws Exception { - when(trackerState.getLastGoodChangeSetCommitTimeInIndex()).thenReturn(8000L); - when(trackerState.isCheckedFirstAclTransactionTime()).thenReturn(true); - when(trackerState.isCheckedLastAclTransactionTime()).thenReturn(true); + TrackerState trackerState = new TrackerState(); + trackerState.setLastGoodChangeSetCommitTimeInIndex(8000L); + trackerState.setCheckedFirstAclTransactionTime(true); + trackerState.setCheckedLastAclTransactionTime(true); // Call the method under test. aclTracker.checkRepoAndIndexConsistency(trackerState); // Check that we don't make any expensive calls to the index or the repo. - verifyNoInteractions(solrAPIClient, informationServer); + verifyNoInteractions(repositoryClient, solrInformationServer); + } + + /** Check that during the first run (with data in the repo but an empty index) then verification is successful. */ + @Test + public void testCheckRepoAndIndexConsistency_populatedRepoEmptyIndex_success() throws Exception + { + TrackerState trackerState = new TrackerState(); + AclChangeSets firstChangeSets = new AclChangeSets(asList(new AclChangeSet(1, 1000, 2)), 8000L, 8L); + when(repositoryClient.getAclChangeSets(null, 0L, + null, INITIAL_MAX_ACL_CHANGE_SET_ID, 1)).thenReturn(firstChangeSets); + + // Call the method under test. + aclTracker.checkRepoAndIndexConsistency(trackerState); + + assertTrue("Expected first ACL transaction to have been checked.", trackerState.isCheckedFirstAclTransactionTime()); + assertTrue("Expected last ACL transaction to have been checked.", trackerState.isCheckedLastAclTransactionTime()); + assertEquals("Expected last good change set commit time to be loaded from repository.", + trackerState.getLastGoodChangeSetCommitTimeInIndex(), 1000L); + assertEquals("Expected last change set commit time to be loaded from repository.", + trackerState.getLastChangeSetCommitTimeOnServer(), 1000L); + assertEquals("Expected last change set id to be loaded from repository.", + trackerState.getLastChangeSetIdOnServer(), 1); } /** Check that after downtime the validation is successful. */ @Test public void testCheckRepoAndIndexConsistency_afterRestart_success() throws Exception { - when(trackerState.getLastGoodChangeSetCommitTimeInIndex()).thenReturn(8000L); + TrackerState trackerState = new TrackerState(); + trackerState.setLastGoodChangeSetCommitTimeInIndex(8000L); AclChangeSets firstChangeSets = new AclChangeSets(asList(new AclChangeSet(1, 1000, 2)), 8000L, 8L); - when(solrAPIClient.getAclChangeSets(null, 0L, + when(repositoryClient.getAclChangeSets(null, 0L, null, INITIAL_MAX_ACL_CHANGE_SET_ID, 1)).thenReturn(firstChangeSets); - when(informationServer.getAclTxDocsSize("1", "1000")).thenReturn(1); + when(solrInformationServer.getAclTxDocsSize("1", "1000")).thenReturn(1); // The index is behind the repo. AclChangeSet lastIndexedChangeSet = new AclChangeSet(7, 7000, 7); - when(informationServer.getMaxAclChangeSetIdAndCommitTimeInIndex()).thenReturn(lastIndexedChangeSet); + when(solrInformationServer.getMaxAclChangeSetIdAndCommitTimeInIndex()).thenReturn(lastIndexedChangeSet); // Call the method under test. aclTracker.checkRepoAndIndexConsistency(trackerState); - verify(trackerState).setCheckedFirstAclTransactionTime(true); - verify(trackerState).setCheckedLastAclTransactionTime(true); + assertTrue("Expected first ACL transaction to have been checked.", trackerState.isCheckedFirstAclTransactionTime()); + assertTrue("Expected last ACL transaction to have been checked.", trackerState.isCheckedLastAclTransactionTime()); } - /** Check that if the index is populated but the repository is empty then we get an exception. */ + /** Check that if the index is populated but missing the first ACL transaction then we get an exception. */ @Test(expected = AlfrescoRuntimeException.class) - public void testCheckRepoAndIndexConsistency_populatedIndexEmptyRepo_runtimeException() throws Exception + public void testCheckRepoAndIndexConsistency_indexMissingFirstACLTx_runtimeException() throws Exception { - when(trackerState.getLastGoodChangeSetCommitTimeInIndex()).thenReturn(8000L); + TrackerState trackerState = new TrackerState(); + trackerState.setLastGoodChangeSetCommitTimeInIndex(8000L); AclChangeSets firstChangeSets = new AclChangeSets(asList(new AclChangeSet(1, 1000, 2)), 8000L, 8L); - when(solrAPIClient.getAclChangeSets(null, 0L, + when(repositoryClient.getAclChangeSets(null, 0L, null, INITIAL_MAX_ACL_CHANGE_SET_ID, 1)).thenReturn(firstChangeSets); - // The first ACL transaction was not found in the repository. - when(informationServer.getAclTxDocsSize("1", "1000")).thenReturn(0); - - AclChangeSet lastIndexedChangeSet = new AclChangeSet(8, 8000, 8); - when(informationServer.getMaxAclChangeSetIdAndCommitTimeInIndex()).thenReturn(lastIndexedChangeSet); + // The first ACL transaction was not found in Solr. + when(solrInformationServer.getAclTxDocsSize("1", "1000")).thenReturn(0); // Call the method under test. aclTracker.checkRepoAndIndexConsistency(trackerState); } + /** Check that allowMissingInitialAclTransaction allows start up if the index is missing the first ACL transaction. */ + @Test + public void testCheckRepoAndIndexConsistency_allowMissingInitialAclTransactionSet_errorIgnored() throws Exception + { + aclTracker.setAllowMissingInitialAclTransaction(true); + + TrackerState trackerState = new TrackerState(); + trackerState.setLastGoodChangeSetCommitTimeInIndex(8000L); + // Pretend that we've already checked the last ACL tx since it's not the purpose of this test. + trackerState.setCheckedLastAclTransactionTime(true); + AclChangeSets firstChangeSets = new AclChangeSets(asList(new AclChangeSet(1, 1000, 2)), 8000L, 8L); + when(repositoryClient.getAclChangeSets(null, 0L, + null, INITIAL_MAX_ACL_CHANGE_SET_ID, 1)).thenReturn(firstChangeSets); + // The first ACL transaction was not found in Solr. + when(solrInformationServer.getAclTxDocsSize("1", "1000")).thenReturn(0); + + // Call the method under test and check no exception is thrown. + aclTracker.checkRepoAndIndexConsistency(trackerState); + } + /** Check that if the last ACL in the index is after the last ACL in the repository then we get an exception. */ @Test (expected = AlfrescoRuntimeException.class) public void testCheckRepoAndIndexConsistency_indexAheadOfRepo_runtimeException() throws Exception { - when(trackerState.getLastGoodChangeSetCommitTimeInIndex()).thenReturn(8000L); + TrackerState trackerState = new TrackerState(); + trackerState.setLastGoodChangeSetCommitTimeInIndex(8000L); AclChangeSets firstChangeSets = new AclChangeSets(asList(new AclChangeSet(1, 1000, 2)), 8000L, 8L); - when(solrAPIClient.getAclChangeSets(null, 0L, + when(repositoryClient.getAclChangeSets(null, 0L, null, INITIAL_MAX_ACL_CHANGE_SET_ID, 1)).thenReturn(firstChangeSets); - when(informationServer.getAclTxDocsSize("1", "1000")).thenReturn(1); + when(solrInformationServer.getAclTxDocsSize("1", "1000")).thenReturn(1); // The index contains an ACL after the last one from the server (id 8 at time 8000L). AclChangeSet lastIndexedChangeSet = new AclChangeSet(9, 9000, 9); - when(informationServer.getMaxAclChangeSetIdAndCommitTimeInIndex()).thenReturn(lastIndexedChangeSet); + when(solrInformationServer.getMaxAclChangeSetIdAndCommitTimeInIndex()).thenReturn(lastIndexedChangeSet); // Call the method under test. aclTracker.checkRepoAndIndexConsistency(trackerState);