diff --git a/README.md b/README.md index 0b78fd2e7..95ab30356 100644 --- a/README.md +++ b/README.md @@ -40,4 +40,4 @@ More details are available at [search-services](/search-services) folder. **Following resources will not be available for Community users** -More details are available at [insight-engine](/insight-engine) folder. +More details are available at [insight-engine](/insight-engine) folder. \ No newline at end of file diff --git a/e2e-test/generator-alfresco-docker-compose/generators/app/index.js b/e2e-test/generator-alfresco-docker-compose/generators/app/index.js index e127caee3..ac5b2f4a2 100644 --- a/e2e-test/generator-alfresco-docker-compose/generators/app/index.js +++ b/e2e-test/generator-alfresco-docker-compose/generators/app/index.js @@ -60,10 +60,14 @@ module.exports = class extends Generator { }, { whenFunction: response => response.httpMode == 'http', - type: 'confirm', + type: 'list', name: 'replication', - message: 'Would you like to use SOLR Replication (2 nodes in master-slave)?', - default: false + message: 'Would you like to use SOLR Replication?', + choices: [ + { name: "No", value: "" }, + { name: "Yes - two nodes in a master-slave configuration", value: "master-slave" }, + { name: "Yes - two nodes in a master-master configuration", value: "master-master" } + ] }, // Enterprise only options { @@ -199,7 +203,7 @@ module.exports = class extends Generator { port: (this.props.httpWebMode == 'http' ? '8080' : '443'), secureComms: (this.props.httpMode == 'http' ? 'none' : 'https'), alfrescoPort: (this.props.httpMode == 'http' ? '8080' : '8443'), - replication: (this.props.replication ? "true" : "false"), + replication: this.props.replication, searchSolrHost: (this.props.replication ? "solr6secondary" : "solr6"), searchPath: searchBasePath, zeppelin: (this.props.zeppelin ? "true" : "false"), diff --git a/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.1/docker-compose-ce.yml b/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.1/docker-compose-ce.yml index c175a9e05..cf47d559b 100755 --- a/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.1/docker-compose-ce.yml +++ b/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.1/docker-compose-ce.yml @@ -47,7 +47,7 @@ services: ALFRESCO_HOSTNAME: alfresco ALFRESCO_COMMS: <%=secureComms%> <% if (httpMode == 'https') { %> TRUSTSTORE_TYPE: JCEKS - KEYSTORE_TYPE: JCEKS <% } %> <% if (replication == 'true') { %> + KEYSTORE_TYPE: JCEKS <% } %> <% if (replication) { %> ENABLE_MASTER: "true" ENABLE_SLAVE: "false" <% } %> mem_limit: 1200m @@ -78,7 +78,7 @@ services: volumes: - ./keystores/solr:/opt/<%=searchPath%>/keystore <% } %> - <% if (replication == 'true') { %> + <% if (replication) { %> solr6secondary: build: context: ./search @@ -88,9 +88,9 @@ services: ALFRESCO_HOSTNAME: alfresco ALFRESCO_COMMS: <%=secureComms%> <% if (httpMode == 'https') { %> TRUSTSTORE_TYPE: JCEKS - KEYSTORE_TYPE: JCEKS <% } %> <% if (replication == 'true') { %> - ENABLE_MASTER: "true" - ENABLE_SLAVE: "false" + KEYSTORE_TYPE: JCEKS <% } %> <% if (replication) { %> + ENABLE_MASTER: <% if (replication == 'master-master') { %>"true"<% } else { %>"false"<% } %> + ENABLE_SLAVE: <% if (replication == 'master-master') { %>"false"<% } else { %>"true"<% } %> MASTER_HOST: solr6 <% } %> mem_limit: 1200m environment: diff --git a/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.1/docker-compose-ee.yml b/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.1/docker-compose-ee.yml index 711f325c0..31859c711 100755 --- a/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.1/docker-compose-ee.yml +++ b/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.1/docker-compose-ee.yml @@ -57,7 +57,7 @@ services: ALFRESCO_HOSTNAME: alfresco ALFRESCO_COMMS: <%=secureComms%> <% if (httpMode == 'https') { %> TRUSTSTORE_TYPE: JCEKS - KEYSTORE_TYPE: JCEKS <% } %> <% if (replication == 'true') { %> + KEYSTORE_TYPE: JCEKS <% } %> <% if (replication) { %> ENABLE_MASTER: "true" ENABLE_SLAVE: "false" <% } %> <% if (sharding == 'true') { %> ENABLE_SHARDING: "true" @@ -103,7 +103,7 @@ services: volumes: - ./keystores/solr:/opt/<%=searchPath%>/keystore <% } %> - <% if (sharding == 'true' || replication == 'true') { %> + <% if (sharding == 'true' || replication) { %> solr6secondary: build: context: ./search @@ -113,9 +113,9 @@ services: ALFRESCO_HOSTNAME: alfresco ALFRESCO_COMMS: <%=secureComms%> <% if (httpMode == 'https') { %> TRUSTSTORE_TYPE: JCEKS - KEYSTORE_TYPE: JCEKS <% } %> <% if (replication == 'true') { %> - ENABLE_MASTER: "true" - ENABLE_SLAVE: "false" + KEYSTORE_TYPE: JCEKS <% } %> <% if (replication) { %> + ENABLE_MASTER: <% if (replication == 'master-master') { %>"true"<% } else { %>"false"<% } %> + ENABLE_SLAVE: <% if (replication == 'master-master') { %>"false"<% } else { %>"true"<% } %> MASTER_HOST: solr6 <% } %> <% if (sharding == 'true') { %> ENABLE_SHARDING: "true" NUM_SHARDS: "2" diff --git a/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/.env b/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/.env index b39fd3b1e..352f5b342 100755 --- a/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/.env +++ b/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/.env @@ -1,15 +1,15 @@ ALFRESCO_TAG=latest -SHARE_TAG=6.1.0-RC3 -POSTGRES_TAG=10.9 -TRANSFORM_ROUTER_TAG=1.1.0-EA2 -PDF_RENDERER_TAG=2.1.0-EA4 -IMAGE_MAGICK_TAG=2.1.0-EA4 -LIBREOFFICE_TAG=2.1.0-EA4 -TIKA_TAG=2.1.0-EA4 -TRANSFORM_MISC_TAG=2.1.0-EA4 +SHARE_TAG=6.2.0 +POSTGRES_TAG=11.4 +TRANSFORM_ROUTER_TAG=1.1.0 +PDF_RENDERER_TAG=2.1.0 +IMAGE_MAGICK_TAG=2.1.0 +LIBREOFFICE_TAG=2.1.0 +TIKA_TAG=2.1.0 +TRANSFORM_MISC_TAG=2.1.0 SHARED_FILE_STORE_TAG=0.5.3 ACTIVE_MQ_TAG=5.15.8 -DIGITAL_WORKSPACE_TAG=1.1.0 +DIGITAL_WORKSPACE_TAG=1.3.0 ACS_NGINX_TAG=3.0.1 ACS_COMMUNITY_NGINX_TAG=1.0.0 SEARCH_TAG=latest diff --git a/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/docker-compose-ce.yml b/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/docker-compose-ce.yml index 28080a4f2..451e57945 100755 --- a/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/docker-compose-ce.yml +++ b/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/docker-compose-ce.yml @@ -63,7 +63,7 @@ services: ALFRESCO_HOSTNAME: alfresco ALFRESCO_COMMS: <%=secureComms%> <% if (httpMode == 'https') { %> TRUSTSTORE_TYPE: JCEKS - KEYSTORE_TYPE: JCEKS <% } %> <% if (replication == 'true') { %> + KEYSTORE_TYPE: JCEKS <% } %> <% if (replication) { %> ENABLE_MASTER: "true" ENABLE_SLAVE: "false" <% } %> mem_limit: 1200m @@ -94,7 +94,7 @@ services: volumes: - ./keystores/solr:/opt/<%=searchPath%>/keystore <% } %> - <% if (replication == 'true') { %> + <% if (replication) { %> solr6secondary: build: context: ./search @@ -104,9 +104,9 @@ services: ALFRESCO_HOSTNAME: alfresco ALFRESCO_COMMS: <%=secureComms%> <% if (httpMode == 'https') { %> TRUSTSTORE_TYPE: JCEKS - KEYSTORE_TYPE: JCEKS <% } %> <% if (replication == 'true') { %> - ENABLE_MASTER: "true" - ENABLE_SLAVE: "false" + KEYSTORE_TYPE: JCEKS <% } %> <% if (replication) { %> + ENABLE_MASTER: <% if (replication == 'master-master') { %>"true"<% } else { %>"false"<% } %> + ENABLE_SLAVE: <% if (replication == 'master-master') { %>"false"<% } else { %>"true"<% } %> MASTER_HOST: solr6 <% } %> mem_limit: 1200m environment: diff --git a/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/docker-compose-ee.yml b/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/docker-compose-ee.yml index 4a5d24982..6a4f4a7fd 100755 --- a/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/docker-compose-ee.yml +++ b/e2e-test/generator-alfresco-docker-compose/generators/app/templates/6.2/docker-compose-ee.yml @@ -66,7 +66,7 @@ services: ALFRESCO_HOSTNAME: alfresco ALFRESCO_COMMS: <%=secureComms%> <% if (httpMode == 'https') { %> TRUSTSTORE_TYPE: JCEKS - KEYSTORE_TYPE: JCEKS <% } %> <% if (replication == 'true') { %> + KEYSTORE_TYPE: JCEKS <% } %> <% if (replication) { %> ENABLE_MASTER: "true" ENABLE_SLAVE: "false" <% } %> <% if (sharding == 'true') { %> ENABLE_SHARDING: "true" @@ -112,7 +112,7 @@ services: volumes: - ./keystores/solr:/opt/<%=searchPath%>/keystore <% } %> - <% if (sharding == 'true' || replication == 'true') { %> + <% if (sharding == 'true' || replication) { %> solr6secondary: build: context: ./search @@ -122,9 +122,9 @@ services: ALFRESCO_HOSTNAME: alfresco ALFRESCO_COMMS: <%=secureComms%> <% if (httpMode == 'https') { %> TRUSTSTORE_TYPE: JCEKS - KEYSTORE_TYPE: JCEKS <% } %> <% if (replication == 'true') { %> - ENABLE_MASTER: "true" - ENABLE_SLAVE: "false" + KEYSTORE_TYPE: JCEKS <% } %> <% if (replication) { %> + ENABLE_MASTER: <% if (replication == 'master-master') { %>"true"<% } else { %>"false"<% } %> + ENABLE_SLAVE: <% if (replication == 'master-master') { %>"false"<% } else { %>"true"<% } %> MASTER_HOST: solr6 <% } %> <% if (sharding == 'true') { %> ENABLE_SHARDING: "true" NUM_SHARDS: "2" diff --git a/e2e-test/pom.xml b/e2e-test/pom.xml index 5cee7b842..f0501aaf0 100644 --- a/e2e-test/pom.xml +++ b/e2e-test/pom.xml @@ -11,9 +11,9 @@ Search Analytics E2E Tests Test Project to test Search Service and Analytics Features on a complete setup of Alfresco, Share - 6.0.1.2 - 6.0.0.4 - 3.0.14 + 1.26 + 1.12 + 3.0.17 3.2.0 src/test/resources/SearchSuite.xml @@ -44,7 +44,7 @@ org.alfresco.tas - restapi-test + restapi ${tas.rest.api.version} test @@ -56,7 +56,7 @@ org.alfresco.tas - cmis-test + cmis ${tas.cmis.api.version} test @@ -71,6 +71,12 @@ alfresco-governance-services-automation-enterprise-rest-api ${rm.version} test + + + org.alfresco.tas + restapi-test + + com.fasterxml.jackson.core @@ -88,6 +94,10 @@ com.fasterxml.jackson.core jackson-databind + + org.alfresco.tas + restapi-test + diff --git a/e2e-test/src/main/java/org/alfresco/search/TestGroup.java b/e2e-test/src/main/java/org/alfresco/search/TestGroup.java index a149ab6f7..fbb90a64e 100644 --- a/e2e-test/src/main/java/org/alfresco/search/TestGroup.java +++ b/e2e-test/src/main/java/org/alfresco/search/TestGroup.java @@ -38,6 +38,7 @@ public class TestGroup public static final String ACS_61n = "ACS_61n"; // Alfresco Content Services 6.1 or above public static final String ACS_611n = "ACS_611n"; // Alfresco Content Services 6.1.1 or above public static final String ACS_62n = "ACS_62n"; // Alfresco Content Services 6.2 or above + public static final String ACS_63n = "ACS_63n"; // Alfresco Content Services 6.3 or above public static final String AGS_302 = "AGS_302"; // Alfresco governance Services 3.0.2 or above diff --git a/e2e-test/src/test/java/org/alfresco/test/search/functional/AbstractE2EFunctionalTest.java b/e2e-test/src/test/java/org/alfresco/test/search/functional/AbstractE2EFunctionalTest.java index daded7776..8073d003d 100644 --- a/e2e-test/src/test/java/org/alfresco/test/search/functional/AbstractE2EFunctionalTest.java +++ b/e2e-test/src/test/java/org/alfresco/test/search/functional/AbstractE2EFunctionalTest.java @@ -6,11 +6,22 @@ */ package org.alfresco.test.search.functional; +import static java.util.Optional.ofNullable; + +import static lombok.AccessLevel.PROTECTED; +import static org.testng.Assert.assertEquals; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.Getter; import org.alfresco.cmis.CmisWrapper; import org.alfresco.dataprep.ContentService; import org.alfresco.dataprep.SiteService.Visibility; import org.alfresco.rest.core.RestProperties; import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.model.RestRequestSpellcheckModel; import org.alfresco.rest.search.RestRequestHighlightModel; import org.alfresco.rest.search.RestRequestQueryModel; import org.alfresco.rest.search.SearchRequest; @@ -37,15 +48,10 @@ import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeSuite; -import lombok.Getter; - -import static java.util.Optional.ofNullable; -import static lombok.AccessLevel.PROTECTED; - /** * @author meenal bhave */ -@ContextConfiguration("classpath:alfresco-search-e2e-context.xml") +@ContextConfiguration ("classpath:alfresco-search-e2e-context.xml") public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringContextTests { /** The number of retries that a query will be tried before giving up. */ @@ -75,27 +81,26 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont protected CmisWrapper cmisApi; @Autowired - @Getter(value = PROTECTED) + // @Getter(value = PROTECTED) protected DataUser dataUser; @Autowired - @Getter(value = PROTECTED) + @Getter (value = PROTECTED) private ContentService contentService; - protected UserModel testUser, adminUserModel; - protected SiteModel testSite; + protected UserModel testUser, adminUserModel, testUser2; + protected SiteModel testSite, testSite2; protected static String unique_searchString; - + protected static final String SEARCH_LANGUAGE_CMIS = "cmis"; - + protected enum SearchLanguage { CMIS, AFTS - } + } - - @BeforeSuite(alwaysRun = true) + @BeforeSuite (alwaysRun = true) public void beforeSuite() throws Exception { super.springTestContextPrepareTestInstance(); @@ -104,7 +109,7 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont deployCustomModel("model/finance-model.xml"); } - @BeforeClass(alwaysRun = true) + @BeforeClass (alwaysRun = true) public void setup() { serverHealth.assertServerIsOnline(); @@ -209,7 +214,7 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont customModel.setNodeRef(modelInRepo.getId()); customModel.setNodeRef(customModel.getNodeRefWithoutVersion()); customModel.setCmisLocation(String.format("/Data Dictionary/Models/%s", fileName)); - LOGGER.info("Custom Model file: " + customModel.getCmisLocation()); + LOGGER.info("Custom Model file: " + customModel.getCmisLocation()); } else { @@ -239,9 +244,9 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont /** * Wait for Solr to finish indexing and search to return appropriate results - * - * @param userQuery Search Query - * @param contentToFind that's expected to be included / excluded from the results + * + * @param userQuery Search Query + * @param contentToFind that's expected to be included / excluded from the results * @param expectedInResults Whether we expect the content in the results or not. * @return true if search returns expected results, i.e. is given content is found or excluded from the results */ @@ -259,10 +264,7 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont if (restClient.getStatusCode().matches(expectedStatusCode)) { - boolean found = response.getEntries().stream() - .map(entry -> entry.getModel().getName()) - .filter(name -> name.equalsIgnoreCase(contentName) || contentName.isBlank()) - .count() > 0; + boolean found = isContentInSearchResponse(response, contentName); // Exit loop if result is as expected. if (expectedInResults == found) @@ -281,10 +283,24 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont return false; } + /** + * Method to check if the contentName is returned in the SearchResponse. + * + * @param response the search response + * @param contentName the text we are using as matching/verifying criteria. + * @return true if if the item with the contentName text is returned in the SearchResponse. + */ + public boolean isContentInSearchResponse(SearchResponse response, String contentName) + { + return response.getEntries().stream() + .map(entry -> entry.getModel().getName()) + .anyMatch(name -> name.equalsIgnoreCase(contentName) || contentName.isBlank()); + } + /** * Wait for Solr to finish indexing: Indexing has caught up = true if search returns appropriate results - * - * @param userQuery: search query, this can include the fieldname, unique search string will guarantee accurate results + * + * @param userQuery: search query, this can include the fieldname, unique search string will guarantee accurate results * @param expectedInResults, true if entry is expected in the results set * @return true (indexing is finished) if search returns appropriate results */ @@ -296,7 +312,7 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont /** * waitForIndexing method that matches / waits for filename, metadata to be indexed. - * + * * @param userQuery * @param expectedInResults * @return @@ -309,7 +325,7 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont /** * waitForIndexing method that matches / waits for content to be indexed, this can take longer than metadata indexing. * Since Metadata is indexed first, use this method where tests, queries need content to be indexed too. - * + * * @param userQuery * @param expectedInResults * @return @@ -321,9 +337,9 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont /** * Wait for Solr to finish indexing: Indexing has caught up = true if search returns appropriate results - * - * @param fieldName: specific field to search for, e.g. name. When specified, the query will become: name:'userQuery' - * @param userQuery: search string, unique search string will guarantee accurate results + * + * @param fieldName: specific field to search for, e.g. name. When specified, the query will become: name:'userQuery' + * @param userQuery: search string, unique search string will guarantee accurate results * @param expectedInResults, true if entry is expected in the results set * @return true (indexing is finished) if search returns appropriate results */ @@ -336,7 +352,7 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont /** * Run a search as admin user and return the response - * + * * @param queryString: string to search for, unique search string will guarantee accurate results * @return the search response from the API */ @@ -347,8 +363,8 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont /** * Run a search as given user and return the response - * - * @param user: UserModel for the user you wish to run the query as + * + * @param user: UserModel for the user you wish to run the query as * @param queryString: string to search for, unique search string will guarantee accurate results * @return the search response from the API */ @@ -360,14 +376,23 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont searchRequest.setQuery(queryModel); return restClient.authenticateUser(user).withSearchAPI().search(searchRequest); } - + /** - * Run a search as given user and return the response - * - * @param user: UserModel for the user you wish to run the query as - * @param queryModel: The queryModel to search for, containing the query + * Run a search with Spellcheck as given user and return the response + * @param user UserModel for the user you wish to run the query as + * @param queryModel The queryModel to search for, containing the query + * @param spellcheckQuery The Spellcheck Model containing the query * @return the search response from the API */ + protected SearchResponse queryAsUser(UserModel user, RestRequestQueryModel queryModel, RestRequestSpellcheckModel spellcheckQuery) + { + SearchRequest searchRequest = new SearchRequest(); + searchRequest.setQuery(queryModel); + searchRequest.setSpellcheck(spellcheckQuery); + + return restClient.authenticateUser(user).withSearchAPI().search(searchRequest); + } + protected SearchResponse queryAsUser(UserModel user, RestRequestQueryModel queryModel) { SearchRequest searchRequest = new SearchRequest(); @@ -396,27 +421,22 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont query.setQuery(queryReq); return query; } - + + protected DataUser getDataUser() + { + return dataUser; + } + /** * Helper method to test if the search query works and count matches where provided - * @param query: AFTS or cmis query string - * @param expectedCount: Only successful response is checked, when expectedCount is null (can not be exactly specified), - * @param setCmis: Query language is set to cmis when setCmis is true, AFTS when false + * + * @param query: AFTS or cmis query string + * @param expectedCount: Only successful response is checked, when expectedCount is null (can not be exactly specified), * @return SearchResponse */ protected SearchResponse testSearchQuery(String query, Integer expectedCount, SearchLanguage queryLanguage) { - RestRequestQueryModel queryModel = new RestRequestQueryModel(); - queryModel.setQuery(query); - - if (ofNullable(queryLanguage).isPresent()) - { - queryModel.setLanguage(queryLanguage.toString()); - } - - SearchResponse response = queryAsUser(testUser, queryModel); - - restClient.assertStatusCodeIs(HttpStatus.OK); + SearchResponse response = performSearch(testUser, query, queryLanguage); if (ofNullable(expectedCount).isPresent()) { @@ -425,4 +445,109 @@ public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringCont return response; } + + /** + * Helper method to test if the search query returns the expected results in the order given. + * + * @param query: AFTS or cmis query string + * @param expectedNames: The ordered list of names expected to be returned, + * @return SearchResponse + */ + protected SearchResponse testSearchQueryOrdered(String query, List expectedNames, SearchLanguage queryLanguage) + { + SearchResponse response = performSearch(testUser, query, queryLanguage); + + List names = response.getEntries().stream().map(s -> s.getModel().getName()).collect(Collectors.toList()); + + // Include lists in failure message as TestNG won't do this for lists. + assertEquals(names, expectedNames, "Unexpected results for query: " + query + " Expected: " + expectedNames + " but got " + names); + + return response; + } + + /** + * Helper method to test if the search query returns the expected set of results. + * + * @param query: AFTS or cmis query string + * @param expectedNames: The ordered list of names expected to be returned, + * @return SearchResponse + */ + protected SearchResponse testSearchQueryUnordered(String query, Set expectedNames, SearchLanguage queryLanguage) + { + SearchResponse response = performSearch(testUser, query, queryLanguage); + + Set names = response.getEntries().stream().map(s -> s.getModel().getName()).collect(Collectors.toSet()); + + assertEquals(names, expectedNames, "Unexpected results for query: " + query); + + return response; + } + + private SearchResponse performSearch(UserModel asUser, String query, SearchLanguage queryLanguage) + { + RestRequestQueryModel queryModel = new RestRequestQueryModel(); + queryModel.setQuery(query); + + if (!ofNullable(asUser).isPresent()) + { + asUser = testUser; + } + + if (ofNullable(queryLanguage).isPresent()) + { + queryModel.setLanguage(queryLanguage.toString()); + } + + SearchResponse response = queryAsUser(asUser, queryModel); + + return response; + } + + /** + * Method to create and run a simple spellcheck query + * When a spellcheck query is run a user, the query inputed and the user query is inputted + * @param query + * @param userQuery + * @return + */ + protected SearchResponse SearchSpellcheckQuery(UserModel user, String query, String userQuery) + { + RestRequestSpellcheckModel spellCheck = new RestRequestSpellcheckModel(); + spellCheck.setQuery(userQuery); + + UserModel searchUser = ofNullable(user).isPresent() ? user : testUser; + SearchRequest searchReq = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery(query); + queryReq.setUserQuery(userQuery); + searchReq.setQuery(queryReq); + searchReq.setSpellcheck(spellCheck); + SearchResponse response = queryAsUser(searchUser, queryReq, spellCheck); + return response; + } + + /** + * Method to check the spellcheck object returned in the Search Response + * @param response SearchResponse + * @param spellCheckType String Values: searchInsteadFor, didYouMean or null + * @param spellCheckSuggestion String Values: suggestion string or null + */ + public void testSearchSpellcheckResponse(SearchResponse response, String spellCheckType, String spellCheckSuggestion) + { + if (ofNullable(spellCheckType).isPresent()) + { + response.getContext().assertThat().field("spellCheck").isNotEmpty(); + response.getContext().getSpellCheck().assertThat().field("type").is(spellCheckType); + } + else + { + response.getContext().assertThat().field("spellCheck").isNull(); + } + + if (ofNullable(spellCheckSuggestion).isPresent()) + { + response.getContext().assertThat().field("spellCheck").isNotEmpty(); + response.getContext().getSpellCheck().assertThat().field("suggestions").contains(spellCheckSuggestion); + } + } } diff --git a/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/cmis/SolrSearchByPropertyTests.java b/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/cmis/SolrSearchByPropertyTests.java index 40ea49483..d9f7638bd 100644 --- a/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/cmis/SolrSearchByPropertyTests.java +++ b/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/cmis/SolrSearchByPropertyTests.java @@ -1,5 +1,8 @@ package org.alfresco.test.search.functional.searchServices.cmis; +import java.util.List; +import java.util.Set; + import org.alfresco.utility.Utility; import org.alfresco.utility.data.CustomObjectTypeProperties; import org.alfresco.utility.data.provider.XMLDataConfig; @@ -15,7 +18,7 @@ public class SolrSearchByPropertyTests extends AbstractCmisE2ETest private FolderModel guestf, tesf, restf, testtttf, testf, testf1, testf2, testf3, testf4; private FileModel guestc, restc, tesc, testtttc, testc, testc1, testc2, testc3; - @BeforeClass(alwaysRun = true) + @BeforeClass (alwaysRun = true) public void dataPreparation() throws Exception { dataContent.usingAdmin().deployContentModel("model/tas-model.xml"); @@ -46,68 +49,464 @@ public class SolrSearchByPropertyTests extends AbstractCmisE2ETest // Sites dataContent.usingUser(testUser).usingSite(testSite).createCustomContent(guestf, "F:tas:folder", new CustomObjectTypeProperties().addProperty("tas:TextPropertyF", "guestf text") - .addProperty("tas:IntPropertyF", 222)); + .addProperty("tas:IntPropertyF", 222)); dataContent.usingUser(testUser).usingSite(testSite).createCustomContent(tesf, "F:tas:folder", new CustomObjectTypeProperties().addProperty("tas:TextPropertyF", "tesf text") - .addProperty("tas:IntPropertyF", 224)); + .addProperty("tas:IntPropertyF", 224)); // Sites >> Folders dataContent.usingUser(testUser).usingResource(guestf).createCustomContent(restf, "F:tas:folder", new CustomObjectTypeProperties().addProperty("tas:TextPropertyF", "restf text") - .addProperty("tas:IntPropertyF", 223)); + .addProperty("tas:IntPropertyF", 223)); dataContent.usingUser(testUser).usingResource(guestf).createCustomContent(testtttf, "F:tas:folder", new CustomObjectTypeProperties().addProperty("tas:TextPropertyF", "testtttf text") - .addProperty("tas:IntPropertyF", 225)); + .addProperty("tas:IntPropertyF", 225)); dataContent.usingUser(testUser).usingResource(guestf).createCustomContent(testf, "F:tas:folder", new CustomObjectTypeProperties().addProperty("tas:TextPropertyF", "testf text") - .addProperty("tas:IntPropertyF", 226)); + .addProperty("tas:IntPropertyF", 226)); dataContent.usingUser(testUser).usingResource(guestf).createCustomContent(testf1, "F:tas:folder", new CustomObjectTypeProperties().addProperty("tas:TextPropertyF", "testf1 text") - .addProperty("tas:IntPropertyF", 2221)); + .addProperty("tas:IntPropertyF", 2221)); dataContent.usingUser(testUser).usingResource(guestf).createCustomContent(testf2, "F:tas:folder", new CustomObjectTypeProperties().addProperty("tas:TextPropertyF", "testf2 text") - .addProperty("tas:IntPropertyF", 2222)); + .addProperty("tas:IntPropertyF", 2222)); dataContent.usingUser(testUser).usingResource(guestf).createCustomContent(testf3, "F:tas:folder", new CustomObjectTypeProperties().addProperty("tas:TextPropertyF", "testf3 text") - .addProperty("tas:IntPropertyF", 2223)); + .addProperty("tas:IntPropertyF", 2223)); dataContent.usingUser(testUser).usingResource(guestf).createCustomContent(testf4, "F:tas:folder", new CustomObjectTypeProperties().addProperty("tas:TextPropertyF", "testf4 text") - .addProperty("tas:IntPropertyF", 2224)); + .addProperty("tas:IntPropertyF", 2224)); // Sites >> Files dataContent.usingUser(testUser).usingResource(tesf).createCustomContent(guestc, "D:tas:document", new CustomObjectTypeProperties().addProperty("tas:TextPropertyC", "guestc text") - .addProperty("tas:IntPropertyC", 222)); + .addProperty("tas:IntPropertyC", 222)); dataContent.usingUser(testUser).usingResource(tesf).createCustomContent(restc, "D:tas:document", new CustomObjectTypeProperties().addProperty("tas:TextPropertyC", "restc text") - .addProperty("tas:IntPropertyC", 223)); + .addProperty("tas:IntPropertyC", 223)); dataContent.usingUser(testUser).usingResource(tesf).createCustomContent(tesc, "D:tas:document", new CustomObjectTypeProperties().addProperty("tas:TextPropertyC", "tesc text") - .addProperty("tas:IntPropertyC", 224)); + .addProperty("tas:IntPropertyC", 224)); dataContent.usingUser(testUser).usingResource(tesf).createCustomContent(testtttc, "D:tas:document", new CustomObjectTypeProperties().addProperty("tas:TextPropertyC", "testtttc text") - .addProperty("tas:IntPropertyC", 225)); + .addProperty("tas:IntPropertyC", 225)); dataContent.usingUser(testUser).usingResource(tesf).createCustomContent(testc, "D:tas:document", new CustomObjectTypeProperties().addProperty("tas:TextPropertyC", "testc text") - .addProperty("tas:IntPropertyC", 226)); + .addProperty("tas:IntPropertyC", 226)); dataContent.usingUser(testUser).usingResource(tesf).createCustomContent(testc1, "D:tas:document", new CustomObjectTypeProperties().addProperty("tas:TextPropertyC", "testc1 text") - .addProperty("tas:IntPropertyC", 2221)); + .addProperty("tas:IntPropertyC", 2221)); dataContent.usingUser(testUser).usingResource(tesf).createCustomContent(testc2, "D:tas:document", new CustomObjectTypeProperties().addProperty("tas:TextPropertyC", "testc2 text") - .addProperty("tas:IntPropertyC", 2222)); + .addProperty("tas:IntPropertyC", 2222)); dataContent.usingUser(testUser).usingResource(tesf).createCustomContent(testc3, "D:tas:document", new CustomObjectTypeProperties().addProperty("tas:TextPropertyC", "testc3 text") - .addProperty("tas:IntPropertyC", 2223)); + .addProperty("tas:IntPropertyC", 2223)); // wait for solr index Utility.waitToLoopTime(getSolrWaitTimeInSeconds()); } - @Test(dataProviderClass = XMLTestDataProvider.class, dataProvider = "getQueriesData") - @XMLDataConfig(file = "src/test/resources/testdata/search-by-property.xml") - public void executeSearchByProperty(QueryModel query) throws Exception + @Test + public void testFileNameEquality() { - cmisApi.authenticateUser(testUser).withQuery(query.getValue()).assertResultsCount().equals(query.getResults()); + String query = "SELECT * FROM tas:document where cmis:name = 'testc.txt'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testc.txt")); + } + + @Test + public void testFileNameInequality() + { + String query = "SELECT * FROM tas:document where cmis:name <> 'testc.txt'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestc.txt", "restc.txt", "tesc.txt", "testtttc.txt", "testc1.txt", "testc2.txt", "testc3.txt")); + } + + @Test + public void testFileNameIn() + { + String query = "SELECT * FROM tas:document where cmis:name IN('testc.txt', 'guestc.txt', 'restc.txt')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testc.txt", "guestc.txt", "restc.txt")); + } + + @Test + public void testFileNameNotIn() + { + // Nb. "gustc" is missing an "e". + String query = "SELECT * FROM tas:document where cmis:name NOT IN('testc.txt', 'gustc.txt', 'restc.txt')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestc.txt", "tesc.txt", "testtttc.txt", "testc1.txt", "testc2.txt", "testc3.txt")); + } + + @Test + public void testFileNameLike() + { + String query = "SELECT * FROM tas:document where cmis:name LIKE '%testc%'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testc.txt", "testc1.txt", "testc2.txt", "testc3.txt")); + } + + @Test + public void testFileNameLikeExact() + { + String query = "SELECT * FROM tas:document where cmis:name LIKE 'testc.txt'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testc.txt")); + } + + @Test + public void testFileNamePrefixSuffix() + { + String query = "SELECT * FROM tas:document where cmis:name LIKE 't%tc.txt'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testc.txt", "testtttc.txt")); + } + + @Test + public void testFileNameUnderscore() + { + String query = "SELECT * FROM tas:document where cmis:name LIKE 't__tc.txt'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testc.txt")); + } + + @Test + public void testFolderNameEquality() + { + String query = "SELECT * FROM tas:folder where cmis:name = 'testf'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testf")); + } + + @Test + public void testFolderNameInequality() + { + String query = "SELECT * FROM tas:folder where cmis:name <> 'testf'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestf", "tesf", "restf", "testtttf", "testf1", "testf2", "testf3", "testf4")); + } + + @Test + public void testFolderNameIn() + { + String query = "SELECT * FROM tas:folder where cmis:name IN('testf', 'guestf', 'restf')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testf", "guestf", "restf")); + } + + @Test + public void testFolderNameNotIn() + { + // Nb. "gustc" is missing an "e". + String query = "SELECT * FROM tas:folder where cmis:name NOT IN('testf', 'gustf', 'restf')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestf", "tesf", "testtttf", "testf1", "testf2", "testf3", "testf4")); + } + + @Test + public void testFolderNameLike() + { + String query = "SELECT * FROM tas:folder where cmis:name LIKE '%testf%'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testf", "testf1", "testf2", "testf3", "testf4")); + } + + @Test + public void testFolderNameLikeExact() + { + String query = "SELECT * FROM tas:folder where cmis:name LIKE 'testf'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testf")); + } + + @Test + public void testFolderNamePrefixSuffix() + { + String query = "SELECT * FROM tas:folder where cmis:name LIKE 't%tf'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testtttf", "testf")); + } + + @Test + public void testFolderNameUnderscore() + { + String query = "SELECT * FROM tas:folder where cmis:name LIKE 't__tf'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testf")); + } + + @Test + public void testFileNameOrderAsc() + { + String query = "SELECT * FROM tas:document where cmis:name LIKE '%testc%' ORDER BY cmis:name ASC"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningOrderedValues("cmis:name", + List.of("testc1.txt", "testc2.txt", "testc3.txt", "testc.txt")); + } + + @Test + public void testFileNameOrderDesc() + { + String query = "SELECT * FROM tas:document where cmis:name LIKE '%testc%' ORDER BY cmis:name DESC"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningOrderedValues("cmis:name", + List.of("testc.txt", "testc3.txt", "testc2.txt", "testc1.txt")); + } + + @Test + public void testFileOrderNameDescDateAsc() + { + String query = "SELECT * FROM tas:document where cmis:name LIKE '%testc%' ORDER BY cmis:name DESC, cmis:creationDate ASC"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningOrderedValues("cmis:name", + List.of("testc.txt", "testc3.txt", "testc2.txt", "testc1.txt")); + } + + @Test + public void testFolderNameOrderAsc() + { + String query = "SELECT * FROM tas:folder where cmis:name LIKE '%testf%' ORDER BY cmis:name ASC"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningOrderedValues("cmis:name", + List.of("testf", "testf1", "testf2", "testf3", "testf4")); + } + + @Test + public void testFolderNameOrderDesc() + { + String query = "SELECT * FROM tas:folder where cmis:name LIKE '%testf%' ORDER BY cmis:name DESC"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningOrderedValues("cmis:name", + List.of("testf4", "testf3", "testf2", "testf1", "testf")); + } + + @Test + public void testFolderOrderNameDescDateAsc() + { + String query = "SELECT * FROM tas:folder where cmis:name LIKE '%testf%' ORDER BY cmis:name DESC, cmis:creationDate ASC"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningOrderedValues("cmis:name", + List.of("testf4", "testf3", "testf2", "testf1", "testf")); + } + + @Test + public void testFileCustomPropertyEquality() + { + String query = "SELECT * FROM tas:document where tas:TextPropertyC = 'restc text'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("restc.txt")); + } + + @Test + public void testFileCustomPropertyInequality() + { + String query = "SELECT * FROM tas:document where tas:TextPropertyC <> 'testc1 text'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestc.txt", "restc.txt", "tesc.txt", "testtttc.txt", "testc.txt", "testc2.txt", "testc3.txt")); + } + + @Test + public void testFileCustomPropertyIn() + { + String query = "SELECT * FROM tas:document where tas:TextPropertyC IN('restc text', 'testc2 text', 'testc3 text')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("restc.txt", "testc2.txt", "testc3.txt")); + } + + + @Test + public void testFileCustomPropertyNotIn() + { + String query = "SELECT * FROM tas:document where tas:TextPropertyC NOT IN('restc text', 'testc2 text', 'testc3 text')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestc.txt", "tesc.txt", "testtttc.txt", "testc.txt", "testc1.txt")); + } + + @Test + public void testFileCustomPropertyLike() + { + String query = "SELECT * FROM tas:document where tas:TextPropertyC LIKE '%restc%'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("restc.txt")); + } + + @Test + public void testFileCustomPropertyUnderscore() + { + String query = "SELECT * FROM tas:document where tas:TextPropertyC LIKE 't__tc text'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testc.txt")); + } + + @Test + public void testFolderCustomPropertyEquality() + { + String query = "SELECT * FROM tas:folder where tas:TextPropertyF = 'restf text'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("restf")); + } + + @Test + public void testFolderCustomPropertyInequality() + { + String query = "SELECT * FROM tas:folder where tas:TextPropertyF <> 'testf1 text'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestf", "tesf", "restf", "testtttf", "testf", "testf2", "testf3", "testf4")); + } + + @Test + public void testFolderCustomPropertyIn() + { + String query = "SELECT * FROM tas:folder where tas:TextPropertyF IN('restf text', 'testf2 text', 'testf3 text')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("restf", "testf2", "testf3")); + } + + @Test + public void testFolderCustomPropertyNotIn() + { + String query = "SELECT * FROM tas:folder where tas:TextPropertyF NOT IN('restf text', 'testf2 text', 'testf3 text')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestf", "tesf", "testtttf", "testf", "testf1", "testf4")); + } + + @Test + public void testFolderCustomPropertyLike() + { + String query = "SELECT * FROM tas:folder where tas:TextPropertyF LIKE '%restf%'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("restf")); + } + + @Test + public void testFolderCustomPropertyUnderscore() + { + String query = "SELECT * FROM tas:folder where tas:TextPropertyF LIKE 't__tf text'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testf")); + } + + @Test + public void testFileIntEquality() + { + String query = "SELECT * FROM tas:document where tas:IntPropertyC = '222'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestc.txt")); + } + + @Test + public void testFileIntInequality() + { + String query = "SELECT * FROM tas:document where tas:IntPropertyC <> '223'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestc.txt", "tesc.txt", "testtttc.txt", "testc.txt", "testc1.txt", "testc2.txt", "testc3.txt")); + } + + @Test + public void testFileIntLessThen() + { + String query = "SELECT * FROM tas:document where tas:IntPropertyC < '223'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestc.txt")); + } + + @Test + public void testFileIntLessThanOrEqual() + { + String query = "SELECT * FROM tas:document where tas:IntPropertyC <= '224'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestc.txt", "restc.txt", "tesc.txt")); + } + + @Test + public void testFileIntGreaterThanOrEqual() + { + String query = "SELECT * FROM tas:document where tas:IntPropertyC >= '224'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("tesc.txt", "testtttc.txt", "testc.txt", "testc1.txt", "testc2.txt", "testc3.txt")); + } + + @Test + public void testFileIntGreaterThan() + { + String query = "SELECT * FROM tas:document where tas:IntPropertyC > '224'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testtttc.txt", "testc.txt", "testc1.txt", "testc2.txt", "testc3.txt")); + } + + @Test + public void testFileIntIn() + { + String query = "SELECT * FROM tas:document where tas:IntPropertyC IN('222', '223', '224', '225')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestc.txt", "restc.txt", "tesc.txt", "testtttc.txt")); + } + + @Test + public void testFileIntNotIn() + { + String query = "SELECT * FROM tas:document where tas:IntPropertyC NOT IN('222', '223', '224')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testtttc.txt", "testc.txt", "testc1.txt", "testc2.txt", "testc3.txt")); + } + + @Test + public void testFolderIntEquality() + { + String query = "SELECT * FROM tas:folder where tas:IntPropertyF = '222'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestf")); + } + + @Test + public void testFolderIntInequality() + { + String query = "SELECT * FROM tas:folder where tas:IntPropertyF <> '223'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestf", "tesf", "testtttf", "testf", "testf1", "testf2", "testf3", "testf4")); + } + + @Test + public void testFolderIntLessThan() + { + String query = "SELECT * FROM tas:folder where tas:IntPropertyF < '223'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestf")); + } + + @Test + public void testFolderIntLessThanOrEqual() + { + String query = "SELECT * FROM tas:folder where tas:IntPropertyF <= '224'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestf", "tesf", "restf")); + } + + @Test + public void testFolderIntGreaterThanOrEqual() + { + String query = "SELECT * FROM tas:folder where tas:IntPropertyF >= '224'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("tesf", "testtttf", "testf", "testf1", "testf2", "testf3", "testf4")); + } + + @Test + public void testFolderIntGreaterThan() + { + String query = "SELECT * FROM tas:folder where tas:IntPropertyF > '224'"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testtttf", "testf", "testf1", "testf2", "testf3", "testf4")); + } + + @Test + public void testFolderIntIn() + { + String query = "SELECT * FROM tas:folder where tas:IntPropertyF IN('222', '223', '224', '225')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("guestf", "tesf", "restf", "testtttf")); + } + + @Test + public void testFolderIntNotIn() + { + String query = "SELECT * FROM tas:folder where tas:IntPropertyF NOT IN('222', '223', '224')"; + cmisApi.authenticateUser(testUser).withQuery(query).assertValues().isReturningValues("cmis:name", + Set.of("testtttf", "testf", "testf1", "testf2", "testf3", "testf4")); } } diff --git a/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/FacetRangeSearchTest.java b/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/FacetRangeSearchTest.java index 4dfc0c2b9..5b52698a2 100644 --- a/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/FacetRangeSearchTest.java +++ b/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/FacetRangeSearchTest.java @@ -22,9 +22,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNull; -import static org.testng.Assert.assertTrue; -import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -66,8 +64,8 @@ import org.testng.annotations.Test; * "gap": "+100DAY" * } * } - * @author Michael Suzuki * + * @author Michael Suzuki */ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest { @@ -78,49 +76,37 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest waitForContentIndexing(file4.getContent(), true); } + /** Check the error messages mention the mandatory fields when they are omitted. */ @Test @TestRail(section = {TestGroup.REST_API, TestGroup.SEARCH }, executionType = ExecutionType.REGRESSION, description = "Check facet intervals mandatory fields") public void checkingFacetsMandatoryErrorMessages() { SearchRequest query = createQuery("cars"); - List ranges = new ArrayList<>(); - RestRequestRangesModel facetRangeModel = new RestRequestRangesModel(); - ranges.add(facetRangeModel); - query.setRanges(ranges); + + // Omit the field. + query.setRanges(List.of(createRangesModel(null, "0", "400", "20"))); query(query); - restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() - .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "field")); - ranges.clear(); - facetRangeModel.setField("content.size"); - ranges.add(facetRangeModel); - query.setRanges(ranges); + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "field")); + // Omit the start. + query.setRanges(List.of(createRangesModel("content.size", null, "400", "20"))); query(query); - restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() - .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "start")); - facetRangeModel.setStart("0"); - ranges.clear(); - ranges.add(facetRangeModel); - query.setRanges(ranges); + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "start")); + // Omit the end. + query.setRanges(List.of(createRangesModel("content.size", "0", null, "20"))); query(query); - restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() - .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "end")); - facetRangeModel.setEnd("400"); - query.setRanges(ranges); - ranges.clear(); - ranges.add(facetRangeModel); + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "end")); + // Omit the gap. + query.setRanges(List.of(createRangesModel("content.size", "0", "400", null))); query(query); - restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() - .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "gap")); - - facetRangeModel.setGap("100"); + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "gap")); } @Test @@ -131,13 +117,8 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest { SearchRequest query = createQuery("* AND SITE:'" + testSite.getId() + "'"); - RestRequestRangesModel facetRangeModel = new RestRequestRangesModel(); - facetRangeModel.setField("content.size"); - facetRangeModel.setStart("0"); - facetRangeModel.setEnd("200"); - facetRangeModel.setGap("20"); - List ranges = new ArrayList<>(); - ranges.add(facetRangeModel); + RestRequestRangesModel facetRangeModel = createRangesModel("content.size", "0", "200", "20"); + List ranges = List.of(facetRangeModel); query.setRanges(ranges); SearchResponse response = query(query); response.assertThat().entriesListIsNotEmpty(); @@ -147,14 +128,14 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest RestGenericBucketModel bucket = facetResponseModel.getBuckets().get(0); bucket.assertThat().field("label").is("[20 - 40)"); bucket.assertThat().field("filterQuery").is("content.size:[\"20\" TO \"40\">"); - Map metric = (Map) bucket.getMetrics().get(0).getValue(); + Map metric = (Map) bucket.getMetrics().get(0).getValue(); assertEquals(Integer.valueOf(metric.get("count")).intValue(), 2, "Unexpected count for first bucket."); Map info = (Map) bucket.getBucketInfo(); - assertEquals(info.get("start"),"20"); - assertEquals(info.get("end"),"40"); + assertEquals(info.get("start"), "20"); + assertEquals(info.get("end"), "40"); assertNull(info.get("count")); - assertEquals(info.get("startInclusive"),"true"); - assertEquals(info.get("endInclusive"),"false"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "false"); bucket = facetResponseModel.getBuckets().get(1); bucket.assertThat().field("label").is("[40 - 120)"); @@ -162,20 +143,20 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest metric = (Map) bucket.getMetrics().get(0).getValue(); assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for second bucket."); info = (Map) bucket.getBucketInfo(); - assertEquals(info.get("start"),"40"); - assertEquals(info.get("end"),"120"); - assertEquals(info.get("startInclusive"),"true"); - assertEquals(info.get("endInclusive"),"false"); + assertEquals(info.get("start"), "40"); + assertEquals(info.get("end"), "120"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "false"); bucket = facetResponseModel.getBuckets().get(2); bucket.assertThat().field("label").is("[120 - 200]"); bucket.assertThat().field("filterQuery").is("content.size:[\"120\" TO \"200\"]"); assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for third bucket."); info = (Map) bucket.getBucketInfo(); - assertEquals(info.get("start"),"120"); - assertEquals(info.get("end"),"200"); - assertEquals(info.get("startInclusive"),"true"); - assertEquals(info.get("endInclusive"),"true"); + assertEquals(info.get("start"), "120"); + assertEquals(info.get("end"), "200"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "true"); } @Test @@ -186,14 +167,9 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest { SearchRequest query = createQuery("* AND SITE:'" + testSite.getId() + "'"); - RestRequestRangesModel facetRangeModel = new RestRequestRangesModel(); - facetRangeModel.setField("content.size"); - facetRangeModel.setStart("0"); - facetRangeModel.setEnd("200"); - facetRangeModel.setGap("20"); + RestRequestRangesModel facetRangeModel = createRangesModel("content.size", "0", "200", "20"); facetRangeModel.setHardend(true); - List ranges = new ArrayList<>(); - ranges.add(facetRangeModel); + List ranges = List.of(facetRangeModel); query.setRanges(ranges); SearchResponse response = query(query); response.assertThat().entriesListIsNotEmpty(); @@ -203,26 +179,26 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest RestGenericBucketModel bucket = facetResponseModel.getBuckets().get(0); bucket.assertThat().field("label").is("[20 - 40)"); bucket.assertThat().field("filterQuery").is("content.size:[\"20\" TO \"40\">"); - Map metric = (Map) bucket.getMetrics().get(0).getValue(); + Map metric = (Map) bucket.getMetrics().get(0).getValue(); assertEquals(Integer.valueOf(metric.get("count")).intValue(), 2, "Unexpected count for first bucket."); Map info = (Map) bucket.getBucketInfo(); - assertEquals(info.get("start"),"20"); - assertEquals(info.get("end"),"40"); - assertEquals(info.get("startInclusive"),"true"); - assertEquals(info.get("endInclusive"),"false"); + assertEquals(info.get("start"), "20"); + assertEquals(info.get("end"), "40"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "false"); assertNull(info.get("count")); bucket = facetResponseModel.getBuckets().get(1); bucket.assertThat().field("label").is("[40 - 120)"); bucket.assertThat().field("filterQuery").is("content.size:[\"40\" TO \"120\">"); info = (Map) bucket.getBucketInfo(); - assertEquals(info.get("start"),"40"); - assertEquals(info.get("end"),"120"); + assertEquals(info.get("start"), "40"); + assertEquals(info.get("end"), "120"); metric = (Map) bucket.getMetrics().get(0).getValue(); assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for second bucket."); assertNull(info.get("count")); - assertEquals(info.get("startInclusive"),"true"); - assertEquals(info.get("endInclusive"),"false"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "false"); bucket = facetResponseModel.getBuckets().get(2); bucket.assertThat().field("label").is("[120 - 200]"); @@ -230,11 +206,11 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest metric = (Map) bucket.getMetrics().get(0).getValue(); assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for third bucket."); info = (Map) bucket.getBucketInfo(); - assertEquals(info.get("start"),"120"); - assertEquals(info.get("end"),"200"); + assertEquals(info.get("start"), "120"); + assertEquals(info.get("end"), "200"); assertNull(info.get("count")); - assertEquals(info.get("startInclusive"),"true"); - assertEquals(info.get("endInclusive"),"true"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "true"); } /** This test relies on a document created in 2015 existing, probably part of the sample site. */ @@ -246,13 +222,8 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest { SearchRequest query = createQuery("name:A*"); - RestRequestRangesModel facetRangeModel = new RestRequestRangesModel(); - facetRangeModel.setField("created"); - facetRangeModel.setStart("2015-09-29T10:45:15.729Z"); - facetRangeModel.setEnd("2016-09-29T10:45:15.729Z"); - facetRangeModel.setGap("+280DAY"); - List ranges = new ArrayList<>(); - ranges.add(facetRangeModel); + RestRequestRangesModel facetRangeModel = createRangesModel("created", "2015-09-29T10:45:15.729Z", "2016-09-29T10:45:15.729Z", "+280DAY"); + List ranges = List.of(facetRangeModel); query.setRanges(ranges); SearchResponse response = query(query); response.assertThat().entriesListIsNotEmpty(); @@ -260,18 +231,18 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().get(0); List buckets = facetResponseModel.getBuckets(); - assertThat(buckets.size(),is(1)); + assertThat(buckets.size(), is(1)); RestGenericBucketModel bucket = buckets.get(0); bucket.assertThat().field("label").is("[2015-09-29T10:45:15.729Z - 2017-04-11T10:45:15.729Z]"); bucket.assertThat().field("filterQuery").is("created:[\"2015-09-29T10:45:15.729Z\" TO \"2017-04-11T10:45:15.729Z\"]"); bucket.getMetrics().get(0).assertThat().field("value").is("{count=1}"); Map info = (Map) bucket.getBucketInfo(); - assertEquals(info.get("start"),"2015-09-29T10:45:15.729Z"); - assertEquals(info.get("end"),"2017-04-11T10:45:15.729Z"); - assertNull(info.get("count"),"1"); - assertEquals(info.get("startInclusive"),"true"); - assertEquals(info.get("endInclusive"),"true"); + assertEquals(info.get("start"), "2015-09-29T10:45:15.729Z"); + assertEquals(info.get("end"), "2017-04-11T10:45:15.729Z"); + assertNull(info.get("count"), "1"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "true"); } @Test @@ -280,19 +251,9 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest public void searchDateAndSizeRanges() { SearchRequest query = createQuery("* AND SITE:'" + testSite.getId() + "'"); - List ranges = new ArrayList<>(); - RestRequestRangesModel facetRangeModel = new RestRequestRangesModel(); - facetRangeModel.setField("created"); - facetRangeModel.setStart("2015-09-29T10:45:15.729Z"); - facetRangeModel.setEnd("2016-09-29T10:45:15.729Z"); - facetRangeModel.setGap("+280DAY"); - ranges.add(facetRangeModel); - RestRequestRangesModel facetCountRangeModel = new RestRequestRangesModel(); - facetCountRangeModel.setField("content.size"); - facetCountRangeModel.setStart("0"); - facetCountRangeModel.setEnd("500"); - facetCountRangeModel.setGap("200"); - ranges.add(facetCountRangeModel); + RestRequestRangesModel facetRangeModel = createRangesModel("created", "2015-09-29T10:45:15.729Z", "2016-09-29T10:45:15.729Z", "+280DAY"); + RestRequestRangesModel facetCountRangeModel = createRangesModel("content.size", "0", "500", "200"); + List ranges = List.of(facetRangeModel, facetCountRangeModel); query.setRanges(ranges); } @@ -304,16 +265,10 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest { SearchRequest query = createQuery("* AND SITE:'" + testSite.getId() + "'"); - RestRequestRangesModel facetRangeModel = new RestRequestRangesModel(); - facetRangeModel.setField("content.size"); - facetRangeModel.setStart("0"); - facetRangeModel.setEnd("200"); - facetRangeModel.setGap("20"); - List include = new ArrayList<>(); - include.add("upper"); + RestRequestRangesModel facetRangeModel = createRangesModel("content.size", "0", "200", "20"); + List include = List.of("upper"); facetRangeModel.setInclude(include); - List ranges = new ArrayList<>(); - ranges.add(facetRangeModel); + List ranges = List.of(facetRangeModel); query.setRanges(ranges); SearchResponse response = query(query); response.assertThat().entriesListIsNotEmpty(); @@ -323,14 +278,14 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest RestGenericBucketModel bucket = facetResponseModel.getBuckets().get(0); bucket.assertThat().field("label").is("(20 - 40]"); bucket.assertThat().field("filterQuery").is("content.size:<\"20\" TO \"40\"]"); - Map metric = (Map) bucket.getMetrics().get(0).getValue(); + Map metric = (Map) bucket.getMetrics().get(0).getValue(); assertEquals(Integer.valueOf(metric.get("count")).intValue(), 2, "Unexpected count for first bucket."); Map info = (Map) bucket.getBucketInfo(); - assertEquals(info.get("start"),"20"); - assertEquals(info.get("end"),"40"); + assertEquals(info.get("start"), "20"); + assertEquals(info.get("end"), "40"); assertNull(info.get("count")); - assertEquals(info.get("startInclusive"),"false"); - assertEquals(info.get("endInclusive"),"true"); + assertEquals(info.get("startInclusive"), "false"); + assertEquals(info.get("endInclusive"), "true"); bucket = facetResponseModel.getBuckets().get(1); bucket.assertThat().field("label").is("(40 - 120]"); @@ -338,10 +293,10 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest metric = (Map) bucket.getMetrics().get(0).getValue(); assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for second bucket."); info = (Map) bucket.getBucketInfo(); - assertEquals(info.get("start"),"40"); - assertEquals(info.get("end"),"120"); - assertEquals(info.get("startInclusive"),"false"); - assertEquals(info.get("endInclusive"),"true"); + assertEquals(info.get("start"), "40"); + assertEquals(info.get("end"), "120"); + assertEquals(info.get("startInclusive"), "false"); + assertEquals(info.get("endInclusive"), "true"); bucket = facetResponseModel.getBuckets().get(2); bucket.assertThat().field("label").is("(120 - 200]"); @@ -349,9 +304,28 @@ public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest metric = (Map) bucket.getMetrics().get(0).getValue(); assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for third bucket."); info = (Map) bucket.getBucketInfo(); - assertEquals(info.get("start"),"120"); - assertEquals(info.get("end"),"200"); - assertEquals(info.get("startInclusive"),"false"); - assertEquals(info.get("endInclusive"),"true"); - } + assertEquals(info.get("start"), "120"); + assertEquals(info.get("end"), "200"); + assertEquals(info.get("startInclusive"), "false"); + assertEquals(info.get("endInclusive"), "true"); + } + + /** + * Create a ranges model with the values given. + * + * @param field The field to facet on. + * @param start The lowest facet value. + * @param end The highest facet value. + * @param gap The size of the buckets. + * @return The facet ranges model. + */ + private RestRequestRangesModel createRangesModel(String field, String start, String end, String gap) + { + RestRequestRangesModel facetRangeModel = new RestRequestRangesModel(); + facetRangeModel.setField(field); + facetRangeModel.setStart(start); + facetRangeModel.setEnd(end); + facetRangeModel.setGap(gap); + return facetRangeModel; + } } \ No newline at end of file diff --git a/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/SearchAFTSInFieldTest.java b/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/SearchAFTSInFieldTest.java new file mode 100644 index 000000000..987f141d1 --- /dev/null +++ b/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/SearchAFTSInFieldTest.java @@ -0,0 +1,326 @@ +/* + * Copyright 2019 Alfresco Software, Ltd. All rights reserved. + * License rights for this program may be obtained from Alfresco Software, Ltd. + * pursuant to a written agreement and any use of this program without such an + * agreement is prohibited. + */ + +package org.alfresco.test.search.functional.searchServices.search; + +import static java.util.List.of; + +import static jersey.repackaged.com.google.common.collect.Sets.newHashSet; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.alfresco.rest.model.RestNodeAssociationModelCollection; +import org.alfresco.rest.model.RestNodeChildAssociationModel; +import org.alfresco.rest.search.SearchResponse; +import org.alfresco.search.TestGroup; +import org.alfresco.test.search.functional.AbstractE2EFunctionalTest; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Test class tests AFTS Search In Field works + * Created for Search-840 + * + * @author Meenal Bhave + */ +public class SearchAFTSInFieldTest extends AbstractE2EFunctionalTest +{ + private FolderModel folder1, folder2; + private FileModel file1, file2, file3, file4, file5; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + // Create Folders: + // Folder1: Expected to be found with file.txt + folder1 = new FolderModel("file txt folder"); + dataContent.usingUser(testUser).usingSite(testSite).createFolder(folder1); + + // Folder2: Not expected to be found with file.txt + folder2 = new FolderModel("txt files folder"); + dataContent.usingUser(testUser).usingSite(testSite).createFolder(folder2); + + // Create File(s): Expected to be found with file.txt + file1 = new FileModel("file.txt", "file.txt", "", FileType.TEXT_PLAIN, "file.txt"); + + file2 = new FileModel("1-file.txt", "1-file.txt", "", FileType.TEXT_PLAIN, "1-file.txt"); + + file3 = new FileModel("file1.txt", "file1.txt", "", FileType.TEXT_PLAIN, "file1.txt"); + + file4 = new FileModel("txt file", "txt file", "", FileType.TEXT_PLAIN, "txt file"); + + // Not Expected to be found with file.txt + file5 = new FileModel("txt files", "txt files", "", FileType.TEXT_PLAIN, "txt files"); + + of(file1, file2, file3, file4, file5).forEach( + f -> dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder1).createContent(f)); + + waitForContentIndexing(file5.getContent(), true); + } + + @Test(priority = 1, groups = { TestGroup.ACS_63n }) + public void testSearchInFieldName() + { + // Field names in various formats + Stream fieldNames = Stream.of("{http://www.alfresco.org/model/content/1.0}name", + "@{http://www.alfresco.org/model/content/1.0}name", + "cm_name", + "cm:name", + "@cm:name", + "name"); + + // For each field name, check that queries return consistent results with / out '' + fieldNames.forEach(fieldName -> + { + // Query string without quotes + String query = fieldName + ":file.txt"; + + Set expectedNames = newHashSet(); + expectedNames.add("file.txt"); // file1 + expectedNames.add("1-file.txt"); // file2 + expectedNames.add("file1.txt"); // file3 + expectedNames.add("txt file"); // file4 + expectedNames.add("file txt folder"); // folder1 + + testSearchQueryUnordered(query, expectedNames, SearchLanguage.AFTS); + + // Query string in single quotes + query = fieldName + ":'file.txt'"; + testSearchQueryUnordered(query, expectedNames, SearchLanguage.AFTS); + }); + } + + @Test(priority = 2, groups = { TestGroup.ACS_63n }) + public void testSearchInFieldTitle() + { + // Field names in various formats + Stream fieldNames = Stream.of("{http://www.alfresco.org/model/content/1.0}title", + "@{http://www.alfresco.org/model/content/1.0}title", + "cm_title", + "cm:title", + "@cm:title"); + + // For each field name, check that queries return consistent results with / out '' + fieldNames.forEach(fieldName -> + { + String query = fieldName + ":" + file2.getName(); + boolean fileFound = isContentInSearchResults(query, file2.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + testSearchQuery(query, 1, SearchLanguage.AFTS); + + query = fieldName + ":'" + file2.getName() + "\'"; + fileFound = isContentInSearchResults(query, file2.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + testSearchQuery(query, 1, SearchLanguage.AFTS); + }); + } + + @Test(priority = 3, groups = { TestGroup.ACS_63n }) + public void testSearchInFieldContent() + { + // Field names in various formats + List fieldNames = new ArrayList<>(); + fieldNames.add("TEXT"); + fieldNames.add("{http://www.alfresco.org/model/dictionary/1.0}content"); + fieldNames.add("cm:content"); + fieldNames.add("d:content"); + + // For each field name, check that queries return consistent results with / out '' + fieldNames.forEach(fieldName -> + { + String query = fieldName + ":" + file3.getContent(); + boolean fileFound = isContentInSearchResults(query, file3.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + Integer resultCount1 = testSearchQuery(query, null, SearchLanguage.AFTS).getPagination().getTotalItems(); + + query = fieldName + ":'" + file3.getContent() + "\'"; + fileFound = isContentInSearchResults(query, file3.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + testSearchQuery(query, resultCount1, SearchLanguage.AFTS).getPagination().getTotalItems(); + }); + } + + @Test(priority = 4) + public void testSearchInFieldSITE() + { + String query = "SITE:" + testSite.getId(); + boolean fileFound = isContentInSearchResults(query, folder1.getName(), true); + Assert.assertTrue(fileFound, "Site Not found for query: " + query); + + Integer resultCount1 = testSearchQuery(query, 8, SearchLanguage.AFTS).getPagination().getTotalItems(); + + query = "SITE:'" + testSite.getId() + "\'"; + fileFound = isContentInSearchResults(query, folder1.getName(), true); + Assert.assertTrue(fileFound, "Site Not found for query: " + query); + + testSearchQuery(query, resultCount1, SearchLanguage.AFTS).getPagination().getTotalItems(); + } + + @Test(priority = 5) + public void testSearchInFieldTYPE() + { + // Field names in various formats + List fieldNames = new ArrayList<>(); + fieldNames.add("TYPE"); + fieldNames.add("EXACTTYPE"); + + // For each field name, check that queries return consistent results with / out '' + fieldNames.forEach(fieldName -> { + + String query = fieldName + ":cm\\:content" + " and =cm:name:" + file1.getName(); + boolean fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "Content Not found for query: " + query); + + Integer resultCount1 = testSearchQuery(query, 1, SearchLanguage.AFTS).getPagination().getTotalItems(); + + query = fieldName + ":'cm:content'" + " and =cm:name:" + file1.getName(); + fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "Content Not found for query: " + query); + + testSearchQuery(query, resultCount1, SearchLanguage.AFTS).getPagination().getTotalItems(); + }); + } + + @Test(priority = 6) + public void testSearchInFieldID() throws Exception + { + String query = "ID:'workspace://SpacesStore/" + file1.getNodeRefWithoutVersion() + "'"; + boolean fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "Content Not found for query: " + query); + + testSearchQuery(query, 1, SearchLanguage.AFTS).getPagination().getTotalItems(); + } + + @Test(priority = 7) + public void testSearchInFieldPARENT() + { + String query = "PARENT:" + folder1.getNodeRefWithoutVersion(); + + Set expectedNames = newHashSet(); + expectedNames.add(file1.getName()); + expectedNames.add(file2.getName()); + expectedNames.add(file3.getName()); + expectedNames.add(file4.getName()); + expectedNames.add(file5.getName()); + + testSearchQueryUnordered(query, expectedNames, SearchLanguage.AFTS); + + query = "PARENT:'" + folder1.getNodeRefWithoutVersion() + "\'"; + + testSearchQueryUnordered(query, expectedNames, SearchLanguage.AFTS); + } + + @Test(priority = 8) + public void testSearchInFieldPRIMARYPARENT() throws Exception + { + // Create Secondary association in testFolder2 + RestNodeChildAssociationModel childAssoc1 = new RestNodeChildAssociationModel(file1.getNodeRefWithoutVersion(), "cm:contains"); + String secondaryChildrenBody = "[" + childAssoc1.toJson() + "]"; + + restClient.authenticateUser(testUser).withCoreAPI().usingResource(folder2).createSecondaryChildren(secondaryChildrenBody); + RestNodeAssociationModelCollection secondaryChildren = restClient.authenticateUser(testUser).withCoreAPI().usingResource(folder2).getSecondaryChildren(); + secondaryChildren.getEntryByIndex(0).assertThat().field("id").is(file1.getNodeRefWithoutVersion()); + + String query = "PRIMARYPARENT:'workspace://SpacesStore/" + folder1.getNodeRef() + "'"; + + Set expectedNames = newHashSet(); + expectedNames.add(file1.getName()); + expectedNames.add(file2.getName()); + expectedNames.add(file3.getName()); + expectedNames.add(file4.getName()); + expectedNames.add(file5.getName()); + + testSearchQueryUnordered(query, expectedNames, SearchLanguage.AFTS); + + query = "PARENT:'workspace://SpacesStore/" + folder2.getNodeRef() + "'"; + boolean fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "Expected Content Not found for query: " + query); + + testSearchQuery(query, 1, SearchLanguage.AFTS); + } + + @Test(priority = 9, groups = { TestGroup.ACS_63n }) + public void testSearchInFieldNameExactMatch() + { + // Check that queries return consistent results with / out '' + String query = "=name:" + file1.getName(); + boolean fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + Integer resultCount1 = testSearchQuery(query, 1, SearchLanguage.AFTS).getPagination().getTotalItems(); + Assert.assertSame(resultCount1, 1, "File count does not match for query: " + query); + + query = "=name:'" + file1.getName() + "\'"; + fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + testSearchQuery(query, resultCount1, SearchLanguage.AFTS).getPagination().getTotalItems(); + } + + @Test(priority = 10, groups = { TestGroup.ACS_63n }) + public void testSearchInFieldNameQueryExpansion() + { + // Check that queries return consistent results with / out '' + String query = "~name:" + file1.getName(); + + Set expectedNames = newHashSet(); + expectedNames.add(file1.getName()); + expectedNames.add(file2.getName()); + expectedNames.add(file3.getName()); + expectedNames.add(file4.getName()); + expectedNames.add(folder1.getName()); + + testSearchQueryUnordered(query, expectedNames, SearchLanguage.AFTS); + + query = "~name:'" + file1.getName() + "\'"; + testSearchQueryUnordered(query, expectedNames, SearchLanguage.AFTS); + } + + @Test(priority = 11, groups = { TestGroup.ACS_63n }) + public void testWithConjunctionDisjunctionAndNegation() + { + // Query string to include Conjunction, Disjunction and Negation + + String query1 = "~name:" + file1.getName(); // Query expected to return 5 results + String query2 = "=name:" + file2.getName(); // Query expected to return 1 result + String query3 = "=name:" + file3.getName(); // Query expected to return 1 result + + // Check Query with Conjunction, Negation, Disjunction: returns right results + // "~name:file.txt and ! (=name:1-file.txt or =name:file1.txt)" + String query = query1 + " and ! (" + query2 + " or " + query3 + ")"; + + // Check that expected files are included in the results + Set expectedNames = newHashSet(); + expectedNames.add("file.txt"); // file1 + expectedNames.add("txt file"); // file4 + expectedNames.add("file txt folder"); // folder1 + + SearchResponse response = testSearchQueryUnordered(query, expectedNames, SearchLanguage.AFTS); + + // Check result count is 5-(1+1)=3 + int resultCount = response.getPagination().getTotalItems(); + Assert.assertEquals(resultCount, 3, "File count does not match for query: " + query); + + // Check that file2 and file3 are excluded from the results + boolean fileFound = isContentInSearchResponse(response, file2.getName()); + Assert.assertFalse(fileFound, "File2 found for query: " + query); + + fileFound = isContentInSearchResponse(response, file3.getName()); + Assert.assertFalse(fileFound, "File3 found for query: " + query); + } +} diff --git a/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/SearchSecondaryAssociationTest.java b/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/SearchSecondaryAssociationTest.java new file mode 100644 index 000000000..b382bc6f3 --- /dev/null +++ b/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/SearchSecondaryAssociationTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019 Alfresco Software, Ltd. All rights reserved. + * License rights for this program may be obtained from Alfresco Software, Ltd. + * pursuant to a written agreement and any use of this program without such an + * agreement is prohibited. + */ + +package org.alfresco.test.search.functional.searchServices.search; + +import org.alfresco.rest.model.RestNodeAssociationModelCollection; +import org.alfresco.rest.model.RestNodeChildAssociationModel; +import org.alfresco.test.search.functional.AbstractE2EFunctionalTest; +import org.alfresco.utility.data.CustomObjectTypeProperties; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Test class tests content in the secondary parent is found too + * Created for Search-1313 + * + * @author Meenal Bhave + */ +public class SearchSecondaryAssociationTest extends AbstractE2EFunctionalTest +{ + private FolderModel testFolder1, testFolder2; + private FileModel file1; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + // Folders + testFolder1 = new FolderModel("folder1"); + testFolder2 = new FolderModel("folder2"); + + // File(s) + file1 = new FileModel("file1.txt"); + file1.setContent("content file 1"); + + // Create folder1 + dataContent.usingUser(testUser).usingSite(testSite).createCustomContent(testFolder1, "cmis:folder", new CustomObjectTypeProperties()); + + // Create file1 + dataContent.usingUser(testUser).usingResource(testFolder1).createCustomContent(file1, "cmis:document", new CustomObjectTypeProperties()); + + // Create folder2 + dataContent.usingUser(testUser).usingSite(testSite).createCustomContent(testFolder2, "cmis:folder", new CustomObjectTypeProperties()); + + // wait for solr index + waitForMetadataIndexing(file1.getName(), true); + } + + @Test(priority = 1) + public void testSearchPathForSecondaryAssociation() throws Exception + { + String queryPathFolder1 = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + testFolder1.getName() + "/cm:" + file1.getName() + "\""; + + // Test if file can be found in folder1: Primary Parent + boolean found = isContentInSearchResults(queryPathFolder1, file1.getName(), true); + Assert.assertTrue(found, "File Not found using Primary Parent Path"); + + String queryPathFolder2 = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + testFolder2.getName() + "/cm:" + file1.getName() + "\""; + + // Test if file can not be found in folder2 + found = isContentInSearchResults(queryPathFolder2, file1.getName(), false); + Assert.assertTrue(found, "File found using Secondary Parent Path"); + + // Create Secondary association in folder2 + RestNodeChildAssociationModel childAssoc1 = new RestNodeChildAssociationModel(file1.getNodeRefWithoutVersion(), "cm:contains"); + String secondaryChildrenBody = "[" + childAssoc1.toJson() + "]"; + + restClient.authenticateUser(testUser).withCoreAPI().usingResource(testFolder2).createSecondaryChildren(secondaryChildrenBody); + RestNodeAssociationModelCollection secondaryChildren = restClient.authenticateUser(testUser).withCoreAPI().usingResource(testFolder2).getSecondaryChildren(); + secondaryChildren.getEntryByIndex(0).assertThat().field("id").is(file1.getNodeRefWithoutVersion()); + + // Test if file can be found in folder2: Secondary Parent + found = isContentInSearchResults(queryPathFolder2, file1.getName(), true); + Assert.assertTrue(found, "File Not found using Secondary Parent Path"); + + // Remove Secondary association + restClient.authenticateUser(testUser).withCoreAPI().usingResource(testFolder2).deleteSecondaryChild(secondaryChildren.getEntryByIndex(0)); + + // Test if file can not be found in folder2 + found = isContentInSearchResults(queryPathFolder2, file1.getName(), false); + Assert.assertTrue(found, "File found using Secondary Parent Path"); + } +} diff --git a/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/SearchSpellCheckTest.java b/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/SearchSpellCheckTest.java index 0f5678065..5afb9fce9 100644 --- a/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/SearchSpellCheckTest.java +++ b/e2e-test/src/test/java/org/alfresco/test/search/functional/searchServices/search/SearchSpellCheckTest.java @@ -18,12 +18,16 @@ */ package org.alfresco.test.search.functional.searchServices.search; +import org.alfresco.dataprep.SiteService.Visibility; import org.alfresco.rest.model.RestRequestSpellcheckModel; import org.alfresco.rest.search.RestRequestQueryModel; import org.alfresco.rest.search.SearchRequest; import org.alfresco.rest.search.SearchResponse; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.data.RandomData; import org.alfresco.utility.model.FileModel; import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.SiteModel; import org.alfresco.search.TestGroup; import org.testng.Assert; import org.testng.annotations.BeforeClass; @@ -141,7 +145,7 @@ public class SearchSpellCheckTest extends AbstractSearchServicesE2ETest assertResponse(query(searchReq)); } - @Test(groups={ TestGroup.ACS_60n}, priority=3) + @Test(groups = { TestGroup.ACS_60n }, priority = 3) public void testSearchWithSpellcheckerAndCorrectSpelling() { SearchRequest searchReq = new SearchRequest(); @@ -154,28 +158,341 @@ public class SearchSpellCheckTest extends AbstractSearchServicesE2ETest Assert.assertNull(res.getContext().getSpellCheck()); res.assertThat().entriesListIsNotEmpty(); } - - @Test(groups={TestGroup.ACS_60n}, priority=4) + + /** + * Test to check the different spellcheck types searchInsteadFor and didYouMean + * Search suggestion is based on maxEdits, count of entries, Alphabetical + * + * @throws Exception + */ + @Test(groups = { TestGroup.ACS_60n }, priority = 4) public void testSpellCheckType() throws Exception { - // Create a file with mis-spelt name, expect spellcheck type = didYouMean - FileModel file = new FileModel(unique_searchString + "-1.txt", "uniquee" + "uniquee", "uniquee", FileType.TEXT_PLAIN, "Unique text file for search "); + // Create a file with word in cm:name only + FileModel file = new FileModel("learning", "", "", FileType.TEXT_PLAIN, ""); dataContent.usingUser(testUser).usingSite(testSite).createContent(file); - waitForIndexing(file.getName(), true); + // Wait for the file to be indexed + Assert.assertTrue(waitForMetadataIndexing(file.getName(), true)); + + // Correct spelling with cm:name field + SearchResponse response = SearchSpellcheckQuery(testUser, "cm:name:learning", "learning"); + + response.assertThat().entriesListIsNotEmpty(); + response.getContext().assertThat().field("spellCheck").isNull(); + + // Matching Result, No Spellcheck object returned + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, null, null); + + // Correct spelling with no specific field + response = SearchSpellcheckQuery(testUser, "learning", "learning"); + + // Matching Result, No Spellcheck object returned + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, null, null); + + // Correct spelling with a different field. Used cm:content field + response = SearchSpellcheckQuery(testUser, "cm:content:learning", "learning"); + + // 0 Results, No Spellcheck object returned + response.assertThat().entriesListIsEmpty(); + testSearchSpellcheckResponse(response, null, null); + + // Incorrect spelling with cm:name field + response = SearchSpellcheckQuery(testUser, "cm:name:lerning", "lerning"); + + // 1 Match with right spelling and Spellcheck = SearchInsteadFor: lerning + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + // TODO: Investigate: Share shows searchInsteadFor = lerning, API shows learning + // testSearchSpellcheckResponse(response, "searchInsteadFor", "lerning"); + + // Incorrect spelling with no field + response = SearchSpellcheckQuery(testUser, "lerning", "lerning"); + + // 1 Match with right spelling and Spellcheck = SearchInsteadFor: lerning + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + // TODO: Investigate: Share shows searchInsteadFor = lerning, API shows learning + // testSearchSpellcheckResponse(response, "searchInsteadFor", "lerning"); + + // Incorrect spelling with a different field. Used cm:content field + response = SearchSpellcheckQuery(testUser, "cm:content:lerning", "lerning"); + + // 0 Results, No Spellcheck object returned + response.assertThat().entriesListIsEmpty(); + response.getContext().assertThat().field("spellCheck").isNull(); + + // Create a file with word with 1 max Edit in cm:name only + FileModel file2 = new FileModel("leaning", "", "", FileType.TEXT_PLAIN, ""); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file2); + + // Create a file with word in cm:name and cm:content only + FileModel file3 = new FileModel("leaning 2", "", "", FileType.TEXT_PLAIN, "leaning 2"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file3); + + Assert.assertTrue(waitForContentIndexing(file3.getContent(), true)); + + // Incorrect spelling with cm:name field + response = SearchSpellcheckQuery(testUser, "cm:name:lerning", "lerning"); + + // Matching Result, Spellcheck = searchInsteadFor: leaning + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file3.getName()), "Expected file not returned in the search results: " + file3.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "leaning"); + + // Incorrect spelling with no field + response = SearchSpellcheckQuery(testUser, "lerning", "lerning"); + + // Matching Result, Spellcheck = searchInsteadFor: leaning + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file3.getName()), "Expected file not returned in the search results: " + file3.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "leaning"); + + // Incorrect spelling with cm:content field + response = SearchSpellcheckQuery(testUser, "cm:content:lerning", "lerning"); + + // Matching Result, Spellcheck = searchInsteadFor: leaning + Assert.assertTrue(isContentInSearchResponse(response, file3.getName()), "Expected file not returned in the search results: " + file3.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "leaning"); + + // Correct spelling with cm:name field + response = SearchSpellcheckQuery(testUser, "cm:name:learning", "learning"); + + // Matching Result, Spellcheck = didYouMean: leaning + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "didYouMean", "leaning"); + + // Correct spelling with no field + response = SearchSpellcheckQuery(testUser, "learning", "learning"); + + // Matching Result, Spellcheck = didYouMean: leaning + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "didYouMean", "leaning"); + + // Correct spelling with cm:content field + response = SearchSpellcheckQuery(testUser, "cm:content:learning", "learning"); + + // Matching Result, Spellcheck = didYouMean: leaning + Assert.assertTrue(isContentInSearchResponse(response, file3.getName()), "Expected file not returned in the search results: " + file3.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "leaning"); + } + + /** + * This is a test for the spellcheck parameters minEdit and maxPrefix + * + * @throws Exception + */ + @Test(groups = { TestGroup.ACS_60n }, priority = 5) + public void testSpellCheckParameters() throws Exception + { + // Create a file with word in cm:name and cm:content + FileModel file = new FileModel("eklipse", "", "", FileType.TEXT_PLAIN, "eklipse"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file); + + // Create a file with word in cm:name, cm:title and cm:content + FileModel file2 = new FileModel("eklipses", "eklipses", "", FileType.TEXT_PLAIN, "eklipses"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file2); + + Assert.assertTrue(waitForContentIndexing(file2.getName(), true)); + + // Search with field not filed in either files + SearchResponse response = SearchSpellcheckQuery(testUser, "cm:description:'eclipse'", "eclipse"); + + // 0 Results, Spellcheck not returned + testSearchSpellcheckResponse(response, null, null); + + // Incorrect spelling with the field on a file as well + response = SearchSpellcheckQuery(testUser, "cm:name:'eclipse'", "eclipse"); + + // Matching Result, Spellcheck = searchInsteadFor: eklipse + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "eklipse"); + + // Incorrect spelling with no field for file1 + response = SearchSpellcheckQuery(testUser, "eclipse", "eclipse"); + + // Matching Result, Spellcheck = searchInsteadFor: eklipse + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "eklipse"); + + // Incorrect spelling with no field for file2 + response = SearchSpellcheckQuery(testUser, "eclipses", "eclipses"); + + // Matching Result, Spellcheck = searchInsteadFor: eklipses + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "eklipses"); + + // Search for the field only filed on file2 and not file1 + response = SearchSpellcheckQuery(testUser, "cm:title:'eclipses'", "eclipses"); + + // Matching Result, Spellcheck = searchInsteadFor: eklipses + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "eklipses"); + + // Query using 3 edits (more than spellcheck works for [maxEdits<=2]) + response = SearchSpellcheckQuery(testUser, "elapssed", "elapssed"); + + // 0 Results, No Spellcheck object returned + testSearchSpellcheckResponse(response, null, null); + + // Query with edit on first letter (does not work with spellcheck [minPrefix=1]) + response = SearchSpellcheckQuery(testUser, "iklipse ", "iklipse "); + + // 0 Results, No Spellcheck object returned + testSearchSpellcheckResponse(response, null, null); + } + + /** + * This is a test to check the fields defined for spellcheck in + * shared.properties work cm:name, cm:title, cm:description and cm:content are + * defined in shared.properties + * + * @throws Exception + */ + @Test(groups = { TestGroup.ACS_60n }, priority = 6) + public void testSpellCheckFields() throws Exception + { + // Create a file with same word in all fields + FileModel file = new FileModel("book", "book", "book", FileType.TEXT_PLAIN, "book"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file); + + Assert.assertTrue(waitForContentIndexing(file.getContent(), true)); + + // Incorrect spelling with no field + SearchResponse response = SearchSpellcheckQuery(testUser, "bo0k", "bo0k"); + + // Matching Result, Spellcheck = searchInsteadFor: book + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "book"); + + // Incorrect spelling with the cm:name field + response = SearchSpellcheckQuery(testUser, "cm:name:'bo0k'", "bo0k"); + + // Matching Result, Spellcheck = searchInsteadFor: book + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "book"); + + // Incorrect spelling with the cm:title field + response = SearchSpellcheckQuery(testUser, "cm:title:'bo0k'", "bo0k"); + + // Matching Result, Spellcheck = searchInsteadFor: book + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "book"); + + // Incorrect spelling with the cm:description field + response = SearchSpellcheckQuery(testUser, "cm:description:'bo0k'", "bo0k"); + + // Matching Result, Spellcheck = searchInsteadFor: book + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "book"); + + // Incorrect spelling with the cm:description field + response = SearchSpellcheckQuery(testUser, "cm:content:'bo0k'", "bo0k"); + + // Matching Result, Spellcheck = searchInsteadFor: book + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "book"); + + // Incorrect spelling with the cm:author field (not suggestable field for Spellcheck + response = SearchSpellcheckQuery(testUser, "cm:author:'bo0k'", "bo0k"); + + testSearchSpellcheckResponse(response, null, null); + } + + /** + * This is a test to check the ACL tracker works with spellcheck enabled + * + * @throws Exception + */ + @Test(groups = { TestGroup.ACS_60n }, priority = 7) + public void testSpellCheckACL() throws Exception + { + // Create User 2 + testUser2 = dataUser.createRandomTestUser("User2"); + + // Create Private Site 2 + testSite2 = new SiteModel(RandomData.getRandomName("Site2")); + testSite2.setVisibility(Visibility.PRIVATE); + + testSite2 = dataSite.usingUser(testUser).createSite(testSite2); + + // Make User 2 Site Collaborator + getDataUser().addUserToSite(testUser2, testSite2, UserRole.SiteCollaborator); + + // Add file to testSite + FileModel file1 = new FileModel("spacebar", "", "", FileType.TEXT_PLAIN, "spacebar"); + + dataContent.usingUser(testUser).usingSite(testSite).createContent(file1); + + // Add file to testSite, testSite2 + FileModel file2 = new FileModel("spacecar", "", "", FileType.TEXT_PLAIN, "spacecar"); + + dataContent.usingUser(testUser).usingSite(testSite).createContent(file2); + dataContent.usingUser(testUser).usingSite(testSite2).createContent(file2); + + Assert.assertTrue(waitForContentIndexing(file2.getContent(), true)); + + // Checks for User 2 + // Incorrect spelling with no field + SearchResponse response = SearchSpellcheckQuery(testUser, "spaceber", "spaceber"); + + // Matching Result, Spellcheck = searchInsteadFor: spacebar: alphabetical + Assert.assertTrue(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "spacebar"); - // Search - SearchRequest searchReq = new SearchRequest(); - RestRequestQueryModel queryReq = new RestRequestQueryModel(); - queryReq.setQuery("cm:title:uniquee"); - queryReq.setUserQuery("uniquee"); - searchReq.setQuery(queryReq); - searchReq.setSpellcheck(new RestRequestSpellcheckModel()); - SearchResponse nodes = query(searchReq); - - nodes.assertThat().entriesListIsNotEmpty(); - nodes.getContext().assertThat().field("spellCheck").isNotEmpty(); - nodes.getContext().getSpellCheck().assertThat().field("suggestions").contains("unique"); - nodes.getContext().getSpellCheck().assertThat().field("type").is("didYouMean"); + // Correct spelling with no field + response = SearchSpellcheckQuery(testUser, "spacebar", "spacebar"); + + // Matching Result, Spellcheck = searchInsteadFor: spacebar + Assert.assertTrue(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file1.getName()); + testSearchSpellcheckResponse(response, "didYouMean", "spacecar"); + + // Incorrect spelling with no field + response = SearchSpellcheckQuery(testUser, "spacebra", "spacebra"); + + // Matching Result, Spellcheck = searchInsteadFor: spacebar + Assert.assertTrue(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file1.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "spacebar"); + + // Correct spelling with no field + response = SearchSpellcheckQuery(testUser, "spacecar", "spacecar"); + + // Matching Result, Spellcheck Not returned + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, null, null); + + // Checks for User 2 + // Incorrect spelling for files created no field + response = SearchSpellcheckQuery(testUser2, "spaceber", "spaceber"); + + // Matching Result, Spellcheck = searchInsteadFor: spacecar + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "spacecar"); + + // correct spelling no field + response = SearchSpellcheckQuery(testUser2, "spacebar", "spacebar"); + + // Matching Result, Spellcheck = searchInsteadFor: spacecar + Assert.assertFalse(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file1.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "spacecar"); + + // Incorrect spelling no field + response = SearchSpellcheckQuery(testUser2, "spacecra", "spacecra"); + + // Matching Result, Spellcheck = searchInsteadFor: spacebar + Assert.assertFalse(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file1.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "spacecar"); + + // Correct spelling no field + response = SearchSpellcheckQuery(testUser2, "spacecar", "spacecar"); + + // Matching Result, Spellcheck not returned + Assert.assertFalse(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file1.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, null, null); } } \ No newline at end of file diff --git a/e2e-test/src/test/resources/testdata/search-by-property.xml b/e2e-test/src/test/resources/testdata/search-by-property.xml deleted file mode 100644 index 0b845ec67..000000000 --- a/e2e-test/src/test/resources/testdata/search-by-property.xml +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pom.xml b/pom.xml index 05f1ac48e..71a242b2d 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ 11 6.6.5 - ${solr.base.version}-patched.1 + ${solr.base.version}-patched.2 diff --git a/search-services/README.md b/search-services/README.md index f44e980ca..0c82e56d1 100644 --- a/search-services/README.md +++ b/search-services/README.md @@ -338,6 +338,22 @@ This Docker Image is available at Alfresco Docker Hub: To use the public image instead of the local one (`searchservices:develop`) just use `alfresco/alfresco-search-services:1.3.x.x` labels. +## Docker Master-Slave setup +### Enable Search Slave Replica config + +To enable slave node specify environment value `REPLICATION_TYPE=slave`, by default Master config is enabled and slave is disabled. + +During deployment time whenever Search Services or Insight Engine image starts, it will execute the script [search_config_setup.sh](/packaging/src/docker) which will configure the slave config setup based on the value specified in the script. + +To run the docker image: + +```bash +$ docker run -p 8984:8983 -e REPLICATION_TYPE=slave -e ALFRESCO_SECURE_COMMS=none -e SOLR_CREATE_ALFRESCO_DEFAULTS=alfresco,archive searchservices:develop +``` +Solr-slave End point: [http://localhost:8984/solr](http://localhost:8984/solr) + +To generate your own Docker-compose file please follow [generator-alfresco-docker-compose](../e2e-test/generator-alfresco-docker-compose/README.md) + ### Use Alfresco Search Services Docker Image with Docker Compose Sample configuration in a Docker Compose file using **Plain HTTP** protocol to communicate with Alfresco Repository. diff --git a/search-services/alfresco-search/doc/architecture/trackers/00001-content-tracker.md b/search-services/alfresco-search/doc/architecture/trackers/00001-content-tracker.md index e267a79a9..0387374eb 100644 --- a/search-services/alfresco-search/doc/architecture/trackers/00001-content-tracker.md +++ b/search-services/alfresco-search/doc/architecture/trackers/00001-content-tracker.md @@ -155,7 +155,6 @@ The following table illustrates the configuration properties used by the Tracker |shard.method|"DB_ID"|Data (Documents, ACLs) Routing criteria among shards| | |Y|Y| | | |alfresco.fingerprint|true|true if we want to compute the content Fingerprint| | |Y|| | | |alfresco.index.transformContent|true| | | |Y|| | | -|alfresco.version|5.0.0|The target Alfresco version| | | | | | | |alfresco.corePoolSize|4|The number of threads to keep in the pool, even if they are idle|Y|Y|Y|Y|Y|Y| |alfresco.maximumPoolSize|-1|The maximum number of threads allowed in the pool|Y|Y|Y|Y|Y|Y| |alfresco.keepAliveTime|120|When the number of threads is greater than the core pool size, this is the maximum time that excess idle threads will wait for new tasks before terminating|Y|Y|Y|Y|Y|Y| diff --git a/search-services/alfresco-search/pom.xml b/search-services/alfresco-search/pom.xml index 6a00d6dd6..3e906956b 100644 --- a/search-services/alfresco-search/pom.xml +++ b/search-services/alfresco-search/pom.xml @@ -96,6 +96,57 @@ 2.3.2 + + org.apache.cxf + cxf-core + ${cxf.version} + + + org.apache.cxf + cxf-rt-bindings-soap + ${cxf.version} + + + org.apache.cxf + cxf-rt-bindings-xml + ${cxf.version} + + + org.apache.cxf + cxf-rt-databinding-jaxb + ${cxf.version} + + + org.apache.cxf + cxf-rt-frontend-jaxws + ${cxf.version} + + + org.apache.cxf + cxf-rt-frontend-simple + ${cxf.version} + + + org.apache.cxf + cxf-rt-transports-http + ${cxf.version} + + + org.apache.cxf + cxf-rt-ws-addr + ${cxf.version} + + + org.apache.cxf + cxf-rt-ws-policy + ${cxf.version} + + + org.apache.cxf + cxf-rt-wsdl + ${cxf.version} + + junit @@ -107,7 +158,7 @@ org.mockito mockito-core - 3.1.0 + 3.2.4 test @@ -120,7 +171,7 @@ com.carrotsearch.randomizedtesting randomizedtesting-runner - 2.7.4 + 2.7.5 test @@ -179,6 +230,45 @@ src/main/resources/solr/instance/templates/rerank/conf solrconfig.xml + solrcore.properties + + + + + + + copy-production-solr-configuration-for-master + generate-test-resources + + copy-resources + + + ${project.build.testOutputDirectory}/test-files/master/conf + + + src/main/resources/solr/instance/templates/rerank/conf + + solrconfig.xml + solrcore.properties + + + + + + + copy-production-solr-configuration-for-slave + generate-test-resources + + copy-resources + + + ${project.build.testOutputDirectory}/test-files/slave/conf + + + src/main/resources/solr/instance/templates/rerank/conf + + solrconfig.xml + solrcore.properties diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCollatableMLTextFieldType.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCollatableMLTextFieldType.java index 03d31c672..89d171642 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCollatableMLTextFieldType.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCollatableMLTextFieldType.java @@ -39,12 +39,9 @@ import org.springframework.extensions.surf.util.I18NUtil; /** * @author Andy - * */ public class AlfrescoCollatableMLTextFieldType extends StrField { - - /* (non-Javadoc) * @see org.apache.solr.schema.StrField#getSortField(org.apache.solr.schema.SchemaField, boolean) */ @@ -75,7 +72,6 @@ public class AlfrescoCollatableMLTextFieldType extends StrField } - public static class MLTextSortFieldComparatorSource extends FieldComparatorSource { @@ -101,16 +97,20 @@ public class AlfrescoCollatableMLTextFieldType extends StrField private final String[] values; private BinaryDocValues docTerms; - - private Bits docsWithField; + + /** + * An array of flags - one for each document in the segment. Each bit is set to true if the document has the + * field or false otherwise. If this is set to null then all docs in the segment have the field. + */ + Bits docsWithField; private final String field; - final Collator collator; + Collator collator; - private String bottom; - - private String top; + String bottom; + + String top; Locale collatorLocale; @@ -138,7 +138,7 @@ public class AlfrescoCollatableMLTextFieldType extends StrField { final String comparableString = findBestValue(doc, docTerms.get(doc)); return compareValues(bottom, comparableString); - + } public void copy(int slot, int doc) @@ -153,13 +153,14 @@ public class AlfrescoCollatableMLTextFieldType extends StrField private String findBestValue(int doc, BytesRef term) { - if (term.length == 0 && docsWithField.get(doc) == false) { + if (term.length == 0 && docsWithField != null && docsWithField.get(doc) == false) + { return null; } - + String withLocale = term.utf8ToString(); - - // split strin into MLText object + + // split string into MLText object if (withLocale == null) { return withLocale; @@ -231,14 +232,15 @@ public class AlfrescoCollatableMLTextFieldType extends StrField { docTerms = DocValues.getBinary(context.reader(), field); docsWithField = DocValues.getDocsWithField(context.reader(), field); - if (docsWithField instanceof Bits.MatchAllBits) { - docsWithField = null; + if (docsWithField instanceof Bits.MatchAllBits) + { + docsWithField = null; } return this; } - + @Override - public int compareValues(String val1, String val2) + public int compareValues(String val1, String val2) { if (val1 == null) { @@ -254,9 +256,10 @@ public class AlfrescoCollatableMLTextFieldType extends StrField } return collator.compare(val1, val2); } - - @Override - public void setScorer(Scorer scorer) {} - } + @Override + public void setScorer(Scorer scorer) + { + } + } } diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCollatableTextFieldType.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCollatableTextFieldType.java index d3d54cd63..604dbcc27 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCollatableTextFieldType.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCollatableTextFieldType.java @@ -104,16 +104,20 @@ public class AlfrescoCollatableTextFieldType extends StrField private final String[] values; private BinaryDocValues docTerms; - - private Bits docsWithField; + + /** + * An array of flags - one for each document in the segment. Each bit is set to true if the document has the + * field or false otherwise. If this is set to null then all docs in the segment have the field. + */ + Bits docsWithField; private final String field; - final Collator collator; + Collator collator; - private String bottom; + String bottom; - private String top; + String top; Locale collatorLocale; @@ -141,7 +145,6 @@ public class AlfrescoCollatableTextFieldType extends StrField { final String comparableString = findBestValue(doc, docTerms.get(doc)); return compareValues(bottom, comparableString); - } public void copy(int slot, int doc) @@ -156,7 +159,8 @@ public class AlfrescoCollatableTextFieldType extends StrField private String findBestValue(int doc, BytesRef term) { - if (term.length == 0 && docsWithField.get(doc) == false) { + if (term.length == 0 && docsWithField != null && docsWithField.get(doc) == false) + { return null; } diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCoreAdminHandler.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCoreAdminHandler.java index f714307dc..4303ae3dd 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCoreAdminHandler.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/AlfrescoCoreAdminHandler.java @@ -19,58 +19,26 @@ package org.alfresco.solr; -import static java.util.Optional.ofNullable; - -import static org.alfresco.solr.HandlerOfResources.extractCustomProperties; -import static org.alfresco.solr.HandlerOfResources.getSafeBoolean; -import static org.alfresco.solr.HandlerOfResources.getSafeLong; -import static org.alfresco.solr.HandlerOfResources.openResource; -import static org.alfresco.solr.HandlerOfResources.updatePropertiesFile; -import static org.alfresco.solr.HandlerOfResources.updateSharedProperties; -import static org.alfresco.solr.HandlerReportBuilder.addCoreSummary; -import static org.alfresco.solr.HandlerReportBuilder.buildAclReport; -import static org.alfresco.solr.HandlerReportBuilder.buildAclTxReport; -import static org.alfresco.solr.HandlerReportBuilder.buildNodeReport; -import static org.alfresco.solr.HandlerReportBuilder.buildTrackerReport; -import static org.alfresco.solr.HandlerReportBuilder.buildTxReport; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - import com.google.common.collect.ImmutableMap; - import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.httpclient.AuthenticationException; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.solr.adapters.IOpenBitSet; import org.alfresco.solr.client.SOLRAPIClientFactory; import org.alfresco.solr.config.ConfigUtil; +import org.alfresco.solr.content.SolrContentStore; import org.alfresco.solr.tracker.AclTracker; +import org.alfresco.solr.tracker.CoreStatePublisher; import org.alfresco.solr.tracker.DBIDRangeRouter; import org.alfresco.solr.tracker.DocRouter; import org.alfresco.solr.tracker.IndexHealthReport; import org.alfresco.solr.tracker.MetadataTracker; +import org.alfresco.solr.tracker.SlaveCoreStatePublisher; import org.alfresco.solr.tracker.SolrTrackerScheduler; import org.alfresco.solr.tracker.Tracker; import org.alfresco.solr.tracker.TrackerRegistry; +import org.alfresco.solr.utils.Utils; +import org.alfresco.util.Pair; import org.alfresco.util.shard.ExplicitShardingPolicy; -import org.apache.commons.codec.EncoderException; import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; import org.apache.commons.io.FileUtils; import org.apache.solr.common.SolrException; @@ -87,20 +55,84 @@ import org.json.JSONException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.Optional.ofNullable; +import static org.alfresco.solr.HandlerOfResources.extractCustomProperties; +import static org.alfresco.solr.HandlerOfResources.getSafeBoolean; +import static org.alfresco.solr.HandlerOfResources.getSafeLong; +import static org.alfresco.solr.HandlerOfResources.openResource; +import static org.alfresco.solr.HandlerOfResources.updatePropertiesFile; +import static org.alfresco.solr.HandlerOfResources.updateSharedProperties; +import static org.alfresco.solr.HandlerReportHelper.addMasterOrStandaloneCoreSummary; +import static org.alfresco.solr.HandlerReportHelper.addSlaveCoreSummary; +import static org.alfresco.solr.HandlerReportHelper.buildAclReport; +import static org.alfresco.solr.HandlerReportHelper.buildAclTxReport; +import static org.alfresco.solr.HandlerReportHelper.buildNodeReport; +import static org.alfresco.solr.HandlerReportHelper.buildTrackerReport; +import static org.alfresco.solr.HandlerReportHelper.buildTxReport; +import static org.alfresco.solr.utils.Utils.notNullOrEmpty; + +/** + * Alfresco Solr administration endpoints provider. + * A customisation of the existing Solr {@link CoreAdminHandler} which offers additional administration endpoints. + * + * Since 1.5 the behaviour of these endpoints differs a bit depending on the target core. This because a lot of these + * endpoints rely on the information obtained from the trackers, and trackers (see SEARCH-1606) are disabled on slave + * cores. + * + * When a request arrives to this handler, the following are the possible scenarios: + * + *
    + *
  • + * a core is specified in the request: if the target core is a slave then a minimal response or an empty + * response with an informational message is returned. If instead the core is a master (or it is a standalone + * core) the service will return as much information as possible (as it happened before 1.5) + *
  • + *
  • + * a core isn't specified in the request: the request is supposed to target all available cores. However, while + * looping, slave cores are filtered out. In case all cores are slave (i.e. we are running a "pure" slave node) + * the response will be empty, it will include an informational message in order to warn the requestor. + * Sometimes this informative behaviour is not feasible: in those cases an empty response will be returned. + *
  • + *
+ * + * @author Andrea Gazzarini + */ public class AlfrescoCoreAdminHandler extends CoreAdminHandler { protected static final Logger LOGGER = LoggerFactory.getLogger(AlfrescoCoreAdminHandler.class); - + + private static final String REPORT = "report"; private static final String ARG_ACLTXID = "acltxid"; - protected static final String ARG_TXID = "txid"; + static final String ARG_TXID = "txid"; private static final String ARG_ACLID = "aclid"; private static final String ARG_NODEID = "nodeid"; private static final String ARG_QUERY = "query"; - public static final String DATA_DIR_ROOT = "data.dir.root"; + private static final String DATA_DIR_ROOT = "data.dir.root"; public static final String ALFRESCO_DEFAULTS = "create.alfresco.defaults"; - public static final String NUM_SHARDS = "num.shards"; - public static final String SHARD_IDS = "shard.ids"; - public static final String DEFAULT_TEMPLATE = "rerank"; + private static final String NUM_SHARDS = "num.shards"; + private static final String SHARD_IDS = "shard.ids"; + static final String DEFAULT_TEMPLATE = "rerank"; static final String ALFRESCO_CORE_NAME = "alfresco"; static final String ARCHIVE_CORE_NAME = "archive"; @@ -112,8 +144,11 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler private SolrTrackerScheduler scheduler; private TrackerRegistry trackerRegistry; - private ConcurrentHashMap informationServers = null; - + private ConcurrentHashMap informationServers; + + private static List CORE_PARAMETER_NAMES = asList(CoreAdminParams.CORE, "coreName", "index"); + private SolrContentStore contentStore; + public AlfrescoCoreAdminHandler() { super(); @@ -122,39 +157,27 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler public AlfrescoCoreAdminHandler(CoreContainer coreContainer) { super(coreContainer); - startup(coreContainer); - } - /** - * Startup services that exist outside of the core. - */ - public void startup(CoreContainer coreContainer) - { - LOGGER.info("Starting Alfresco core container services"); + LOGGER.info("Starting Alfresco Core Administration Services"); trackerRegistry = new TrackerRegistry(); informationServers = new ConcurrentHashMap<>(); this.scheduler = new SolrTrackerScheduler(this); + if (coreContainer != null) + { + this.contentStore = new SolrContentStore(coreContainer.getSolrHome()); + } String createDefaultCores = ConfigUtil.locateProperty(ALFRESCO_DEFAULTS, ""); - int numShards = Integer.valueOf(ConfigUtil.locateProperty(NUM_SHARDS, "1")); + int numShards = Integer.parseInt(ConfigUtil.locateProperty(NUM_SHARDS, "1")); String shardIds = ConfigUtil.locateProperty(SHARD_IDS, null); if (createDefaultCores != null && !createDefaultCores.isEmpty()) { - Runnable runnable = () -> + Thread thread = new Thread(() -> { - try - { - TimeUnit.SECONDS.sleep(10); //Wait a little for the container to start up - } - catch (InterruptedException e) - { - //Don't care - } + waitForTenSeconds(); setupNewDefaultCores(createDefaultCores, numShards, 1, 1, 1, shardIds); - }; - - Thread thread = new Thread(runnable); + }); thread.start(); } } @@ -179,7 +202,7 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler * @param numNodes - Not sure why the core needs to know this. * @param shardIds A comma separated list of shard ids for this core (or null). */ - void setupNewDefaultCores(String names, int numShards, int replicationFactor, int nodeInstance, int numNodes, String shardIds) + private void setupNewDefaultCores(String names, int numShards, int replicationFactor, int nodeInstance, int numNodes, String shardIds) { try { @@ -195,7 +218,7 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler .filter(coreName -> !coreName.isEmpty()) .forEach(coreName -> { LOGGER.info("Attempting to create default alfresco core: {}", coreName); - if (!STORE_REF_MAP.keySet().contains(coreName)) + if (!STORE_REF_MAP.containsKey(coreName)) { throw new AlfrescoRuntimeException("Invalid '" + ALFRESCO_DEFAULTS + "' permitted values are " + STORE_REF_MAP.keySet()); } @@ -213,34 +236,47 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler /** * Shut down services that exist outside of the core. */ - public void shutdown() + @Override + public void shutdown() { super.shutdown(); - try + try { LOGGER.info("Shutting down Alfresco core container services"); + AlfrescoSolrDataModel.getInstance().close(); SOLRAPIClientFactory.close(); MultiThreadedHttpConnectionManager.shutdownAll(); - //Remove any core trackers still hanging around - trackerRegistry.getCoreNames().forEach(coreName -> trackerRegistry.removeTrackersForCore(coreName)); - - //Remove any information servers + coreNames().forEach(trackerRegistry::removeTrackersForCore); informationServers.clear(); - //Shutdown the scheduler and model tracker. if (!scheduler.isShutdown()) { scheduler.pauseAll(); - if (trackerRegistry.getModelTracker() != null) trackerRegistry.getModelTracker().shutdown(); + + if (trackerRegistry.getModelTracker() != null) + trackerRegistry.getModelTracker().shutdown(); + trackerRegistry.setModelTracker(null); scheduler.shutdown(); } - } - catch(Exception e) + } + catch (Exception exception) { - LOGGER.error("Problem shutting down", e); + LOGGER.error( + "Unable to properly shut down Alfresco core container services. See the exception below for further details.", + exception); + } + + try + { + contentStore.close(); + } + catch (Exception exception) + { + LOGGER.error("Unable to properly shut down the ContentStore. See the exception below for further details.", + exception); } } @@ -257,7 +293,7 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler } catch (ClassNotFoundException e) { - return; + // Do nothing here } catch (Exception e) { @@ -267,117 +303,80 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler protected void handleCustomAction(SolrQueryRequest req, SolrQueryResponse rsp) { - LOGGER.info("######## Handle Custom Action ###########"); SolrParams params = req.getParams(); - String cname = params.get(CoreAdminParams.CORE); - String action = params.get(CoreAdminParams.ACTION); - action = action==null?"":action.toUpperCase(); + String action = + ofNullable(params.get(CoreAdminParams.ACTION)) + .map(String::trim) + .map(String::toUpperCase) + .orElse(""); try { - switch (action) { + switch (action) + { case "NEWCORE": + case "NEWINDEX": newCore(req, rsp); break; case "UPDATECORE": - updateCore(req, rsp); + case "UPDATEINDEX": + updateCore(req); break; case "UPDATESHARED": - updateShared(req, rsp); + updateShared(req); break; case "REMOVECORE": - removeCore(req, rsp); + removeCore(req); break; case "NEWDEFAULTINDEX": + case "NEWDEFAULTCORE": newDefaultCore(req, rsp); break; case "CHECK": - actionCHECK(cname); + actionCHECK(params); break; case "NODEREPORT": - actionNODEREPORTS(rsp, params, cname); + actionNODEREPORTS(rsp, params); break; case "ACLREPORT": - actionACLREPORT(rsp, params, cname); + actionACLREPORT(rsp, params); break; case "TXREPORT": - actionTXREPORT(rsp, params, cname); + actionTXREPORT(rsp, params); break; case "ACLTXREPORT": - actionACLTXREPORT(rsp, params, cname); + actionACLTXREPORT(rsp, params); break; case "RANGECHECK": - rangeCheck(rsp, cname); + rangeCheck(rsp, params); break; case "EXPAND": - expand(rsp, params, cname); + expand(rsp, params); break; case "REPORT": - actionREPORT(rsp, params, cname); + actionREPORT(rsp, params); break; case "PURGE": - if (cname != null) { - actionPURGE(params, cname); - } else { - for (String coreName : getTrackerRegistry().getCoreNames()) { - actionPURGE(params, coreName); - } - } + actionPURGE(params); break; case "REINDEX": - if (cname != null) { - actionREINDEX(params, cname); - } else { - for (String coreName : getTrackerRegistry().getCoreNames()) { - actionREINDEX(params, coreName); - } - } + actionREINDEX(params); break; case "RETRY": - if (cname != null) { - actionRETRY(rsp, cname); - } else { - for (String coreName : getTrackerRegistry().getCoreNames()) { - actionRETRY(rsp, coreName); - } - } + actionRETRY(rsp, params); break; case "INDEX": - if (cname != null) { - actionINDEX(params, cname); - } else { - for (String coreName : getTrackerRegistry().getCoreNames()) { - actionINDEX(params, coreName); - } - } + actionINDEX(params); break; case "FIX": - if (cname != null) { - actionFIX(cname); - } else { - for (String coreName : getTrackerRegistry().getCoreNames()) { - actionFIX(coreName); - } - } + actionFIX(params); break; case "SUMMARY": - if (cname != null) { - NamedList report = new SimpleOrderedMap(); - actionSUMMARY(params, report, cname); - rsp.add("Summary", report); - } else { - NamedList report = new SimpleOrderedMap(); - for (String coreName : getTrackerRegistry().getCoreNames()) { - actionSUMMARY(params, report, coreName); - } - rsp.add("Summary", report); - } + actionSUMMARY(rsp, params); break; case "LOG4J": - String resource = "log4j-solr.properties"; - if (params.get("resource") != null) { - resource = params.get("resource"); - } - initResourceBasedLogging(resource); + initResourceBasedLogging( + ofNullable(params.get("resource")) + .orElse("log4j-solr.properties")); break; default: super.handleCustomAction(req, rsp); @@ -391,60 +390,61 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler } } - private boolean newCore(SolrQueryRequest req, SolrQueryResponse rsp) { + private void newCore(SolrQueryRequest req, SolrQueryResponse rsp) + { SolrParams params = req.getParams(); req.getContext(); - // If numCore > 1 we are createing a collection of cores for a sole node in a cluster + // If numCore > 1 we are creating a collection of cores for a sole node in a cluster int numShards = params.getInt("numShards", 1); - String store = ""; - if (params.get("storeRef") != null) { - store = params.get("storeRef"); - } - if ((store == null) || (store.length() == 0)) { - return false; + String store = params.get("storeRef"); + if (store == null || store.trim().length() == 0) + { + return; } + StoreRef storeRef = new StoreRef(store); - String templateName = "vanilla"; - if (params.get("template") != null) { - templateName = params.get("template"); - } + String templateName = ofNullable(params.get("template")).orElse("vanilla"); int replicationFactor = params.getInt("replicationFactor", 1); int nodeInstance = params.getInt("nodeInstance", -1); int numNodes = params.getInt("numNodes", 1); - String coreName = params.get("coreName"); + String coreName = coreName(params); String shardIds = params.get("shardIds"); - Properties properties = extractCustomProperties(params); - return newCore(coreName, numShards, storeRef, templateName, replicationFactor, nodeInstance, numNodes, shardIds, properties, rsp); + newCore(coreName, numShards, storeRef, templateName, replicationFactor, nodeInstance, numNodes, shardIds, extractCustomProperties(params), rsp); } - private boolean newDefaultCore(SolrQueryRequest req, SolrQueryResponse rsp) { - + private void newDefaultCore(SolrQueryRequest req, SolrQueryResponse response) + { SolrParams params = req.getParams(); - String coreName = params.get("coreName") != null?params.get("coreName"):"alfresco"; - StoreRef storeRef = StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; - String templateName = params.get("template") != null?params.get("template"): DEFAULT_TEMPLATE; + String coreName = ofNullable(coreName(params)).orElse(ALFRESCO_CORE_NAME); + String templateName = + params.get("template") != null + ? params.get("template") + : DEFAULT_TEMPLATE; + Properties extraProperties = extractCustomProperties(params); - if (params.get("storeRef") != null) { - String store = params.get("storeRef"); - storeRef = new StoreRef(store); - } - - return newDefaultCore(coreName, storeRef, templateName, extraProperties, rsp); + newDefaultCore( + coreName, + ofNullable(params.get("storeRef")) + .map(StoreRef::new) + .orElse(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE), + templateName, + extraProperties, + response); } - protected boolean newDefaultCore(String coreName, StoreRef storeRef, String templateName, Properties extraProperties, SolrQueryResponse rsp) + private void newDefaultCore(String coreName, StoreRef storeRef, String templateName, Properties extraProperties, SolrQueryResponse rsp) { - return newCore(coreName, 1, storeRef, templateName, 1, 1, 1, null, extraProperties, rsp); + newCore(coreName, 1, storeRef, templateName, 1, 1, 1, null, extraProperties, rsp); } - protected boolean newCore(String coreName, int numShards, StoreRef storeRef, String templateName, int replicationFactor, int nodeInstance, int numNodes, String shardIds, Properties extraProperties, SolrQueryResponse rsp) + protected void newCore(String coreName, int numShards, StoreRef storeRef, String templateName, int replicationFactor, int nodeInstance, int numNodes, String shardIds, Properties extraProperties, SolrQueryResponse rsp) { try { @@ -453,9 +453,8 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler File templates = new File(solrHome, "templates"); File template = new File(templates, templateName); - if(numShards > 1 ) + if(numShards > 1) { - String collectionName = templateName + "--" + storeRef.getProtocol() + "-" + storeRef.getIdentifier() + "--shards--"+numShards + "-x-"+replicationFactor+"--node--"+nodeInstance+"-of-"+numNodes; String coreBase = storeRef.getProtocol() + "-" + storeRef.getIdentifier() + "-"; if (coreName != null) @@ -463,14 +462,14 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler collectionName = templateName + "--" + coreName + "--shards--"+numShards + "-x-"+replicationFactor+"--node--"+nodeInstance+"-of-"+numNodes; coreBase = coreName + "-"; } - - File baseDirectory = new File(solrHome, collectionName); - + + File baseDirectory = new File(solrHome, collectionName); + if(nodeInstance == -1) { - return false; + return; } - + List shards; if(shardIds != null) { @@ -481,31 +480,29 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler ExplicitShardingPolicy policy = new ExplicitShardingPolicy(numShards, replicationFactor, numNodes); if(!policy.configurationIsValid()) { - return false; + return; } shards = policy.getShardIdsForNode(nodeInstance); } - + for(Integer shard : shards) { - coreName = coreBase+shard; + coreName = coreBase + shard; File newCore = new File(baseDirectory, coreName); String solrCoreName = coreName; if (coreName == null) { if(storeRef.equals(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE)) { - solrCoreName = "alfresco-"+shard; + solrCoreName = "alfresco-" + shard; } else if(storeRef.equals(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE)) { - solrCoreName = "archive-"+shard; + solrCoreName = "archive-" + shard; } } createAndRegisterNewCore(rsp, extraProperties, storeRef, template, solrCoreName, newCore, numShards, shard, templateName); } - - return true; } else { @@ -515,54 +512,44 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler } File newCore = new File(solrHome, coreName); createAndRegisterNewCore(rsp, extraProperties, storeRef, template, coreName, newCore, 0, 0, templateName); - - return true; } - } - catch (IOException e) + catch (IOException exception) { - e.printStackTrace(); - return false; + LOGGER.error("I/O Failure detected while creating the new core " + + "(name={}, numShard={}, storeRef={}, template={}, replication factor={}, node instance={}, num nodes={}, shard ids={})", + coreName, + numShards, + storeRef, + templateName, + replicationFactor, + nodeInstance, + numNodes, + shardIds, + exception); } } /** - * @param shardIds - * @return + * Extracts the list of shard identifiers from the given input string. + * the "excludeFromShardId" parameter is used to filter out those shards whose identifier is equal or greater than + * that parameter. + * + * @param shardIds the shards input string, where shards are separated by comma. + * @param excludeFromShardId filter out those shards whose identifier is equal or greater than this value. + * @return the list of shard identifiers. */ - private List extractShards(String shardIds, int numShards) + List extractShards(String shardIds, int excludeFromShardId) { - ArrayList shards = new ArrayList(); - for(String shardId : shardIds.split(",")) - { - try - { - Integer shard = Integer.valueOf(shardId); - if(shard.intValue() < numShards) - { - shards.add(shard); - } - } - catch(NumberFormatException nfe) - { - // ignore - } - } - return shards; + return stream(Objects.requireNonNullElse(shardIds, "").split(",")) + .map(String::trim) + .map(Utils::toIntOrNull) + .filter(Objects::nonNull) + .filter(shard -> shard < excludeFromShardId) + .collect(Collectors.toList()); } - /** - * @param rsp - * @param storeRef - * @param template - * @param coreName - * @param newCore - * @throws IOException - * @throws FileNotFoundException - */ - private void createAndRegisterNewCore(SolrQueryResponse rsp, Properties extraProperties, StoreRef storeRef, File template, String coreName, File newCore, int shardCount, int shardInstance, String templateName) throws IOException, - FileNotFoundException + private void createAndRegisterNewCore(SolrQueryResponse rsp, Properties extraProperties, StoreRef storeRef, File template, String coreName, File newCore, int shardCount, int shardInstance, String templateName) throws IOException { if (coreContainer.getLoadedCoreNames().contains(coreName)) { @@ -613,84 +600,60 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler properties.store(fileOutputStream, null); } - SolrCore core = coreContainer.create(coreName, newCore.toPath(), new HashMap(), false); + SolrCore core = coreContainer.create(coreName, newCore.toPath(), new HashMap<>(), false); rsp.add("core", core.getName()); } - /** - * Tests to see if one of the cores is an Alfresco special core! - * @param cores - * @return - */ - public boolean hasAlfrescoCore(Collection cores) + boolean hasAlfrescoCore(Collection cores) { - if (cores == null || cores.isEmpty()) return false; - for (SolrCore core:cores) - { - if (trackerRegistry.hasTrackersForCore(core.getName())) return true; - } - return false; + return notNullOrEmpty(cores).stream() + .map(SolrCore::getName) + .anyMatch(trackerRegistry::hasTrackersForCore); } - private boolean updateShared(SolrQueryRequest req, SolrQueryResponse rsp) + private void updateShared(SolrQueryRequest req) { SolrParams params = req.getParams(); - try { - + try + { File config = new File(AlfrescoSolrDataModel.getResourceDirectory(), AlfrescoSolrDataModel.SHARED_PROPERTIES); updateSharedProperties(params, config, hasAlfrescoCore(coreContainer.getCores())); - coreContainer.getCores().forEach(aCore -> coreContainer.reload(aCore.getName())); - - return true; - } catch (IOException e) + coreContainer.getCores().stream() + .map(SolrCore::getName) + .forEach(coreContainer::reload); + } + catch (IOException e) { LOGGER.error("Failed to update Shared properties ", e); } - return false; } - private boolean updateCore(SolrQueryRequest req, SolrQueryResponse rsp) + private void updateCore(SolrQueryRequest req) { - String coreName = null; - SolrParams params = req.getParams(); + ofNullable(coreName(req.getParams())) + .map(String::trim) + .filter(coreName -> !coreName.isEmpty()) + .ifPresent(coreName -> { + try (SolrCore core = coreContainer.getCore(coreName)) + { - if (params.get("coreName") != null) - { - coreName = params.get("coreName"); - } - - if ((coreName == null) || (coreName.length() == 0)) { return false; } + if (core == null) + { + return; + } - SolrCore core = null; - try { - core = coreContainer.getCore(coreName); + String configLocaltion = core.getResourceLoader().getConfigDir(); + File config = new File(configLocaltion, "solrcore.properties"); + updatePropertiesFile(req.getParams(), config, null); - if(core == null) - { - return false; - } - - String configLocaltion = core.getResourceLoader().getConfigDir(); - File config = new File(configLocaltion, "solrcore.properties"); - updatePropertiesFile(params, config, null); - - coreContainer.reload(coreName); - - return true; - } - finally - { - //Decrement core open count - if(core != null) - { - core.close(); - } - } + coreContainer.reload(coreName); + } + }); } - private boolean removeCore(SolrQueryRequest req, SolrQueryResponse rsp) + private void removeCore(SolrQueryRequest req) { String store = ""; SolrParams params = req.getParams(); @@ -699,414 +662,515 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler store = params.get("storeRef"); } - if ((store == null) || (store.length() == 0)) { return false; } + if ((store == null) || (store.length() == 0)) { return; } StoreRef storeRef = new StoreRef(store); - String coreName = storeRef.getProtocol() + "-" + storeRef.getIdentifier(); - if (params.get("coreName") != null) - { - coreName = params.get("coreName"); - } - // remove core + String coreName = ofNullable(coreName(req.getParams())).orElse(storeRef.getProtocol() + "-" + storeRef.getIdentifier()); coreContainer.unload(coreName, true, true, true); - - return true; } - - - private void actionFIX(String coreName) throws AuthenticationException, IOException, JSONException, EncoderException + private void actionCHECK(SolrParams params) { - // Gets Metadata health and fixes any problems - MetadataTracker metadataTracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); - IndexHealthReport indexHealthReport = metadataTracker.checkIndex(null, null, null, null); - IOpenBitSet toReindex = indexHealthReport.getTxInIndexButNotInDb(); - toReindex.or(indexHealthReport.getDuplicatedTxInIndex()); - toReindex.or(indexHealthReport.getMissingTxFromIndex()); - long current = -1; - // Goes through problems in the index - while ((current = toReindex.nextSetBit(current + 1)) != -1) + String cname = coreName(params); + coreNames().stream() + .filter(coreName -> cname == null || coreName.equals(cname)) + .map(trackerRegistry::getTrackersForCore) + .flatMap(Collection::stream) + .map(Tracker::getTrackerState) + .forEach(state -> state.setCheck(true)); + } + + private void actionNODEREPORTS(SolrQueryResponse rsp, SolrParams params) throws JSONException + { + Long dbid = + ofNullable(params.get(ARG_NODEID)) + .map(Long::valueOf) + .orElseThrow(() -> new AlfrescoRuntimeException("No dbid parameter set.")); + + NamedList report = new SimpleOrderedMap<>(); + rsp.add(REPORT, report); + + String requestedCoreName = coreName(params); + + coreNames().stream() + .filter(coreName -> requestedCoreName == null || coreName.equals(requestedCoreName)) + .filter(trackerRegistry::hasTrackersForCore) + .map(coreName -> new Pair<>(coreName, coreStatePublisher(coreName))) + .filter(coreNameAndPublisher -> coreNameAndPublisher.getSecond() != null) + .forEach(coreNameAndPublisher -> + report.add( + coreNameAndPublisher.getFirst(), + buildNodeReport(coreNameAndPublisher.getSecond(), dbid))); + } + + private void actionACLREPORT(SolrQueryResponse rsp, SolrParams params) throws JSONException + { + Long aclid = + ofNullable(params.get(ARG_ACLID)) + .map(Long::valueOf) + .orElseThrow(() -> new AlfrescoRuntimeException("No " + ARG_ACLID + " parameter set.")); + + NamedList report = new SimpleOrderedMap<>(); + rsp.add(REPORT, report); + + String requestedCoreName = coreName(params); + + coreNames().stream() + .filter(coreName -> requestedCoreName == null || coreName.equals(requestedCoreName)) + .map(coreName -> new Pair<>(coreName, trackerRegistry.getTrackerForCore(coreName, AclTracker.class))) + .filter(coreNameAndAclTracker -> coreNameAndAclTracker.getSecond() != null) + .forEach(coreNameAndAclTracker -> + report.add( + coreNameAndAclTracker.getFirst(), + buildAclReport(coreNameAndAclTracker.getSecond(), aclid))); + + if (report.size() == 0) { - metadataTracker.addTransactionToReindex(current); - } - - // Gets the Acl health and fixes any problems - AclTracker aclTracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - indexHealthReport = aclTracker.checkIndex(null, null, null, null); - toReindex = indexHealthReport.getAclTxInIndexButNotInDb(); - toReindex.or(indexHealthReport.getDuplicatedAclTxInIndex()); - toReindex.or(indexHealthReport.getMissingAclTxFromIndex()); - current = -1; - // Goes through the problems in the index - while ((current = toReindex.nextSetBit(current + 1)) != -1) - { - aclTracker.addAclChangeSetToReindex(current); + addAlertMessage(report); } } - private void actionCHECK(String cname) + private void actionTXREPORT(SolrQueryResponse rsp, SolrParams params) throws JSONException { - if (cname != null) + String coreName = + ofNullable(coreName(params)) + .orElseThrow(() -> new AlfrescoRuntimeException("No " + CoreAdminParams.CORE + " parameter set.")); + + NamedList report = new SimpleOrderedMap<>(); + rsp.add(REPORT, report); + + if (isMasterOrStandalone(coreName)) { - for (Tracker tracker : trackerRegistry.getTrackersForCore(cname)) + MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); + Long txid = + ofNullable(params.get(ARG_TXID)) + .map(Long::valueOf) + .orElseThrow(() -> new AlfrescoRuntimeException("No " + ARG_TXID + " parameter set.")); + + report.add(coreName, buildTxReport(trackerRegistry, informationServers.get(coreName), coreName, tracker, txid)); + } + else + { + addAlertMessage(report); + } + } + + private void actionACLTXREPORT(SolrQueryResponse rsp, SolrParams params) throws JSONException + { + Long acltxid = + ofNullable(params.get(ARG_ACLTXID)) + .map(Long::valueOf) + .orElseThrow(() -> new AlfrescoRuntimeException("No " + ARG_ACLTXID + " parameter set.")); + + NamedList report = new SimpleOrderedMap<>(); + rsp.add(REPORT, report); + + String requestedCoreName = coreName(params); + + coreNames().stream() + .filter(coreName -> requestedCoreName == null || coreName.equals(requestedCoreName)) + .map(coreName -> new Pair<>(coreName, trackerRegistry.getTrackerForCore(coreName, AclTracker.class))) + .filter(coreNameAndAclTracker -> coreNameAndAclTracker.getSecond() != null) + .forEach(coreNameAndAclTracker -> + report.add( + coreNameAndAclTracker.getFirst(), + buildAclTxReport( + trackerRegistry, + informationServers.get(coreNameAndAclTracker.getFirst()), + coreNameAndAclTracker.getFirst(), + coreNameAndAclTracker.getSecond(), + acltxid))); + + if (report.size() == 0) + { + addAlertMessage(report); + } + } + + private void rangeCheck(SolrQueryResponse rsp, SolrParams params) throws IOException + { + String coreName = + ofNullable(coreName(params)) + .orElseThrow(() -> new AlfrescoRuntimeException("No " + CoreAdminParams.CORE + " parameter set.")); + + if (isMasterOrStandalone(coreName)) + { + InformationServer informationServer = informationServers.get(coreName); + + DocRouter docRouter = getDocRouter(coreName); + + if(docRouter instanceof DBIDRangeRouter) { - tracker.getTrackerState().setCheck(true); + DBIDRangeRouter dbidRangeRouter = (DBIDRangeRouter) docRouter; + + if(!dbidRangeRouter.getInitialized()) + { + rsp.add("expand", 0); + rsp.add("exception", "DBIDRangeRouter not initialized yet."); + return; + } + + long startRange = dbidRangeRouter.getStartRange(); + long endRange = dbidRangeRouter.getEndRange(); + + long maxNodeId = informationServer.maxNodeId(); + long minNodeId = informationServer.minNodeId(); + long nodeCount = informationServer.nodeCount(); + + long bestGuess = -1; // -1 means expansion cannot be done. Either because expansion + // has already happened or we're above safe range + + long range = endRange - startRange; // We want this many nodes on the server + + long midpoint = startRange + ((long) (range * .5)); + + long safe = startRange + ((long) (range * .75)); + + long offset = maxNodeId-startRange; + + double density = 0; + + if(offset > 0) + { + density = ((double)nodeCount) / ((double)offset); // This is how dense we are so far. + } + + if (!dbidRangeRouter.getExpanded()) + { + if(maxNodeId <= safe) + { + if (maxNodeId >= midpoint) + { + if(density >= 1 || density == 0) + { + //This is fully dense shard or an empty shard. + // If it does happen, no expand is required. + bestGuess=0; + } + else + { + double multiplier = 1/density; + bestGuess = (long)(range*multiplier)-range; // This is how much to add + } + } + else + { + bestGuess = 0; // We're below the midpoint so it's to early to make a guess. + } + } + } + + rsp.add("start", startRange); + rsp.add("end", endRange); + rsp.add("nodeCount", nodeCount); + rsp.add("minDbid", minNodeId); + rsp.add("maxDbid", maxNodeId); + rsp.add("density", Math.abs(density)); + rsp.add("expand", bestGuess); + rsp.add("expanded", dbidRangeRouter.getExpanded()); + } + else + { + rsp.add("expand", -1); + rsp.add("exception", "ERROR: Wrong document router type:"+docRouter.getClass().getSimpleName()); } } else { - for (String core : trackerRegistry.getCoreNames()) + NamedList report = new SimpleOrderedMap<>(); + rsp.add(REPORT, report); + addAlertMessage(report); + } + } + + private synchronized void expand(SolrQueryResponse rsp, SolrParams params) throws IOException + { + String coreName = + ofNullable(coreName(params)) + .orElseThrow(() -> new AlfrescoRuntimeException("No " + CoreAdminParams.CORE + " parameter set.")); + + if (isMasterOrStandalone(coreName)) + { + InformationServer informationServer = informationServers.get(coreName); + DocRouter docRouter = getDocRouter(coreName); + + if(docRouter instanceof DBIDRangeRouter) { - for (Tracker tracker : trackerRegistry.getTrackersForCore(core)) + long expansion = Long.parseLong(params.get("add")); + DBIDRangeRouter dbidRangeRouter = (DBIDRangeRouter)docRouter; + + if(!dbidRangeRouter.getInitialized()) { - tracker.getTrackerState().setCheck(true); + rsp.add("expand", -1); + rsp.add("exception", "DBIDRangeRouter not initialized yet."); + return; + } + + if(dbidRangeRouter.getExpanded()) + { + rsp.add("expand", -1); + rsp.add("exception", "dbid range has already been expanded."); + return; + } + + long currentEndRange = dbidRangeRouter.getEndRange(); + long startRange = dbidRangeRouter.getStartRange(); + long maxNodeId = informationServer.maxNodeId(); + + long range = currentEndRange - startRange; + long safe = startRange + ((long) (range * .75)); + + if(maxNodeId > safe) + { + rsp.add("expand", -1); + rsp.add("exception", "Expansion cannot occur if max DBID in the index is more then 75% of range."); + return; + } + + long newEndRange = expansion+dbidRangeRouter.getEndRange(); + try + { + informationServer.capIndex(newEndRange); + informationServer.hardCommit(); + dbidRangeRouter.setEndRange(newEndRange); + dbidRangeRouter.setExpanded(true); + assert newEndRange == dbidRangeRouter.getEndRange(); + rsp.add("expand", dbidRangeRouter.getEndRange()); + } + catch(Throwable t) + { + rsp.add("expand", -1); + rsp.add("exception", t.getMessage()); + LOGGER.error("exception expanding", t); } } - } - } - - private void actionACLREPORT(SolrQueryResponse rsp, SolrParams params, String cname) throws IOException, - JSONException - { - if (params.get(ARG_ACLID) == null) - { - throw new AlfrescoRuntimeException("No aclid parameter set"); - } - - if (cname != null) - { - Long aclid = Long.valueOf(params.get(ARG_ACLID)); - NamedList report = new SimpleOrderedMap(); - AclTracker tracker = trackerRegistry.getTrackerForCore(cname, AclTracker.class); - report.add(cname, buildAclReport(tracker, aclid)); - rsp.add("report", report); + else + { + rsp.add("expand", -1); + rsp.add("exception", "Wrong document router type:" + docRouter.getClass().getSimpleName()); + } } else { - Long aclid = Long.valueOf(params.get(ARG_ACLID)); - NamedList report = new SimpleOrderedMap(); - for (String coreName : trackerRegistry.getCoreNames()) - { - AclTracker tracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - report.add(coreName, buildAclReport(tracker, aclid)); - } - rsp.add("report", report); + NamedList report = new SimpleOrderedMap<>(); + rsp.add(REPORT, report); + addAlertMessage(report); } } - private void actionTXREPORT(SolrQueryResponse rsp, SolrParams params, String cname) throws AuthenticationException, - IOException, JSONException, EncoderException + private void actionREPORT(SolrQueryResponse rsp, SolrParams params) throws JSONException { - if (params.get(ARG_TXID) == null) - { - throw new AlfrescoRuntimeException("No txid parameter set"); - } - if (cname == null) - { - throw new AlfrescoRuntimeException("No cname parameter set"); - } + NamedList report = new SimpleOrderedMap<>(); + rsp.add(REPORT, report); - MetadataTracker tracker = trackerRegistry.getTrackerForCore(cname, MetadataTracker.class); - Long txid = Long.valueOf(params.get(ARG_TXID)); - NamedList report = new SimpleOrderedMap(); - report.add(cname, buildTxReport(getTrackerRegistry(), informationServers.get(cname), cname, tracker, txid)); - rsp.add("report", report); - } - - private void actionACLTXREPORT(SolrQueryResponse rsp, SolrParams params, String cname) - throws AuthenticationException, IOException, JSONException, EncoderException - { - if (params.get(ARG_ACLTXID) == null) - { - throw new AlfrescoRuntimeException("No acltxid parameter set"); - } - - if (cname != null) - { - AclTracker tracker = trackerRegistry.getTrackerForCore(cname, AclTracker.class); - Long acltxid = Long.valueOf(params.get(ARG_ACLTXID)); - NamedList report = new SimpleOrderedMap(); - report.add(cname, buildAclTxReport(getTrackerRegistry(), informationServers.get(cname), cname, tracker, acltxid)); - rsp.add("report", report); - } - else - { - Long acltxid = Long.valueOf(params.get(ARG_ACLTXID)); - NamedList report = new SimpleOrderedMap(); - for (String coreName : trackerRegistry.getCoreNames()) - { - AclTracker tracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - report.add(coreName, buildAclTxReport(getTrackerRegistry(), informationServers.get(coreName), coreName, tracker, acltxid)); - } - rsp.add("report", report); - } - } - - private void actionREPORT(SolrQueryResponse rsp, SolrParams params, String cname) throws IOException, - JSONException, AuthenticationException, EncoderException - { Long fromTime = getSafeLong(params, "fromTime"); Long toTime = getSafeLong(params, "toTime"); Long fromTx = getSafeLong(params, "fromTx"); Long toTx = getSafeLong(params, "toTx"); Long fromAclTx = getSafeLong(params, "fromAclTx"); Long toAclTx = getSafeLong(params, "toAclTx"); - - if (cname != null) + + String requestedCoreName = coreName(params); + + coreNames().stream() + .filter(coreName -> requestedCoreName == null || coreName.equals(requestedCoreName)) + .filter(trackerRegistry::hasTrackersForCore) + .filter(this::isMasterOrStandalone) + .forEach(coreName -> + report.add( + coreName, + buildTrackerReport( + trackerRegistry, + informationServers.get(coreName), + coreName, + fromTx, + toTx, + fromAclTx, + toAclTx, + fromTime, + toTime))); + + if (report.size() == 0) { - NamedList report = new SimpleOrderedMap(); - if (trackerRegistry.hasTrackersForCore(cname)) - { - report.add(cname, buildTrackerReport(getTrackerRegistry(), informationServers.get(cname),cname, fromTx, toTx, fromAclTx, toAclTx, fromTime, toTime)); - rsp.add("report", report); - } - else - { - report.add(cname, "Core unknown"); - } - } - else - { - NamedList report = new SimpleOrderedMap(); - for (String coreName : trackerRegistry.getCoreNames()) - { - if (trackerRegistry.hasTrackersForCore(coreName)) - { - report.add(coreName, buildTrackerReport(getTrackerRegistry(), informationServers.get(coreName), coreName, fromTx, toTx, fromAclTx, toAclTx, fromTime, toTime)); - } - else - { - report.add(coreName, "Core unknown"); - } - } - rsp.add("report", report); + addAlertMessage(report); } } - private DocRouter getDocRouter(String cname) + private void actionPURGE(SolrParams params) { - Collection trackers = trackerRegistry.getTrackersForCore(cname); - MetadataTracker metadataTracker = null; - for(Tracker tracker : trackers) - { - if(tracker instanceof MetadataTracker) - { - metadataTracker = (MetadataTracker)tracker; - } - } + Consumer purgeOnSpecificCore = coreName -> { + final MetadataTracker metadataTracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); + final AclTracker aclTracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - DocRouter docRouter = metadataTracker.getDocRouter(); - return docRouter; + apply(params, ARG_TXID, metadataTracker::addTransactionToPurge); + apply(params, ARG_ACLTXID, aclTracker::addAclChangeSetToPurge); + apply(params, ARG_NODEID, metadataTracker::addNodeToPurge); + apply(params, ARG_ACLID, aclTracker::addAclToPurge); + }; + + String requestedCoreName = coreName(params); + + coreNames().stream() + .filter(coreName -> requestedCoreName == null || coreName.equals(requestedCoreName)) + .filter(this::isMasterOrStandalone) + .forEach(purgeOnSpecificCore); } - - private void rangeCheck(SolrQueryResponse rsp,String cname) throws IOException + private void actionREINDEX(SolrParams params) { - InformationServer informationServer = informationServers.get(cname); + Consumer reindexOnSpecificCore = coreName -> { + final MetadataTracker metadataTracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); + final AclTracker aclTracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - DocRouter docRouter = getDocRouter(cname); + apply(params, ARG_TXID, metadataTracker::addTransactionToReindex); + apply(params, ARG_ACLTXID, aclTracker::addAclChangeSetToReindex); + apply(params, ARG_NODEID, metadataTracker::addNodeToReindex); + apply(params, ARG_ACLID, aclTracker::addAclToReindex); - if(docRouter instanceof DBIDRangeRouter) { + ofNullable(params.get(ARG_QUERY)).ifPresent(metadataTracker::addQueryToReindex); + }; - DBIDRangeRouter dbidRangeRouter = (DBIDRangeRouter) docRouter; + String requestedCoreName = coreName(params); - if(!dbidRangeRouter.getInitialized()) - { - rsp.add("expand", 0); - rsp.add("exception", "DBIDRangeRouter not initialized yet."); - return; - } - - long startRange = dbidRangeRouter.getStartRange(); - long endRange = dbidRangeRouter.getEndRange(); - - long maxNodeId = informationServer.maxNodeId(); - long minNodeId = informationServer.minNodeId(); - long nodeCount = informationServer.nodeCount(); - - long bestGuess = -1; // -1 means expansion cannot be done. Either because expansion - // has already happened or we're above safe range - - long range = endRange - startRange; // We want this many nodes on the server - - long midpoint = startRange + ((long) (range * .5)); - - long safe = startRange + ((long) (range * .75)); - - long offset = maxNodeId-startRange; - - double density = 0; - - if(offset > 0) { - density = ((double)nodeCount) / ((double)offset); // This is how dense we are so far. - } - - if (!dbidRangeRouter.getExpanded()) - { - if(maxNodeId <= safe) - { - if (maxNodeId >= midpoint) - { - if(density >= 1 || density == 0) - { - //This is fully dense shard or an empty shard. - // If it does happen, no expand is required. - bestGuess=0; - } - else - { - double multiplier = 1/density; - bestGuess = (long)(range*multiplier)-range; // This is how much to add - } - } - else - { - bestGuess = 0; // We're below the midpoint so it's to early to make a guess. - } - } - } - - rsp.add("start", startRange); - rsp.add("end", endRange); - rsp.add("nodeCount", nodeCount); - rsp.add("minDbid", minNodeId); - rsp.add("maxDbid", maxNodeId); - rsp.add("density", Math.abs(density)); - rsp.add("expand", bestGuess); - rsp.add("expanded", dbidRangeRouter.getExpanded()); - } else { - rsp.add("expand", -1); - rsp.add("exception", "ERROR: Wrong document router type:"+docRouter.getClass().getSimpleName()); - return; - } + coreNames().stream() + .filter(coreName -> requestedCoreName == null || coreName.equals(requestedCoreName)) + .filter(this::isMasterOrStandalone) + .forEach(reindexOnSpecificCore); } - private synchronized void expand(SolrQueryResponse rsp, SolrParams params, String cname) - throws IOException + private void actionRETRY(SolrQueryResponse rsp, SolrParams params) { - InformationServer informationServer = informationServers.get(cname); - DocRouter docRouter = getDocRouter(cname); + final Consumer retryOnSpecificCore = coreName -> { + MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); + InformationServer srv = informationServers.get(coreName); - if(docRouter instanceof DBIDRangeRouter) - { - long expansion = Long.parseLong(params.get("add")); - DBIDRangeRouter dbidRangeRouter = (DBIDRangeRouter)docRouter; - - if(!dbidRangeRouter.getInitialized()) - { - rsp.add("expand", -1); - rsp.add("exception", "DBIDRangeRouter not initialized yet."); - return; - } - - if(dbidRangeRouter.getExpanded()) - { - rsp.add("expand", -1); - rsp.add("exception", "dbid range has already been expanded."); - return; - } - - long currentEndRange = dbidRangeRouter.getEndRange(); - long startRange = dbidRangeRouter.getStartRange(); - long maxNodeId = informationServer.maxNodeId(); - - long range = currentEndRange-startRange; - long safe = startRange + ((long) (range * .75)); - - if(maxNodeId > safe) - { - rsp.add("expand", -1); - rsp.add("exception", "Expansion cannot occur if max DBID in the index is more then 75% of range."); - return; - } - - long newEndRange = expansion+dbidRangeRouter.getEndRange(); try { - informationServer.capIndex(newEndRange); - informationServer.hardCommit(); - dbidRangeRouter.setEndRange(newEndRange); - dbidRangeRouter.setExpanded(true); - assert newEndRange == dbidRangeRouter.getEndRange(); - rsp.add("expand", dbidRangeRouter.getEndRange()); - return; - } - catch(Throwable t) - { - rsp.add("expand", -1); - rsp.add("exception", t.getMessage()); - LOGGER.error("exception expanding", t); - return; - } - } - else - { - rsp.add("expand", -1); - rsp.add("exception", "Wrong document router type:"+docRouter.getClass().getSimpleName()); - return; - } - } - - private void actionNODEREPORTS(SolrQueryResponse rsp, SolrParams params, String cname) throws IOException, - JSONException - { - if (cname != null) - { - MetadataTracker tracker = trackerRegistry.getTrackerForCore(cname, - MetadataTracker.class); - Long dbid = null; - if (params.get(ARG_NODEID) != null) - { - dbid = Long.valueOf(params.get(ARG_NODEID)); - NamedList report = new SimpleOrderedMap(); - report.add(cname, buildNodeReport(tracker, dbid)); - rsp.add("report", report); - - } - else - { - throw new AlfrescoRuntimeException("No dbid parameter set"); - } - } - else - { - Long dbid = null; - if (params.get(ARG_NODEID) != null) - { - dbid = Long.valueOf(params.get(ARG_NODEID)); - NamedList report = new SimpleOrderedMap(); - for (String coreName : trackerRegistry.getCoreNames()) + for (Long nodeid : srv.getErrorDocIds()) { - MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, - MetadataTracker.class); - report.add(coreName, buildNodeReport(tracker, dbid)); + tracker.addNodeToReindex(nodeid); } - rsp.add("report", report); + rsp.add(coreName, srv.getErrorDocIds()); } - else + catch (Exception exception) { - throw new AlfrescoRuntimeException("No dbid parameter set"); + LOGGER.error("I/O Exception while adding Node to reindex.", exception); + } + }; + + String requestedCoreName = coreName(params); + + coreNames().stream() + .filter(coreName -> requestedCoreName == null || coreName.equals(requestedCoreName)) + .filter(this::isMasterOrStandalone) + .forEach(retryOnSpecificCore); + } + + private void actionINDEX(SolrParams params) + { + Consumer indexOnSpecificCore = coreName -> { + final MetadataTracker metadataTracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); + final AclTracker aclTracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); + + apply(params, ARG_TXID, metadataTracker::addTransactionToIndex); + apply(params, ARG_ACLTXID, aclTracker::addAclChangeSetToIndex); + apply(params, ARG_NODEID, metadataTracker::addNodeToIndex); + apply(params, ARG_ACLID, aclTracker::addAclToIndex); + }; + + String requestedCoreName = coreName(params); + + coreNames().stream() + .filter(coreName -> requestedCoreName == null || coreName.equals(requestedCoreName)) + .filter(this::isMasterOrStandalone) + .forEach(indexOnSpecificCore); + } + + private void actionFIX(SolrParams params) throws JSONException + { + String requestedCoreName = coreName(params); + + coreNames().stream() + .filter(coreName -> requestedCoreName == null || coreName.equals(requestedCoreName)) + .filter(this::isMasterOrStandalone) + .forEach(this::fixOnSpecificCore); + } + + private void fixOnSpecificCore(String coreName) + { + try + { + // Gets Metadata health and fixes any problems + MetadataTracker metadataTracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); + IndexHealthReport indexHealthReport = metadataTracker.checkIndex(null, null, null, null); + IOpenBitSet toReindex = indexHealthReport.getTxInIndexButNotInDb(); + toReindex.or(indexHealthReport.getDuplicatedTxInIndex()); + toReindex.or(indexHealthReport.getMissingTxFromIndex()); + long current = -1; + // Goes through problems in the index + while ((current = toReindex.nextSetBit(current + 1)) != -1) { + metadataTracker.addTransactionToReindex(current); } + // Gets the Acl health and fixes any problems + AclTracker aclTracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); + indexHealthReport = aclTracker.checkIndex(null, null, null, null); + toReindex = indexHealthReport.getAclTxInIndexButNotInDb(); + toReindex.or(indexHealthReport.getDuplicatedAclTxInIndex()); + toReindex.or(indexHealthReport.getMissingAclTxFromIndex()); + current = -1; + // Goes through the problems in the index + while ((current = toReindex.nextSetBit(current + 1)) != -1) { + aclTracker.addAclChangeSetToReindex(current); + } + } + catch(Exception exception) + { + throw new AlfrescoRuntimeException("", exception); } } - private void actionSUMMARY(SolrParams params, NamedList report, String coreName) throws IOException + void actionSUMMARY(SolrQueryResponse rsp, SolrParams params) + { + NamedList report = new SimpleOrderedMap<>(); + rsp.add("Summary", report); + + String requestedCoreName = coreName(params); + + coreNames().stream() + .filter(coreName -> requestedCoreName == null || coreName.equals(requestedCoreName)) + .forEach(coreName -> coreSummary(params, report, coreName)); + } + + private void coreSummary(SolrParams params, NamedList report, String coreName) { boolean detail = getSafeBoolean(params, "detail"); boolean hist = getSafeBoolean(params, "hist"); boolean values = getSafeBoolean(params, "values"); boolean reset = getSafeBoolean(params, "reset"); - + InformationServer srv = informationServers.get(coreName); if (srv != null) { - addCoreSummary(trackerRegistry, coreName, detail, hist, values, srv, report); - - if (reset) + try { - srv.getTrackerStats().reset(); + if (isMasterOrStandalone(coreName)) + { + addMasterOrStandaloneCoreSummary(trackerRegistry, coreName, detail, hist, values, srv, report); + + if (reset) + { + srv.getTrackerStats().reset(); + } + } else + { + addSlaveCoreSummary(trackerRegistry, coreName, detail, hist, values, srv, report); + } + } + catch(Exception exception) + { + throw new AlfrescoRuntimeException("", exception); } } else @@ -1115,107 +1179,11 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler } } - - private void actionINDEX(SolrParams params, String coreName) + DocRouter getDocRouter(String cname) { - if (params.get(ARG_TXID) != null) - { - Long txid = Long.valueOf(params.get(ARG_TXID)); - MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); - tracker.addTransactionToIndex(txid); - } - if (params.get(ARG_ACLTXID) != null) - { - Long acltxid = Long.valueOf(params.get(ARG_ACLTXID)); - AclTracker tracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - tracker.addAclChangeSetToIndex(acltxid); - } - if (params.get(ARG_NODEID) != null) - { - Long nodeid = Long.valueOf(params.get(ARG_NODEID)); - MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); - tracker.addNodeToIndex(nodeid); - } - if (params.get(ARG_ACLID) != null) - { - Long aclid = Long.valueOf(params.get(ARG_ACLID)); - AclTracker tracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - tracker.addAclToIndex(aclid); - } - } - - private void actionRETRY(SolrQueryResponse rsp, String coreName) throws IOException - { - MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); - InformationServer srv = informationServers.get(coreName); - Set errorDocIds = srv.getErrorDocIds(); - for (Long nodeid : errorDocIds) - { - tracker.addNodeToReindex(nodeid); - } - rsp.add(coreName, errorDocIds); - } - - private void actionREINDEX(SolrParams params, String coreName) - { - if (params.get(ARG_TXID) != null) - { - Long txid = Long.valueOf(params.get(ARG_TXID)); - MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); - tracker.addTransactionToReindex(txid); - } - if (params.get(ARG_ACLTXID) != null) - { - Long acltxid = Long.valueOf(params.get(ARG_ACLTXID)); - AclTracker tracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - tracker.addAclChangeSetToReindex(acltxid); - } - if (params.get(ARG_NODEID) != null) - { - Long nodeid = Long.valueOf(params.get(ARG_NODEID)); - MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); - tracker.addNodeToReindex(nodeid); - } - if (params.get(ARG_ACLID) != null) - { - Long aclid = Long.valueOf(params.get(ARG_ACLID)); - AclTracker tracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - tracker.addAclToReindex(aclid); - } - if (params.get(ARG_QUERY) != null) - { - String query = params.get(ARG_QUERY); - MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); - tracker.addQueryToReindex(query); - } - } - - private void actionPURGE(SolrParams params, String coreName) - { - if (params.get(ARG_TXID) != null) - { - Long txid = Long.valueOf(params.get(ARG_TXID)); - MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); - tracker.addTransactionToPurge(txid); - } - if (params.get(ARG_ACLTXID) != null) - { - Long acltxid = Long.valueOf(params.get(ARG_ACLTXID)); - AclTracker tracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - tracker.addAclChangeSetToPurge(acltxid); - } - if (params.get(ARG_NODEID) != null) - { - Long nodeid = Long.valueOf(params.get(ARG_NODEID)); - MetadataTracker tracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); - tracker.addNodeToPurge(nodeid); - } - if (params.get(ARG_ACLID) != null) - { - Long aclid = Long.valueOf(params.get(ARG_ACLID)); - AclTracker tracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - tracker.addAclToPurge(aclid); - } + return ofNullable(trackerRegistry.getTrackerForCore(cname, MetadataTracker.class)) + .map(MetadataTracker::getDocRouter) + .orElse(null); } public ConcurrentHashMap getInformationServers() @@ -1228,7 +1196,7 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler return trackerRegistry; } - protected void setTrackerRegistry(TrackerRegistry trackerRegistry) + void setTrackerRegistry(TrackerRegistry trackerRegistry) { this.trackerRegistry = trackerRegistry; } @@ -1237,4 +1205,90 @@ public class AlfrescoCoreAdminHandler extends CoreAdminHandler { return scheduler; } -} + + private void waitForTenSeconds() + { + try + { + TimeUnit.SECONDS.sleep(10); + } + catch (InterruptedException e) + { + //Don't care + } + } + + /** + * Returns, for the given core, the component which is in charge to publish the core state. + * + * @param coreName the owning core name. + * @return the component which is in charge to publish the core state. + */ + CoreStatePublisher coreStatePublisher(String coreName) + { + return ofNullable(trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class)) + .map(CoreStatePublisher.class::cast) + .orElse(trackerRegistry.getTrackerForCore(coreName, SlaveCoreStatePublisher.class)); + } + + /** + * Quickly checks if the given name is associated to a master or standalone core. + * + * @param coreName the core name. + * @return true if the name is associated with a master or standalone mode, false otherwise. + */ + boolean isMasterOrStandalone(String coreName) + { + return trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class) != null; + } + + /** + * Adds to the returned report an information message alerting the receiver that this core is a slave, + * and therefore the same request should be re-submited to the corresponding master. + * + * @param report the response report. + */ + private void addAlertMessage(NamedList report) + { + report.add( + "WARNING", + "The requested endpoint is not available on the slave. " + + "Please re-submit the same request to the corresponding Master"); + } + + Collection coreNames() + { + return notNullOrEmpty(trackerRegistry.getCoreNames()); + } + + private void apply(SolrParams params, String parameterName, Consumer executeSideEffectAction) + { + ofNullable(params.get(parameterName)) + .map(Long::valueOf) + .ifPresent(executeSideEffectAction); + } + + /** + * Returns the core name indicated in the request parameters. + * A first attempt is done in order to check if a standard {@link CoreAdminParams#CORE} parameter is in the request. + * If not, the alternative "coreName" parameter name is used. + * + * @param params the request parameters. + * @return the core name specified in the request, null if the parameter is not found. + */ + String coreName(SolrParams params) + { + return CORE_PARAMETER_NAMES.stream() + .map(params::get) + .filter(Objects::nonNull) + .map(String::trim) + .findFirst() + .orElse(null); + } + + public SolrContentStore getSolrContentStore() + { + return contentStore; + } + +} \ No newline at end of file diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/HandlerReportBuilder.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/HandlerReportBuilder.java deleted file mode 100644 index 5ccd37ed8..000000000 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/HandlerReportBuilder.java +++ /dev/null @@ -1,484 +0,0 @@ -/* - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - */ -package org.alfresco.solr; - -import org.alfresco.httpclient.AuthenticationException; -import org.alfresco.service.cmr.repository.datatype.Duration; -import org.alfresco.solr.client.Node; -import org.alfresco.solr.tracker.*; -import org.alfresco.util.CachingDateFormat; -import org.apache.commons.codec.EncoderException; -import org.apache.solr.common.util.NamedList; -import org.apache.solr.common.util.SimpleOrderedMap; -import org.json.JSONException; - -import java.io.IOException; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Methods taken from AlfrescoCoreAdminHandler that deal with building reports - */ -public class HandlerReportBuilder { - - /** - * Builds AclReport - * @param tracker - * @param aclid - * @return - * @throws IOException - * @throws JSONException - */ - public static NamedList buildAclReport(AclTracker tracker, Long aclid) throws IOException, JSONException - { - AclReport aclReport = tracker.checkAcl(aclid); - - NamedList nr = new SimpleOrderedMap(); - nr.add("Acl Id", aclReport.getAclId()); - nr.add("Acl doc in index", aclReport.getIndexAclDoc()); - if (aclReport.getIndexAclDoc() != null) - { - nr.add("Acl tx in Index", aclReport.getIndexAclTx()); - } - - return nr; - } - - /** - * Builds TxReport - * @param trackerRegistry - * @param srv - * @param coreName - * @param tracker - * @param txid - * @return - * @throws AuthenticationException - * @throws IOException - * @throws JSONException - * @throws EncoderException - */ - public static NamedList buildTxReport(TrackerRegistry trackerRegistry, InformationServer srv, String coreName, MetadataTracker tracker, Long txid) - throws AuthenticationException, IOException, JSONException, EncoderException - { - NamedList nr = new SimpleOrderedMap(); - nr.add("TXID", txid); - nr.add("transaction", buildTrackerReport(trackerRegistry, srv, coreName, txid, txid, 0l, 0l, null, null)); - NamedList nodes = new SimpleOrderedMap(); - // add node reports .... - List dbNodes = tracker.getFullNodesForDbTransaction(txid); - for (Node node : dbNodes) - { - nodes.add("DBID " + node.getId(), buildNodeReport(tracker, node)); - } - - nr.add("txDbNodeCount", dbNodes.size()); - nr.add("nodes", nodes); - return nr; - } - - /** - * Builds AclTxReport - * @param trackerRegistry - * @param srv - * @param coreName - * @param tracker - * @param acltxid - * @return - * @throws AuthenticationException - * @throws IOException - * @throws JSONException - * @throws EncoderException - */ - public static NamedList buildAclTxReport(TrackerRegistry trackerRegistry, InformationServer srv, String coreName, AclTracker tracker, Long acltxid) - throws AuthenticationException, IOException, JSONException, EncoderException - { - NamedList nr = new SimpleOrderedMap(); - nr.add("TXID", acltxid); - nr.add("transaction", buildTrackerReport(trackerRegistry, srv, coreName, 0l, 0l, acltxid, acltxid, null, null)); - NamedList nodes = new SimpleOrderedMap(); - // add node reports .... - List dbAclIds = tracker.getAclsForDbAclTransaction(acltxid); - for (Long aclid : dbAclIds) - { - nodes.add("ACLID " + aclid, buildAclReport(tracker, aclid)); - } - nr.add("aclTxDbAclCount", dbAclIds.size()); - nr.add("nodes", nodes); - return nr; - } - - /** - * Builds Node report - * @param tracker - * @param node - * @return - * @throws IOException - * @throws JSONException - */ - public static NamedList buildNodeReport(MetadataTracker tracker, Node node) throws IOException, JSONException - { - NodeReport nodeReport = tracker.checkNode(node); - - NamedList nr = new SimpleOrderedMap(); - nr.add("Node DBID", nodeReport.getDbid()); - nr.add("DB TX", nodeReport.getDbTx()); - nr.add("DB TX status", nodeReport.getDbNodeStatus().toString()); - if (nodeReport.getIndexLeafDoc() != null) - { - nr.add("Leaf tx in Index", nodeReport.getIndexLeafTx()); - } - if (nodeReport.getIndexAuxDoc() != null) - { - nr.add("Aux tx in Index", nodeReport.getIndexAuxTx()); - } - nr.add("Indexed Node Doc Count", nodeReport.getIndexedNodeDocCount()); - return nr; - } - - /** - * Builds Node Report - * @param tracker - * @param dbid - * @return - * @throws IOException - * @throws JSONException - */ - public static NamedList buildNodeReport(MetadataTracker tracker, Long dbid) throws IOException, JSONException - { - NodeReport nodeReport = tracker.checkNode(dbid); - - NamedList nr = new SimpleOrderedMap(); - nr.add("Node DBID", nodeReport.getDbid()); - nr.add("DB TX", nodeReport.getDbTx()); - nr.add("DB TX status", nodeReport.getDbNodeStatus().toString()); - if (nodeReport.getIndexLeafDoc() != null) - { - nr.add("Leaf tx in Index", nodeReport.getIndexLeafTx()); - } - if (nodeReport.getIndexAuxDoc() != null) - { - nr.add("Aux tx in Index", nodeReport.getIndexAuxTx()); - } - nr.add("Indexed Node Doc Count", nodeReport.getIndexedNodeDocCount()); - return nr; - } - - /** - * Builds Tracker report - * @param trackerRegistry - * @param srv - * @param coreName - * @param fromTx - * @param toTx - * @param fromAclTx - * @param toAclTx - * @param fromTime - * @param toTime - * @return - * @throws IOException - * @throws JSONException - * @throws AuthenticationException - * @throws EncoderException - */ - public static NamedList buildTrackerReport(TrackerRegistry trackerRegistry, InformationServer srv, String coreName, Long fromTx, Long toTx, Long fromAclTx, Long toAclTx, - Long fromTime, Long toTime) throws IOException, JSONException, AuthenticationException, EncoderException - { - // ACL - AclTracker aclTracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); - IndexHealthReport aclReport = aclTracker.checkIndex(toTx, toAclTx, fromTime, toTime); - NamedList ihr = new SimpleOrderedMap(); - ihr.add("Alfresco version", aclTracker.getAlfrescoVersion()); - ihr.add("DB acl transaction count", aclReport.getDbAclTransactionCount()); - ihr.add("Count of duplicated acl transactions in the index", aclReport.getDuplicatedAclTxInIndex() - .cardinality()); - if (aclReport.getDuplicatedAclTxInIndex().cardinality() > 0) - { - ihr.add("First duplicate acl tx", aclReport.getDuplicatedAclTxInIndex().nextSetBit(0L)); - } - ihr.add("Count of acl transactions in the index but not the DB", aclReport.getAclTxInIndexButNotInDb() - .cardinality()); - if (aclReport.getAclTxInIndexButNotInDb().cardinality() > 0) - { - ihr.add("First acl transaction in the index but not the DB", aclReport.getAclTxInIndexButNotInDb() - .nextSetBit(0L)); - } - ihr.add("Count of missing acl transactions from the Index", aclReport.getMissingAclTxFromIndex() - .cardinality()); - if (aclReport.getMissingAclTxFromIndex().cardinality() > 0) - { - ihr.add("First acl transaction missing from the Index", aclReport.getMissingAclTxFromIndex() - .nextSetBit(0L)); - } - ihr.add("Index acl transaction count", aclReport.getAclTransactionDocsInIndex()); - ihr.add("Index unique acl transaction count", aclReport.getAclTransactionDocsInIndex()); - TrackerState aclState = aclTracker.getTrackerState(); - ihr.add("Last indexed change set commit time", aclState.getLastIndexedChangeSetCommitTime()); - Date lastChangeSetDate = new Date(aclState.getLastIndexedChangeSetCommitTime()); - ihr.add("Last indexed change set commit date", CachingDateFormat.getDateFormat().format(lastChangeSetDate)); - ihr.add("Last changeset id before holes", aclState.getLastIndexedChangeSetIdBeforeHoles()); - - // Metadata - MetadataTracker metadataTracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); - IndexHealthReport metaReport = metadataTracker.checkIndex(toTx, toAclTx, fromTime, toTime); - ihr.add("DB transaction count", metaReport.getDbTransactionCount()); - ihr.add("Count of duplicated transactions in the index", metaReport.getDuplicatedTxInIndex() - .cardinality()); - if (metaReport.getDuplicatedTxInIndex().cardinality() > 0) - { - ihr.add("First duplicate", metaReport.getDuplicatedTxInIndex().nextSetBit(0L)); - } - ihr.add("Count of transactions in the index but not the DB", metaReport.getTxInIndexButNotInDb() - .cardinality()); - if (metaReport.getTxInIndexButNotInDb().cardinality() > 0) - { - ihr.add("First transaction in the index but not the DB", metaReport.getTxInIndexButNotInDb() - .nextSetBit(0L)); - } - ihr.add("Count of missing transactions from the Index", metaReport.getMissingTxFromIndex().cardinality()); - if (metaReport.getMissingTxFromIndex().cardinality() > 0) - { - ihr.add("First transaction missing from the Index", metaReport.getMissingTxFromIndex() - .nextSetBit(0L)); - } - ihr.add("Index transaction count", metaReport.getTransactionDocsInIndex()); - ihr.add("Index unique transaction count", metaReport.getTransactionDocsInIndex()); - ihr.add("Index node count", metaReport.getLeafDocCountInIndex()); - ihr.add("Count of duplicate nodes in the index", metaReport.getDuplicatedLeafInIndex().cardinality()); - if (metaReport.getDuplicatedLeafInIndex().cardinality() > 0) - { - ihr.add("First duplicate node id in the index", metaReport.getDuplicatedLeafInIndex().nextSetBit(0L)); - } - ihr.add("Index error count", metaReport.getErrorDocCountInIndex()); - ihr.add("Count of duplicate error docs in the index", metaReport.getDuplicatedErrorInIndex() - .cardinality()); - if (metaReport.getDuplicatedErrorInIndex().cardinality() > 0) - { - ihr.add("First duplicate error in the index", SolrInformationServer.PREFIX_ERROR - + metaReport.getDuplicatedErrorInIndex().nextSetBit(0L)); - } - ihr.add("Index unindexed count", metaReport.getUnindexedDocCountInIndex()); - ihr.add("Count of duplicate unindexed docs in the index", metaReport.getDuplicatedUnindexedInIndex() - .cardinality()); - if (metaReport.getDuplicatedUnindexedInIndex().cardinality() > 0) - { - ihr.add("First duplicate unindexed in the index", - metaReport.getDuplicatedUnindexedInIndex().nextSetBit(0L)); - } - TrackerState metaState = metadataTracker.getTrackerState(); - ihr.add("Last indexed transaction commit time", metaState.getLastIndexedTxCommitTime()); - Date lastTxDate = new Date(metaState.getLastIndexedTxCommitTime()); - ihr.add("Last indexed transaction commit date", CachingDateFormat.getDateFormat().format(lastTxDate)); - ihr.add("Last TX id before holes", metaState.getLastIndexedTxIdBeforeHoles()); - - srv.addFTSStatusCounts(ihr); - - return ihr; - } - - - /** - * Adds a core summary - * @param cname - * @param detail - * @param hist - * @param values - * @param srv - * @param report - * @throws IOException - */ - public static void addCoreSummary(TrackerRegistry trackerRegistry, String cname, boolean detail, boolean hist, boolean values, - InformationServer srv, NamedList report) throws IOException - { - NamedList coreSummary = new SimpleOrderedMap(); - coreSummary.addAll((SimpleOrderedMap) srv.getCoreStats()); - - MetadataTracker metaTrkr = trackerRegistry.getTrackerForCore(cname, MetadataTracker.class); - TrackerState metadataTrkrState = metaTrkr.getTrackerState(); - long lastIndexTxCommitTime = metadataTrkrState.getLastIndexedTxCommitTime(); - - long lastIndexedTxId = metadataTrkrState.getLastIndexedTxId(); - long lastTxCommitTimeOnServer = metadataTrkrState.getLastTxCommitTimeOnServer(); - long lastTxIdOnServer = metadataTrkrState.getLastTxIdOnServer(); - Date lastIndexTxCommitDate = new Date(lastIndexTxCommitTime); - Date lastTxOnServerDate = new Date(lastTxCommitTimeOnServer); - long transactionsToDo = lastTxIdOnServer - lastIndexedTxId; - if (transactionsToDo < 0) - { - transactionsToDo = 0; - } - - AclTracker aclTrkr = trackerRegistry.getTrackerForCore(cname, AclTracker.class); - TrackerState aclTrkrState = aclTrkr.getTrackerState(); - long lastIndexChangeSetCommitTime = aclTrkrState.getLastIndexedChangeSetCommitTime(); - long lastIndexedChangeSetId = aclTrkrState.getLastIndexedChangeSetId(); - long lastChangeSetCommitTimeOnServer = aclTrkrState.getLastChangeSetCommitTimeOnServer(); - long lastChangeSetIdOnServer = aclTrkrState.getLastChangeSetIdOnServer(); - Date lastIndexChangeSetCommitDate = new Date(lastIndexChangeSetCommitTime); - Date lastChangeSetOnServerDate = new Date(lastChangeSetCommitTimeOnServer); - long changeSetsToDo = lastChangeSetIdOnServer - lastIndexedChangeSetId; - if (changeSetsToDo < 0) - { - changeSetsToDo = 0; - } - - long nodesToDo = 0; - long remainingTxTimeMillis = 0; - if (transactionsToDo > 0) - { - // We now use the elapsed time as seen by the single thread farming out metadata indexing - double meanDocsPerTx = srv.getTrackerStats().getMeanDocsPerTx(); - double meanNodeElaspedIndexTime = srv.getTrackerStats().getMeanNodeElapsedIndexTime(); - nodesToDo = (long)(transactionsToDo * meanDocsPerTx); - remainingTxTimeMillis = (long) (nodesToDo * meanNodeElaspedIndexTime); - } - Date now = new Date(); - Date end = new Date(now.getTime() + remainingTxTimeMillis); - Duration remainingTx = new Duration(now, end); - - long remainingChangeSetTimeMillis = 0; - if (changeSetsToDo > 0) - { - // We now use the elapsed time as seen by the single thread farming out alc indexing - double meanAclsPerChangeSet = srv.getTrackerStats().getMeanAclsPerChangeSet(); - double meanAclElapsedIndexTime = srv.getTrackerStats().getMeanAclElapsedIndexTime(); - remainingChangeSetTimeMillis = (long) (changeSetsToDo * meanAclsPerChangeSet * meanAclElapsedIndexTime); - } - now = new Date(); - end = new Date(now.getTime() + remainingChangeSetTimeMillis); - Duration remainingChangeSet = new Duration(now, end); - - NamedList ftsSummary = new SimpleOrderedMap(); - long remainingContentTimeMillis = 0; - srv.addFTSStatusCounts(ftsSummary); - long cleanCount = ((Long)ftsSummary.get("Node count with FTSStatus Clean")).longValue(); - long dirtyCount = ((Long)ftsSummary.get("Node count with FTSStatus Dirty")).longValue(); - long newCount = ((Long)ftsSummary.get("Node count with FTSStatus New")).longValue(); - long nodesInIndex = ((Long)coreSummary.get("Alfresco Nodes in Index")); - long contentYetToSee = nodesInIndex > 0 ? nodesToDo * (cleanCount + dirtyCount + newCount)/nodesInIndex : 0;; - if (dirtyCount + newCount + contentYetToSee > 0) - { - // We now use the elapsed time as seen by the single thread farming out alc indexing - double meanContentElapsedIndexTime = srv.getTrackerStats().getMeanContentElapsedIndexTime(); - remainingContentTimeMillis = (long) ((dirtyCount + newCount + contentYetToSee) * meanContentElapsedIndexTime); - } - now = new Date(); - end = new Date(now.getTime() + remainingContentTimeMillis); - Duration remainingContent = new Duration(now, end); - coreSummary.add("FTS",ftsSummary); - - Duration txLag = new Duration(lastIndexTxCommitDate, lastTxOnServerDate); - if (lastIndexTxCommitDate.compareTo(lastTxOnServerDate) > 0) - { - txLag = new Duration(); - } - long txLagSeconds = (lastTxCommitTimeOnServer - lastIndexTxCommitTime) / 1000; - if (txLagSeconds < 0) - { - txLagSeconds = 0; - } - - Duration changeSetLag = new Duration(lastIndexChangeSetCommitDate, lastChangeSetOnServerDate); - if (lastIndexChangeSetCommitDate.compareTo(lastChangeSetOnServerDate) > 0) - { - changeSetLag = new Duration(); - } - long changeSetLagSeconds = (lastChangeSetCommitTimeOnServer - lastIndexChangeSetCommitTime) / 1000; - if (txLagSeconds < 0) - { - txLagSeconds = 0; - } - - ContentTracker contentTrkr = trackerRegistry.getTrackerForCore(cname, ContentTracker.class); - TrackerState contentTrkrState = contentTrkr.getTrackerState(); - // Leave ModelTracker out of this check, because it is common - boolean aTrackerIsRunning = aclTrkrState.isRunning() || metadataTrkrState.isRunning() - || contentTrkrState.isRunning(); - coreSummary.add("Active", aTrackerIsRunning); - - ModelTracker modelTrkr = trackerRegistry.getModelTracker(); - TrackerState modelTrkrState = modelTrkr.getTrackerState(); - coreSummary.add("ModelTracker Active", modelTrkrState.isRunning()); - coreSummary.add("ContentTracker Active", contentTrkrState.isRunning()); - coreSummary.add("MetadataTracker Active", metadataTrkrState.isRunning()); - coreSummary.add("AclTracker Active", aclTrkrState.isRunning()); - - // TX - - coreSummary.add("Last Index TX Commit Time", lastIndexTxCommitTime); - coreSummary.add("Last Index TX Commit Date", lastIndexTxCommitDate); - coreSummary.add("TX Lag", txLagSeconds + " s"); - coreSummary.add("TX Duration", txLag.toString()); - coreSummary.add("Timestamp for last TX on server", lastTxCommitTimeOnServer); - coreSummary.add("Date for last TX on server", lastTxOnServerDate); - coreSummary.add("Id for last TX on server", lastTxIdOnServer); - coreSummary.add("Id for last TX in index", lastIndexedTxId); - coreSummary.add("Approx transactions remaining", transactionsToDo); - coreSummary.add("Approx transaction indexing time remaining", remainingTx.largestComponentformattedString()); - - // Change set - - coreSummary.add("Last Index Change Set Commit Time", lastIndexChangeSetCommitTime); - coreSummary.add("Last Index Change Set Commit Date", lastIndexChangeSetCommitDate); - coreSummary.add("Change Set Lag", changeSetLagSeconds + " s"); - coreSummary.add("Change Set Duration", changeSetLag.toString()); - coreSummary.add("Timestamp for last Change Set on server", lastChangeSetCommitTimeOnServer); - coreSummary.add("Date for last Change Set on server", lastChangeSetOnServerDate); - coreSummary.add("Id for last Change Set on server", lastChangeSetIdOnServer); - coreSummary.add("Id for last Change Set in index", lastIndexedChangeSetId); - coreSummary.add("Approx change sets remaining", changeSetsToDo); - coreSummary.add("Approx change set indexing time remaining", - remainingChangeSet.largestComponentformattedString()); - - coreSummary.add("Approx content indexing time remaining", - remainingContent.largestComponentformattedString()); - - // Stats - - coreSummary.add("Model sync times (ms)", - srv.getTrackerStats().getModelTimes().getNamedList(detail, hist, values)); - coreSummary.add("Acl index time (ms)", - srv.getTrackerStats().getAclTimes().getNamedList(detail, hist, values)); - coreSummary.add("Node index time (ms)", - srv.getTrackerStats().getNodeTimes().getNamedList(detail, hist, values)); - coreSummary.add("Docs/Tx", srv.getTrackerStats().getTxDocs().getNamedList(detail, hist, values)); - coreSummary.add("Doc Transformation time (ms)", srv.getTrackerStats().getDocTransformationTimes() - .getNamedList(detail, hist, values)); - - // Model - - Map> modelErrors = srv.getModelErrors(); - if (modelErrors.size() > 0) - { - NamedList errorList = new SimpleOrderedMap(); - for (Map.Entry> modelNameToErrors : modelErrors.entrySet()) - { - errorList.add(modelNameToErrors.getKey(), modelNameToErrors.getValue()); - } - coreSummary.add("Model changes are not compatible with the existing data model and have not been applied", - errorList); - } - - report.add(cname, coreSummary); - } -} diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/HandlerReportHelper.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/HandlerReportHelper.java new file mode 100644 index 000000000..72c3e27c0 --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/HandlerReportHelper.java @@ -0,0 +1,554 @@ +/* + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.solr; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.datatype.Duration; +import org.alfresco.solr.client.Node; +import org.alfresco.solr.tracker.*; +import org.alfresco.util.CachingDateFormat; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.SimpleOrderedMap; +import org.json.JSONException; + +import java.io.IOException; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.Optional.ofNullable; + +/** + * Methods taken from AlfrescoCoreAdminHandler that deal with building reports + */ +class HandlerReportHelper +{ + static NamedList buildAclReport(AclTracker tracker, Long aclid) throws JSONException + { + AclReport aclReport = tracker.checkAcl(aclid); + + NamedList nr = new SimpleOrderedMap<>(); + nr.add("Acl Id", aclReport.getAclId()); + nr.add("Acl doc in index", aclReport.getIndexAclDoc()); + if (aclReport.getIndexAclDoc() != null) + { + nr.add("Acl tx in Index", aclReport.getIndexAclTx()); + } + + return nr; + } + + static NamedList buildTxReport(TrackerRegistry trackerRegistry, InformationServer srv, String coreName, MetadataTracker tracker, Long txid) throws JSONException + { + NamedList nr = new SimpleOrderedMap<>(); + nr.add("TXID", txid); + nr.add("transaction", buildTrackerReport(trackerRegistry, srv, coreName, txid, txid, 0L, 0L, null, null)); + NamedList nodes = new SimpleOrderedMap<>(); + + // add node reports .... + List dbNodes = tracker.getFullNodesForDbTransaction(txid); + for (Node node : dbNodes) + { + nodes.add("DBID " + node.getId(), buildNodeReport(tracker, node)); + } + + nr.add("txDbNodeCount", dbNodes.size()); + nr.add("nodes", nodes); + return nr; + } + + static NamedList buildAclTxReport(TrackerRegistry trackerRegistry, InformationServer srv, String coreName, AclTracker tracker, Long acltxid) throws JSONException + { + try { + NamedList nr = new SimpleOrderedMap<>(); + nr.add("TXID", acltxid); + nr.add("transaction", buildTrackerReport(trackerRegistry, srv, coreName, 0L, 0L, acltxid, acltxid, null, null)); + NamedList nodes = new SimpleOrderedMap<>(); + + // add node reports .... + List dbAclIds = tracker.getAclsForDbAclTransaction(acltxid); + for (Long aclid : dbAclIds) { + nodes.add("ACLID " + aclid, buildAclReport(tracker, aclid)); + } + nr.add("aclTxDbAclCount", dbAclIds.size()); + nr.add("nodes", nodes); + return nr; + } + catch (Exception exception) + { + throw new AlfrescoRuntimeException("", exception); + } + } + + static NamedList buildNodeReport(MetadataTracker tracker, Node node) throws JSONException + { + NodeReport nodeReport = tracker.checkNode(node); + + NamedList nr = new SimpleOrderedMap<>(); + nr.add("Node DBID", nodeReport.getDbid()); + nr.add("DB TX", nodeReport.getDbTx()); + nr.add("DB TX status", nodeReport.getDbNodeStatus().toString()); + if (nodeReport.getIndexLeafDoc() != null) + { + nr.add("Leaf tx in Index", nodeReport.getIndexLeafTx()); + } + if (nodeReport.getIndexAuxDoc() != null) + { + nr.add("Aux tx in Index", nodeReport.getIndexAuxTx()); + } + nr.add("Indexed Node Doc Count", nodeReport.getIndexedNodeDocCount()); + return nr; + } + + static NamedList buildNodeReport(CoreStatePublisher publisher, Long dbid) throws JSONException + { + NodeReport nodeReport = publisher.checkNode(dbid); + + NamedList payload = new SimpleOrderedMap<>(); + payload.add("Node DBID", nodeReport.getDbid()); + + if (publisher.isOnMasterOrStandalone()) + { + ofNullable(nodeReport.getDbTx()).ifPresent(value -> payload.add("DB TX", value)); + ofNullable(nodeReport.getDbNodeStatus()).map(Object::toString).ifPresent(value -> payload.add("DB TX Status", value)); + ofNullable(nodeReport.getIndexLeafTx()).ifPresent(value -> payload.add("Leaf tx in Index", value)); + ofNullable(nodeReport.getIndexAuxDoc()).ifPresent(value -> payload.add("Aux tx in Index", value)); + } + else + { + payload.add("WARNING", "This response comes from a slave core and it contains minimal information about the node. " + + "Please consider to re-submit the same request to the corresponding Master, in order to get more information."); + } + + ofNullable(nodeReport.getIndexedNodeDocCount()).ifPresent(value -> payload.add("Indexed Node Doc Count", value)); + + return payload; + } + + static NamedList buildTrackerReport(TrackerRegistry trackerRegistry, InformationServer srv, String coreName, Long fromTx, Long toTx, Long fromAclTx, Long toAclTx, + Long fromTime, Long toTime) throws JSONException + { + try + { + // ACL + AclTracker aclTracker = trackerRegistry.getTrackerForCore(coreName, AclTracker.class); + IndexHealthReport aclReport = aclTracker.checkIndex(toTx, toAclTx, fromTime, toTime); + NamedList ihr = new SimpleOrderedMap<>(); + ihr.add("DB acl transaction count", aclReport.getDbAclTransactionCount()); + ihr.add("Count of duplicated acl transactions in the index", aclReport.getDuplicatedAclTxInIndex() + .cardinality()); + if (aclReport.getDuplicatedAclTxInIndex().cardinality() > 0) { + ihr.add("First duplicate acl tx", aclReport.getDuplicatedAclTxInIndex().nextSetBit(0L)); + } + ihr.add("Count of acl transactions in the index but not the DB", aclReport.getAclTxInIndexButNotInDb() + .cardinality()); + if (aclReport.getAclTxInIndexButNotInDb().cardinality() > 0) { + ihr.add("First acl transaction in the index but not the DB", aclReport.getAclTxInIndexButNotInDb() + .nextSetBit(0L)); + } + ihr.add("Count of missing acl transactions from the Index", aclReport.getMissingAclTxFromIndex() + .cardinality()); + if (aclReport.getMissingAclTxFromIndex().cardinality() > 0) { + ihr.add("First acl transaction missing from the Index", aclReport.getMissingAclTxFromIndex() + .nextSetBit(0L)); + } + ihr.add("Index acl transaction count", aclReport.getAclTransactionDocsInIndex()); + ihr.add("Index unique acl transaction count", aclReport.getAclTransactionDocsInIndex()); + TrackerState aclState = aclTracker.getTrackerState(); + ihr.add("Last indexed change set commit time", aclState.getLastIndexedChangeSetCommitTime()); + Date lastChangeSetDate = new Date(aclState.getLastIndexedChangeSetCommitTime()); + ihr.add("Last indexed change set commit date", CachingDateFormat.getDateFormat().format(lastChangeSetDate)); + ihr.add("Last changeset id before holes", aclState.getLastIndexedChangeSetIdBeforeHoles()); + + // Metadata + MetadataTracker metadataTracker = trackerRegistry.getTrackerForCore(coreName, MetadataTracker.class); + IndexHealthReport metaReport = metadataTracker.checkIndex(toTx, toAclTx, fromTime, toTime); + ihr.add("DB transaction count", metaReport.getDbTransactionCount()); + ihr.add("Count of duplicated transactions in the index", metaReport.getDuplicatedTxInIndex() + .cardinality()); + if (metaReport.getDuplicatedTxInIndex().cardinality() > 0) { + ihr.add("First duplicate", metaReport.getDuplicatedTxInIndex().nextSetBit(0L)); + } + ihr.add("Count of transactions in the index but not the DB", metaReport.getTxInIndexButNotInDb() + .cardinality()); + if (metaReport.getTxInIndexButNotInDb().cardinality() > 0) { + ihr.add("First transaction in the index but not the DB", metaReport.getTxInIndexButNotInDb() + .nextSetBit(0L)); + } + ihr.add("Count of missing transactions from the Index", metaReport.getMissingTxFromIndex().cardinality()); + if (metaReport.getMissingTxFromIndex().cardinality() > 0) { + ihr.add("First transaction missing from the Index", metaReport.getMissingTxFromIndex() + .nextSetBit(0L)); + } + ihr.add("Index transaction count", metaReport.getTransactionDocsInIndex()); + ihr.add("Index unique transaction count", metaReport.getTransactionDocsInIndex()); + ihr.add("Index node count", metaReport.getLeafDocCountInIndex()); + ihr.add("Count of duplicate nodes in the index", metaReport.getDuplicatedLeafInIndex().cardinality()); + if (metaReport.getDuplicatedLeafInIndex().cardinality() > 0) { + ihr.add("First duplicate node id in the index", metaReport.getDuplicatedLeafInIndex().nextSetBit(0L)); + } + ihr.add("Index error count", metaReport.getErrorDocCountInIndex()); + ihr.add("Count of duplicate error docs in the index", metaReport.getDuplicatedErrorInIndex() + .cardinality()); + if (metaReport.getDuplicatedErrorInIndex().cardinality() > 0) { + ihr.add("First duplicate error in the index", SolrInformationServer.PREFIX_ERROR + + metaReport.getDuplicatedErrorInIndex().nextSetBit(0L)); + } + ihr.add("Index unindexed count", metaReport.getUnindexedDocCountInIndex()); + ihr.add("Count of duplicate unindexed docs in the index", metaReport.getDuplicatedUnindexedInIndex() + .cardinality()); + if (metaReport.getDuplicatedUnindexedInIndex().cardinality() > 0) { + ihr.add("First duplicate unindexed in the index", + metaReport.getDuplicatedUnindexedInIndex().nextSetBit(0L)); + } + TrackerState metaState = metadataTracker.getTrackerState(); + ihr.add("Last indexed transaction commit time", metaState.getLastIndexedTxCommitTime()); + Date lastTxDate = new Date(metaState.getLastIndexedTxCommitTime()); + ihr.add("Last indexed transaction commit date", CachingDateFormat.getDateFormat().format(lastTxDate)); + ihr.add("Last TX id before holes", metaState.getLastIndexedTxIdBeforeHoles()); + + srv.addFTSStatusCounts(ihr); + + return ihr; + } + catch (Exception exception) + { + throw new AlfrescoRuntimeException("", exception); + } + } + + static void addSlaveCoreSummary(TrackerRegistry trackerRegistry, String cname, boolean detail, boolean hist, boolean values, + InformationServer srv, NamedList report) throws IOException + { + NamedList coreSummary = new SimpleOrderedMap<>(); + coreSummary.addAll((SimpleOrderedMap) srv.getCoreStats()); + + SlaveCoreStatePublisher statePublisher = trackerRegistry.getTrackerForCore(cname, SlaveCoreStatePublisher.class); + TrackerState trackerState = statePublisher.getTrackerState(); + long lastIndexTxCommitTime = trackerState.getLastIndexedTxCommitTime(); + + long lastIndexedTxId = trackerState.getLastIndexedTxId(); + long lastTxCommitTimeOnServer = trackerState.getLastTxCommitTimeOnServer(); + long lastTxIdOnServer = trackerState.getLastTxIdOnServer(); + + Date lastIndexTxCommitDate = new Date(lastIndexTxCommitTime); + Date lastTxOnServerDate = new Date(lastTxCommitTimeOnServer); + long transactionsToDo = lastTxIdOnServer - lastIndexedTxId; + if (transactionsToDo < 0) + { + transactionsToDo = 0; + } + + long nodesToDo = 0; + long remainingTxTimeMillis = 0; + if (transactionsToDo > 0) + { + // We now use the elapsed time as seen by the single thread farming out metadata indexing + double meanDocsPerTx = srv.getTrackerStats().getMeanDocsPerTx(); + double meanNodeElaspedIndexTime = srv.getTrackerStats().getMeanNodeElapsedIndexTime(); + nodesToDo = (long)(transactionsToDo * meanDocsPerTx); + remainingTxTimeMillis = (long) (nodesToDo * meanNodeElaspedIndexTime); + } + Date now = new Date(); + Date end = new Date(now.getTime() + remainingTxTimeMillis); + Duration remainingTx = new Duration(now, end); + + NamedList ftsSummary = new SimpleOrderedMap<>(); + long remainingContentTimeMillis = 0; + srv.addFTSStatusCounts(ftsSummary); + long cleanCount = + ofNullable(ftsSummary.get("Node count with FTSStatus Clean")) + .map(Number.class::cast) + .map(Number::longValue) + .orElse(0L); + long dirtyCount = + ofNullable(ftsSummary.get("Node count with FTSStatus Dirty")) + .map(Number.class::cast) + .map(Number::longValue) + .orElse(0L); + long newCount = + ofNullable(ftsSummary.get("Node count with FTSStatus New")) + .map(Number.class::cast) + .map(Number::longValue) + .orElse(0L); + + long nodesInIndex = + ofNullable(coreSummary.get("Alfresco Nodes in Index")) + .map(Number.class::cast) + .map(Number::longValue) + .orElse(0L); + + long contentYetToSee = nodesInIndex > 0 ? nodesToDo * (cleanCount + dirtyCount + newCount)/nodesInIndex : 0; + if (dirtyCount + newCount + contentYetToSee > 0) + { + // We now use the elapsed time as seen by the single thread farming out alc indexing + double meanContentElapsedIndexTime = srv.getTrackerStats().getMeanContentElapsedIndexTime(); + remainingContentTimeMillis = (long) ((dirtyCount + newCount + contentYetToSee) * meanContentElapsedIndexTime); + } + now = new Date(); + end = new Date(now.getTime() + remainingContentTimeMillis); + Duration remainingContent = new Duration(now, end); + coreSummary.add("FTS",ftsSummary); + + Duration txLag = new Duration(lastIndexTxCommitDate, lastTxOnServerDate); + if (lastIndexTxCommitDate.compareTo(lastTxOnServerDate) > 0) + { + txLag = new Duration(); + } + long txLagSeconds = (lastTxCommitTimeOnServer - lastIndexTxCommitTime) / 1000; + if (txLagSeconds < 0) + { + txLagSeconds = 0; + } + + ModelTracker modelTrkr = trackerRegistry.getModelTracker(); + TrackerState modelTrkrState = modelTrkr.getTrackerState(); + coreSummary.add("ModelTracker Active", modelTrkrState.isRunning()); + coreSummary.add("NodeState Publisher Active", trackerState.isRunning()); + + // TX + + coreSummary.add("Last Index TX Commit Time", lastIndexTxCommitTime); + coreSummary.add("Last Index TX Commit Date", lastIndexTxCommitDate); + coreSummary.add("TX Lag", txLagSeconds + " s"); + coreSummary.add("TX Duration", txLag.toString()); + coreSummary.add("Timestamp for last TX on server", lastTxCommitTimeOnServer); + coreSummary.add("Date for last TX on server", lastTxOnServerDate); + coreSummary.add("Id for last TX on server", lastTxIdOnServer); + coreSummary.add("Id for last TX in index", lastIndexedTxId); + coreSummary.add("Approx transactions remaining", transactionsToDo); + coreSummary.add("Approx transaction indexing time remaining", remainingTx.largestComponentformattedString()); + // Stats + + coreSummary.add("Model sync times (ms)", srv.getTrackerStats().getModelTimes().getNamedList(detail, hist, values)); + coreSummary.add("Docs/Tx", srv.getTrackerStats().getTxDocs().getNamedList(detail, hist, values)); + + // Model + + Map> modelErrors = srv.getModelErrors(); + if (modelErrors.size() > 0) + { + NamedList errorList = new SimpleOrderedMap<>(); + for (Map.Entry> modelNameToErrors : modelErrors.entrySet()) + { + errorList.add(modelNameToErrors.getKey(), modelNameToErrors.getValue()); + } + coreSummary.add("Model changes are not compatible with the existing data model and have not been applied", errorList); + } + + report.add(cname, coreSummary); + } + + static void addMasterOrStandaloneCoreSummary(TrackerRegistry trackerRegistry, String cname, boolean detail, boolean hist, boolean values, + InformationServer srv, NamedList report) throws IOException + { + NamedList coreSummary = new SimpleOrderedMap<>(); + coreSummary.addAll((SimpleOrderedMap) srv.getCoreStats()); + + MetadataTracker metaTrkr = trackerRegistry.getTrackerForCore(cname, MetadataTracker.class); + TrackerState metadataTrkrState = metaTrkr.getTrackerState(); + long lastIndexTxCommitTime = metadataTrkrState.getLastIndexedTxCommitTime(); + + long lastIndexedTxId = metadataTrkrState.getLastIndexedTxId(); + long lastTxCommitTimeOnServer = metadataTrkrState.getLastTxCommitTimeOnServer(); + long lastTxIdOnServer = metadataTrkrState.getLastTxIdOnServer(); + Date lastIndexTxCommitDate = new Date(lastIndexTxCommitTime); + Date lastTxOnServerDate = new Date(lastTxCommitTimeOnServer); + long transactionsToDo = lastTxIdOnServer - lastIndexedTxId; + if (transactionsToDo < 0) + { + transactionsToDo = 0; + } + + AclTracker aclTrkr = trackerRegistry.getTrackerForCore(cname, AclTracker.class); + TrackerState aclTrkrState = aclTrkr.getTrackerState(); + long lastIndexChangeSetCommitTime = aclTrkrState.getLastIndexedChangeSetCommitTime(); + long lastIndexedChangeSetId = aclTrkrState.getLastIndexedChangeSetId(); + long lastChangeSetCommitTimeOnServer = aclTrkrState.getLastChangeSetCommitTimeOnServer(); + long lastChangeSetIdOnServer = aclTrkrState.getLastChangeSetIdOnServer(); + Date lastIndexChangeSetCommitDate = new Date(lastIndexChangeSetCommitTime); + Date lastChangeSetOnServerDate = new Date(lastChangeSetCommitTimeOnServer); + long changeSetsToDo = lastChangeSetIdOnServer - lastIndexedChangeSetId; + if (changeSetsToDo < 0) + { + changeSetsToDo = 0; + } + + long nodesToDo = 0; + long remainingTxTimeMillis = 0; + if (transactionsToDo > 0) + { + // We now use the elapsed time as seen by the single thread farming out metadata indexing + double meanDocsPerTx = srv.getTrackerStats().getMeanDocsPerTx(); + double meanNodeElaspedIndexTime = srv.getTrackerStats().getMeanNodeElapsedIndexTime(); + nodesToDo = (long)(transactionsToDo * meanDocsPerTx); + remainingTxTimeMillis = (long) (nodesToDo * meanNodeElaspedIndexTime); + } + Date now = new Date(); + Date end = new Date(now.getTime() + remainingTxTimeMillis); + Duration remainingTx = new Duration(now, end); + + long remainingChangeSetTimeMillis = 0; + if (changeSetsToDo > 0) + { + // We now use the elapsed time as seen by the single thread farming out alc indexing + double meanAclsPerChangeSet = srv.getTrackerStats().getMeanAclsPerChangeSet(); + double meanAclElapsedIndexTime = srv.getTrackerStats().getMeanAclElapsedIndexTime(); + remainingChangeSetTimeMillis = (long) (changeSetsToDo * meanAclsPerChangeSet * meanAclElapsedIndexTime); + } + now = new Date(); + end = new Date(now.getTime() + remainingChangeSetTimeMillis); + Duration remainingChangeSet = new Duration(now, end); + + NamedList ftsSummary = new SimpleOrderedMap<>(); + long remainingContentTimeMillis = 0; + srv.addFTSStatusCounts(ftsSummary); + long cleanCount = + ofNullable(ftsSummary.get("Node count with FTSStatus Clean")) + .map(Number.class::cast) + .map(Number::longValue) + .orElse(0L); + long dirtyCount = + ofNullable(ftsSummary.get("Node count with FTSStatus Dirty")) + .map(Number.class::cast) + .map(Number::longValue) + .orElse(0L); + long newCount = + ofNullable(ftsSummary.get("Node count with FTSStatus New")) + .map(Number.class::cast) + .map(Number::longValue) + .orElse(0L); + + long nodesInIndex = + ofNullable(coreSummary.get("Alfresco Nodes in Index")) + .map(Number.class::cast) + .map(Number::longValue) + .orElse(0L); + + long contentYetToSee = nodesInIndex > 0 ? nodesToDo * (cleanCount + dirtyCount + newCount)/nodesInIndex : 0; + if (dirtyCount + newCount + contentYetToSee > 0) + { + // We now use the elapsed time as seen by the single thread farming out alc indexing + double meanContentElapsedIndexTime = srv.getTrackerStats().getMeanContentElapsedIndexTime(); + remainingContentTimeMillis = (long) ((dirtyCount + newCount + contentYetToSee) * meanContentElapsedIndexTime); + } + now = new Date(); + end = new Date(now.getTime() + remainingContentTimeMillis); + Duration remainingContent = new Duration(now, end); + coreSummary.add("FTS",ftsSummary); + + Duration txLag = new Duration(lastIndexTxCommitDate, lastTxOnServerDate); + if (lastIndexTxCommitDate.compareTo(lastTxOnServerDate) > 0) + { + txLag = new Duration(); + } + long txLagSeconds = (lastTxCommitTimeOnServer - lastIndexTxCommitTime) / 1000; + if (txLagSeconds < 0) + { + txLagSeconds = 0; + } + + Duration changeSetLag = new Duration(lastIndexChangeSetCommitDate, lastChangeSetOnServerDate); + if (lastIndexChangeSetCommitDate.compareTo(lastChangeSetOnServerDate) > 0) + { + changeSetLag = new Duration(); + } + long changeSetLagSeconds = (lastChangeSetCommitTimeOnServer - lastIndexChangeSetCommitTime) / 1000; + if (txLagSeconds < 0) + { + txLagSeconds = 0; + } + + ContentTracker contentTrkr = trackerRegistry.getTrackerForCore(cname, ContentTracker.class); + TrackerState contentTrkrState = contentTrkr.getTrackerState(); + // Leave ModelTracker out of this check, because it is common + boolean aTrackerIsRunning = aclTrkrState.isRunning() || metadataTrkrState.isRunning() + || contentTrkrState.isRunning(); + coreSummary.add("Active", aTrackerIsRunning); + + ModelTracker modelTrkr = trackerRegistry.getModelTracker(); + TrackerState modelTrkrState = modelTrkr.getTrackerState(); + coreSummary.add("ModelTracker Active", modelTrkrState.isRunning()); + coreSummary.add("ContentTracker Active", contentTrkrState.isRunning()); + coreSummary.add("MetadataTracker Active", metadataTrkrState.isRunning()); + coreSummary.add("AclTracker Active", aclTrkrState.isRunning()); + + // TX + + coreSummary.add("Last Index TX Commit Time", lastIndexTxCommitTime); + coreSummary.add("Last Index TX Commit Date", lastIndexTxCommitDate); + coreSummary.add("TX Lag", txLagSeconds + " s"); + coreSummary.add("TX Duration", txLag.toString()); + coreSummary.add("Timestamp for last TX on server", lastTxCommitTimeOnServer); + coreSummary.add("Date for last TX on server", lastTxOnServerDate); + coreSummary.add("Id for last TX on server", lastTxIdOnServer); + coreSummary.add("Id for last TX in index", lastIndexedTxId); + coreSummary.add("Approx transactions remaining", transactionsToDo); + coreSummary.add("Approx transaction indexing time remaining", remainingTx.largestComponentformattedString()); + + // Change set + + coreSummary.add("Last Index Change Set Commit Time", lastIndexChangeSetCommitTime); + coreSummary.add("Last Index Change Set Commit Date", lastIndexChangeSetCommitDate); + coreSummary.add("Change Set Lag", changeSetLagSeconds + " s"); + coreSummary.add("Change Set Duration", changeSetLag.toString()); + coreSummary.add("Timestamp for last Change Set on server", lastChangeSetCommitTimeOnServer); + coreSummary.add("Date for last Change Set on server", lastChangeSetOnServerDate); + coreSummary.add("Id for last Change Set on server", lastChangeSetIdOnServer); + coreSummary.add("Id for last Change Set in index", lastIndexedChangeSetId); + coreSummary.add("Approx change sets remaining", changeSetsToDo); + coreSummary.add("Approx change set indexing time remaining", + remainingChangeSet.largestComponentformattedString()); + + coreSummary.add("Approx content indexing time remaining", + remainingContent.largestComponentformattedString()); + + // Stats + + coreSummary.add("Model sync times (ms)", + srv.getTrackerStats().getModelTimes().getNamedList(detail, hist, values)); + coreSummary.add("Acl index time (ms)", + srv.getTrackerStats().getAclTimes().getNamedList(detail, hist, values)); + coreSummary.add("Node index time (ms)", + srv.getTrackerStats().getNodeTimes().getNamedList(detail, hist, values)); + coreSummary.add("Docs/Tx", srv.getTrackerStats().getTxDocs().getNamedList(detail, hist, values)); + coreSummary.add("Doc Transformation time (ms)", srv.getTrackerStats().getDocTransformationTimes() + .getNamedList(detail, hist, values)); + + // Model + + Map> modelErrors = srv.getModelErrors(); + if (modelErrors.size() > 0) + { + NamedList errorList = new SimpleOrderedMap<>(); + for (Map.Entry> modelNameToErrors : modelErrors.entrySet()) + { + errorList.add(modelNameToErrors.getKey(), modelNameToErrors.getValue()); + } + coreSummary.add("Model changes are not compatible with the existing data model and have not been applied", + errorList); + } + + report.add(cname, coreSummary); + } +} \ No newline at end of file diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/InformationServer.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/InformationServer.java index 13d4173f3..ec64e11b6 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/InformationServer.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/InformationServer.java @@ -179,4 +179,6 @@ public interface InformationServer extends InformationServerCollectionProvider String getHostName(); String getBaseUrl(); + + void flushContentStore() throws IOException; } diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/SolrInformationServer.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/SolrInformationServer.java index d4d81df1f..e545e35b4 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/SolrInformationServer.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/SolrInformationServer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Alfresco Software Limited. + * Copyright (C) 2019 Alfresco Software Limited. * * This file is part of Alfresco * @@ -178,14 +178,14 @@ import org.springframework.extensions.surf.util.I18NUtil; import org.springframework.util.FileCopyUtils; /** - * This is the Solr4 implementation of the information server (index). + * This is the Apache Solr implementation of the information server (index). + * * @author Ahmed Owian * @since 5.0 */ public class SolrInformationServer implements InformationServer { private final static Log LOGGER = new Log(SolrInformationServer.class); - private final static long TWO_MINUTES = 120000; private static final String NO_SITE = "_REPOSITORY_"; private static final String SHARED_FILES = "_SHARED_FILES_"; @@ -200,23 +200,23 @@ public class SolrInformationServer implements InformationServer public static final String AND = " AND "; public static final String OR = " OR "; - public static final String REQUEST_HANDLER_ALFRESCO_FULL_TEXT_SEARCH = "/afts"; - public static final String REQUEST_HANDLER_NATIVE = "/native"; - public static final String REQUEST_HANDLER_ALFRESCO = "/alfresco"; - public static final String REQUEST_HANDLER_SELECT = "/select"; - public static final String REQUEST_HANDLER_GET = "/get"; - public static final String RESPONSE_DEFAULT_IDS = "response"; - public static final String RESPONSE_DEFAULT_ID = "doc"; + //public static final String REQUEST_HANDLER_ALFRESCO_FULL_TEXT_SEARCH = "/afts"; + private static final String REQUEST_HANDLER_NATIVE = "/native"; + //public static final String REQUEST_HANDLER_ALFRESCO = "/alfresco"; + //public static final String REQUEST_HANDLER_SELECT = "/select"; + static final String REQUEST_HANDLER_GET = "/get"; + private static final String RESPONSE_DEFAULT_IDS = "response"; + static final String RESPONSE_DEFAULT_ID = "doc"; - public static final String PREFIX_ERROR = "ERROR-"; + static final String PREFIX_ERROR = "ERROR-"; public static final String DOC_TYPE_NODE = "Node"; - public static final String DOC_TYPE_UNINDEXED_NODE = "UnindexedNode"; - public static final String DOC_TYPE_ERROR_NODE = "ErrorNode"; + private static final String DOC_TYPE_UNINDEXED_NODE = "UnindexedNode"; + private static final String DOC_TYPE_ERROR_NODE = "ErrorNode"; public static final String DOC_TYPE_ACL = "Acl"; public static final String DOC_TYPE_TX = "Tx"; public static final String DOC_TYPE_ACL_TX = "AclTx"; - public static final String DOC_TYPE_STATE = "State"; + private static final String DOC_TYPE_STATE = "State"; public static final String SOLR_HOST = "solr.host"; public static final String SOLR_PORT = "solr.port"; @@ -235,7 +235,6 @@ public class SolrInformationServer implements InformationServer private final TrackerStats trackerStats = new TrackerStats(this); private final AlfrescoSolrDataModel dataModel; private final SolrContentStore solrContentStore; - private final String alfrescoVersion; private final boolean transformContent; private final boolean recordUnindexedNodes; private final long lag; @@ -271,7 +270,7 @@ public class SolrInformationServer implements InformationServer protected enum FTSStatus {New, Dirty, Clean} - class DocListCollector implements Collector, LeafCollector + static class DocListCollector implements Collector, LeafCollector { private IntArrayList docs = new IntArrayList(); private int docBase; @@ -303,7 +302,7 @@ public class SolrInformationServer implements InformationServer } } - class TxnCacheFilter extends DelegatingCollector + static class TxnCacheFilter extends DelegatingCollector { private NumericDocValues currentLongs; private Map txnLRU; @@ -330,7 +329,7 @@ public class SolrInformationServer implements InformationServer } } - class TxnCollector extends DelegatingCollector + static class TxnCollector extends DelegatingCollector { private NumericDocValues currentLongs; private long txnFloor; @@ -372,7 +371,7 @@ public class SolrInformationServer implements InformationServer } } - class LRU extends LinkedHashMap + static class LRU extends LinkedHashMap { private int maxSize; @@ -395,7 +394,7 @@ public class SolrInformationServer implements InformationServer boolean isDefinitionExists(QName qName); } - abstract class TransactionInfoReporter + static abstract class TransactionInfoReporter { protected final IndexHealthReport report; @@ -428,7 +427,6 @@ public class SolrInformationServer implements InformationServer this.solrContentStore = solrContentStore; Properties p = core.getResourceLoader().getCoreProperties(); - alfrescoVersion = p.getProperty("alfresco.version", "Unknown"); transformContent = Boolean.parseBoolean(p.getProperty("alfresco.index.transformContent", "true")); recordUnindexedNodes = Boolean.parseBoolean(p.getProperty("alfresco.recordUnindexedNodes", "true")); lag = Integer.parseInt(p.getProperty("alfresco.lag", "1000")); @@ -505,11 +503,6 @@ public class SolrInformationServer implements InformationServer } } - public String getAlfrescoVersion() - { - return this.alfrescoVersion; - } - @Override public void afterInitModels() { @@ -1547,7 +1540,7 @@ public class SolrInformationServer implements InformationServer StringPropertyValue pValue = (StringPropertyValue) properties.get(ContentModel.PROP_IS_INDEXED); if (pValue != null) { - boolean isIndexed = Boolean.valueOf(pValue.getValue()); + boolean isIndexed = Boolean.parseBoolean(pValue.getValue()); if (!isIndexed) { LOGGER.debug("Clearing unindexed"); @@ -1902,7 +1895,7 @@ public class SolrInformationServer implements InformationServer StringPropertyValue pValue = (StringPropertyValue) properties.get(ContentModel.PROP_IS_INDEXED); if (pValue != null) { - boolean isIndexed = Boolean.valueOf(pValue.getValue()); + boolean isIndexed = Boolean.parseBoolean(pValue.getValue()); if (!isIndexed) { LOGGER.debug("Clearing unindexed"); @@ -1958,7 +1951,7 @@ public class SolrInformationServer implements InformationServer } } - private void addToNewDocAndCache(NodeMetaData nodeMetaData, SolrInputDocument newDoc) throws IOException + private void addToNewDocAndCache(NodeMetaData nodeMetaData, SolrInputDocument newDoc) { addFieldsToDoc(nodeMetaData, newDoc); SolrInputDocument cachedDoc = null; @@ -2232,7 +2225,7 @@ public class SolrInformationServer implements InformationServer .get(ContentModel.PROP_IS_CONTENT_INDEXED); if (pValue != null) { - boolean isIndexed = Boolean.valueOf(pValue.getValue()); + boolean isIndexed = Boolean.parseBoolean(pValue.getValue()); if (!isIndexed) { isContentIndexed = false; @@ -2374,13 +2367,19 @@ public class SolrInformationServer implements InformationServer String transformationStatusFieldName = getSolrFieldNameForContentPropertyMetadata(propertyQName, AlfrescoSolrDataModel.ContentFieldType.TRANSFORMATION_STATUS); - newDoc.addField(transformationStatusFieldName, cachedDoc.getFieldValue(transformationStatusFieldName)); + if (transformationStatusFieldName != null){ + newDoc.addField(transformationStatusFieldName, cachedDoc.getFieldValue(transformationStatusFieldName)); + } String transformationExceptionFieldName = getSolrFieldNameForContentPropertyMetadata(propertyQName, AlfrescoSolrDataModel.ContentFieldType.TRANSFORMATION_EXCEPTION); - newDoc.addField(transformationExceptionFieldName, cachedDoc.getFieldValue(transformationExceptionFieldName)); - String transformationTimeFieldName = getSolrFieldNameForContentPropertyMetadata(propertyQName, + if (transformationExceptionFieldName != null){ + newDoc.addField(transformationExceptionFieldName, cachedDoc.getFieldValue(transformationExceptionFieldName)); + } + String transformationTimeFieldName = getSolrFieldNameForContentPropertyMetadata(propertyQName, AlfrescoSolrDataModel.ContentFieldType.TRANSFORMATION_TIME); - newDoc.addField(transformationTimeFieldName, cachedDoc.getFieldValue(transformationTimeFieldName)); + if (transformationTimeFieldName != null){ + newDoc.addField(transformationTimeFieldName, cachedDoc.getFieldValue(transformationTimeFieldName)); + } // Gets the new content docid and compares to that of the cachedDoc to mark the content as clean/dirty String fldName = getSolrFieldNameForContentPropertyMetadata(propertyQName, @@ -2393,7 +2392,7 @@ public class SolrInformationServer implements InformationServer if(cachedDoc.getFieldValue(fldName) != null) { - long cachedDocContentDocid = Long.valueOf(String.valueOf(cachedDoc.getFieldValue(fldName))); + long cachedDocContentDocid = Long.parseLong(String.valueOf(cachedDoc.getFieldValue(fldName))); long currentContentDocid = contentPropertyValue.getId(); // If we have used out of date content we mark it as dirty // Otherwise we leave it alone - it could already be marked as dirty/New and require an update @@ -3584,7 +3583,7 @@ public class SolrInformationServer implements InformationServer field, 1); // Min count of 1 ensures that the id returned is in the index for (Map.Entry idCount : idCounts) { - long idInIndex = Long.valueOf(idCount.getKey()); + long idInIndex = Long.parseLong(idCount.getKey()); // Only looks at facet values that fit the query if (batchStartId <= idInIndex && idInIndex <= batchEndId) @@ -3892,4 +3891,10 @@ public class SolrInformationServer implements InformationServer } return searchers; } + + @Override + public void flushContentStore() throws IOException + { + solrContentStore.flushChangeSet(); + } } \ No newline at end of file diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/AccessMode.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/AccessMode.java new file mode 100644 index 000000000..42873d17b --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/AccessMode.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.solr.content; + +import org.alfresco.solr.client.NodeMetaData; +import org.apache.solr.common.SolrInputDocument; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Behavioural interface which indicates the type of content store access mode for the owning node. + * A replication interaction is composed at least by 2 roles: a master and a slave. + * + * Despite the same interface, the behaviour of the content store management changes depending on the node kind: + * + *
    + *
  • Master Node: READ + WRITE + Changes tracking. Writes and changes tracking are a direct consequence of the "indexing" nature of the master node.
  • + *
  • Slave Node: READ ONLY (i.e. never write: changes are applied on the master and replicated on slaves)
  • + *
+ * + * Important: the owning entity *is not* the SolrCore instance: the Content store is shared across all cores of + * A node can transit from Read to Write mode + * + * @author Andrea Gazzarini + * @since 1.5 + */ +interface AccessMode extends Closeable +{ + /** + * Returns the last persisted content store version. + * + * @return the last persisted content store version, SolrContentStore#NO_VERSION_AVAILABLE in case the version isn't available. + */ + long getLastCommittedVersion(); + + /** + * Persists the last committed version on the hosting node. + * Note that this is tipically valid only on slave node, because the master already manages the content store version on + * a persistent storage, so it doesn't need to call this method. + * + * @param version the last committed content store version. + */ + void setLastCommittedVersion(long version); + + Map>> getChanges(long version); + + /** + * Stores a {@link SolrInputDocument} into Alfresco solr content store. + * + * @param tenant the owning tenant. + * @param dbId the document DBID + * @param doc the document itself. + */ + void storeDocOnSolrContentStore(String tenant, long dbId, SolrInputDocument doc); + + /** + * Stores a {@link SolrInputDocument} into Alfresco solr content store. + * + * @param nodeMetaData the node metadata. + * @param doc the document itself. + */ + void storeDocOnSolrContentStore(NodeMetaData nodeMetaData, SolrInputDocument doc); + + /** + * Removes a node from the content store. + * + * @param nodeMetaData the node metadata. + */ + void removeDocFromContentStore(NodeMetaData nodeMetaData); + + /** + * Flushes pending changesets. + * + * @throws IOException in case of I/O failure. + */ + void flushChangeSet() throws IOException; + + /** + * Tries to transition from this mode to the readOnlyMode. + */ + void switchOnReadOnlyMode(); + + /** + * Tries to transition from this mode to the read/write mode. + */ + void switchOnReadWriteMode(); +} \ No newline at end of file diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/ChangeSet.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/ChangeSet.java new file mode 100644 index 000000000..21eee54d5 --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/ChangeSet.java @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.solr.content; + +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.Collections.emptySet; +import static java.util.Optional.ofNullable; + +import org.alfresco.util.Pair; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.FieldDoc; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.SearcherManager; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.SortedNumericSortField; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.FSDirectory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; + +/** + * Stores and manages changes occurred in a content store. + * + * Note this entity is part of the content store only when it is set on writable mode (i.e. only when the hosting node + * is a standalone instance or it is a master node). + * + * That means if the hosting node has at least one standalone core or one master node, then the managed content store + * will be set in write mode and therefore a {@link ChangeSet} instance will track all changes applied to it. + * + * In that mode, each change is associated to a given (incremental) version; changes consist of adds/updates and deletes. + * Each time the {@link org.alfresco.solr.tracker.CommitTracker} executes a commit in the main index, all content store + * changes accumulated in the meantime in the current {@link ChangeSet} instance are flushed in an auxiliary Lucene index + * and therefore persisted. + * + * On the other side, when all cores on a given node are configured as slaves, the content store is in read-only mode, + * and there's no need to track any change (i.e. changes are coming from master through the replication procedure). + * + * As a side note: we can have a persistent or transient {@link ChangeSet} instance. The first one is used to manage, as + * the name suggests, in a persistent way the content store changes accumulated during the indexing operations (see the + * description above about its behaviour in master nodes). + * + * The second one is used instead for computing/reducing the "merged" list of changes that we need to communicate to slave nodes: + * as part of the replication mechanism, a slave communicates its last synched content store version; on the master side, + * we need to compute and communicate back all changes that the slave needs to apply since that version in order to be in + * synch with master. + * + * @author Andrea Gazzarini + * @since 1.5 + */ +public class ChangeSet implements AutoCloseable +{ + private final static Logger LOGGER = LoggerFactory.getLogger(ChangeSet.class); + private final static ChangeSet EMPTY_CHANGESET = new ChangeSet.Builder().empty().build(); + + /** + * Builds class for creating {@link ChangeSet} instances. + * The builder is here for creating three kind of {@link ChangeSet} instances: + * + *
    + *
  • Persistent: used for tracking and persisting content store changes.
  • + *
  • Transient: used for computing the merged list of changes a slave should apply for being in synch with master.
  • + *
  • Empty: an immutable "NullObject" used for denoting an empty {@link ChangeSet} instance.
  • + *
+ * + * @see Null Object Design Pattern + */ + public static class Builder + { + private String root; + private boolean immutable; + + /** + * Adds a content store root folder to this builder. + * This automatically implies we are creating a persistent {@link ChangeSet} instance. + * + * @param root the absolute path of the content store root folder. + * @return this builder, for being used in a fluent mode. + */ + Builder withContentStoreRoot(String root) + { + if (root == null) throw new IllegalArgumentException("Unable to build the Changeset structures with a null content store root folder."); + if (!new File(root).canWrite()) throw new IllegalArgumentException("Unable to build the Changeset structures with a non-writeable content store root folder."); + + this.root = root; + return this; + } + + /** + * We are interested in building an empty, immutable {@link ChangeSet} instance. + * + * @return this builder, for being used in a fluent mode. + */ + Builder empty() + { + this.root = null; + this.immutable = true; + return this; + } + + /** + * Builds the product of this builder (i.e. the {@link ChangeSet} instance). + * + * @return the product of this builder (i.e. the {@link ChangeSet} instance). + */ + ChangeSet build() + { + if (root == null) + { + // Creates a transient changeset (no persistence); mainly used for reducing the changes during replication. + return new ChangeSet( + immutable ? emptySet() : new HashSet<>(), + immutable ? emptySet() : new HashSet<>()); + } + + IndexWriter writer = null; + try + { + File indexDirectory = new File(root, CHANGESETS_ROOT_FOLDER_NAME); + + writer = new IndexWriter(FSDirectory.open(indexDirectory.toPath()), new IndexWriterConfig()); + writer.commit(); + + final SearcherManager searcher = new SearcherManager(writer, null); + LOGGER.info("ContentStore Changeset index has been correctly mounted on {}", indexDirectory.getAbsolutePath()); + + return new ChangeSet( + searcher, + writer, + immutable ? emptySet() : new HashSet<>(), + immutable ? emptySet() : new HashSet<>()); + } + catch (Exception exception) + { + ofNullable(writer).ifPresent(ChangeSet::silentyClose); + throw new IllegalArgumentException("Unable to create a ContentStore ChangeSet data structure. See further details in the stacktrack below.", exception); + } + } + } + + private final static String VERSION_FIELD_NAME = "version"; + private final static String RVERSION_FIELD_NAME = "rversion"; + private final static String ADDS_FIELD_NAME = "adds"; + private final static String DELETES_FIELD_NAME = "deletes"; + final static String CHANGESETS_ROOT_FOLDER_NAME = "changeSets"; + + Set deletes; + Set adds; + + SearcherManager searcher; + private final IndexWriter writer; + + Query selectEverything = new MatchAllDocsQuery(); + + /** + * Builds a new transient {@link ChangeSet} with the given Lucene facades. + * + * @param deletesContainer the container which will hold the deletes. + * @param addsContainer the container which will hold the adds/updates. + */ + private ChangeSet( + final Set deletesContainer, + final Set addsContainer) + { + this(null, null, deletesContainer, addsContainer); + } + + /** + * Builds a new {@link ChangeSet} with the given Lucene facades. + * + * @param searcher the searcher reference (actually a {@link SearcherManager} instance instead of dealing with {@link IndexSearcher} directly. + * @param writer the {@link IndexWriter} instance used for persisting the content store changes. + * @param deletesContainer the container which will hold the deletes. + * @param addsContainer the container which will hold the adds/updates. + */ + private ChangeSet( + final SearcherManager searcher, + final IndexWriter writer, + final Set deletesContainer, + final Set addsContainer) + { + this.searcher = searcher; + this.writer = writer; + this.deletes = deletesContainer; + this.adds = addsContainer; + } + + /** + * Records a delete change. + * + * @param path the relative path of the file which has been deleted. + */ + synchronized void delete(String path) + { + adds.remove(path); + deletes.add(path); + + LOGGER.debug("ContentStore change recorded: item {} has been deleted.", path); + + debugPendingChanges(); + } + + /** + * Records an add or update change. + * + * @param path the relative path of the file which has been updated or added. + */ + synchronized void addOrReplace(String path) + { + deletes.remove(path); + adds.add(path); + + LOGGER.debug("ContentStore change recorded: item {} has been added/updated.", path); + + debugPendingChanges(); + } + + /** + * Flushes all pending collected content store changes. + * + * @throws IOException in case of I/O failure. + */ + void flush() throws IOException + { + // No ops if this is a transient changeset + if (searcher == null || writer == null) { + return; + } + + if (adds.isEmpty() && deletes.isEmpty()) + { + LOGGER.debug("No changes in contentstore to flush"); + return; + } + + final long version = System.currentTimeMillis(); + + LOGGER.debug("About to add a new Changeset entry (version = {}, deletes = {}, adds = {})", version, deletes.size(), adds.size()); + + final Document document = new Document(); + document.add(new NumericDocValuesField(VERSION_FIELD_NAME, version)); + document.add(new LongPoint(RVERSION_FIELD_NAME, version)); + + Set tmpDel; + Set tmpAdd; + + synchronized (this) { + tmpDel = deletes; + deletes = new HashSet<>(); + + tmpAdd = adds; + adds = new HashSet<>(); + } + + tmpDel.stream() + .map(item -> new StoredField(DELETES_FIELD_NAME, item)) + .forEach(document::add); + + tmpAdd.stream() + .map(item -> new StoredField(ADDS_FIELD_NAME, item)) + .forEach(document::add); + + writer.addDocument(document); + writer.commit(); + searcher.maybeRefresh(); + + LOGGER.debug("New Changeset entry have been added (version = {}, deletes = {}, adds = {})", version, deletes.size(), adds.size()); + } + + /** + * Sanity check for making sure the requestor sent a valid and known content store version. + * + * @param version the content store version (sent by a requestor) + * @return true if the given version is unknown, that is, is not part of the content store version history. + */ + boolean isUnknownVersion(long version) + { + try + { + TopDocs hits = searcher().search(LongPoint.newExactQuery(RVERSION_FIELD_NAME, version), 1); + return hits.totalHits != 1; + } + catch(Exception exception) + { + LOGGER.error("Unable to check the requested version ({}) in the local versioning store. See further details in the stacktrace below.", version, exception); + return true; + } + } + + @Override + public void close() + { + ofNullable(writer).ifPresent(ChangeSet::silentyClose); + ofNullable(searcher).ifPresent(ChangeSet::silentyClose); + } + + /** + * Returns the last persisted content store version. + * + * @return the last persisted content store version, SolrContentStore#NO_VERSION_AVAILABLE in case the version isn't available. + */ + long getLastCommittedVersion() + { + try + { + TopDocs hits = searcher().search( + selectEverything, + 1, + new Sort(new SortedNumericSortField(VERSION_FIELD_NAME, SortField.Type.LONG, true)), + false, + false); + + return ofNullable(hits.scoreDocs) + .filter(docs -> docs.length > 0) + .map(docs -> ((FieldDoc) docs[0]).fields) + .filter(fields -> fields.length > 0) + .map(fields -> (Long)fields[0]) + .orElse(SolrContentStore.NO_VERSION_AVAILABLE); + } + catch(Exception exception) + { + LOGGER.error("Unable to retrieve the last committed content store changeset version. " + + "As consequence of that a dummy value of " + SolrContentStore.NO_VERSION_AVAILABLE + + " will be returned. See further details in the stacktrack below.", + exception); + return SolrContentStore.NO_VERSION_AVAILABLE; + } + } + + /** + * Returns the content store changes (adds / deletes) since the given version (exclusive). + * + * @param version the start offset version (exclusive). + * @return the content store changes (adds / deletes) since the given version (exclusive). + */ + public ChangeSet since(long version) + { + try + { + Query query = LongPoint.newRangeQuery(RVERSION_FIELD_NAME, Math.addExact(version, 1), Long.MAX_VALUE); + TopDocs hits = searcher().search( + query, + 100, + new Sort(new SortedNumericSortField(VERSION_FIELD_NAME, SortField.Type.LONG)), + false, + false); + + final BiFunction,List>, ChangeSet> accumulator = + (partial, nth) -> { + final List nthDeletes = nth.getFirst(); + final List nthAdds = nth.getSecond(); + + nthDeletes.forEach(partial::delete); + nthAdds.forEach(partial::addOrReplace); + + return partial; + }; + + final BinaryOperator combiner = (c1, c2) -> { + c1.deletes.forEach(c2::delete); + c1.adds.forEach(c2::addOrReplace); + return c1; + }; + + return stream(hits.scoreDocs) + .map(this::toDoc) + .map(doc -> + new Pair<>( + asList(doc.getValues(DELETES_FIELD_NAME)), + asList(doc.getValues(ADDS_FIELD_NAME)))) + .reduce(new ChangeSet.Builder().build(), accumulator, combiner); + } + catch(Exception exception) + { + LOGGER.error("Unable to retrieve the changeset since version {}. " + + "As consequence of that an empty result will be returned. " + + "See further details in the stacktrack below.", + version, + exception); + return EMPTY_CHANGESET; + } + } + + private Document toDoc(ScoreDoc hit) + { + try + { + return searcher().doc(hit.doc); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + private void debugPendingChanges() + { + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("ContentStore pending deletes: " + deletes); + LOGGER.debug("ContentStore pending adds/updates: " + adds); + } + } + + /** + * Silently close (i.e. without any exception re-throwing) the incoming resource. + * + * @param resource the closeable resource. + */ + private static void silentyClose(Closeable resource) + { + try + { + resource.close(); + } + catch (Exception exception) + { + LOGGER.error("Unable to properly close the resource instance {}. See further details in the stacktrace below.", resource, exception); + } + } + + private IndexSearcher searcher() throws IOException + { + return searcher.acquire(); + } +} \ No newline at end of file diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/InitialisableAccessMode.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/InitialisableAccessMode.java new file mode 100644 index 000000000..78a0fea2e --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/InitialisableAccessMode.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.solr.content; + +/** + * {@link AccessMode} specialisation for those modes that need some kind of initialisation. + * + * @author Andrea Gazzarini + * @since 1.5 + */ +public interface InitialisableAccessMode extends AccessMode +{ + /** + * Initialises this access mode instance. + */ + void init(); +} \ No newline at end of file diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrContentStore.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrContentStore.java index d66d3c33b..1a3694a11 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrContentStore.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrContentStore.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2014 Alfresco Software Limited. + * Copyright (C) 2005-2019 Alfresco Software Limited. * * This file is part of Alfresco * @@ -18,52 +18,489 @@ */ package org.alfresco.solr.content; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - import org.alfresco.repo.content.ContentContext; -import org.alfresco.repo.content.ContentStore; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.solr.AlfrescoSolrDataModel; import org.alfresco.solr.client.NodeMetaData; import org.alfresco.solr.config.ConfigUtil; +import org.alfresco.solr.handler.AlfrescoReplicationHandler; import org.apache.commons.io.FileUtils; import org.apache.lucene.util.BytesRef; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.util.JavaBinCodec; import org.apache.solr.core.SolrResourceLoader; +import org.apache.solr.handler.SnapShooter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.Closeable; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Optional.of; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toList; +import static org.alfresco.solr.content.SolrContentUrlBuilder.FILE_EXTENSION; +import static org.alfresco.solr.content.SolrContentUrlBuilder.logger; + /** * A content store specific to SOLR's requirements: The URL is generated from a * set of properties such as: + * *
    - *
  • ACL ID
  • - *
  • DB ID
  • - *
  • Other metadata
  • + *
  • ACL ID
  • + *
  • DB ID
  • + *
  • Other metadata
  • *
- * The URL, if not known, can be reliably regenerated using the - * {@link SolrContentUrlBuilder}. - * + * + * The URL, if not known, can be reliably regenerated using the {@link SolrContentUrlBuilder}. + *
+ * + * Since version 1.5 this class acts as a logical singleton: there will be only one instance per node. + * That reflects exactly what we physically have in the filesystem (i.e. there's only one content store per node). + * + * That unique instance is created at startup in {@link org.alfresco.solr.AlfrescoCoreAdminHandler} and then passed to + * each registered core (see {@link org.alfresco.solr.lifecycle.SolrCoreLoadListener}) + * + * The State pattern implemented by means of the {@link AccessMode} interface allows for a given {@link SolrContentStore} instance to act: + * + *
    + *
  • in READ/WRITE mode: when at least one core of the hosting node is a master or it is a standalone shard/instance.
  • + *
  • in READ ONLY mode: when all cores of the hosting node are slaves.
  • + *
+ * + * The Finite State Machine (FST) provides three possible states: Initial, Read Only, Read/Write. + * The allowed transitions are: + * + *
    + *
  • Initial -> ReadOnly: a slave core has been registered, the content store hasn't been yet initialised.
  • + *
  • + * ReadOnly -> Read/Write: this scenario is unusual because we should have on the same instance a mixed set of cores + * (some master/standalone, some slaves). It happens when a first slave core registers and causes the transition + * described in the first point (initial -> read only). Then a master or standalone core registers so we need to + * move from a read only to a complete readable/writable managed content store. + *
  • + *
  • Initial -> Read/Write: a master or standalone core has been registered, and the content store hasn't been yet initialised.
  • + *
+ * + * Note the following transitions are not allowed: + * + *
    + *
  • Coming back to Initial state: once it has been initialised the Content Store cannot return back to the "Initial" state.
  • + *
  • + * Read/Write -> ReadOnly: if at least one master or standalone core registers, the content store is permanently moved in Read/Write mode. + * So even a further slave node registers (not very usually as described above) the content store remains in RW mode. + *
  • + *
+ * * @author Derek Hulley * @author Michael Suzuki - * @since 5.0 + * @author Andrea Gazzarini + * @since 1.5 + * @see org.alfresco.solr.lifecycle.SolrCoreLoadListener + * @see State Pattern */ -public class SolrContentStore implements ContentStore +public final class SolrContentStore implements Closeable, AccessMode { - protected final static Logger log = LoggerFactory.getLogger(SolrContentStore.class); + private final static Logger LOGGER = LoggerFactory.getLogger(SolrContentStore.class); + + public static final long NO_VERSION_AVAILABLE = -1L; + public static final long NO_CONTENT_STORE_REPLICATION_REQUIRED = -2L; + static final String CONTENT_STORE = "contentstore"; static final String SOLR_CONTENT_DIR = "solr.content.dir"; + public static final String INFO = "info"; + public static final String FULL_REPLICATION = "full-replication"; + public static final String DELETES = "deletes"; + public static final String ADDS = "adds"; + + private final Predicate onlyDatafiles = file -> file.isFile() && file.getName().endsWith(FILE_EXTENSION); + private final String root; + /** - * Constructor. - * @param solrHome + * Used for denoting the very beginning state, when the {@link SolrContentStore} has been created + * but we don't know (yet) which is the role played by the cores that will register on the hosting Solr node. + */ + AccessMode notYetSet = new AccessMode() + { + @Override + public long getLastCommittedVersion() + { + throw new IllegalStateException("ContentStore hasn't been properly initialised."); + } + + @Override + public void setLastCommittedVersion(long version) + { + throw new IllegalStateException("ContentStore hasn't been properly initialised."); + } + + @Override + public Map>> getChanges(long version) + { + throw new IllegalStateException("ContentStore hasn't been properly initialised."); + } + + @Override + public void storeDocOnSolrContentStore(String tenant, long dbId, SolrInputDocument doc) + { + throw new IllegalStateException("ContentStore hasn't been properly initialised."); + } + + @Override + public void storeDocOnSolrContentStore(NodeMetaData nodeMetaData, SolrInputDocument doc) + { + throw new IllegalStateException("ContentStore hasn't been properly initialised."); + } + + @Override + public void removeDocFromContentStore(NodeMetaData nodeMetaData) + { + throw new IllegalStateException("ContentStore hasn't been properly initialised."); + } + + @Override + public void flushChangeSet() + { + throw new IllegalStateException("ContentStore hasn't been properly initialised."); + } + + @Override + public void switchOnReadOnlyMode() + { + logger.info("Switching the content store to ReadOnly mode."); + currentAccessMode = readOnly; + } + + @Override + public void switchOnReadWriteMode() + { + logger.info("Switching the content store to Read/Write mode."); + readWrite.init(); + currentAccessMode = readWrite; + } + + @Override + public void close() + { + // Nothing to close here + } + }; + + final InitialisableAccessMode readOnly = new InitialisableAccessMode() + { + @Override + public void init() + { + // Nothing to be done here... + } + + @Override + public void switchOnReadWriteMode() + { + logger.info("Switching from ReadOnly to Read/Write Content Store."); + + readWrite.init(); + currentAccessMode = readWrite; + + logger.info("Switching from ReadOnly to Read/Write Content Store."); + } + + @Override + public void switchOnReadOnlyMode() + { + logger.info("The content store is already in ReadOnly mode so this call won't have any effect."); + } + + @Override + public long getLastCommittedVersion() + { + try + { + return Files.readAllLines(Paths.get(root, ".version")) + .stream() + .map(Long::parseLong) + .findFirst() + .orElse(NO_VERSION_AVAILABLE); + } + catch (Exception e) + { + return NO_VERSION_AVAILABLE; + } + } + + @Override + public void setLastCommittedVersion(long version) + { + + File tmpFile = new File(root, ".version-" + new SimpleDateFormat(SnapShooter.DATE_FMT, Locale.ROOT).format(new Date())); + try + { + FileWriter wr = new FileWriter(tmpFile); + wr.write(Long.toString(version)); + wr.close(); + + // file.renameTo(..) does not work on windows. Use Files.move instead. + Files.move(tmpFile.toPath(), new File(root, ".version").toPath(), StandardCopyOption.ATOMIC_MOVE); + + } + catch (IOException exception) + { + logger.error("Unable to persist the last committed content store version {}. See the stacktrace below for furtger details.", version, exception); + try + { + Files.delete(tmpFile.toPath()); + } + catch (IOException e) + { + logger.error("Unable to delete tmp contentstore version file {}.", version); + } + } + } + + @Override + public Map>> getChanges(long version) + { + logger.warn("NoOp SolrContentStore changes call on slave side: this shouldn't happen because the ContentStore is in read-only mode when the hosting node is a slave."); + return emptyMap(); + } + + @Override + public void storeDocOnSolrContentStore(String tenant, long dbId, SolrInputDocument doc) + { + logger.warn("NoOp SolrContentStore write call on slave side: this shouldn't happen because the ContentStore is in read-only mode when the hosting node is a slave."); + } + + @Override + public void storeDocOnSolrContentStore(NodeMetaData nodeMetaData, SolrInputDocument doc) + { + logger.warn("NoOp SolrContentStore write call on slave side: this shouldn't happen because the ContentStore is in read-only mode when the hosting node is a slave."); + } + + @Override + public void removeDocFromContentStore(NodeMetaData nodeMetaData) + { + logger.warn("NoOp SolrContentStore write call on slave side: this shouldn't happen because the ContentStore is in read-only mode when the hosting node is a slave."); + } + + @Override + public void flushChangeSet() + { + logger.warn("NoOp ChangeSet tracking call on slave side: this shouldn't happen because the ContentStore is in read-only mode when the hosting node is a slave."); + } + + @Override + public void close() + { + // There's nothing to close on slave side + } + }; + + final InitialisableAccessMode readWrite = new InitialisableAccessMode() + { + private ChangeSet changeSet; + + @Override + public void init() + { + changeSet = ofNullable(changeSet).orElseGet(() -> new ChangeSet.Builder().withContentStoreRoot(root).build()); + } + + @Override + public long getLastCommittedVersion() + { + return changeSet.getLastCommittedVersion(); + } + + @Override + public void setLastCommittedVersion(long version) + { + // Do nothing here, as we are on master-side + } + + @Override + public Map>> getChanges(long version) + { + // The slave doesn't have a version, we are listing the whole content store + if (version <= NO_VERSION_AVAILABLE || changeSet.isUnknownVersion(version)) + { + String message = "A slave requested the content store synchronization " + + ((version <= NO_VERSION_AVAILABLE) + ? " without providing any local version (actually {})." + : " with an invalid/unknown local version number ({}).") + + "As consequence of that this master will list the whole content store because a full replication is needed."; + + logger.info(message, version); + + return Map.of( + INFO, singletonList(Map.of(FULL_REPLICATION, true)), + ADDS, fullContentStore(), + DELETES, emptyList()); + } + + ChangeSet changes = changeSet.since(version); + + return Map.of( + INFO, singletonList(Map.of(FULL_REPLICATION, false)), + DELETES, + changes.deletes.stream() + .map(path -> Map.of("name", path)) + .collect(toList()), + ADDS, + changes.adds.stream() + .map(relativePath -> root + relativePath) + .map(File::new) + .map(file -> new AlfrescoReplicationHandler.FileInfo(file, file.getAbsolutePath().replace(root, ""))) + .map(AlfrescoReplicationHandler.FileInfo::getAsMap) + .collect(toList())); + } + + @Override + public void storeDocOnSolrContentStore(String tenant, long dbId, SolrInputDocument doc) + { + ContentContext contentContext = + of(SolrContentUrlBuilder + .start() + .add(SolrContentUrlBuilder.KEY_TENANT, tenant) + .add(SolrContentUrlBuilder.KEY_DB_ID, String.valueOf(dbId))) + .map(SolrContentUrlBuilder::getContentContext) + .orElseThrow(() -> new IllegalArgumentException("Unable to build a Content Context from tenant " + tenant + " and DBID " + dbId)); + + this.delete(contentContext.getContentUrl()); + + ContentWriter writer = this.getWriter(contentContext); + + LOGGER.debug("Writing {}/{} to {}", tenant, dbId, contentContext.getContentUrl()); + + try (OutputStream contentOutputStream = writer.getContentOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(contentOutputStream)) + { + JavaBinCodec codec = new JavaBinCodec(resolver); + codec.marshal(doc, gzip); + + File file = getFileFromUrl(contentContext.getContentUrl()); + changeSet.addOrReplace(relativePath(file)); + } + catch (Exception exception) + { + LOGGER.warn("Unable to write to Content Store using URL: {}", contentContext.getContentUrl(), exception); + } + } + + @Override + public void storeDocOnSolrContentStore(NodeMetaData nodeMetaData, SolrInputDocument doc) { + String fixedTenantDomain = AlfrescoSolrDataModel.getTenantId(nodeMetaData.getTenantDomain()); + storeDocOnSolrContentStore(fixedTenantDomain, nodeMetaData.getId(), doc); + } + + @Override + public void removeDocFromContentStore(NodeMetaData nodeMetaData) + { + String fixedTenantDomain = AlfrescoSolrDataModel.getTenantId(nodeMetaData.getTenantDomain()); + String contentUrl = SolrContentUrlBuilder + .start() + .add(SolrContentUrlBuilder.KEY_TENANT, fixedTenantDomain) + .add(SolrContentUrlBuilder.KEY_DB_ID, String.valueOf(nodeMetaData.getId())) + .getContentContext() + .getContentUrl(); + delete(contentUrl); + } + + @Override + public void flushChangeSet() throws IOException + { + changeSet.flush(); + } + + @Override + public void switchOnReadWriteMode() + { + logger.debug("The content store is already in ReadWrite mode; as consequence of that, the incoming \"SET-TO-RW-MODE\" call won't have any effect."); + } + + @Override + public void switchOnReadOnlyMode() + { + logger.debug("A writable content store cannot switch in ReadOnly mode. This could happen in an edge case where" + + " on the same Solr node we have masters and slaves nodes"); + } + + @Override + public void close() + { + changeSet.close(); + } + + private List> fullContentStore() + { + try + { + return Files.walk(Paths.get(root)) + .map(Path::toFile) + .filter(onlyDatafiles) + .map(file -> new AlfrescoReplicationHandler.FileInfo(file, file.getAbsolutePath().replace(root, ""))) + .map(AlfrescoReplicationHandler.FileInfo::getAsMap) + .collect(toList()); + } + catch (Exception e) + { + LOGGER.error("An exception occurred while retrieving the whole ContentStore filelist. " + + "As consequence of that an empty list will be returned (i.e. no ContentStore synch will happen)."); + return emptyList(); + } + } + + private void delete(String contentUrl) + { + File file = getFileFromUrl(contentUrl); + if (file.delete()) changeSet.delete(relativePath(file)); + } + + private ContentWriter getWriter(ContentContext context) + { + String url = context.getContentUrl(); + File file = getFileFromUrl(url); + return new SolrFileContentWriter(file, url); + } + }; + + AccessMode currentAccessMode = notYetSet; + + private final JavaBinCodec.ObjectResolver resolver = (o, codec) -> { + if(o instanceof BytesRef) + { + BytesRef br = (BytesRef)o; + codec.writeByteArray(br.bytes,br.offset,br.length); + return null; + } + return o; + }; + + /** + * Builds a new {@link SolrContentStore} instance with the given SOLR HOME. + * + * @param solrHome the Solr HOME. */ public SolrContentStore(String solrHome) { @@ -77,122 +514,92 @@ public class SolrContentStore implements ContentStore { //Its very unlikely that solrHome would not exist so we will log an error //but continue because solr.content.dir may be specified, so it keeps working - log.error(solrHomeFile.getAbsolutePath() + " does not exist."); + LOGGER.error(solrHomeFile.getAbsolutePath() + " does not exist."); } - - String path = solrHomeFile.getParent()+"/"+CONTENT_STORE; - log.warn(path + " will be used as a default path if " + SOLR_CONTENT_DIR + " property is not defined"); + + String path = solrHomeFile.getParent() + "/" + CONTENT_STORE; + LOGGER.warn(path + " will be used as a default path if " + SOLR_CONTENT_DIR + " property is not defined"); File rootFile = new File(ConfigUtil.locateProperty(SOLR_CONTENT_DIR, path)); try { FileUtils.forceMkdir(rootFile); - } - catch (Exception e) - { + } + catch (Exception e) { throw new RuntimeException("Failed to create directory for content store: " + rootFile, e); } + this.root = rootFile.getAbsolutePath(); } - // write a BytesRef as a byte array - private JavaBinCodec.ObjectResolver resolver = new JavaBinCodec.ObjectResolver() + /** + * Returns the content store changes since the given (requestor) version. + * + * @param version the requestor version. + * @return the content store changes since the given (requestor) version. + */ + @Override + public Map>> getChanges(long version) { - @Override public Object resolve(Object o,JavaBinCodec codec)throws IOException - { - if(o instanceof BytesRef) - { - BytesRef br=(BytesRef)o; - codec.writeByteArray(br.bytes,br.offset,br.length); - return null; - } - return o; - } - }; + return currentAccessMode.getChanges(version); + } /** * Retrieve document from SolrContentStore. + * * @param tenant identifier * @param dbId identifier * @return {@link SolrInputDocument} searched document - * @throws IOException if error */ - public SolrInputDocument retrieveDocFromSolrContentStore(String tenant, long dbId) throws IOException + public SolrInputDocument retrieveDocFromSolrContentStore(String tenant, long dbId) { - String contentUrl = SolrContentUrlBuilder.start().add(SolrContentUrlBuilder.KEY_TENANT, tenant) - .add(SolrContentUrlBuilder.KEY_DB_ID, String.valueOf(dbId)).get(); + String contentUrl = + SolrContentUrlBuilder.start() + .add(SolrContentUrlBuilder.KEY_TENANT, tenant) + .add(SolrContentUrlBuilder.KEY_DB_ID, String.valueOf(dbId)) + .get(); + ContentReader reader = this.getReader(contentUrl); - SolrInputDocument cachedDoc = null; - if (reader.exists()) + if (!reader.exists()) { - // try-with-resources statement closes all these InputStreams - try (InputStream contentInputStream = reader.getContentInputStream(); - // Uncompresses the document - GZIPInputStream gzip = new GZIPInputStream(contentInputStream);) - { - cachedDoc = (SolrInputDocument) new JavaBinCodec(resolver).unmarshal(gzip); - } catch (Exception e) - { - // Don't fail for this - log.warn("Failed to get doc from store using URL: " + contentUrl, e); - return null; - } + return null; + } + + try (InputStream contentInputStream = reader.getContentInputStream(); + InputStream gzip = new GZIPInputStream(contentInputStream)) + { + return (SolrInputDocument) new JavaBinCodec(resolver).unmarshal(gzip); + } + catch (Exception exception) + { + // Don't fail for this + LOGGER.warn("Failed to get doc from store using URL: " + contentUrl, exception); + return null; } - return cachedDoc; } - private final String root; - @Override - public boolean isContentUrlSupported(String contentUrl) + public long getLastCommittedVersion() { - return (contentUrl != null && contentUrl.startsWith(SolrContentUrlBuilder.SOLR_PROTOCOL_PREFIX)); + return currentAccessMode.getLastCommittedVersion(); } - - /** - * @return true always - */ @Override - public boolean isWriteSupported() + public void setLastCommittedVersion(long version) { - return true; + currentAccessMode.setLastCommittedVersion(version); } /** - * @return -1 always + * Returns the absolute path of the content store root folder. + * + * @return the absolute path of the content store root folder. */ - @Override - public long getSpaceFree() - { - return -1L; - } - - /** - * @return -1 always - */ - @Override - public long getSpaceTotal() - { - return -1L; - } - - @Override public String getRootLocation() { return root; } - /** - * Convert a content URL into a File, whether it exists or not - */ - private File getFileFromUrl(String contentUrl) - { - String path = contentUrl.replace(SolrContentUrlBuilder.SOLR_PROTOCOL_PREFIX, root + "/"); - return new File(path); - } - - @Override public boolean exists(String contentUrl) { File file = getFileFromUrl(contentUrl); @@ -200,93 +607,101 @@ public class SolrContentStore implements ContentStore } @Override - public ContentReader getReader(String contentUrl) + public void storeDocOnSolrContentStore(String tenant, long dbId, SolrInputDocument doc) + { + currentAccessMode.storeDocOnSolrContentStore(tenant, dbId, doc); + } + + /** + * Store {@link SolrInputDocument} in to Alfresco solr content store. + * + * @param nodeMetaData the incoming node metadata. + * @param doc the document itself. + */ + @Override + public void storeDocOnSolrContentStore(NodeMetaData nodeMetaData, SolrInputDocument doc) + { + currentAccessMode.storeDocOnSolrContentStore(nodeMetaData, doc); + } + + /** + * Removes {@link SolrInputDocument} from Alfresco solr content store. + * + * @param nodeMetaData the incoming node metadata. + */ + @Override + public void removeDocFromContentStore(NodeMetaData nodeMetaData) + { + currentAccessMode.removeDocFromContentStore(nodeMetaData); + } + + @Override + public void flushChangeSet() throws IOException + { + currentAccessMode.flushChangeSet(); + } + + @Override + public void close() throws IOException + { + currentAccessMode.close(); + } + + @Override + public void switchOnReadWriteMode() + { + currentAccessMode = readWrite; + } + + @Override + public void switchOnReadOnlyMode() + { + currentAccessMode = readOnly; + } + + /** + * Assuming the input file belongs to the content store, it returns the corresponding relative path. + * + * @param file the content store file. + * @return the relative file path. + */ + private String relativePath(File file) + { + return file.getAbsolutePath().replace(root, ""); + } + + /** + * Convert a content URL into a File, whether it exists or not + */ + private File getFileFromUrl(String contentUrl) + { + return new File(contentUrl.replace(SolrContentUrlBuilder.SOLR_PROTOCOL_PREFIX, root + "/")); + } + + private ContentReader getReader(String contentUrl) { File file = getFileFromUrl(contentUrl); return new SolrFileContentReader(file, contentUrl); } - @Override - public ContentWriter getWriter(ContentContext context) - { - // Ensure that there is a context and that it has a URL - if (context == null || context.getContentUrl() == null) - { - throw new IllegalArgumentException("Retrieve a writer with a URL-providing ContentContext."); - } - String url = context.getContentUrl(); - File file = getFileFromUrl(url); - SolrFileContentWriter writer = new SolrFileContentWriter(file, url); - // Done - return writer; - } - - @Override - public boolean delete(String contentUrl) - { - File file = getFileFromUrl(contentUrl); - return file.delete(); - } /** - * Stores a {@link SolrInputDocument} into Alfresco solr content store. - * @param tenant - * @param dbId - * @param doc - * @throws IOException + * Enables/disables the content store access mode. + * The term "toggles" is just for indicating that this method will be called several times (one for each registered + * core). The underlying FSM makes sure this call will be idempotent so in case of read/write content store the + * data structure used for maintaining the content store versioning will be initialised only once, even if this + * method is called repeatedly. + * + * @param enableReadOnlyMode a flag indicating if the requesting core requires a readOnly (true) or readWrite (false) content store. */ - public void storeDocOnSolrContentStore(String tenant, long dbId, SolrInputDocument doc) throws IOException + public synchronized void toggleReadOnlyMode(boolean enableReadOnlyMode) { - ContentContext contentContext = SolrContentUrlBuilder - .start() - .add(SolrContentUrlBuilder.KEY_TENANT, tenant) - .add(SolrContentUrlBuilder.KEY_DB_ID, String.valueOf(dbId)) - .getContentContext(); - this.delete(contentContext.getContentUrl()); - ContentWriter writer = this.getWriter(contentContext); - if (log.isDebugEnabled()) + if (enableReadOnlyMode) { - log.debug("Writing doc to " + contentContext.getContentUrl()); + currentAccessMode.switchOnReadOnlyMode(); } - try ( - OutputStream contentOutputStream = writer.getContentOutputStream(); - // Compresses the document - GZIPOutputStream gzip = new GZIPOutputStream(contentOutputStream); - ) + else { - JavaBinCodec codec = new JavaBinCodec(resolver); - codec.marshal(doc, gzip); - } - catch (Exception e) - { - // A failure to write to the store is acceptable as long as it's logged - log.warn("Failed to write to store using URL: " + contentContext.getContentUrl(), e); + currentAccessMode.switchOnReadWriteMode(); } } - /** - * Store {@link SolrInputDocument} in to Alfresco solr content store. - * @param nodeMetaData identifier - * @param doc to store - * @throws IOException if error - */ - public void storeDocOnSolrContentStore(NodeMetaData nodeMetaData, SolrInputDocument doc) throws IOException - { - String fixedTenantDomain = AlfrescoSolrDataModel.getTenantId(nodeMetaData.getTenantDomain()); - storeDocOnSolrContentStore(fixedTenantDomain, nodeMetaData.getId(), doc); - } - /** - * Removes {@link SolrInputDocument} from Alfresco solr content store. - * @param nodeMetaData - */ - public void removeDocFromContentStore(NodeMetaData nodeMetaData) - { - String fixedTenantDomain = AlfrescoSolrDataModel.getTenantId(nodeMetaData.getTenantDomain()); - String contentUrl = SolrContentUrlBuilder - .start() - .add(SolrContentUrlBuilder.KEY_TENANT, fixedTenantDomain) - .add(SolrContentUrlBuilder.KEY_DB_ID, String.valueOf(nodeMetaData.getId())) - .getContentContext() - .getContentUrl(); - this.delete(contentUrl); - } - -} +} \ No newline at end of file diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrContentUrlBuilder.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrContentUrlBuilder.java index b280da4f4..36b38b07b 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrContentUrlBuilder.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrContentUrlBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2014 Alfresco Software Limited. + * Copyright (C) 2005-2019 Alfresco Software Limited. * * This file is part of Alfresco * @@ -18,7 +18,7 @@ */ package org.alfresco.solr.content; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.TreeMap; import java.util.zip.CRC32; @@ -43,20 +43,13 @@ import org.slf4j.LoggerFactory; */ public class SolrContentUrlBuilder { - /** - * solr is the prefix for SOLR content URLs - * @see #isContentUrlSupported(String) - */ - public static final String SOLR_PROTOCOL = "solr"; - public static final String SOLR_PROTOCOL_PREFIX = SOLR_PROTOCOL + ContentStore.PROTOCOL_DELIMITER; - public static final String FILE_EXTENSION = ".gz"; + private static final String SOLR_PROTOCOL = "solr"; + static final String SOLR_PROTOCOL_PREFIX = SOLR_PROTOCOL + ContentStore.PROTOCOL_DELIMITER; + static final String FILE_EXTENSION = ".gz"; - /** The key for the tenant name */ - public static final String KEY_TENANT = "tenant"; - /** The key for the DB ID */ - public static final String KEY_DB_ID = "dbId"; - /** The key for the ACL ID */ - public static final String KEY_ACL_ID = "aclId"; + static final String KEY_TENANT = "tenant"; + static final String KEY_DB_ID = "dbId"; + static final String KEY_ACL_ID = "aclId"; protected final static Logger logger = LoggerFactory.getLogger(SolrContentUrlBuilder.class); @@ -66,9 +59,9 @@ public class SolrContentUrlBuilder /** * Protected constructor used by {@link SolrContentUrlBuilder#start()} */ - protected SolrContentUrlBuilder() + private SolrContentUrlBuilder() { - this.metadata = new TreeMap(); + this.metadata = new TreeMap<>(); } /** @@ -83,32 +76,34 @@ public class SolrContentUrlBuilder /** * Add some metadata to the URL generator. The order in which metadata is added is irrelevant. - *

* Note that there are specific keys that are commonly used and, if provided, may not be null or empty. + * *

    *
  • {@link #KEY_TENANT}: The name of the tenant or 'default' if missing.
  • *
  • {@link #KEY_DB_ID}: The database ID.
  • *
  • {@link #KEY_ACL_ID}: The ACL ID.
  • *
* - * @param key an arbitrary metadata key (never null> - * @param value some metadata value (null is supported) - * @return this builder for more building + * @param key an arbitrary metadata key (never null> + * @param value some metadata value (null is supported) + * @return this builder for more building * - * @throws IllegalArgumentException if the key is null - * @throws IllegalStateException if the key has been used already + * @throws IllegalArgumentException if the key is null + * @throws IllegalStateException if the key has been used already */ - public synchronized SolrContentUrlBuilder add(String key, String value) + public SolrContentUrlBuilder add(String key, String value) { if (key == null) { throw new IllegalArgumentException("The metadata 'key' may not be null."); } + String previous = metadata.put(key, value); if (previous != null) { throw new IllegalStateException("The metadata key, '" + key + "', has already been used."); } + // Check well-known keys if (key.equals(KEY_TENANT) || key.equals(KEY_DB_ID) || key.equals(KEY_ACL_ID)) { @@ -131,8 +126,8 @@ public class SolrContentUrlBuilder /** * Get the final content URL using the {@link #add(String, String) supplied metadata}. * - * @return the SOLR content URL - * @throws IllegalStateException if no metadata has been added + * @return the SOLR content URL + * @throws IllegalStateException if no metadata has been added */ public synchronized String get() { @@ -169,21 +164,13 @@ public class SolrContentUrlBuilder sb.append("misc/"); // Calculate the CRC CRC32 crc = new CRC32(); - try + for (Map.Entry entry : metadata.entrySet()) { - for (Map.Entry entry : metadata.entrySet()) - { - // This is ordered, so just add each entry as "key = value". - // DO NOT USE entry.toString() because the format is not a contract - // and we have to have the same string for the same metadata - String entryStr = entry.getKey() + "=" + entry.getValue() + "; "; - crc.update(entryStr.getBytes("UTF-8")); - } - } - catch (UnsupportedEncodingException e) - { - // Yeah, right. - throw new RuntimeException("UTF-8 is not supported.", e); + // This is ordered, so just add each entry as "key = value". + // DO NOT USE entry.toString() because the format is not a contract + // and we have to have the same string for the same metadata + String entryStr = entry.getKey() + "=" + entry.getValue() + "; "; + crc.update(entryStr.getBytes(StandardCharsets.UTF_8)); } numSb.append(crc.getValue()); } @@ -218,7 +205,7 @@ public class SolrContentUrlBuilder /** * Helper method to retrieve a {@link ContentContext} constructed using the final {@link #get()} url. */ - public ContentContext getContentContext() + ContentContext getContentContext() { String url = get(); return new ContentContext(null, url); diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrFileContentReader.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrFileContentReader.java index 357e63ffd..c6c7e2a14 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrFileContentReader.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrFileContentReader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2014 Alfresco Software Limited. + * Copyright (C) 2005-2019 Alfresco Software Limited. * * This file is part of Alfresco * @@ -42,7 +42,7 @@ import org.springframework.util.FileCopyUtils; * @author Derek Hulley * @since 5.0 */ -public class SolrFileContentReader implements ContentReader +class SolrFileContentReader implements ContentReader { private final File file; private final String contentUrl; @@ -51,7 +51,7 @@ public class SolrFileContentReader implements ContentReader * @param file the file to write to * @param contentUrl the content URL for information purposes */ - protected SolrFileContentReader(File file, String contentUrl) + SolrFileContentReader(File file, String contentUrl) { this.file = file; this.contentUrl = contentUrl; @@ -120,9 +120,8 @@ public class SolrFileContentReader implements ContentReader } try { - InputStream is = new BufferedInputStream(new FileInputStream(file)); // done - return is; + return new BufferedInputStream(new FileInputStream(file)); } catch (Throwable e) { @@ -192,12 +191,9 @@ public class SolrFileContentReader implements ContentReader ByteArrayOutputStream os = new ByteArrayOutputStream(); FileCopyUtils.copy(is, os); // both streams are closed byte[] bytes = os.toByteArray(); - // get the encoding for the string + String encoding = "UTF-8"; - // create the string from the byte[] using encoding if necessary - String content = (encoding == null) ? new String(bytes) : new String(bytes, encoding); - // done - return content; + return new String(bytes, encoding); } catch (IOException e) { @@ -266,4 +262,4 @@ public class SolrFileContentReader implements ContentReader { throw new UnsupportedOperationException("Auto-created method not implemented."); } -} +} \ No newline at end of file diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrFileContentWriter.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrFileContentWriter.java index 9dd57aea1..2a5a05013 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrFileContentWriter.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/SolrFileContentWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2014 Alfresco Software Limited. + * Copyright (C) 2005-2019 Alfresco Software Limited. * * This file is part of Alfresco * @@ -41,7 +41,7 @@ import org.apache.commons.io.FileUtils; * @author Derek Hulley * @since 5.0 */ -public class SolrFileContentWriter implements ContentWriter +class SolrFileContentWriter implements ContentWriter { private final File file; private final String contentUrl; @@ -51,11 +51,10 @@ public class SolrFileContentWriter implements ContentWriter * @param file the file to write to * @param contentUrl the content URL for information purposes */ - protected SolrFileContentWriter(File file, String contentUrl) + SolrFileContentWriter(File file, String contentUrl) { this.file = file; this.contentUrl = contentUrl; - this.written = false; } @Override @@ -109,7 +108,7 @@ public class SolrFileContentWriter implements ContentWriter @Override public synchronized OutputStream getContentOutputStream() throws ContentIOException { - if (written == true) + if (written) { throw new IllegalStateException("The writer has already been used: " + file); } @@ -117,6 +116,7 @@ public class SolrFileContentWriter implements ContentWriter { throw new IllegalStateException("The file already exists: " + file); } + try { OutputStream is = new BufferedOutputStream(FileUtils.openOutputStream(file)); @@ -139,7 +139,7 @@ public class SolrFileContentWriter implements ContentWriter @Override public synchronized void putContent(InputStream is) throws ContentIOException { - if (written == true) + if (written) { throw new IllegalStateException("The writer has already been used: " + file); } @@ -147,6 +147,7 @@ public class SolrFileContentWriter implements ContentWriter { throw new IllegalStateException("The file already exists: " + file); } + try { FileUtils.copyInputStreamToFile(is, file); @@ -161,7 +162,7 @@ public class SolrFileContentWriter implements ContentWriter @Override public synchronized void putContent(File sourceFile) throws ContentIOException { - if (written == true) + if (written) { throw new IllegalStateException("The writer has already been used: " + this.file); } @@ -173,6 +174,7 @@ public class SolrFileContentWriter implements ContentWriter { throw new IllegalStateException("The source file does not exist: " + sourceFile); } + try { FileUtils.copyFile(sourceFile, this.file, false); diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/package-info.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/package-info.java new file mode 100644 index 000000000..9fd22aca6 --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/content/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +/** + * The package contains all components that manage the Solr ContentStore. + * Note that in SearchServices 2.0 the whole package will be deprecated/removed because the content store will be + * replaced by the built-in Solr storage capability (i.e. stored fields). + */ +package org.alfresco.solr.content; \ No newline at end of file diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/AlfrescoIndexFetcher.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/AlfrescoIndexFetcher.java new file mode 100644 index 000000000..bb36889e7 --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/AlfrescoIndexFetcher.java @@ -0,0 +1,2762 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + * Modification copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see + * + */ +package org.alfresco.solr.handler; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import org.alfresco.solr.content.SolrContentStore; +import org.apache.commons.io.FilenameUtils; +import org.apache.http.client.HttpClient; +import org.apache.lucene.codecs.CodecUtil; +import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.HttpClientUtil; +import org.apache.solr.client.solrj.impl.HttpSolrClient; +import org.apache.solr.client.solrj.request.QueryRequest; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrException.ErrorCode; +import org.apache.solr.common.params.CommonParams; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.util.ExecutorUtil; +import org.apache.solr.common.util.FastInputStream; +import org.apache.solr.common.util.IOUtils; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.SuppressForbidden; +import org.apache.solr.core.DirectoryFactory; +import org.apache.solr.core.DirectoryFactory.DirContext; +import org.apache.solr.core.IndexDeletionPolicyWrapper; +import org.apache.solr.core.SolrCore; +import org.apache.solr.handler.SnapShooter; +import org.apache.solr.request.LocalSolrQueryRequest; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.search.SolrIndexSearcher; +import org.apache.solr.update.CdcrUpdateLog; +import org.apache.solr.update.CommitUpdateCommand; +import org.apache.solr.update.UpdateLog; +import org.apache.solr.update.VersionInfo; +import org.apache.solr.util.DefaultSolrThreadFactory; +import org.apache.solr.util.FileUtils; +import org.apache.solr.util.PropertiesOutputStream; +import org.apache.solr.util.RTimer; +import org.apache.solr.util.RefCounted; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.EOFException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.lang.invoke.MethodHandles; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.zip.Adler32; +import java.util.zip.Checksum; +import java.util.zip.InflaterInputStream; + +import static java.util.List.of; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.ALIAS; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CHECKSUM; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CMD_CONTENT_STORE_FILES; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CMD_DETAILS; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CMD_GET_FILE; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CMD_GET_FILE_LIST; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CMD_INDEX_VERSION; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.COMMAND; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.COMPRESSION; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CONF_FILES; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CONF_FILE_SHORT; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CONTENT_STORE_FILES; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CONTENT_STORE_FILE_LIST; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.CONTENT_STORE_VERSION; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.EXTERNAL; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.FILE; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.FILE_STREAM; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.FileInfo; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.GENERATION; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.INTERNAL; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.MASTER_URL; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.NO_INDEX_REPLICATION_REQUIRED; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.OFFSET; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.SIZE; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.TLOG_FILE; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.TLOG_FILES; +import static org.alfresco.solr.handler.AlfrescoReplicationHandler.getCheckSum; +import static org.apache.solr.common.params.CommonParams.JAVABIN; +import static org.apache.solr.common.params.CommonParams.NAME; + +/** + *

Provides functionality of downloading changed index files as well as config files and a timer for scheduling fetches from the + * master.

+ * + * This class has been modified in order to allow the alfresco contentstore to be efficiently replicated in a master slave environment. + * @author Elia + */ +class AlfrescoIndexFetcher +{ + static final String REPLICATION_PROPERTIES = "replication.properties"; + static final String INDEX_REPLICATED_AT = "indexReplicatedAt"; + static final String TIMES_INDEX_REPLICATED = "timesIndexReplicated"; + static final String CONF_FILES_REPLICATED = "confFilesReplicated"; + static final String CONF_FILES_REPLICATED_AT = "confFilesReplicatedAt"; + static final String TIMES_CONFIG_REPLICATED = "timesConfigReplicated"; + static final String LAST_CYCLE_BYTES_DOWNLOADED = "lastCycleBytesDownloaded"; + static final String TIMES_FAILED = "timesFailed"; + static final String REPLICATION_FAILED_AT = "replicationFailedAt"; + static final String PREVIOUS_CYCLE_TIME_TAKEN = "previousCycleTimeInSeconds"; + static final String INDEX_REPLICATED_AT_LIST = "indexReplicatedAtList"; + static final String REPLICATION_FAILED_AT_LIST = "replicationFailedAtList"; + private static final int _100K = 100000; + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final int CONTENT_STORE_PARTITION_SIZE = 50; + private static final String INTERRUPT_RESPONSE_MESSAGE = "Interrupted while waiting for modify lock"; + private static final int MAX_RETRIES = 5; + private static final int NO_CONTENT = 1; + private static final int ERR = 2; + private final String masterUrl; + private final AlfrescoReplicationHandler alfrescoReplicationHandler; + private final SolrContentStore contentStore; + private final SolrCore solrCore; + private final HttpClient myHttpClient; + private final Map confFileInfoCache = new HashMap<>(); + private volatile Date replicationStartTimeStamp; + private RTimer replicationTimer; + + /** + * The map contains the following fields: + * NAME : String -> file name(with path for contentstore files) + * SIZE : long -> file size + * CHECKSUM : long -> checksum + */ + private volatile List> filesToDownload; + private volatile List> confFilesToDownload; + private volatile List> tlogFilesToDownload; + private volatile List> contentStoreFilesToDownload; + private volatile List> contentStoreFilesToDelete; + private volatile List> filesDownloaded; + private volatile List> confFilesDownloaded; + private volatile List> tlogFilesDownloaded; + private volatile List> contentStoreFilesDownloaded; + + private volatile Map currentFile; + private volatile DirectoryFileFetcher dirFileFetcher; + private volatile LocalFsFileFetcher localFileFetcher; + private volatile ContentStoreFetcher contentStoreFileFetcher; + private volatile ExecutorService fsyncService; + private volatile boolean stop = false; + private boolean useInternalCompression; + private boolean useExternalCompression; + private boolean fullContentStoreReplication = false; + private volatile Exception fsyncException; + + AlfrescoIndexFetcher(final NamedList initArgs, final AlfrescoReplicationHandler handler, final SolrCore sc, SolrContentStore contentStore) + { + this.contentStore = contentStore; + solrCore = sc; + String masterUrl = (String) initArgs.get(MASTER_URL); + if (masterUrl == null) + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "'masterUrl' is required for a slave"); + if (masterUrl.endsWith(AlfrescoReplicationHandler.PATH)) + { + masterUrl = masterUrl.substring(0, masterUrl.length() - 12); + LOG.warn("'masterUrl' must be specified without the " + AlfrescoReplicationHandler.PATH + " suffix"); + } + this.masterUrl = masterUrl; + + this.alfrescoReplicationHandler = handler; + String compress = (String) initArgs.get(COMPRESSION); + useInternalCompression = INTERNAL.equals(compress); + useExternalCompression = EXTERNAL.equals(compress); + String connTimeout = (String) initArgs.get(HttpClientUtil.PROP_CONNECTION_TIMEOUT); + // allow a master override for tests - you specify this in /replication slave section of solrconfig and some + // test don't want to define this + String readTimeout = System.getProperty("solr.indexfetcher.sotimeout", "-1"); + if (readTimeout.equals("-1")) + { + readTimeout = (String) initArgs.get(HttpClientUtil.PROP_SO_TIMEOUT); + } + String httpBasicAuthUser = (String) initArgs.get(HttpClientUtil.PROP_BASIC_AUTH_USER); + String httpBasicAuthPassword = (String) initArgs.get(HttpClientUtil.PROP_BASIC_AUTH_PASS); + myHttpClient = createHttpClient(solrCore, connTimeout, readTimeout, httpBasicAuthUser, httpBasicAuthPassword, + useExternalCompression); + } + + private static HttpClient createHttpClient(SolrCore core, String connTimeout, String readTimeout, String httpBasicAuthUser, String httpBasicAuthPassword, boolean useCompression) + { + final ModifiableSolrParams httpClientParams = new ModifiableSolrParams(); + httpClientParams.set(HttpClientUtil.PROP_CONNECTION_TIMEOUT, connTimeout != null ? connTimeout : "5000"); + httpClientParams.set(HttpClientUtil.PROP_SO_TIMEOUT, readTimeout != null ? readTimeout : "20000"); + httpClientParams.set(HttpClientUtil.PROP_BASIC_AUTH_USER, httpBasicAuthUser); + httpClientParams.set(HttpClientUtil.PROP_BASIC_AUTH_PASS, httpBasicAuthPassword); + httpClientParams.set(HttpClientUtil.PROP_ALLOW_COMPRESSION, useCompression); + + return HttpClientUtil + .createClient(httpClientParams, core.getCoreContainer().getUpdateShardHandler().getConnectionManager()); + } + + private static boolean filesToAlwaysDownloadIfNoChecksums(String filename, long size, CompareResult compareResult) + { + // without checksums to compare, we always download .si, .liv, segments_N, + // and any very small files + return !compareResult.checkSummed && (filename.endsWith(".si") || filename.endsWith(".liv") || filename + .startsWith("segments_") || size < _100K); + } + + /** + * Check if the same version of the file to download is already in the contentstore. + * + * @param contentStoreDirectoryRoot the content store top directory. + * @param filename the filename. + * @param length the file length. + * @param checksum the file chechsum. + * @return true if the same file already exists in the contentstore. + */ + private static boolean compareContentStoreFiles(File contentStoreDirectoryRoot, String filename, long length, long checksum) + { + File f = new File(contentStoreDirectoryRoot.getAbsolutePath() + "/" + filename); + if (f.length() != length) + { + return true; + } + + Checksum localCheckSum = new Adler32(); + + if (getCheckSum(localCheckSum, f) != checksum) + { + return true; + } + + LOG.debug("{} already in contentstore", filename); + return false; + } + + private static CompareResult compareFile(Directory indexDir, String filename, Long backupIndexFileLen, Long backupIndexFileChecksum) + { + CompareResult compareResult = new CompareResult(); + try + { + try (final IndexInput indexInput = indexDir.openInput(filename, IOContext.READONCE)) + { + long indexFileLen = indexInput.length(); + long indexFileChecksum = 0; + + if (backupIndexFileChecksum != null) + { + try + { + indexFileChecksum = CodecUtil.retrieveChecksum(indexInput); + compareResult.checkSummed = true; + } + catch (Exception e) + { + LOG.warn("Could not retrieve checksum from file.", e); + } + } + + if (!compareResult.checkSummed) + { + // we don't have checksums to compare + + if (indexFileLen == backupIndexFileLen) + { + compareResult.equal = true; + return compareResult; + } + else + { + LOG.info("File {} did not match. expected length is {} and actual length is {}", filename, + backupIndexFileLen, indexFileLen); + compareResult.equal = false; + return compareResult; + } + } + + // we have checksums to compare + + if (indexFileLen == backupIndexFileLen && backupIndexFileChecksum!= null && indexFileChecksum == backupIndexFileChecksum) + { + compareResult.equal = true; + return compareResult; + } + else + { + LOG.warn("File {} did not match. expected checksum is {} and actual is checksum {}. " + + "expected length is {} and actual length is {}", filename, backupIndexFileChecksum, + indexFileChecksum, backupIndexFileLen, indexFileLen); + compareResult.equal = false; + return compareResult; + } + } + } + catch (NoSuchFileException | FileNotFoundException e) + { + compareResult.equal = false; + return compareResult; + } + catch (IOException e) + { + LOG.error("Could not read file " + filename + ". Downloading it again", e); + compareResult.equal = false; + return compareResult; + } + } + + /** + * Returns true if the file exists (can be opened), false + * if it cannot be opened, and (unlike Java's + * File.exists) throws IOException if there's some + * unexpected error. + */ + private static boolean slowFileExists(Directory dir, String fileName) throws IOException + { + try + { + dir.openInput(fileName, IOContext.DEFAULT).close(); + return true; + } + catch (NoSuchFileException | FileNotFoundException e) + { + return false; + } + } + + private static boolean delTree(File dir) + { + try + { + org.apache.lucene.util.IOUtils.rm(dir.toPath()); + return true; + } + catch (IOException e) + { + LOG.warn("Unable to delete directory : " + dir, e); + return false; + } + } + + /** + * Gets the latest commit version and generation from the master + */ + private NamedList getLatestVersion() throws IOException + { + ModifiableSolrParams params = new ModifiableSolrParams(); + params.set(COMMAND, CMD_INDEX_VERSION); + params.set(CommonParams.WT, JAVABIN); + params.set(CommonParams.QT, AlfrescoReplicationHandler.PATH); + QueryRequest req = new QueryRequest(params); + + try (HttpSolrClient client = new HttpSolrClient.Builder(masterUrl).withHttpClient(myHttpClient).build()) + { + client.setSoTimeout(60000); + client.setConnectionTimeout(15000); + + return client.request(req); + } + catch (SolrServerException e) + { + throw new SolrException(ErrorCode.SERVER_ERROR, e.getMessage(), e); + } + } + + /** + * Fetches the list of files in a given index commit point and updates internal list of files to download. + * The list is composed by: + * - Index file list + * - Conf file list + * - TLog file list + * - Content store file list + */ + @SuppressWarnings("unchecked") + private void fetchFileList(long indexGeneration, long contentStoreGeneration) throws IOException + { + ModifiableSolrParams params = new ModifiableSolrParams(); + params.set(COMMAND, CMD_GET_FILE_LIST); + params.set(GENERATION, String.valueOf(indexGeneration)); + params.set(CONTENT_STORE_VERSION, String.valueOf(contentStoreGeneration)); + params.set(CommonParams.WT, JAVABIN); + params.set(CommonParams.QT, AlfrescoReplicationHandler.PATH); + QueryRequest req = new QueryRequest(params); + + try (HttpSolrClient client = new HttpSolrClient.Builder(masterUrl).withHttpClient(myHttpClient).build()) + { + client.setSoTimeout(60000); + client.setConnectionTimeout(15000); + NamedList response = client.request(req); + + List> files = (List>) response.get(CMD_GET_FILE_LIST); + if (files != null) + { + filesToDownload = Collections.synchronizedList(files); + } + else + { + filesToDownload = Collections.emptyList(); + LOG.info("No files to download for index generation: " + indexGeneration); + } + + files = (List>) response.get(CONF_FILES); + if (files != null) + { + confFilesToDownload = Collections.synchronizedList(files); + } + + files = (List>) response.get(TLOG_FILES); + if (files != null) + { + tlogFilesToDownload = Collections.synchronizedList(files); + } + Map>> contentStoreMap = (Map>>) response + .get(CONTENT_STORE_FILES); + + fullContentStoreReplication = false; + + if (contentStoreMap != null) + { + contentStoreFilesToDownload = Collections.synchronizedList(contentStoreMap.get(SolrContentStore.ADDS)); + contentStoreFilesToDelete = Collections.synchronizedList(contentStoreMap.get(SolrContentStore.DELETES)); + + List> infomap = contentStoreMap.get(SolrContentStore.INFO); + fullContentStoreReplication = of(infomap).stream().flatMap(List::stream) + .map(e -> e.get(SolrContentStore.FULL_REPLICATION)) + .map(e -> e instanceof Boolean ? (Boolean) e : false).findFirst().orElse(false); + + } + + } + catch (SolrServerException e) + { + throw new IOException(e); + } + } + + IndexFetchResult fetchLatestIndex(boolean forceReplication, boolean replicateContentStore) + throws IOException, InterruptedException + { + return fetchLatestIndex(forceReplication, false, replicateContentStore); + } + + /** + * This command downloads all the necessary files from master to install a index commit point. Only changed files are + * downloaded. It also downloads the conf files (if they are modified). + * + * @param forceReplication force a replication in all cases + * @param forceCoreReload force a core reload in all cases + * @param replicateContentStore downloads the changed content store files + * @return true on success, false if slave is already in sync + * @throws IOException if an exception occurs + */ + private IndexFetchResult fetchLatestIndex(boolean forceReplication, boolean forceCoreReload, boolean replicateContentStore) + throws IOException, InterruptedException + { + + boolean cleanupDone = false; + boolean successfulInstall = false; + markReplicationStart(); + Directory tmpIndexDir = null; + String tmpIndex; + Directory indexDir = null; + String indexDirPath; + boolean deleteTmpIdxDir = true; + File tmpTlogDir = null; + + if (!solrCore.getSolrCoreState().getLastReplicateIndexSuccess()) + { + // if the last replication was not a success, we force a full replication + // when we are a bit more confident we may want to try a partial replication + // if the error is connection related or something, but we have to be careful + forceReplication = true; + } + + try + { + //get the current 'replicateable' index version in the master + NamedList response; + try + { + response = getLatestVersion(); + } + catch (Exception e) + { + final String errorMsg = e.toString(); + if (!Strings.isNullOrEmpty(errorMsg) && errorMsg.contains(INTERRUPT_RESPONSE_MESSAGE)) + { + LOG.warn("Master at: " + masterUrl + + " is not available. Index fetch failed by interrupt. Exception: " + errorMsg); + return new IndexFetchResult(IndexFetchResult.FAILED_BY_INTERRUPT_MESSAGE, false, e); + } + else + { + LOG.warn("Master at: " + masterUrl + " is not available. Index fetch failed by exception: " + + errorMsg); + return new IndexFetchResult(IndexFetchResult.FAILED_BY_EXCEPTION_MESSAGE, false, e); + } + } + + long latestVersion = (Long) response.get(CMD_INDEX_VERSION); + long masterContentStoreVersion = (Long) response.get(CONTENT_STORE_VERSION); + long latestGeneration = (Long) response.get(GENERATION); + + // The following session should make sure that if replication is happening in more cores at the same time, + // the contentStore replication is done only once. + long slaveContentStoreVersion = replicateContentStore ? + contentStore.getLastCommittedVersion() : + SolrContentStore.NO_CONTENT_STORE_REPLICATION_REQUIRED; + boolean contentStoreReplicationNeeded = + replicateContentStore && (masterContentStoreVersion != slaveContentStoreVersion); + boolean indexReplicationNeeded = true; + + LOG.info("Master's generation: " + latestGeneration); + LOG.info("Master's version: " + latestVersion); + + IndexCommit commit = solrCore.getDeletionPolicy().getLatestCommit(); + if (commit == null) + { + // Presumably the IndexWriter hasn't been opened yet, and hence the deletion policy hasn't been updated with commit points + RefCounted searcherRefCounted = null; + try + { + searcherRefCounted = solrCore.getNewestSearcher(false); + if (searcherRefCounted == null) + { + LOG.warn("No open searcher found - fetch aborted"); + return IndexFetchResult.NO_INDEX_COMMIT_EXIST; + } + commit = searcherRefCounted.get().getIndexReader().getIndexCommit(); + } + finally + { + if (searcherRefCounted != null) + searcherRefCounted.decref(); + } + } + + LOG.info("Slave's generation: " + commit.getGeneration()); + LOG.info("Slave's version: " + IndexDeletionPolicyWrapper.getCommitTimestamp(commit)); + + if (latestVersion == 0L) + { + if (forceReplication && commit.getGeneration() != 0) + { + // since we won't get the files for an empty index, + // we just clear ours and commit + RefCounted iw = solrCore.getUpdateHandler().getSolrCoreState() + .getIndexWriter(solrCore); + try + { + iw.get().deleteAll(); + } + finally + { + iw.decref(); + } + SolrQueryRequest req = new LocalSolrQueryRequest(solrCore, new ModifiableSolrParams()); + solrCore.getUpdateHandler().commit(new CommitUpdateCommand(req, false)); + } + + //there is nothing to be replicated + successfulInstall = true; + return IndexFetchResult.MASTER_VERSION_ZERO; + } + + if (!forceReplication && IndexDeletionPolicyWrapper.getCommitTimestamp(commit) == latestVersion) + { + //master and slave are already in sync just return + LOG.info("Slave index in sync with master."); + successfulInstall = true; + indexReplicationNeeded = false; + } + + if (!indexReplicationNeeded && !contentStoreReplicationNeeded) + { + return IndexFetchResult.ALREADY_IN_SYNC; + } + + if (indexReplicationNeeded) + LOG.info("Starting index replication process"); + + if (contentStoreReplicationNeeded) + LOG.info("Starting content store replication process"); + + if (masterContentStoreVersion < slaveContentStoreVersion) + { + LOG.error("slave content store version is not valid. Full content store replication required"); + slaveContentStoreVersion = SolrContentStore.NO_VERSION_AVAILABLE; + } + + // get the list of files first + fetchFileList(indexReplicationNeeded ? latestGeneration : NO_INDEX_REPLICATION_REQUIRED, + slaveContentStoreVersion); + // this can happen if the commit point is deleted before we fetch the file list. + if (filesToDownload.isEmpty() && indexReplicationNeeded) + { + return IndexFetchResult.PEER_INDEX_COMMIT_DELETED; + } + + if (indexReplicationNeeded) + { + + LOG.info("Number of files in latest index in master: " + filesToDownload.size()); + if (tlogFilesToDownload != null) + { + LOG.info("Number of tlog files in master: " + tlogFilesToDownload.size()); + } + } + + // Create the sync service + fsyncService = ExecutorUtil.newMDCAwareSingleThreadExecutor(new DefaultSolrThreadFactory("fsyncService")); + // use a synchronized list because the list is read by other threads (to show details) + filesDownloaded = Collections.synchronizedList(new ArrayList<>()); + + // if the generation of master is older than that of the slave , it means they are not compatible to be copied + // then a new index directory to be created and all the files need to be copied + boolean isFullCopyNeeded = + indexReplicationNeeded && (IndexDeletionPolicyWrapper.getCommitTimestamp(commit) >= latestVersion + || commit.getGeneration() >= latestGeneration || forceReplication); + + String timestamp = new SimpleDateFormat(SnapShooter.DATE_FMT, Locale.ROOT).format(new Date()); + String tmpIdxDirName = "index." + timestamp; + tmpIndex = solrCore.getDataDir() + tmpIdxDirName; + + tmpIndexDir = solrCore.getDirectoryFactory() + .get(tmpIndex, DirContext.DEFAULT, solrCore.getSolrConfig().indexConfig.lockType); + + // tmp dir for tlog files + if (tlogFilesToDownload != null) + { + tmpTlogDir = new File(solrCore.getUpdateHandler().getUpdateLog().getLogDir(), "tlog." + timestamp); + } + + // cindex dir... + indexDirPath = solrCore.getIndexDir(); + indexDir = solrCore.getDirectoryFactory() + .get(indexDirPath, DirContext.DEFAULT, solrCore.getSolrConfig().indexConfig.lockType); + + try + { + + if (indexReplicationNeeded) + { + + //We will compare all the index files from the master vs the index files on disk to see if there is a mismatch + //in the metadata. If there is a mismatch for the same index file then we download the entire index again. + if (!isFullCopyNeeded && isIndexStale(indexDir)) + { + isFullCopyNeeded = true; + } + + if (!isFullCopyNeeded) + { + // a searcher might be using some flushed but not committed segments + // because of soft commits (which open a searcher on IW's data) + // so we need to close the existing searcher on the last commit + // and wait until we are able to clean up all unused lucene files + if (solrCore.getCoreContainer().isZooKeeperAware()) + { + solrCore.closeSearcher(); + } + + // rollback and reopen index writer and wait until all unused files + // are successfully deleted + solrCore.getUpdateHandler().newIndexWriter(true); + RefCounted writer = solrCore.getUpdateHandler().getSolrCoreState() + .getIndexWriter(null); + try + { + IndexWriter indexWriter = writer.get(); + int c = 0; + indexWriter.deleteUnusedFiles(); + while (hasUnusedFiles(indexDir, commit)) + { + indexWriter.deleteUnusedFiles(); + LOG.info("Sleeping for 1000ms to wait for unused lucene index files to be delete-able"); + Thread.sleep(1000); + c++; + if (c >= 30) + { + LOG.warn( + "IndexFetcher unable to cleanup unused lucene index files so we must do a full copy instead"); + isFullCopyNeeded = true; + break; + } + } + if (c > 0) + { + LOG.info("IndexFetcher slept for " + (c * 1000) + + "ms for unused lucene index files to be delete-able"); + } + } + finally + { + writer.decref(); + } + } + } + + boolean reloadCore = false; + + try + { + // we have to be careful and do this after we know isFullCopyNeeded won't be flipped + if (!isFullCopyNeeded) + { + solrCore.getUpdateHandler().getSolrCoreState().closeIndexWriter(solrCore, true); + } + + LOG.info("Starting download (fullCopy={}) to {}", isFullCopyNeeded, tmpIndexDir); + successfulInstall = false; + + long bytesDownloaded = 0; + + if (indexReplicationNeeded) + { + downloadIndexFiles(isFullCopyNeeded, indexDir, tmpIndexDir, latestGeneration); + + if (tlogFilesToDownload != null) + { + assert tmpTlogDir != null; + bytesDownloaded += downloadTlogFiles(tmpTlogDir, latestGeneration); + reloadCore = true; // reload update log + } + } + + try { + + if (contentStoreReplicationNeeded) + { + + if (contentStoreFilesToDownload != null) + { + bytesDownloaded += downloadContentStoreFiles(contentStore.getRootLocation()); + } + + if (contentStoreFilesToDelete != null) + { + deleteContentStoreFiles(contentStore.getRootLocation(), contentStoreFilesToDelete); + } + + if (fullContentStoreReplication) + { + cleanUpContentStore(contentStore.getRootLocation()); + } + + contentStore.setLastCommittedVersion(masterContentStoreVersion); + LOG.info("content store has been updated to version: {}", masterContentStoreVersion); + } + + } catch (Exception e) { + LOG.error("impossible to complete content store replication {}", e); + } + + final long timeTakenSeconds = getReplicationTimeElapsed(); + final Long bytesDownloadedPerSecond = (timeTakenSeconds != 0 ? bytesDownloaded / timeTakenSeconds : + null); + LOG.info("Total time taken for download (fullCopy={},bytesDownloaded={}) : {} secs ({} bytes/sec)", + isFullCopyNeeded, bytesDownloaded, timeTakenSeconds, bytesDownloadedPerSecond); + + if (indexReplicationNeeded) + { + + Collection> modifiedConfFiles = getModifiedConfFiles(confFilesToDownload); + if (!modifiedConfFiles.isEmpty()) + { + reloadCore = true; + downloadConfFiles(confFilesToDownload, latestGeneration); + if (isFullCopyNeeded) + { + successfulInstall = solrCore.modifyIndexProps(tmpIdxDirName); + if (successfulInstall) + { + deleteTmpIdxDir = false; + } + } + else + { + successfulInstall = moveIndexFiles(tmpIndexDir, indexDir); + } + if (tlogFilesToDownload != null) + { + // move tlog files and refresh ulog only if we successfully installed a new index + successfulInstall &= moveTlogFiles(tmpTlogDir); + } + if (successfulInstall) + { + if (isFullCopyNeeded) + { + // let the system know we are changing dir's and the old one + // may be closed + if (indexDir != null) + { + solrCore.getDirectoryFactory().doneWithDirectory(indexDir); + // Cleanup all index files not associated with any *named* snapshot. + solrCore.deleteNonSnapshotIndexFiles(indexDirPath); + } + } + + LOG.info("Configuration files are modified, core will be reloaded"); + logReplicationTimeAndConfFiles(modifiedConfFiles, + successfulInstall);// write to a file time of replication and + // conf files. + } + } + else + { + terminateAndWaitFsyncService(); + if (isFullCopyNeeded) + { + successfulInstall = solrCore.modifyIndexProps(tmpIdxDirName); + if (successfulInstall) + { + deleteTmpIdxDir = false; + } + } + else + { + successfulInstall = moveIndexFiles(tmpIndexDir, indexDir); + } + if (tlogFilesToDownload != null) + { + // move tlog files and refresh ulog only if we successfully installed a new index + successfulInstall &= moveTlogFiles(tmpTlogDir); + } + if (successfulInstall) + { + logReplicationTimeAndConfFiles(modifiedConfFiles, successfulInstall); + } + } + } + } + finally + { + if (!isFullCopyNeeded && indexReplicationNeeded) + { + solrCore.getUpdateHandler().getSolrCoreState().openIndexWriter(solrCore); + } + } + + if (indexReplicationNeeded) + { + + // we must reload the core after we open the IW back up + if (successfulInstall && (reloadCore || forceCoreReload)) + { + LOG.info("Reloading SolrCore {}", solrCore.getName()); + reloadCore(); + } + + if (successfulInstall) + { + if (isFullCopyNeeded) + { + // let the system know we are changing dir's and the old one + // may be closed + if (indexDir != null) + { + LOG.info("removing old index directory " + indexDir); + solrCore.getDirectoryFactory().doneWithDirectory(indexDir); + solrCore.getDirectoryFactory().remove(indexDir); + } + } + if (isFullCopyNeeded) + { + solrCore.getUpdateHandler().newIndexWriter(isFullCopyNeeded); + } + + openNewSearcherAndUpdateCommitPoint(); + } + + if (!isFullCopyNeeded && !forceReplication && !successfulInstall) + { + cleanup(solrCore, tmpIndexDir, indexDir, deleteTmpIdxDir, tmpTlogDir, successfulInstall); + cleanupDone = true; + // we try with a full copy of the index + LOG.warn( + "Replication attempt was not successful - trying a full index replication reloadCore={}", + reloadCore); + successfulInstall = fetchLatestIndex(true, reloadCore).getSuccessful(); + } + } + + markReplicationStop(); + return successfulInstall ? IndexFetchResult.INDEX_FETCH_SUCCESS : IndexFetchResult.INDEX_FETCH_FAILURE; + } + catch (ReplicationHandlerException e) + { + LOG.error("User aborted Replication"); + return new IndexFetchResult(IndexFetchResult.FAILED_BY_EXCEPTION_MESSAGE, false, e); + } + catch (SolrException e) + { + throw e; + } + catch (InterruptedException e) + { + throw new InterruptedException("Index fetch interrupted"); + } + catch (Exception e) + { + throw new SolrException(ErrorCode.SERVER_ERROR, "Index fetch failed : ", e); + } + } + finally + { + if (!cleanupDone) + { + cleanup(solrCore, tmpIndexDir, indexDir, deleteTmpIdxDir, tmpTlogDir, successfulInstall); + } + } + } + + /** + * Download content store required files. + */ + private long downloadContentStoreFiles(String contentStoreDirectory) throws Exception + { + LOG.info("Starting download of content store files from master: " + tlogFilesToDownload); + contentStoreFilesDownloaded = Collections.synchronizedList(new ArrayList<>()); + long bytesDownloaded = 0; + + File tmpContentStoreDirectory = new File(contentStoreDirectory, "contentstore." + getDateAsStr(new Date())); + + LOG.info("content store replication: {} files to downlad", contentStoreFilesToDownload.size()); + + List> contentStoreFilesToDownloadFiltered = contentStoreFilesToDownload.stream() + .filter(file -> compareContentStoreFiles(new File(contentStoreDirectory), (String) file.get(NAME), + (Long) file.get(SIZE), (Long) file.get(CHECKSUM))).collect(Collectors.toList()); + + if (contentStoreFilesToDownloadFiltered.size() != contentStoreFilesToDownload.size()) + { + LOG.info("content store replication: some of the files are already in sync. {} files to download", + contentStoreFilesToDownloadFiltered.size()); + } + + if (!contentStoreFilesToDownloadFiltered.isEmpty()) + { + for (List> partition : Lists + .partition(contentStoreFilesToDownloadFiltered, CONTENT_STORE_PARTITION_SIZE)) + { + contentStoreFileFetcher = new ContentStoreFetcher(tmpContentStoreDirectory, + AlfrescoReplicationHandler.CONTENT_STORE_FILES, partition); + + contentStoreFileFetcher.fetchContentStore(); + bytesDownloaded += contentStoreFileFetcher.getBytesDownloaded(); + contentStoreFilesDownloaded.addAll(partition); + } + + terminateAndWaitFsyncService(); + copyTmpContentStoreToContentStore(tmpContentStoreDirectory, contentStoreDirectory); + + delTree(tmpContentStoreDirectory); + LOG.info("content store files successfully downloaded"); + } + + return bytesDownloaded; + } + + private void cleanup(final SolrCore core, + Directory tmpIndexDir, + Directory indexDir, + boolean deleteTmpIdxDir, + File tmpTlogDir, + boolean successfulInstall) + { + try + { + if (!successfulInstall) + { + try + { + logReplicationTimeAndConfFiles(null, successfulInstall); + } + catch (Exception e) + { + // this can happen on shutdown, a fetch may be running in a thread after DirectoryFactory is closed + LOG.warn("Could not log failed replication details", e); + } + } + + if (core.getCoreContainer().isZooKeeperAware()) + { + // we only track replication success in SolrCloud mode + core.getUpdateHandler().getSolrCoreState().setLastReplicateIndexSuccess(successfulInstall); + } + + filesToDownload = filesDownloaded = confFilesDownloaded = confFilesToDownload = tlogFilesToDownload = tlogFilesDownloaded = contentStoreFilesDownloaded = contentStoreFilesToDownload = null; + markReplicationStop(); + dirFileFetcher = null; + localFileFetcher = null; + if (fsyncService != null && !fsyncService.isShutdown()) + { + fsyncService.shutdown(); + } + fsyncService = null; + stop = false; + fsyncException = null; + } + finally + { + // order below is important + try + { + if (tmpIndexDir != null && deleteTmpIdxDir) + { + core.getDirectoryFactory().doneWithDirectory(tmpIndexDir); + core.getDirectoryFactory().remove(tmpIndexDir); + } + } + catch (Exception e) + { + SolrException.log(LOG, e); + } + finally + { + try + { + if (tmpIndexDir != null){ + core.getDirectoryFactory().release(tmpIndexDir); + } + } + catch (Exception e) + { + SolrException.log(LOG, e); + } + try + { + if (indexDir != null) + { + core.getDirectoryFactory().release(indexDir); + } + } + catch (Exception e) + { + SolrException.log(LOG, e); + } + try + { + if (tmpTlogDir != null) + { + delTree(tmpTlogDir); + } + } + catch (Exception e) + { + SolrException.log(LOG, e); + } + } + } + } + + private boolean hasUnusedFiles(Directory indexDir, IndexCommit commit) throws IOException + { + String segmentsFileName = commit.getSegmentsFileName(); + SegmentInfos infos = SegmentInfos.readCommit(indexDir, segmentsFileName); + Set currentFiles = new HashSet<>(infos.files(true)); + String[] allFiles = indexDir.listAll(); + for (String file : allFiles) + { + if (!file.equals(segmentsFileName) && !currentFiles.contains(file) && !file.endsWith(".lock")) + { + LOG.info("Found unused file: " + file); + return true; + } + } + return false; + } + + /** + * terminate the fsync service and wait for all the tasks to complete. If it is already terminated + */ + private void terminateAndWaitFsyncService() throws Exception + { + if (fsyncService.isTerminated()){ + return; + } + fsyncService.shutdown(); + // give a long wait say 1 hr + fsyncService.awaitTermination(3600, TimeUnit.SECONDS); + // if any fsync failed, throw that exception back + Exception fsyncExceptionCopy = fsyncException; + if (fsyncExceptionCopy != null) + { + throw fsyncExceptionCopy; + } + } + + /** + * Helper method to record the last replication's details so that we can show them on the statistics page across + * restarts. + * + * @throws IOException on IO error + */ + @SuppressForbidden(reason = "Need currentTimeMillis for debugging/stats") + private void logReplicationTimeAndConfFiles(Collection> modifiedConfFiles, boolean successfulInstall) + throws IOException + { + List confFiles = new ArrayList<>(); + if (modifiedConfFiles != null && !modifiedConfFiles.isEmpty()){ + for (Map map1 : modifiedConfFiles) + { + confFiles.add((String) map1.get(NAME)); + } + } + + Properties props = alfrescoReplicationHandler.loadReplicationProperties(); + long replicationTime = System.currentTimeMillis(); + long replicationTimeTaken = getReplicationTimeElapsed(); + Directory dir = null; + try + { + dir = solrCore.getDirectoryFactory() + .get(solrCore.getDataDir(), DirContext.META_DATA, solrCore.getSolrConfig().indexConfig.lockType); + + int indexCount = 1, confFilesCount = 1; + if (props.containsKey(TIMES_INDEX_REPLICATED)) + { + indexCount = Integer.parseInt(props.getProperty(TIMES_INDEX_REPLICATED)) + 1; + } + StringBuilder sb = readToStringBuilder(replicationTime, props.getProperty(INDEX_REPLICATED_AT_LIST)); + props.setProperty(INDEX_REPLICATED_AT_LIST, sb.toString()); + props.setProperty(INDEX_REPLICATED_AT, String.valueOf(replicationTime)); + props.setProperty(PREVIOUS_CYCLE_TIME_TAKEN, String.valueOf(replicationTimeTaken)); + props.setProperty(TIMES_INDEX_REPLICATED, String.valueOf(indexCount)); + if (modifiedConfFiles != null && !modifiedConfFiles.isEmpty()) + { + props.setProperty(CONF_FILES_REPLICATED, confFiles.toString()); + props.setProperty(CONF_FILES_REPLICATED_AT, String.valueOf(replicationTime)); + if (props.containsKey(TIMES_CONFIG_REPLICATED)) + { + confFilesCount = Integer.parseInt(props.getProperty(TIMES_CONFIG_REPLICATED)) + 1; + } + props.setProperty(TIMES_CONFIG_REPLICATED, String.valueOf(confFilesCount)); + } + + props.setProperty(LAST_CYCLE_BYTES_DOWNLOADED, String.valueOf(getTotalBytesDownloaded())); + if (!successfulInstall) + { + int numFailures = 1; + if (props.containsKey(TIMES_FAILED)) + { + numFailures = Integer.parseInt(props.getProperty(TIMES_FAILED)) + 1; + } + props.setProperty(TIMES_FAILED, String.valueOf(numFailures)); + props.setProperty(REPLICATION_FAILED_AT, String.valueOf(replicationTime)); + sb = readToStringBuilder(replicationTime, props.getProperty(REPLICATION_FAILED_AT_LIST)); + props.setProperty(REPLICATION_FAILED_AT_LIST, sb.toString()); + } + + String tmpFileName = REPLICATION_PROPERTIES + "." + System.nanoTime(); + final IndexOutput out = dir.createOutput(tmpFileName, DirectoryFactory.IOCONTEXT_NO_CACHE); + Writer outFile = new OutputStreamWriter(new PropertiesOutputStream(out), StandardCharsets.UTF_8); + try + { + props.store(outFile, "Replication details"); + dir.sync(Collections.singleton(tmpFileName)); + } + finally + { + IOUtils.closeQuietly(outFile); + } + + solrCore.getDirectoryFactory().renameWithOverwrite(dir, tmpFileName, REPLICATION_PROPERTIES); + } + catch (Exception e) + { + LOG.warn("Exception while updating statistics", e); + } + finally + { + if (dir != null) + { + solrCore.getDirectoryFactory().release(dir); + } + } + } + + long getTotalBytesDownloaded() + { + long bytesDownloaded = 0; + //get size from list of files to download + for (Map file : getFilesDownloaded()) + { + bytesDownloaded += (Long) file.get(SIZE); + } + + //get size from list of conf files to download + for (Map file : getConfFilesDownloaded()) + { + bytesDownloaded += (Long) file.get(SIZE); + } + + //get size from current file being downloaded + Map currentFile = getCurrentFile(); + if (currentFile != null) + { + if (currentFile.containsKey("bytesDownloaded")) + { + bytesDownloaded += (Long) currentFile.get("bytesDownloaded"); + } + } + return bytesDownloaded; + } + + private StringBuilder readToStringBuilder(long replicationTime, String str) + { + StringBuilder sb = new StringBuilder(); + List l = new ArrayList<>(); + if (str != null && str.length() != 0) + { + String[] ss = str.split(","); + Collections.addAll(l, ss); + } + sb.append(replicationTime); + if (!l.isEmpty()) + { + for (int i = 0; i < l.size() || i < 9; i++) + { + if (i == l.size() || i == 9) + { + break; + } + String s = l.get(i); + sb.append(",").append(s); + } + } + return sb; + } + + private void openNewSearcherAndUpdateCommitPoint() throws IOException + { + RefCounted searcher = null; + IndexCommit commitPoint; + // must get the latest solrCore object because the one we have might be closed because of a reload + try (SolrCore core = solrCore.getCoreContainer().getCore(solrCore.getName())) + { + Future[] waitSearcher = new Future[1]; + searcher = core.getSearcher(true, true, waitSearcher, true); + if (waitSearcher[0] != null) + { + try + { + waitSearcher[0].get(); + } + catch (InterruptedException | ExecutionException e) + { + SolrException.log(LOG, e); + } + } + commitPoint = searcher.get().getIndexReader().getIndexCommit(); + } + finally + { + if (searcher != null) + { + searcher.decref(); + } + } + + // update the commit point in replication handler + alfrescoReplicationHandler.indexCommitPoint = commitPoint; + + } + + private void reloadCore() + { + final CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { + try + { + solrCore.getCoreContainer().reload(solrCore.getName()); + } + catch (Exception e) + { + LOG.error("Could not reload core ", e); + } + finally + { + latch.countDown(); + } + }).start(); + try + { + latch.await(); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for core reload to finish", e); + } + } + + private void downloadConfFiles(List> confFilesToDownload, long latestGeneration) + throws Exception + { + LOG.info("Starting download of configuration files from master: " + confFilesToDownload); + confFilesDownloaded = Collections.synchronizedList(new ArrayList<>()); + File tmpconfDir = new File(solrCore.getResourceLoader().getConfigDir(), "conf." + getDateAsStr(new Date())); + try + { + boolean status = tmpconfDir.mkdirs(); + if (!status) + { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Failed to create temporary config folder: " + tmpconfDir.getName()); + } + for (Map file : confFilesToDownload) + { + String saveAs = (String) (file.get(ALIAS) == null ? file.get(NAME) : file.get(ALIAS)); + localFileFetcher = new LocalFsFileFetcher(tmpconfDir, file, saveAs, CONF_FILE_SHORT, latestGeneration); + currentFile = file; + localFileFetcher.fetchFile(); + confFilesDownloaded.add(new HashMap<>(file)); + } + // this is called before copying the files to the original conf dir + // so that if there is an exception avoid corrupting the original files. + terminateAndWaitFsyncService(); + copyTmpConfFiles2Conf(tmpconfDir); + } + finally + { + delTree(tmpconfDir); + } + } + + /** + * Download all the tlog files to the temp tlog directory. + */ + private long downloadTlogFiles(File tmpTlogDir, long latestGeneration) throws Exception + { + LOG.info("Starting download of tlog files from master: " + tlogFilesToDownload); + tlogFilesDownloaded = Collections.synchronizedList(new ArrayList<>()); + long bytesDownloaded = 0; + + boolean status = tmpTlogDir.mkdirs(); + if (!status) + { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Failed to create temporary tlog folder: " + tmpTlogDir.getName()); + } + for (Map file : tlogFilesToDownload) + { + String saveAs = (String) (file.get(ALIAS) == null ? file.get(NAME) : file.get(ALIAS)); + localFileFetcher = new LocalFsFileFetcher(tmpTlogDir, file, saveAs, TLOG_FILE, latestGeneration); + currentFile = file; + localFileFetcher.fetchFile(); + bytesDownloaded += localFileFetcher.getBytesDownloaded(); + tlogFilesDownloaded.add(new HashMap<>(file)); + } + return bytesDownloaded; + } + + /** + * Download the index files. If a new index is needed, download all the files. + * + * @param downloadCompleteIndex is it a fresh index copy + * @param tmpIndexDir the directory to which files need to be downloadeed to + * @param indexDir the indexDir to be merged to + * @param latestGeneration the version number + * @return number of bytes downloaded + */ + private long downloadIndexFiles(boolean downloadCompleteIndex, Directory indexDir, Directory tmpIndexDir, long latestGeneration) + throws Exception + { + if (LOG.isDebugEnabled()) + { + LOG.debug("Download files to dir: " + Arrays.asList(indexDir.listAll())); + } + long bytesDownloaded = 0; + for (Map file : filesToDownload) + { + String filename = (String) file.get(NAME); + long size = (Long) file.get(SIZE); + CompareResult compareResult = compareFile(indexDir, filename, size, (Long) file.get(CHECKSUM)); + boolean alwaysDownload = filesToAlwaysDownloadIfNoChecksums(filename, size, compareResult); + + LOG.debug("Downloading file={} size={} checksum={} alwaysDownload={}", filename, size, file.get(CHECKSUM), + alwaysDownload); + + if (!compareResult.equal || downloadCompleteIndex || alwaysDownload) + { + dirFileFetcher = new DirectoryFileFetcher(tmpIndexDir, file, (String) file.get(NAME), FILE, + latestGeneration); + currentFile = file; + dirFileFetcher.fetchFile(); + bytesDownloaded += dirFileFetcher.getBytesDownloaded(); + filesDownloaded.add(new HashMap<>(file)); + } + else + { + LOG.info("Skipping download for " + file.get(NAME) + " because it already exists"); + } + } + return bytesDownloaded; + } + + /** + * All the files which are common between master and slave must have same size and same checksum else we assume + * they are not compatible (stale). + * + * @return true if the index stale and we need to download a fresh copy, false otherwise. + * @throws IOException if low level io error + */ + private boolean isIndexStale(Directory dir) throws IOException + { + for (Map file : filesToDownload) + { + String filename = (String) file.get(NAME); + Long length = (Long) file.get(SIZE); + Long checksum = (Long) file.get(CHECKSUM); + if (slowFileExists(dir, filename)) + { + if (checksum != null) + { + if (!(compareFile(dir, filename, length, checksum).equal)) + { + // file exists and size or checksum is different, therefore we must download it again + return true; + } + } + else + { + if (length != dir.fileLength(filename)) + { + LOG.warn("File {} did not match. expected length is {} and actual length is {}", filename, + length, dir.fileLength(filename)); + return true; + } + } + } + } + return false; + } + + /** + * Copy a file by the File#renameTo() method. If it fails, it is considered a failure + *

+ */ + private boolean moveAFile(Directory tmpIdxDir, Directory indexDir, String fname) + { + LOG.debug("Moving file: {}", fname); + boolean success = false; + try + { + if (slowFileExists(indexDir, fname)) + { + LOG.warn("Cannot complete replication attempt because file already exists:" + fname); + + // we fail - we downloaded the files we need, if we can't move one in, we can't + // count on the correct index + return false; + } + } + catch (IOException e) + { + SolrException.log(LOG, "could not check if a file exists", e); + return false; + } + try + { + solrCore.getDirectoryFactory().move(tmpIdxDir, indexDir, fname, DirectoryFactory.IOCONTEXT_NO_CACHE); + success = true; + } + catch (IOException e) + { + SolrException.log(LOG, "Could not move file", e); + } + return success; + } + + /** + * Copy all index files from the temp index dir to the actual index. The segments_N file is copied last. + */ + private boolean moveIndexFiles(Directory tmpIdxDir, Directory indexDir) + { + if (LOG.isDebugEnabled()) + { + try + { + LOG.info("From dir files:" + Arrays.asList(tmpIdxDir.listAll())); + LOG.info("To dir files:" + Arrays.asList(indexDir.listAll())); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + String segmentsFile = null; + for (Map f : filesDownloaded) + { + String fname = (String) f.get(NAME); + // the segments file must be copied last + // or else if there is a failure in between the + // index will be corrupted + if (fname.startsWith("segments_")) + { + //The segments file must be copied in the end + //Otherwise , if the copy fails index ends up corrupted + segmentsFile = fname; + continue; + } + if (!moveAFile(tmpIdxDir, indexDir, fname)) + return false; + } + //copy the segments file last + if (segmentsFile != null) + { + return moveAFile(tmpIdxDir, indexDir, segmentsFile); + } + return true; + } + + /** + *

+ * Copy all the tlog files from the temp tlog dir to the actual tlog dir, and reset + * the {@link UpdateLog}. The copy will try to preserve the original tlog directory + * if the copy fails. + *

+ *

+ * This assumes that the tlog files transferred from the leader are in synch with the + * index files transferred from the leader. The reset of the update log relies on the version + * of the latest operations found in the tlog files. If the tlogs are ahead of the latest commit + * point, it will not copy all the needed buffered updates for the replay and it will miss + * some operations. + *

+ */ + private boolean moveTlogFiles(File tmpTlogDir) + { + UpdateLog ulog = solrCore.getUpdateHandler().getUpdateLog(); + + VersionInfo vinfo = ulog.getVersionInfo(); + vinfo.blockUpdates(); // block updates until the new update log is initialised + try + { + // reset the update log before copying the new tlog directory + CdcrUpdateLog.BufferedUpdates bufferedUpdates = ((CdcrUpdateLog) ulog).resetForRecovery(); + // try to move the temp tlog files to the tlog directory + if (!copyTmpTlogFiles2Tlog(tmpTlogDir)) + return false; + // reinitialise the update log and copy the buffered updates + if (bufferedUpdates.tlog != null) + { + // map file path to its new backup location + File parentDir = FileSystems.getDefault() + .getPath(solrCore.getUpdateHandler().getUpdateLog().getLogDir()).getParent().toFile(); + File backupTlogDir = new File(parentDir, tmpTlogDir.getName()); + bufferedUpdates.tlog = new File(backupTlogDir, bufferedUpdates.tlog.getName()); + } + // init the update log with the new set of tlog files, and copy the buffered updates + ((CdcrUpdateLog) ulog).initForRecovery(bufferedUpdates.tlog, bufferedUpdates.offset); + } + catch (Exception e) + { + LOG.error("Unable to copy tlog files", e); + return false; + } + finally + { + vinfo.unblockUpdates(); + } + return true; + } + + /** + * Make file list + */ + private List makeTmpConfDirFileList(File dir, List fileList) + { + File[] files = dir.listFiles(); + + if (files != null) + { + for (File file : files) + { + if (file.isFile()) + { + fileList.add(file); + } + else if (file.isDirectory()) + { + fileList = makeTmpConfDirFileList(file, fileList); + } + } + } + return fileList; + } + + /** + * The conf files are copied to the tmp dir to the conf dir. A backup of the old file is maintained + */ + private void copyTmpConfFiles2Conf(File tmpconfDir) + { + boolean status; + File confDir = new File(solrCore.getResourceLoader().getConfigDir()); + for (File file : makeTmpConfDirFileList(tmpconfDir, new ArrayList<>())) + { + File oldFile = new File(confDir, + file.getPath().substring(tmpconfDir.getPath().length(), file.getPath().length())); + if (!oldFile.getParentFile().exists()) + { + status = oldFile.getParentFile().mkdirs(); + if (!status) + { + throw new SolrException(ErrorCode.SERVER_ERROR, "Unable to mkdirs: " + oldFile.getParentFile()); + } + } + if (oldFile.exists()) + { + File backupFile = new File(oldFile.getPath() + "." + getDateAsStr(new Date(oldFile.lastModified()))); + if (!backupFile.getParentFile().exists()) + { + status = backupFile.getParentFile().mkdirs(); + if (!status) + { + throw new SolrException(ErrorCode.SERVER_ERROR, + "Unable to mkdirs: " + backupFile.getParentFile()); + } + } + status = oldFile.renameTo(backupFile); + if (!status) + { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Unable to rename: " + oldFile + " to: " + backupFile); + } + } + status = file.renameTo(oldFile); + if (!status) + { + throw new SolrException(ErrorCode.SERVER_ERROR, "Unable to rename: " + file + " to: " + oldFile); + } + } + } + + /** + * Copy the downloaded content store files into the contentstore directory + * @param tmpContentStoreDir + * @param contentStorePath + * @throws IOException + */ + private void copyTmpContentStoreToContentStore(File tmpContentStoreDir, String contentStorePath) throws Exception + { + + String tmpContentStorePath = tmpContentStoreDir.getPath(); + + Files.walk(tmpContentStoreDir.toPath()).forEach(p -> { + File tmpFile = new File(p.toUri()); + if (!tmpFile.isDirectory()) + { + File csFile = new File(p.toString().replace(tmpContentStorePath, contentStorePath)); + try + { + Files.createDirectories(Paths.get(csFile.getParent())); + Files.move(tmpFile.toPath(), csFile.toPath(), StandardCopyOption.ATOMIC_MOVE); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + }); + } + + + /** + * Deletes the files in filesToDelete list from contentStore + * @param contentStorePath + * @param filesToDelete + */ + private void deleteContentStoreFiles(String contentStorePath, List> filesToDelete) + { + filesToDelete.stream().map(f -> (String) f.get(NAME)).forEach(p -> { + File f = new File(contentStorePath, p); + f.delete(); + }); + + LOG.info("deleted {} files from content store", filesToDelete.size()); + } + + /** + * Deletes from contentstore all the files that has not been updated. + * @param contentStorePath + */ + private void cleanUpContentStore(String contentStorePath) throws Exception + { + AtomicInteger fileDeleted = new AtomicInteger(); + + // This is the set of the ONLY files that should be in contentStore. + // This set is computed from the information got from master. After a full replication, only the files + // that have been downloaded from master (contentStoreFilesToDownload) should be in contentStore. + // The file paths are translated in the current OS path notation. + Set contentStoreFiles = contentStoreFilesToDownload.stream() + .map(e -> (String) e.get(NAME)) + .map(FilenameUtils::separatorsToSystem) + .collect(Collectors.toSet()); + try + { + Files.walk(Paths.get(contentStorePath)).forEach(p -> { + File f = new File(p.toUri()); + if (!f.isDirectory() && !contentStoreFiles.contains(p.toString().replace(contentStorePath, ""))) + { + try + { + Files.delete(p); + fileDeleted.getAndIncrement(); + } + catch (IOException ex) + { + LOG.error("Impossible delete file {}", p); + } + } + }); + } + catch (Exception e) + { + LOG.error("Impossible to delete unnecessary files. Content store may contains unused contents"); + throw(e); + } + + LOG.info("deleted {} unnecessary files from content store", fileDeleted); + } + + /** + * The tlog files are moved from the tmp dir to the tlog dir as an atomic filesystem operation. + * A backup of the old directory is maintained. If the directory move fails, it will try to revert back the original + * tlog directory. + */ + private boolean copyTmpTlogFiles2Tlog(File tmpTlogDir) + { + Path tlogDir = FileSystems.getDefault().getPath(solrCore.getUpdateHandler().getUpdateLog().getLogDir()); + Path backupTlogDir = FileSystems.getDefault() + .getPath(tlogDir.getParent().toAbsolutePath().toString(), tmpTlogDir.getName()); + + try + { + Files.move(tlogDir, backupTlogDir, StandardCopyOption.ATOMIC_MOVE); + } + catch (IOException e) + { + SolrException.log(LOG, "Unable to rename: " + tlogDir + " to: " + backupTlogDir, e); + return false; + } + + Path src = FileSystems.getDefault().getPath(backupTlogDir.toAbsolutePath().toString(), tmpTlogDir.getName()); + try + { + Files.move(src, tlogDir, StandardCopyOption.ATOMIC_MOVE); + } + catch (IOException e) + { + SolrException.log(LOG, "Unable to rename: " + src + " to: " + tlogDir, e); + + // In case of error, try to revert back the original tlog directory + try + { + Files.move(backupTlogDir, tlogDir, StandardCopyOption.ATOMIC_MOVE); + } + catch (IOException e2) + { + // bad, we were not able to revert back the original tlog directory + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Unable to rename: " + backupTlogDir + " to: " + tlogDir); + } + + return false; + } + + return true; + } + + private String getDateAsStr(Date d) + { + return new SimpleDateFormat(SnapShooter.DATE_FMT, Locale.ROOT).format(d); + } + + /** + * The local conf files are compared with the conf files in the master. If they are same (by checksum) do not copy. + * + * @param confFilesToDownload The list of files obtained from master + * @return a list of configuration files which have changed on the master and need to be downloaded. + */ + @SuppressWarnings("unchecked") + private Collection> getModifiedConfFiles(List> confFilesToDownload) + { + if (confFilesToDownload == null || confFilesToDownload.isEmpty()) + return Collections.emptyList(); + //build a map with alias/name as the key + Map> nameVsFile = new HashMap<>(); + NamedList names = new NamedList(); + for (Map map : confFilesToDownload) + { + //if alias is present that is the name the file may have in the slave + String name = (String) (map.get(ALIAS) == null ? map.get(NAME) : map.get(ALIAS)); + nameVsFile.put(name, map); + names.add(name, null); + } + //get the details of the local conf files with the same alias/name + List> localFilesInfo = alfrescoReplicationHandler + .getConfFileInfoFromCache(names, confFileInfoCache); + //compare their size/checksum to see if + for (Map fileInfo : localFilesInfo) + { + String name = (String) fileInfo.get(NAME); + Map m = nameVsFile.get(name); + if (m == null) + continue; // the file is not even present locally (so must be downloaded) + if (m.get(CHECKSUM).equals(fileInfo.get(CHECKSUM))) + { + nameVsFile.remove(name); //checksums are same so the file need not be downloaded + } + } + return nameVsFile.isEmpty() ? Collections.EMPTY_LIST : nameVsFile.values(); + } + + /** + * Stops the ongoing fetch + */ + void abortFetch() + { + stop = true; + } + + @SuppressForbidden(reason = "Need currentTimeMillis for debugging/stats") + private void markReplicationStart() + { + replicationTimer = new RTimer(); + replicationStartTimeStamp = new Date(); + } + + private void markReplicationStop() + { + replicationStartTimeStamp = null; + replicationTimer = null; + } + + Date getReplicationStartTimeStamp() + { + return replicationStartTimeStamp; + } + + long getReplicationTimeElapsed() + { + long timeElapsed = 0; + if (replicationStartTimeStamp != null) + timeElapsed = TimeUnit.SECONDS.convert((long) replicationTimer.getTime(), TimeUnit.MILLISECONDS); + return timeElapsed; + } + + List> getTlogFilesToDownload() + { + //make a copy first because it can be null later + List> tmp = tlogFilesToDownload; + //create a new instance. or else iterator may fail + return tmp == null ? Collections.emptyList() : new ArrayList<>(tmp); + } + + List> getTlogFilesDownloaded() + { + //make a copy first because it can be null later + List> tmp = tlogFilesDownloaded; + // NOTE: it's safe to make a copy of a SynchronizedCollection(ArrayList) + return tmp == null ? Collections.emptyList() : new ArrayList<>(tmp); + } + + List> getConfFilesToDownload() + { + //make a copy first because it can be null later + List> tmp = confFilesToDownload; + //create a new instance. or else iterator may fail + return tmp == null ? Collections.emptyList() : new ArrayList<>(tmp); + } + + List> getConfFilesDownloaded() + { + //make a copy first because it can be null later + List> tmp = confFilesDownloaded; + // NOTE: it's safe to make a copy of a SynchronizedCollection(ArrayList) + return tmp == null ? Collections.emptyList() : new ArrayList<>(tmp); + } + + List> getContentStoreFilesDownloaded() + { + //make a copy first because it can be null later + List> tmp = contentStoreFilesDownloaded; + // NOTE: it's safe to make a copy of a SynchronizedCollection(ArrayList) + return tmp == null ? Collections.emptyList() : new ArrayList<>(tmp); + } + + List> getFilesToDownload() + { + //make a copy first because it can be null later + List> tmp = filesToDownload; + return tmp == null ? Collections.emptyList() : new ArrayList<>(tmp); + } + + List> getContentStoreFileToDownload() + { + //make a copy first because it can be null later + List> tmp = contentStoreFilesToDownload; + return tmp == null ? Collections.emptyList() : new ArrayList<>(tmp); + } + + List> getFilesDownloaded() + { + List> tmp = filesDownloaded; + return tmp == null ? Collections.emptyList() : new ArrayList<>(tmp); + } + + + Map getCurrentFile() + { + Map tmp = currentFile; + DirectoryFileFetcher tmpFileFetcher = dirFileFetcher; + if (tmp == null) + return null; + tmp = new HashMap<>(tmp); + if (tmpFileFetcher != null) + tmp.put("bytesDownloaded", tmpFileFetcher.getBytesDownloaded()); + return tmp; + } + + NamedList getDetails() throws IOException, SolrServerException + { + ModifiableSolrParams params = new ModifiableSolrParams(); + params.set(COMMAND, CMD_DETAILS); + params.set("slave", false); + params.set(CommonParams.QT, AlfrescoReplicationHandler.PATH); + + try (HttpSolrClient client = new HttpSolrClient.Builder(masterUrl).withHttpClient(myHttpClient).build()) + { + client.setSoTimeout(60000); + client.setConnectionTimeout(15000); + QueryRequest request = new QueryRequest(params); + return client.request(request); + } + } + + void destroy() + { + abortFetch(); + } + + String getMasterUrl() + { + return masterUrl; + } + + private interface FileInterface + { + void sync() throws IOException; + + void write(byte[] buf, int packetSize) throws IOException; + + void close() throws Exception; + + void delete() throws Exception; + } + + public static class IndexFetchResult + { + static final String FAILED_BY_INTERRUPT_MESSAGE = "Fetching index failed by interrupt"; + static final String FAILED_BY_EXCEPTION_MESSAGE = "Fetching index failed by exception"; + /** + * pre-defined results + */ + static final IndexFetchResult ALREADY_IN_SYNC = new IndexFetchResult( + "Local index commit is already in sync with peer", true, null); + static final IndexFetchResult INDEX_FETCH_FAILURE = new IndexFetchResult("Fetching lastest index is failed", + false, null); + static final IndexFetchResult INDEX_FETCH_SUCCESS = new IndexFetchResult("Fetching latest index is successful", + true, null); + static final IndexFetchResult LOCK_OBTAIN_FAILED = new IndexFetchResult("Obtaining SnapPuller lock failed", + false, null); + static final IndexFetchResult MASTER_VERSION_ZERO = new IndexFetchResult( + "Index in peer is empty and never committed yet", true, null); + static final IndexFetchResult NO_INDEX_COMMIT_EXIST = new IndexFetchResult("No IndexCommit in local index", + false, null); + static final IndexFetchResult PEER_INDEX_COMMIT_DELETED = new IndexFetchResult( + "No files to download because IndexCommit in peer was deleted", false, null); + private final String message; + private final boolean successful; + private final Throwable exception; + + IndexFetchResult(String message, boolean successful, Throwable exception) + { + this.message = message; + this.successful = successful; + this.exception = exception; + } + + /* + * @return exception thrown if failed by exception or interrupt, otherwise null + */ + public Throwable getException() + { + return this.exception; + } + + /* + * @return true if index fetch was successful, false otherwise + */ + boolean getSuccessful() + { + return this.successful; + } + + public String getMessage() + { + return this.message; + } + } + + protected static class CompareResult + { + boolean equal = false; + boolean checkSummed = false; + } + + private static class ReplicationHandlerException extends InterruptedException + { + ReplicationHandlerException(String message) + { + super(message); + } + } + + private static class DirectoryFile implements FileInterface + { + private final String saveAs; + private Directory copy2Dir; + private IndexOutput outStream; + + DirectoryFile(Directory tmpIndexDir, String saveAs) throws IOException + { + this.saveAs = saveAs; + this.copy2Dir = tmpIndexDir; + outStream = copy2Dir.createOutput(this.saveAs, DirectoryFactory.IOCONTEXT_NO_CACHE); + } + + public void sync() throws IOException + { + copy2Dir.sync(Collections.singleton(saveAs)); + } + + public void write(byte[] buf, int packetSize) throws IOException + { + outStream.writeBytes(buf, 0, packetSize); + } + + public void close() throws Exception + { + outStream.close(); + } + + public void delete() throws Exception + { + copy2Dir.deleteFile(saveAs); + } + } + + private static class LocalFsFile implements FileInterface + { + FileChannel fileChannel; + File file; + private FileOutputStream fileOutputStream; + + LocalFsFile(File dir, String saveAs) throws IOException + { + + this.file = new File(dir, saveAs); + + File parentDir = this.file.getParentFile(); + if (!parentDir.exists()) + { + if (!parentDir.mkdirs()) + { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Failed to create (sub)directory for file: " + saveAs); + } + } + + this.fileOutputStream = new FileOutputStream(file); + this.fileChannel = this.fileOutputStream.getChannel(); + } + + public void sync() throws IOException + { + FileUtils.sync(file); + } + + public void write(byte[] buf, int packetSize) throws IOException + { + fileChannel.write(ByteBuffer.wrap(buf, 0, packetSize)); + } + + public void close() throws Exception + { + //close the FileOutputStream (which also closes the Channel) + fileOutputStream.close(); + } + + public void delete() throws Exception + { + Files.delete(file.toPath()); + } + } + + /** + * The class acts as a client for ReplicationHandler.FileStream. It understands the protocol of wt=filestream + */ + public class FileFetcher + { + final String solrParamOutput; + final Long indexGen; + final Checksum checksum; + protected FileInterface file; + protected long size; + protected byte[] buf = new byte[1024 * 1024]; + String fileName; + String saveAs; + boolean includeChecksum = true; + long bytesDownloaded = 0; + int errorCount = 0; + boolean aborted = false; + + FileFetcher(FileInterface file, Map fileDetails, String saveAs, String solrParamOutput, long latestGen) + { + this.file = file; + this.fileName = (String) fileDetails.get(NAME); + this.size = (Long) fileDetails.get(SIZE); + this.solrParamOutput = solrParamOutput; + this.saveAs = saveAs; + indexGen = latestGen; + + if (includeChecksum) + { + checksum = new Adler32(); + } + else + { + checksum = null; + } + } + + FileFetcher(String solrParamOutput, long latestGen) + { + + this.solrParamOutput = solrParamOutput; + indexGen = latestGen; + if (includeChecksum) + { + checksum = new Adler32(); + } + else + { + checksum = null; + } + } + + long getBytesDownloaded() + { + return bytesDownloaded; + } + + /** + * The main method which downloads file + */ + void fetchFile() throws Exception + { + bytesDownloaded = 0; + try + { + fetch(); + } + catch (Exception e) + { + if (!aborted) + { + SolrException.log(AlfrescoIndexFetcher.LOG, "Error fetching file, doing one retry...", e); + // one retry + fetch(); + } + else + { + throw e; + } + } + } + + protected void fetch() throws Exception + { + try + { + while (true) + { + final FastInputStream is = getStream(); + int result; + try + { + //fetch packets one by one in a single request + result = fetchPackets(is); + if (result == 0 || result == NO_CONTENT) + { + return; + } + //if there is an error continue. But continue from the point where it got broken + } + finally + { + IOUtils.closeQuietly(is); + } + } + } + finally + { + cleanup(); + //if cleanup succeeds . The file is downloaded fully. do an fsync + fsyncService.submit(() -> { + try + { + file.sync(); + } + catch (IOException e) + { + fsyncException = e; + } + }); + } + } + + protected int fetchPackets(FastInputStream fis) throws Exception + { + byte[] intbytes = new byte[4]; + byte[] longbytes = new byte[8]; + try + { + + while (true) + { + if (stop) + { + stop = false; + aborted = true; + throw new ReplicationHandlerException("User aborted replication"); + } + long checkSumServer = -1; + fis.readFully(intbytes); + //read the size of the packet + int packetSize = readInt(intbytes); + if (packetSize <= 0) + { + LOG.warn("No content received for file: {}", fileName); + return NO_CONTENT; + } + if (buf.length < packetSize) + buf = new byte[packetSize]; + if (checksum != null) + { + //read the checksum + fis.readFully(longbytes); + checkSumServer = readLong(longbytes); + } + //then read the packet of bytes + fis.readFully(buf, 0, packetSize); + //compare the checksum as sent from the master + if (includeChecksum) + { + assert checksum != null; + checksum.reset(); + checksum.update(buf, 0, packetSize); + long checkSumClient = checksum.getValue(); + if (checkSumClient != checkSumServer) + { + LOG.error("Checksum not matched between client and server for file: {}", fileName); + //if checksum is wrong it is a problem return for retry + return 1; + } + } + //if everything is fine, write down the packet to the file + file.write(buf, packetSize); + bytesDownloaded += packetSize; + LOG.debug("Fetched and wrote {} bytes of file: {}", bytesDownloaded, fileName); + if (bytesDownloaded >= size) + return 0; + //errorCount is always set to zero after a successful packet + errorCount = 0; + } + } + catch (ReplicationHandlerException e) + { + throw e; + } + catch (Exception e) + { + LOG.warn("Error in fetching file: {} (downloaded {} of {} bytes)", fileName, bytesDownloaded, size, e); + //for any failure, increment the error count + errorCount++; + //if it fails for the same packet for MAX_RETRIES fail and come out + if (errorCount > MAX_RETRIES) + { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Failed to fetch file: " + fileName + " (downloaded " + bytesDownloaded + " of " + size + + " bytes" + ", error count: " + errorCount + " > " + MAX_RETRIES + ")", e); + } + return ERR; + } + } + + /** + * The webcontainer flushes the data only after it fills the buffer size. So, all data has to be read as readFully() + * other wise it fails. So read everything as bytes and then extract an integer out of it + */ + int readInt(byte[] b) + { + return (((b[0] & 0xff) << 24) | ((b[1] & 0xff) << 16) | ((b[2] & 0xff) << 8) | (b[3] & 0xff)); + + } + + /** + * Same as above but to read longs from a byte array + */ + long readLong(byte[] b) + { + return (((long) (b[0] & 0xff)) << 56) | (((long) (b[1] & 0xff)) << 48) | (((long) (b[2] & 0xff)) << 40) | ( + ((long) (b[3] & 0xff)) << 32) | (((long) (b[4] & 0xff)) << 24) | ((b[5] & 0xff) << 16) | ( + (b[6] & 0xff) << 8) | ((b[7] & 0xff)); + + } + + /** + * cleanup everything + */ + private void cleanup() + { + try + { + file.close(); + } + catch (Exception e) + { + /* no-op */ + LOG.error("Error closing file: {}", this.saveAs, e); + } + if (bytesDownloaded != size) + { + //if the download is not complete then + //delete the file being downloaded + try + { + file.delete(); + } + catch (Exception e) + { + LOG.error("Error deleting file: {}", this.saveAs, e); + } + //if the failure is due to a user abort it is returned normally else an exception is thrown + if (!aborted) + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Unable to download " + fileName + " completely. Downloaded " + bytesDownloaded + "!=" + + size); + } + } + + /** + * Open a new stream using HttpClient + */ + protected FastInputStream getStream() throws IOException + { + + ModifiableSolrParams params = new ModifiableSolrParams(); + + // //the method is command=filecontent + params.set(COMMAND, CMD_GET_FILE); + params.set(GENERATION, Long.toString(indexGen)); + params.set(CommonParams.QT, AlfrescoReplicationHandler.PATH); + + //add the version to download. This is used to reserve the download + params.set(solrParamOutput, fileName); + if (useInternalCompression) + { + params.set(COMPRESSION, "true"); + } + + if (this.includeChecksum) + { + params.set(CHECKSUM, true); + } + //wt=filestream this is a custom protocol + params.set(CommonParams.WT, FILE_STREAM); + + // This happen if there is a failure there is a retry. the offset= ensures that + // the server starts from the offset + if (bytesDownloaded > 0) + { + params.set(OFFSET, Long.toString(bytesDownloaded)); + } + + NamedList response; + InputStream is = null; + + try (HttpSolrClient client = new HttpSolrClient.Builder(masterUrl).withHttpClient(myHttpClient) + .withResponseParser(null).build()) + { + client.setSoTimeout(60000); + client.setConnectionTimeout(15000); + QueryRequest req = new QueryRequest(params); + response = client.request(req); + is = (InputStream) response.get("stream"); + if (useInternalCompression) + { + is = new InflaterInputStream(is); + } + return new FastInputStream(is); + } + catch (Exception e) + { + //close stream on error + org.apache.commons.io.IOUtils.closeQuietly(is); + throw new IOException("Could not download file '" + fileName + "'", e); + } + } + } + + /** + * File fetcher specialized for downloading index files + */ + private class DirectoryFileFetcher extends FileFetcher + { + DirectoryFileFetcher(Directory tmpIndexDir, Map fileDetails, String saveAs, String solrParamOutput, long latestGen) + throws IOException + { + super(new DirectoryFile(tmpIndexDir, saveAs), fileDetails, saveAs, solrParamOutput, latestGen); + } + } + + + /** + * File fetcher specialized for downloading conf and tlogs files + */ + class LocalFsFileFetcher extends FileFetcher + { + LocalFsFileFetcher(File dir, Map fileDetails, String saveAs, String solrParamOutput, long latestGen) + throws IOException + { + super(new LocalFsFile(dir, saveAs), fileDetails, saveAs, solrParamOutput, latestGen); + } + } + + + /** + * File fetcher specialized for downloading conf and contentstore files. + * In order to handle the possibly high number of files, the requested files are downloaded in a single stream. + */ + class ContentStoreFetcher extends FileFetcher + { + private final File dir; + private final Set filesToDownload; + private final Set filesDownloaded; + + ContentStoreFetcher(File dir, String solrParamOutput, List> filesDetails) + { + super(solrParamOutput, 0); + this.dir = dir; + this.filesToDownload = new HashSet<>(); + this.filesDownloaded = new HashSet<>(); + filesDetails.forEach(e -> filesToDownload.add((String) e.get(NAME))); + } + + @Override + protected void fetch() throws Exception + { + + while (true) + { + final FastInputStream is = getStream(); + int result; + try + { + //fetch packets one by one in a single request + result = fetchPackets(is); + if (result == 0 || result == NO_CONTENT) + { + return; + } + //if there is an error continue. But continue from the point where it got broken + } + finally + { + IOUtils.closeQuietly(is); + } + } + } + + void fetchContentStore() throws Exception + { + this.fetchFile(); + } + + @Override + protected FastInputStream getStream() throws IOException + { + + ModifiableSolrParams params = new ModifiableSolrParams(); + + params.set(COMMAND, CMD_CONTENT_STORE_FILES); + params.set(GENERATION, Long.toString(indexGen)); + params.set(CommonParams.QT, AlfrescoReplicationHandler.PATH); + + params.set(CONTENT_STORE_FILE_LIST, filesToDownload.toArray(String[]::new)); + + //add the version to download. This is used to reserve the download + // params.set(solrParamOutput, fileName); + if (useInternalCompression) + { + params.set(COMPRESSION, "true"); + } + //use checksum + if (this.includeChecksum) + { + params.set(CHECKSUM, true); + } + //wt=filestream this is a custom protocol + params.set(CommonParams.WT, FILE_STREAM); + // This happen if there is a failure there is a retry. the offset= ensures that + // the server starts from the offset + if (bytesDownloaded > 0) + { + params.set(OFFSET, Long.toString(bytesDownloaded)); + } + + NamedList response; + InputStream is = null; + + try (HttpSolrClient client = new HttpSolrClient.Builder(masterUrl).withHttpClient(myHttpClient) + .withResponseParser(null).build()) + { + client.setSoTimeout(60000); + client.setConnectionTimeout(15000); + QueryRequest req = new QueryRequest(params); + response = client.request(req); + is = (InputStream) response.get("stream"); + if (useInternalCompression) + { + is = new InflaterInputStream(is); + } + return new FastInputStream(is); + } + catch (Exception e) + { + //close stream on error + org.apache.commons.io.IOUtils.closeQuietly(is); + throw new IOException("Could not download file '" + fileName + "'", e); + } + } + + @Override + protected int fetchPackets(FastInputStream fis) throws Exception + { + byte[] intbytes = new byte[4]; + byte[] longbytes = new byte[8]; + try + { + while (true) + { + int fileNameSize; + try + { + fis.readFully(intbytes); + fileNameSize = readInt(intbytes); + } + catch (EOFException e) + { + LOG.debug("Fetched the whole batch of files"); + return 0; + } + + byte[] filenameBytes = new byte[fileNameSize]; + fis.readFully(filenameBytes, 0, fileNameSize); + + String fileName = new String(filenameBytes); + + // Error, file not requested + if (!filesToDownload.contains(fileName)) + { + throw new Exception("file " + fileName + " not requested"); + } + + FileInterface file = new LocalFsFile(dir, fileName); + fis.readFully(intbytes); + + int fileSize = readInt(intbytes); + + long fileSizeDownloaded = 0; + if (fileSize == 0) + { + file.close(); + return 0; + } + + while (true) + { + if (stop) + { + stop = false; + aborted = true; + throw new ReplicationHandlerException("User aborted replication"); + } + long checkSumServer = -1; + fis.readFully(intbytes); + //read the size of the packet + int packetSize = readInt(intbytes); + if (packetSize <= 0) + { + LOG.warn("No content received"); + file.close(); + return NO_CONTENT; + } + + if (buf.length < packetSize) + { + buf = new byte[packetSize]; + } + + if (checksum != null) + { + //read the checksum + fis.readFully(longbytes); + checkSumServer = readLong(longbytes); + } + + //then read the packet of bytes + fis.readFully(buf, 0, packetSize); + //compare the checksum as sent from the master + if (includeChecksum) + { + assert checksum != null; + checksum.reset(); + checksum.update(buf, 0, packetSize); + long checkSumClient = checksum.getValue(); + if (checkSumClient != checkSumServer) + { + LOG.warn("Checksum not matched between client and server for file: {}", this.fileName); + //if checksum is wrong it is a problem return for retry + file.close(); + return ERR; + } + } + //if everything is fine, write down the packet to the file + file.write(buf, packetSize); + fileSizeDownloaded += packetSize; + + if (fileSizeDownloaded >= fileSize) + { + file.close(); + bytesDownloaded += fileSizeDownloaded; + + fsyncService.submit(() -> { + try + { + file.sync(); + } + catch (IOException e) + { + fsyncException = e; + } + }); + filesToDownload.remove(fileName); + filesDownloaded.add(fileName); + break; + } + //errorCount is always set to zero after a successful packet + errorCount = 0; + } + + LOG.debug("downloaded content store file: {}", fileName); + } + } + catch (ReplicationHandlerException e) + { + throw e; + } + catch (Exception e) + { + LOG.warn("Error in fetching file: {} (downloaded {} of {} bytes)", fileName, bytesDownloaded, size, e); + //for any failure, increment the error count + errorCount++; + //if it fails for the same packet for MAX_RETRIES fail and come out + if (errorCount > MAX_RETRIES) + { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Failed to fetch content store bucket: " + " (downloaded " + filesDownloaded.size() + + " files out of " + filesToDownload.size() + filesDownloaded.size() + + ", error count: " + errorCount + " > " + MAX_RETRIES + ")", e); + } + return ERR; + } + } + } +} diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/AlfrescoReplicationHandler.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/AlfrescoReplicationHandler.java new file mode 100644 index 000000000..b382314e9 --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/AlfrescoReplicationHandler.java @@ -0,0 +1,2377 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + * + * Modification copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see + * + * + */ +package org.alfresco.solr.handler; + +import org.alfresco.solr.AlfrescoCoreAdminHandler; +import org.alfresco.solr.content.SolrContentStore; +import org.apache.commons.io.IOUtils; +import org.apache.lucene.codecs.CodecUtil; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.index.IndexDeletionPolicy; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.SegmentCommitInfo; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.RateLimiter; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrException.ErrorCode; +import org.apache.solr.common.params.CommonParams; +import org.apache.solr.common.params.CoreAdminParams; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.ExecutorUtil; +import org.apache.solr.common.util.FastOutputStream; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.SimpleOrderedMap; +import org.apache.solr.common.util.StrUtils; +import org.apache.solr.common.util.SuppressForbidden; +import org.apache.solr.core.CloseHook; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.core.DirectoryFactory.DirContext; +import org.apache.solr.core.IndexDeletionPolicyWrapper; +import org.apache.solr.core.SolrCore; +import org.apache.solr.core.SolrDeletionPolicy; +import org.apache.solr.core.SolrEventListener; +import org.apache.solr.core.backup.repository.BackupRepository; +import org.apache.solr.core.backup.repository.LocalFileSystemRepository; +import org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager; +import org.apache.solr.handler.RequestHandlerBase; +import org.apache.solr.handler.RestoreCore; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.search.SolrIndexSearcher; +import org.apache.solr.update.CdcrUpdateLog; +import org.apache.solr.update.SolrIndexWriter; +import org.apache.solr.update.VersionInfo; +import org.apache.solr.util.DefaultSolrThreadFactory; +import org.apache.solr.util.NumberUtils; +import org.apache.solr.util.PropertiesInputStream; +import org.apache.solr.util.RefCounted; +import org.apache.solr.util.plugin.SolrCoreAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.Adler32; +import java.util.zip.Checksum; +import java.util.zip.DeflaterOutputStream; + +import static org.apache.solr.common.params.CommonParams.NAME; + +/** + *

A Handler which provides a REST API for replication and serves replication requests from Slaves.

+ *

When running on the master, it provides the following commands

  1. Get the current replicable index version + * (command=indexversion)
  2. Get the list of files for a given index version + * (command=filelist&indexversion=<VERSION>)
  3. Get full or a part (chunk) of a given index or a config + * file (command=filecontent&file=<FILE_NAME>) You can optionally specify an offset and length to get that + * chunk of the file. You can request a configuration file by using "cf" parameter instead of the "file" parameter.
  4. + *
  5. Get status/statistics (command=details)

When running on the slave, it provides the following + * commands

  1. Perform an index fetch now (command=snappull)
  2. Get status/statistics (command=details)
  3. + *
  4. Abort an index fetch (command=abort)
  5. Enable/Disable polling the master for new versions (command=enablepoll + * or command=disablepoll)
+ * + * @since solr 1.4 + * + * + * This class has been modified in order to allow the alfresco contentstore to be efficiently replicated in a master slave environment. + * @author Elia + * + */ +public class AlfrescoReplicationHandler extends RequestHandlerBase implements SolrCoreAware +{ + + public static final String PATH = "/replication"; + + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private SolrCore core; + + private SolrContentStore contentStore; + private boolean contentStoreReplication = false; + + public static final class CommitVersionInfo + { + public final long version; + final long generation; + + private CommitVersionInfo(long g, long v) + { + generation = g; + version = v; + } + + @Override + public String toString() + { + return "Commit [version = " + version + ", generation = " + generation + "]"; + } + + /** + * builds a CommitVersionInfo data for the specified IndexCommit. + * Will never be null, ut version and generation may be zero if + * there are problems extracting them from the commit data + */ + public static CommitVersionInfo build(IndexCommit commit) + { + long generation = commit.getGeneration(); + long version = 0; + try + { + final Map commitData = commit.getUserData(); + String commitTime = commitData.get(SolrIndexWriter.COMMIT_TIME_MSEC_KEY); + if (commitTime != null) + { + try + { + version = Long.parseLong(commitTime); + } + catch (NumberFormatException e) + { + LOG.warn("Version in commitData was not formatted correctly: " + commitTime, e); + } + } + } + catch (IOException e) + { + LOG.warn("Unable to get version from commitData, commit: " + commit, e); + } + return new CommitVersionInfo(generation, version); + } + } + + private AlfrescoIndexFetcher pollingAlfrescoIndexFetcher; + + private ReentrantLock indexFetchLock = new ReentrantLock(); + + private static Lock contentStoreReplicationLock = new ReentrantLock(); + + private static boolean isContentStoreReplicating = false; + + private ExecutorService restoreExecutor = ExecutorUtil + .newMDCAwareSingleThreadExecutor(new DefaultSolrThreadFactory("restoreExecutor")); + + private volatile Future restoreFuture; + + private volatile String currentRestoreName; + + private String includeConfFiles; + + private NamedList confFileNameAlias = new NamedList<>(); + + private boolean isMaster; + + private boolean isSlave; + + private boolean replicateOnOptimize; + + private boolean replicateOnCommit; + + private boolean replicateOnStart; + + private ScheduledExecutorService executorService; + + private volatile long executorStartTime; + + private int numberBackupsToKeep = 0; //zero: do not delete old backups + + private int numTimesReplicated = 0; + + private final Map confFileInfoCache = new HashMap<>(); + + private Integer reserveCommitDuration = readIntervalMs("00:00:10"); + + volatile IndexCommit indexCommitPoint; + + volatile NamedList snapShootDetails; + + private AtomicBoolean replicationEnabled = new AtomicBoolean(true); + + private Long pollIntervalNs; + private String pollIntervalStr; + + /** + * Disable the timer task for polling + */ + private AtomicBoolean pollDisabled = new AtomicBoolean(false); + + private String getPollInterval() + { + return pollIntervalStr; + } + + @Override + public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception + { + rsp.setHttpCaching(false); + final SolrParams solrParams = req.getParams(); + String command = solrParams.get(COMMAND); + if (command == null) + { + rsp.add(STATUS, OK_STATUS); + rsp.add("message", "No command"); + return; + } + // This command does not give the current index version of the master + // It gives the current 'replicateable' index version + if (command.equals(CMD_INDEX_VERSION)) + { + IndexCommit commitPoint = indexCommitPoint; // make a copy so it won't change + + if (commitPoint == null) + { + // if this handler is 'lazy', we may not have tracked the last commit + // because our commit listener is registered on inform + commitPoint = core.getDeletionPolicy().getLatestCommit(); + } + + if (commitPoint != null && replicationEnabled.get()) + { + // + // There is a race condition here. The commit point may be changed / deleted by the time + // we get around to reserving it. This is a very small window though, and should not result + // in a catastrophic failure, but will result in the client getting an empty file list for + // the CMD_GET_FILE_LIST command. + // + core.getDeletionPolicy().setReserveDuration(commitPoint.getGeneration(), reserveCommitDuration); + rsp.add(CMD_INDEX_VERSION, IndexDeletionPolicyWrapper.getCommitTimestamp(commitPoint)); + rsp.add(GENERATION, commitPoint.getGeneration()); + rsp.add(CONTENT_STORE_VERSION, contentStore.getLastCommittedVersion()); + } + else + { + // This happens when replication is not configured to happen after startup and no commit/optimize + // has happened yet. + rsp.add(CMD_INDEX_VERSION, 0L); + rsp.add(GENERATION, 0L); + } + } + else if (command.equals(CMD_GET_FILE)) + { + getFileStream(solrParams, rsp); + } + else if (command.equals(CMD_CONTENT_STORE_FILES)) + { + getContetntStore(new ModifiableSolrParams(solrParams), rsp); + } + else if (command.equals(CMD_GET_FILE_LIST)) + { + getFileList(solrParams, rsp); + } + else if (command.equalsIgnoreCase(CMD_BACKUP)) + { + doSnapShoot(new ModifiableSolrParams(solrParams), rsp, req); + rsp.add(STATUS, OK_STATUS); + } + else if (command.equalsIgnoreCase(CMD_RESTORE)) + { + restore(new ModifiableSolrParams(solrParams), rsp, req); + rsp.add(STATUS, OK_STATUS); + } + else if (command.equalsIgnoreCase(CMD_RESTORE_STATUS)) + { + rsp.add(CMD_RESTORE_STATUS, getRestoreStatus()); + } + else if (command.equalsIgnoreCase(CMD_DELETE_BACKUP)) + { + deleteSnapshot(new ModifiableSolrParams(solrParams)); + rsp.add(STATUS, OK_STATUS); + } + else if (command.equalsIgnoreCase(CMD_FETCH_INDEX)) + { + String masterUrl = solrParams.get(MASTER_URL); + if (!isSlave && masterUrl == null) + { + rsp.add(STATUS, ERR_STATUS); + rsp.add("message", "No slave configured or no 'masterUrl' Specified"); + return; + } + final SolrParams paramsCopy = new ModifiableSolrParams(solrParams); + Thread fetchThread = new Thread(() -> doFetch(paramsCopy, false), "explicit-fetchindex-cmd"); + fetchThread.setDaemon(false); + fetchThread.start(); + if (solrParams.getBool(WAIT, false)) + { + fetchThread.join(); + } + rsp.add(STATUS, OK_STATUS); + } + else if (command.equalsIgnoreCase(CMD_DISABLE_POLL)) + { + if (pollingAlfrescoIndexFetcher != null) + { + disablePoll(); + rsp.add(STATUS, OK_STATUS); + } + else + { + rsp.add(STATUS, ERR_STATUS); + rsp.add("message", "No slave configured"); + } + } + else if (command.equalsIgnoreCase(CMD_ENABLE_POLL)) + { + if (pollingAlfrescoIndexFetcher != null) + { + enablePoll(); + rsp.add(STATUS, OK_STATUS); + } + else + { + rsp.add(STATUS, ERR_STATUS); + rsp.add("message", "No slave configured"); + } + } + else if (command.equalsIgnoreCase(CMD_ABORT_FETCH)) + { + if (abortFetch()) + { + rsp.add(STATUS, OK_STATUS); + } + else + { + rsp.add(STATUS, ERR_STATUS); + rsp.add("message", "No slave configured"); + } + } + else if (command.equals(CMD_SHOW_COMMITS)) + { + rsp.add(CMD_SHOW_COMMITS, getCommits()); + } + else if (command.equals(CMD_DETAILS)) + { + rsp.add(CMD_DETAILS, getReplicationDetails(solrParams.getBool("slave", true))); + } + else if (CMD_ENABLE_REPL.equalsIgnoreCase(command)) + { + replicationEnabled.set(true); + rsp.add(STATUS, OK_STATUS); + } + else if (CMD_DISABLE_REPL.equalsIgnoreCase(command)) + { + replicationEnabled.set(false); + rsp.add(STATUS, OK_STATUS); + } + } + + private boolean abortFetch() + { + AlfrescoIndexFetcher fetcher = currentAlfrescoIndexFetcher; + if (fetcher != null) + { + fetcher.abortFetch(); + return true; + } + else + { + return false; + } + } + + private void deleteSnapshot(ModifiableSolrParams params) + { + String name = params.get(NAME); + if (name == null) + { + throw new SolrException(ErrorCode.BAD_REQUEST, "Missing mandatory param: name"); + } + + SnapShooter snapShooter = new SnapShooter(core, params.get(CoreAdminParams.BACKUP_LOCATION), params.get(NAME)); + snapShooter.validateDeleteSnapshot(); + snapShooter.deleteSnapAsync(this); + } + + private List> getCommits() + { + Map commits = core.getDeletionPolicy().getCommits(); + List> l = new ArrayList<>(); + + for (IndexCommit c : commits.values()) + { + try + { + NamedList nl = new NamedList<>(); + nl.add("indexVersion", IndexDeletionPolicyWrapper.getCommitTimestamp(c)); + nl.add(GENERATION, c.getGeneration()); + List commitList = new ArrayList<>(c.getFileNames().size()); + commitList.addAll(c.getFileNames()); + Collections.sort(commitList); + nl.add(CMD_GET_FILE_LIST, commitList); + l.add(nl); + } + catch (IOException e) + { + LOG.warn("Exception while reading files for commit " + c, e); + } + } + return l; + } + + static long getCheckSum(Checksum checksum, File f) + { + checksum.reset(); + byte[] buffer = new byte[1024 * 1024]; + int bytesRead; + try (FileInputStream fis = new FileInputStream(f)) + { + while ((bytesRead = fis.read(buffer)) >= 0) + { + checksum.update(buffer, 0, bytesRead); + } + return checksum.getValue(); + } + catch (Exception e) + { + LOG.warn("Exception in finding checksum of " + f, e); + throw new RuntimeException(e); + } + } + + private volatile AlfrescoIndexFetcher currentAlfrescoIndexFetcher; + + private boolean acquireContentStoreReplicationTask() + { + contentStoreReplicationLock.lock(); + if (!isContentStoreReplicating) + { + contentStoreReplication = true; + isContentStoreReplicating = true; + } + + contentStoreReplicationLock.unlock(); + return contentStoreReplication; + } + + private void releaseContentStoreReplicationTask() + { + if (contentStoreReplication) + { + contentStoreReplicationLock.lock(); + isContentStoreReplicating = false; + contentStoreReplication = false; + contentStoreReplicationLock.unlock(); + } + } + + private AlfrescoIndexFetcher.IndexFetchResult doFetch(SolrParams solrParams, boolean forceReplication) + { + String masterUrl = solrParams == null ? null : solrParams.get(MASTER_URL); + if (!indexFetchLock.tryLock()) + return AlfrescoIndexFetcher.IndexFetchResult.LOCK_OBTAIN_FAILED; + try + { + if (masterUrl != null) + { + if (currentAlfrescoIndexFetcher != null && currentAlfrescoIndexFetcher != pollingAlfrescoIndexFetcher) + { + currentAlfrescoIndexFetcher.destroy(); + } + currentAlfrescoIndexFetcher = new AlfrescoIndexFetcher(solrParams.toNamedList(), this, core, contentStore); + } + else + { + currentAlfrescoIndexFetcher = pollingAlfrescoIndexFetcher; + } + + return currentAlfrescoIndexFetcher.fetchLatestIndex(forceReplication, acquireContentStoreReplicationTask()); + } + catch (Exception e) + { + SolrException.log(LOG, "Index fetch failed ", e); + return new AlfrescoIndexFetcher.IndexFetchResult(AlfrescoIndexFetcher.IndexFetchResult.FAILED_BY_EXCEPTION_MESSAGE, false, + e); + } + finally + { + if (pollingAlfrescoIndexFetcher != null) + { + currentAlfrescoIndexFetcher = pollingAlfrescoIndexFetcher; + } + + releaseContentStoreReplicationTask(); + indexFetchLock.unlock(); + } + } + + private boolean isReplicating() + { + return indexFetchLock.isLocked(); + } + + private void restore(SolrParams params, SolrQueryResponse rsp, SolrQueryRequest req) throws IOException + { + if (restoreFuture != null && !restoreFuture.isDone()) + { + throw new SolrException(ErrorCode.BAD_REQUEST, + "Restore in progress. Cannot run multiple restore operations" + "for the same core"); + } + + String name = params.get(NAME); + String location = params.get(CoreAdminParams.BACKUP_LOCATION); + + String repoName = params.get(CoreAdminParams.BACKUP_REPOSITORY); + CoreContainer cc = core.getCoreContainer(); + BackupRepository repo; + if (repoName != null) + { + repo = cc.newBackupRepository(Optional.of(repoName)); + location = repo.getBackupLocation(location); + if (location == null) + { + throw new IllegalArgumentException("location is required"); + } + } + else + { + repo = new LocalFileSystemRepository(); + } + + //If location is not provided then assume that the restore index is present inside the data directory. + if (location == null) + { + location = core.getDataDir(); + } + + URI locationUri = repo.createURI(location); + + //If name is not provided then look for the last unnamed( the ones with the snapshot.timestamp format) + //snapshot folder since we allow snapshots to be taken without providing a name. Pick the latest timestamp. + if (name == null) + { + String[] filePaths = repo.listAll(locationUri); + List dirs = new ArrayList<>(); + for (String f : filePaths) + { + OldBackupDirectory obd = new OldBackupDirectory(locationUri, f); + if (obd.getTimestamp().isPresent()) + { + dirs.add(obd); + } + } + Collections.sort(dirs); + if (dirs.size() == 0) + { + throw new SolrException(ErrorCode.BAD_REQUEST, + "No backup name specified and none found in " + core.getDataDir()); + } + name = dirs.get(0).getDirName(); + } + else + { + //"snapshot." is prefixed by snapshooter + name = "snapshot." + name; + } + + RestoreCore restoreCore = new RestoreCore(repo, core, locationUri, name); + try + { + MDC.put("RestoreCore.core", core.getName()); + MDC.put("RestoreCore.backupLocation", location); + MDC.put("RestoreCore.backupName", name); + restoreFuture = restoreExecutor.submit(restoreCore); + currentRestoreName = name; + } + finally + { + MDC.remove("RestoreCore.core"); + MDC.remove("RestoreCore.backupLocation"); + MDC.remove("RestoreCore.backupName"); + } + } + + private NamedList getRestoreStatus() + { + NamedList status = new SimpleOrderedMap<>(); + + if (restoreFuture == null) + { + status.add(STATUS, "No restore actions in progress"); + return status; + } + + status.add("snapshotName", currentRestoreName); + if (restoreFuture.isDone()) + { + try + { + boolean success = restoreFuture.get(); + if (success) + { + status.add(STATUS, SUCCESS); + } + else + { + status.add(STATUS, FAILED); + } + } + catch (Exception e) + { + status.add(STATUS, FAILED); + status.add(EXCEPTION, e.getMessage()); + } + } + else + { + status.add(STATUS, "In Progress"); + } + return status; + } + + private void doSnapShoot(SolrParams params, SolrQueryResponse rsp, SolrQueryRequest req) + { + try + { + int numberToKeep = params.getInt(NUMBER_BACKUPS_TO_KEEP_REQUEST_PARAM, 0); + if (numberToKeep > 0 && numberBackupsToKeep > 0) + { + throw new SolrException(ErrorCode.BAD_REQUEST, + "Cannot use " + NUMBER_BACKUPS_TO_KEEP_REQUEST_PARAM + " if " + + NUMBER_BACKUPS_TO_KEEP_INIT_PARAM + " was specified in the configuration."); + } + numberToKeep = Math.max(numberToKeep, numberBackupsToKeep); + if (numberToKeep < 1) + { + numberToKeep = Integer.MAX_VALUE; + } + + IndexCommit indexCommit; + String commitName = params.get(CoreAdminParams.COMMIT_NAME); + if (commitName != null) + { + SolrSnapshotMetaDataManager snapshotMgr = core.getSnapshotMetaDataManager(); + Optional commit = snapshotMgr.getIndexCommitByName(commitName); + if (commit.isPresent()) + { + indexCommit = commit.get(); + } + else + { + throw new SolrException(ErrorCode.BAD_REQUEST, + "Unable to find an index commit with name " + commitName + " for core " + core.getName()); + } + } + else + { + IndexDeletionPolicyWrapper delPolicy = core.getDeletionPolicy(); + indexCommit = delPolicy.getLatestCommit(); + + if (indexCommit == null) + { + indexCommit = req.getSearcher().getIndexReader().getIndexCommit(); + } + } + + String location = params.get(CoreAdminParams.BACKUP_LOCATION); + String repoName = params.get(CoreAdminParams.BACKUP_REPOSITORY); + CoreContainer cc = core.getCoreContainer(); + BackupRepository repo; + if (repoName != null) + { + repo = cc.newBackupRepository(Optional.of(repoName)); + location = repo.getBackupLocation(location); + if (location == null) + { + throw new IllegalArgumentException("location is required"); + } + } + else + { + repo = new LocalFileSystemRepository(); + if (location == null) + { + location = core.getDataDir(); + } + else + { + location = core.getCoreDescriptor().getInstanceDir().resolve(location).normalize().toString(); + } + } + + // small race here before the commit point is saved + URI locationUri = repo.createURI(location); + SnapShooter snapShooter = new SnapShooter(repo, core, locationUri, params.get(NAME), commitName); + snapShooter.validateCreateSnapshot(); + snapShooter.createSnapAsync(indexCommit, numberToKeep, (nl) -> snapShootDetails = nl); + + } + catch (Exception e) + { + LOG.warn("Exception during creating a snapshot", e); + rsp.add("exception", e); + } + } + + /** + * This method adds an Object of FileStream to the response . The FileStream implements a custom protocol which is + * understood by IndexFetcher.FileFetcher + */ + private void getFileStream(SolrParams solrParams, SolrQueryResponse rsp) + { + ModifiableSolrParams rawParams = new ModifiableSolrParams(solrParams); + rawParams.set(CommonParams.WT, FILE_STREAM); + + String cfileName = solrParams.get(CONF_FILE_SHORT); + String tlogFileName = solrParams.get(TLOG_FILE); + if (cfileName != null) + { + rsp.add(FILE_STREAM, new LocalFsConfFileStream(solrParams)); + } + else if (tlogFileName != null) + { + rsp.add(FILE_STREAM, new LocalFsTlogFileStream(solrParams)); + } + else + { + rsp.add(FILE_STREAM, new DirectoryFileStream(solrParams)); + } + } + + private void getContetntStore(SolrParams solrParams, SolrQueryResponse rsp) + { + rsp.add(FILE_STREAM, new ContentStoreFilesStream(solrParams)); + } + + private void getFileList(SolrParams solrParams, SolrQueryResponse rsp) + { + String v = solrParams.get(GENERATION); + + if (v == null) + { + rsp.add("status", "no index generation specified"); + return; + } + + long indexGeneration = Long.parseLong(v); + + v = solrParams.get(CONTENT_STORE_VERSION); + if (v == null) + { + rsp.add("status", "no content store generation specified"); + return; + } + + long contentStoreGeneration = Long.parseLong(v); + + if (indexGeneration != NO_INDEX_REPLICATION_REQUIRED) + { + + IndexCommit commit = core.getDeletionPolicy().getCommitPoint(indexGeneration); + + if (commit == null) + { + rsp.add("status", "invalid index generation"); + return; + } + + // reserve the indexcommit for sometime + core.getDeletionPolicy().setReserveDuration(indexGeneration, reserveCommitDuration); + List> result = new ArrayList<>(); + Directory dir = null; + try + { + dir = core.getDirectoryFactory() + .get(core.getNewIndexDir(), DirContext.DEFAULT, core.getSolrConfig().indexConfig.lockType); + SegmentInfos infos = SegmentInfos.readCommit(dir, commit.getSegmentsFileName()); + + for (SegmentCommitInfo commitInfo : infos) + { + for (String file : commitInfo.files()) + { + Map fileMeta = new HashMap<>(); + fileMeta.put(NAME, file); + fileMeta.put(SIZE, dir.fileLength(file)); + + try (final IndexInput in = dir.openInput(file, IOContext.READONCE)) + { + try + { + long checksum = CodecUtil.retrieveChecksum(in); + fileMeta.put(CHECKSUM, checksum); + } + catch (Exception e) + { + LOG.warn("Could not read checksum from index file: " + file, e); + } + } + + result.add(fileMeta); + } + } + + // add the segments_N file + + Map fileMeta = new HashMap<>(); + fileMeta.put(NAME, infos.getSegmentsFileName()); + fileMeta.put(SIZE, dir.fileLength(infos.getSegmentsFileName())); + if (infos.getId() != null) + { + try ( + final IndexInput in = dir.openInput(infos.getSegmentsFileName(), IOContext.READONCE)) + { + try + { + fileMeta.put(CHECKSUM, CodecUtil.retrieveChecksum(in)); + } + catch (Exception e) + { + LOG.warn("Could not read checksum from index file: " + infos.getSegmentsFileName(), e); + } + } + } + result.add(fileMeta); + } + catch (IOException e) + { + rsp.add("status", "unable to get file names for given index generation"); + rsp.add(EXCEPTION, e); + LOG.error("Unable to get file names for indexCommit generation: " + indexGeneration, e); + } + finally + { + if (dir != null) + { + try + { + core.getDirectoryFactory().release(dir); + } + catch (IOException e) + { + SolrException.log(LOG, "Could not release directory after fetching file list", e); + } + } + } + rsp.add(CMD_GET_FILE_LIST, result); + + // fetch list of tlog files only if cdcr is activated + if (solrParams.getBool(TLOG_FILES, true) && core.getUpdateHandler().getUpdateLog() != null && core + .getUpdateHandler().getUpdateLog() instanceof CdcrUpdateLog) + { + try + { + List> tlogfiles = getTlogFileList(commit); + LOG.info("Adding tlog files to list: " + tlogfiles); + rsp.add(TLOG_FILES, tlogfiles); + } + catch (IOException e) + { + rsp.add("status", "unable to get tlog file names for given index generation"); + rsp.add(EXCEPTION, e); + LOG.error("Unable to get tlog file names for indexCommit generation: " + indexGeneration, e); + } + } + + if (confFileNameAlias.size() < 1 || core.getCoreContainer().isZooKeeperAware()) + { + return; + } + LOG.debug("Adding config files to list: " + includeConfFiles); + //if configuration files need to be included get their details + rsp.add(CONF_FILES, getConfFileInfoFromCache(confFileNameAlias, confFileInfoCache)); + } + + if (contentStoreGeneration != SolrContentStore.NO_CONTENT_STORE_REPLICATION_REQUIRED) + { + Map>> changes = contentStore.getChanges(contentStoreGeneration); + rsp.add(CONTENT_STORE_FILES, changes); + } + } + + /** + * Retrieves the list of tlog files associated to a commit point. + */ + private List> getTlogFileList(IndexCommit commit) throws IOException + { + long maxVersion = this.getMaxVersion(commit); + CdcrUpdateLog ulog = (CdcrUpdateLog) core.getUpdateHandler().getUpdateLog(); + String[] logList = ulog.getLogList(new File(ulog.getLogDir())); + List> tlogFiles = new ArrayList<>(); + for (String fileName : logList) + { + // filter out tlogs that are older than the current index commit generation, so that the list of tlog files is + // in synch with the latest index commit point + long startVersion = Math.abs(Long.parseLong(fileName.substring(fileName.lastIndexOf('.') + 1))); + if (startVersion < maxVersion) + { + Map fileMeta = new HashMap<>(); + fileMeta.put(NAME, fileName); + fileMeta.put(SIZE, new File(ulog.getLogDir(), fileName).length()); + tlogFiles.add(fileMeta); + } + } + return tlogFiles; + } + + /** + * Retrieves the maximum version number from an index commit. + */ + private long getMaxVersion(IndexCommit commit) throws IOException + { + try (DirectoryReader reader = DirectoryReader.open(commit)) + { + IndexSearcher searcher = new IndexSearcher(reader); + VersionInfo vinfo = core.getUpdateHandler().getUpdateLog().getVersionInfo(); + return Math.abs(vinfo.getMaxVersionFromIndex(searcher)); + } + } + + /** + * For configuration files, checksum of the file is included because, unlike index files, they may have same content + * but different timestamps. + *

+ * The local conf files information is cached so that everytime it does not have to compute the checksum. The cache is + * refreshed only if the lastModified of the file changes + */ + List> getConfFileInfoFromCache(NamedList nameAndAlias, final Map confFileInfoCache) + { + List> confFiles = new ArrayList<>(); + synchronized (confFileInfoCache) + { + File confDir = new File(core.getResourceLoader().getConfigDir()); + Checksum checksum = null; + for (int i = 0; i < nameAndAlias.size(); i++) + { + String cf = nameAndAlias.getName(i); + File f = new File(confDir, cf); + if (!f.exists() || f.isDirectory()) + continue; //must not happen + FileInfo info = confFileInfoCache.get(cf); + if (info == null || info.lastmodified != f.lastModified() || info.size != f.length()) + { + if (checksum == null) + checksum = new Adler32(); + info = new FileInfo(f.lastModified(), cf, f.length(), getCheckSum(checksum, f)); + confFileInfoCache.put(cf, info); + } + Map m = info.getAsMap(); + if (nameAndAlias.getVal(i) != null) + m.put(ALIAS, nameAndAlias.getVal(i)); + confFiles.add(m); + } + } + return confFiles; + } + + public static class FileInfo + { + long lastmodified; + String name; + long size; + long checksum; + + public FileInfo(File file, String name) + { + Checksum checksum = new Adler32(); + + this.lastmodified = file.lastModified(); + this.name = name; + this.size = file.length(); + this.checksum = getCheckSum(checksum, file); + } + + FileInfo(long lasmodified, String name, long size, long checksum) + { + this.lastmodified = lasmodified; + this.name = name; + this.size = size; + this.checksum = checksum; + } + + public Map getAsMap() + { + Map map = new HashMap<>(); + map.put(NAME, name); + map.put(SIZE, size); + map.put(CHECKSUM, checksum); + return map; + } + } + + private void disablePoll() + { + if (isSlave) + { + pollDisabled.set(true); + LOG.info("inside disable poll, value of pollDisabled = " + pollDisabled); + } + } + + private void enablePoll() + { + if (isSlave) + { + pollDisabled.set(false); + LOG.info("inside enable poll, value of pollDisabled = " + pollDisabled); + } + } + + private boolean isPollingDisabled() + { + return pollDisabled.get(); + } + + @SuppressForbidden(reason = "Need currentTimeMillis, to output next execution time in replication details") + private void markScheduledExecutionStart() + { + executorStartTime = System.currentTimeMillis(); + } + + private Date getNextScheduledExecTime() + { + Date nextTime = null; + if (executorStartTime > 0) + nextTime = new Date( + executorStartTime + TimeUnit.MILLISECONDS.convert(pollIntervalNs, TimeUnit.NANOSECONDS)); + return nextTime; + } + + @SuppressWarnings("unused") + int getTimesReplicatedSinceStartup() + { + return numTimesReplicated; + } + + @SuppressWarnings("unused") + void setTimesReplicatedSinceStartup() + { + numTimesReplicated++; + } + + @Override + public Category getCategory() + { + return Category.REPLICATION; + } + + @Override + public String getDescription() + { + return "ReplicationHandler provides replication of index and configuration files from Master to Slaves"; + } + + /** + * returns the CommitVersionInfo for the current searcher, or null on error. + */ + private CommitVersionInfo getIndexVersion() + { + CommitVersionInfo v = null; + RefCounted searcher = core.getSearcher(); + try + { + v = CommitVersionInfo.build(searcher.get().getIndexReader().getIndexCommit()); + } + catch (IOException e) + { + LOG.warn("Unable to get index commit: ", e); + } + finally + { + searcher.decref(); + } + return v; + } + + @Override + @SuppressWarnings("unchecked") + public NamedList getStatistics() + { + NamedList list = super.getStatistics(); + if (core != null) + { + list.add("indexSize", NumberUtils.readableSize(core.getIndexSize())); + CommitVersionInfo vInfo = (core != null && !core.isClosed()) ? getIndexVersion() : null; + list.add("indexVersion", null == vInfo ? 0 : vInfo.version); + list.add(GENERATION, null == vInfo ? 0 : vInfo.generation); + + list.add("indexPath", core.getIndexDir()); + list.add("isMaster", String.valueOf(isMaster)); + list.add("isSlave", String.valueOf(isSlave)); + + AlfrescoIndexFetcher fetcher = currentAlfrescoIndexFetcher; + if (fetcher != null) + { + list.add(MASTER_URL, fetcher.getMasterUrl()); + if (getPollInterval() != null) + { + list.add(POLL_INTERVAL, getPollInterval()); + } + list.add("isPollingDisabled", String.valueOf(isPollingDisabled())); + list.add("isReplicating", String.valueOf(isReplicating())); + long elapsed = fetcher.getReplicationTimeElapsed(); + long val = fetcher.getTotalBytesDownloaded(); + if (elapsed > 0) + { + list.add("timeElapsed", elapsed); + list.add("bytesDownloaded", val); + list.add("downloadSpeed", val / elapsed); + } + Properties props = loadReplicationProperties(); + addVal(list, AlfrescoIndexFetcher.PREVIOUS_CYCLE_TIME_TAKEN, props, Long.class); + addVal(list, AlfrescoIndexFetcher.INDEX_REPLICATED_AT, props, Date.class); + addVal(list, AlfrescoIndexFetcher.CONF_FILES_REPLICATED_AT, props, Date.class); + addVal(list, AlfrescoIndexFetcher.REPLICATION_FAILED_AT, props, Date.class); + addVal(list, AlfrescoIndexFetcher.TIMES_FAILED, props, Integer.class); + addVal(list, AlfrescoIndexFetcher.TIMES_INDEX_REPLICATED, props, Integer.class); + addVal(list, AlfrescoIndexFetcher.LAST_CYCLE_BYTES_DOWNLOADED, props, Long.class); + addVal(list, AlfrescoIndexFetcher.TIMES_CONFIG_REPLICATED, props, Integer.class); + addVal(list, AlfrescoIndexFetcher.CONF_FILES_REPLICATED, props, String.class); + } + if (isMaster) + { + if (includeConfFiles != null) + list.add("confFilesToReplicate", includeConfFiles); + list.add(REPLICATE_AFTER, getReplicateAfterStrings()); + list.add("replicationEnabled", String.valueOf(replicationEnabled.get())); + } + } + return list; + } + + /** + * Used for showing statistics and progress information. + */ + private NamedList getReplicationDetails(boolean showSlaveDetails) + { + NamedList details = new SimpleOrderedMap<>(); + NamedList master = new SimpleOrderedMap<>(); + NamedList slave = new SimpleOrderedMap<>(); + + details.add("indexSize", NumberUtils.readableSize(core.getIndexSize())); + details.add("indexPath", core.getIndexDir()); + details.add(CMD_SHOW_COMMITS, getCommits()); + details.add("isMaster", String.valueOf(isMaster)); + details.add("isSlave", String.valueOf(isSlave)); + CommitVersionInfo vInfo = getIndexVersion(); + details.add("indexVersion", null == vInfo ? 0 : vInfo.version); + details.add("robavaria", 50); + details.add(GENERATION, null == vInfo ? 0 : vInfo.generation); + + IndexCommit commit = indexCommitPoint; // make a copy so it won't change + + if (isMaster) + { + if (includeConfFiles != null) + master.add(CONF_FILES, includeConfFiles); + master.add(REPLICATE_AFTER, getReplicateAfterStrings()); + master.add("replicationEnabled", String.valueOf(replicationEnabled.get())); + } + + if (isMaster && commit != null) + { + CommitVersionInfo repCommitInfo = CommitVersionInfo.build(commit); + master.add("replicableVersion", repCommitInfo.version); + master.add("replicableGeneration", repCommitInfo.generation); + } + + AlfrescoIndexFetcher fetcher = currentAlfrescoIndexFetcher; + if (fetcher != null) + { + Properties props = loadReplicationProperties(); + if (showSlaveDetails) + { + try + { + NamedList nl = fetcher.getDetails(); + slave.add("masterDetails", nl.get(CMD_DETAILS)); + } + catch (Exception e) + { + LOG.warn("Exception while invoking 'details' method for replication on master ", e); + slave.add(ERR_STATUS, "invalid_master"); + } + } + slave.add(MASTER_URL, fetcher.getMasterUrl()); + if (getPollInterval() != null) + { + slave.add(POLL_INTERVAL, getPollInterval()); + } + Date nextScheduled = getNextScheduledExecTime(); + if (nextScheduled != null && !isPollingDisabled()) + { + slave.add(NEXT_EXECUTION_AT, nextScheduled.toString()); + } + else if (isPollingDisabled()) + { + slave.add(NEXT_EXECUTION_AT, "Polling disabled"); + } + addVal(slave, AlfrescoIndexFetcher.INDEX_REPLICATED_AT, props, Date.class); + addVal(slave, AlfrescoIndexFetcher.INDEX_REPLICATED_AT_LIST, props, List.class); + addVal(slave, AlfrescoIndexFetcher.REPLICATION_FAILED_AT_LIST, props, List.class); + addVal(slave, AlfrescoIndexFetcher.TIMES_INDEX_REPLICATED, props, Integer.class); + addVal(slave, AlfrescoIndexFetcher.CONF_FILES_REPLICATED, props, Integer.class); + addVal(slave, AlfrescoIndexFetcher.TIMES_CONFIG_REPLICATED, props, Integer.class); + addVal(slave, AlfrescoIndexFetcher.CONF_FILES_REPLICATED_AT, props, Integer.class); + addVal(slave, AlfrescoIndexFetcher.LAST_CYCLE_BYTES_DOWNLOADED, props, Long.class); + addVal(slave, AlfrescoIndexFetcher.TIMES_FAILED, props, Integer.class); + addVal(slave, AlfrescoIndexFetcher.REPLICATION_FAILED_AT, props, Date.class); + addVal(slave, AlfrescoIndexFetcher.PREVIOUS_CYCLE_TIME_TAKEN, props, Long.class); + + slave.add("currentDate", new Date().toString()); + slave.add("isPollingDisabled", String.valueOf(isPollingDisabled())); + boolean isReplicating = isReplicating(); + slave.add("isReplicating", String.valueOf(isReplicating)); + if (isReplicating) + { + try + { + long bytesToDownload = 0; + List filesToDownload = new ArrayList<>(); + for (Map file : fetcher.getFilesToDownload()) + { + filesToDownload.add((String) file.get(NAME)); + bytesToDownload += (Long) file.get(SIZE); + } + + //get list of conf files to download + for (Map file : fetcher.getConfFilesToDownload()) + { + filesToDownload.add((String) file.get(NAME)); + bytesToDownload += (Long) file.get(SIZE); + } + + //get list of conf files to download + for (Map file : fetcher.getContentStoreFileToDownload()) + { + filesToDownload.add((String) file.get(NAME)); + bytesToDownload += (Long) file.get(SIZE); + } + + slave.add("filesToDownload", filesToDownload); + slave.add("numFilesToDownload", String.valueOf(filesToDownload.size())); + slave.add("bytesToDownload", NumberUtils.readableSize(bytesToDownload)); + + long bytesDownloaded = 0; + List filesDownloaded = new ArrayList<>(); + for (Map file : fetcher.getFilesDownloaded()) + { + filesDownloaded.add((String) file.get(NAME)); + bytesDownloaded += (Long) file.get(SIZE); + } + + //get list of conf files downloaded + for (Map file : fetcher.getConfFilesDownloaded()) + { + filesDownloaded.add((String) file.get(NAME)); + bytesDownloaded += (Long) file.get(SIZE); + } + + for (Map file : fetcher.getContentStoreFilesDownloaded()) + { + filesDownloaded.add((String) file.get(NAME)); + bytesDownloaded += (Long) file.get(SIZE); + } + + Map currentFile = fetcher.getCurrentFile(); + String currFile = null; + long currFileSize = 0, currFileSizeDownloaded = 0; + float percentDownloaded = 0; + if (currentFile != null) + { + currFile = (String) currentFile.get(NAME); + currFileSize = (Long) currentFile.get(SIZE); + if (currentFile.containsKey("bytesDownloaded")) + { + currFileSizeDownloaded = (Long) currentFile.get("bytesDownloaded"); + bytesDownloaded += currFileSizeDownloaded; + if (currFileSize > 0) + percentDownloaded = (currFileSizeDownloaded * 100) / currFileSize; + } + } + slave.add("filesDownloaded", filesDownloaded); + slave.add("numFilesDownloaded", String.valueOf(filesDownloaded.size())); + + long estimatedTimeRemaining = 0; + + Date replicationStartTimeStamp = fetcher.getReplicationStartTimeStamp(); + if (replicationStartTimeStamp != null) + { + slave.add("replicationStartTime", replicationStartTimeStamp.toString()); + } + long elapsed = fetcher.getReplicationTimeElapsed(); + slave.add("timeElapsed", elapsed + "s"); + + if (bytesDownloaded > 0) + estimatedTimeRemaining = ((bytesToDownload - bytesDownloaded) * elapsed) / bytesDownloaded; + float totalPercent = 0; + long downloadSpeed = 0; + if (bytesToDownload > 0) + totalPercent = (bytesDownloaded * 100.f) / bytesToDownload; + if (elapsed > 0) + downloadSpeed = (bytesDownloaded / elapsed); + if (currFile != null) + slave.add("currentFile", currFile); + slave.add("currentFileSize", NumberUtils.readableSize(currFileSize)); + slave.add("currentFileSizeDownloaded", NumberUtils.readableSize(currFileSizeDownloaded)); + slave.add("currentFileSizePercent", String.valueOf(percentDownloaded)); + slave.add("bytesDownloaded", NumberUtils.readableSize(bytesDownloaded)); + slave.add("totalPercent", String.valueOf(totalPercent)); + slave.add("timeRemaining", estimatedTimeRemaining + "s"); + slave.add("downloadSpeed", NumberUtils.readableSize(downloadSpeed)); + } + catch (Exception e) + { + LOG.error("Exception while writing replication details: ", e); + } + } + } + + if (isMaster) + details.add("master", master); + if (slave.size() > 0) + details.add("slave", slave); + + NamedList snapshotStats = snapShootDetails; + if (snapshotStats != null) + details.add(CMD_BACKUP, snapshotStats); + + return details; + } + + private void addVal(NamedList nl, String key, Properties props, Class clzz) + { + String s = props.getProperty(key); + if (s == null || s.trim().length() == 0) + return; + if (clzz == Date.class) + { + try + { + long l = Long.parseLong(s); + nl.add(key, new Date(l).toString()); + } + catch (NumberFormatException e) + {/*no op*/ } + } + else if (clzz == List.class) + { + String[] ss = s.split(","); + List l = new ArrayList<>(); + for (String s1 : ss) + { + l.add(new Date(Long.parseLong(s1)).toString()); + } + nl.add(key, l); + } + else + { + nl.add(key, s); + } + + } + + private List getReplicateAfterStrings() + { + List replicateAfter = new ArrayList<>(); + if (replicateOnCommit) + replicateAfter.add("commit"); + if (replicateOnOptimize) + replicateAfter.add("optimize"); + if (replicateOnStart) + replicateAfter.add("startup"); + return replicateAfter; + } + + Properties loadReplicationProperties() + { + Directory dir = null; + try + { + try + { + dir = core.getDirectoryFactory() + .get(core.getDataDir(), DirContext.META_DATA, core.getSolrConfig().indexConfig.lockType); + IndexInput input; + try + { + input = dir.openInput(AlfrescoIndexFetcher.REPLICATION_PROPERTIES, IOContext.DEFAULT); + } + catch (FileNotFoundException | NoSuchFileException e) + { + return new Properties(); + } + + try + { + final InputStream is = new PropertiesInputStream(input); + Properties props = new Properties(); + props.load(new InputStreamReader(is, StandardCharsets.UTF_8)); + return props; + } + finally + { + input.close(); + } + } + finally + { + if (dir != null) + { + core.getDirectoryFactory().release(dir); + } + } + } + catch (IOException e) + { + throw new SolrException(ErrorCode.SERVER_ERROR, e); + } + } + + private void setupPolling(String intervalStr) + { + pollIntervalStr = intervalStr; + pollIntervalNs = readIntervalNs(pollIntervalStr); + if (pollIntervalNs == null || pollIntervalNs <= 0) + { + LOG.info(" No value set for 'pollInterval'. Timer Task not started."); + return; + } + + Runnable task = () -> { + if (pollDisabled.get()) + { + LOG.info("Poll disabled"); + return; + } + try + { + LOG.debug("Polling for index modifications"); + markScheduledExecutionStart(); + doFetch(null, false); + } + catch (Exception e) + { + LOG.error("Exception in fetching index", e); + } + }; + executorService = Executors.newSingleThreadScheduledExecutor(new DefaultSolrThreadFactory("indexFetcher")); + // Randomize initial delay, with a minimum of 1ms + long initialDelayNs = + new Random().nextLong() % pollIntervalNs + TimeUnit.NANOSECONDS.convert(1, TimeUnit.MILLISECONDS); + executorService.scheduleAtFixedRate(task, initialDelayNs, pollIntervalNs, TimeUnit.NANOSECONDS); + LOG.info("Poll scheduled at an interval of {}ms", + TimeUnit.MILLISECONDS.convert(pollIntervalNs, TimeUnit.NANOSECONDS)); + } + + @Override + public void inform(SolrCore core) + { + this.core = core; + + CoreContainer coreContainer = core.getCoreContainer(); + AlfrescoCoreAdminHandler coreAdminHandler = (AlfrescoCoreAdminHandler) coreContainer.getMultiCoreHandler(); + + contentStore = coreAdminHandler.getSolrContentStore(); + registerCloseHook(); + Object nbtk = initArgs.get(NUMBER_BACKUPS_TO_KEEP_INIT_PARAM); + if (nbtk != null) + { + numberBackupsToKeep = Integer.parseInt(nbtk.toString()); + } + else + { + numberBackupsToKeep = 0; + } + NamedList slave = (NamedList) initArgs.get("slave"); + boolean enableSlave = isEnabled(slave); + if (enableSlave) + { + currentAlfrescoIndexFetcher = pollingAlfrescoIndexFetcher = new AlfrescoIndexFetcher(slave, this, core, contentStore); + setupPolling((String) slave.get(POLL_INTERVAL)); + isSlave = true; + } + NamedList master = (NamedList) initArgs.get("master"); + boolean enableMaster = isEnabled(master); + + if (enableMaster || enableSlave) + { + if (core.getCoreContainer().getZkController() != null) + { + LOG.warn("SolrCloud is enabled for core " + core.getName() + + " but so is old-style replication. Make sure you" + + " intend this behavior, it usually indicates a mis-configuration. Master setting is " + + enableMaster + " and slave setting is " + enableSlave); + } + } + + if (!enableSlave && !enableMaster) + { + enableMaster = true; + master = new NamedList<>(); + } + + if (enableMaster) + { + includeConfFiles = (String) master.get(CONF_FILES); + if (includeConfFiles != null && includeConfFiles.trim().length() > 0) + { + String[] files = includeConfFiles.split(","); + for (String file : files) + { + if (file.trim().length() == 0) + continue; + String[] strs = file.trim().split(":"); + // if there is an alias add it or it is null + confFileNameAlias.add(strs[0], strs.length > 1 ? strs[1] : null); + } + LOG.info("Replication enabled for following config files: " + includeConfFiles); + } + List backup = master.getAll("backupAfter"); + boolean backupOnCommit = backup.contains("commit"); + boolean backupOnOptimize = !backupOnCommit && backup.contains("optimize"); + List replicateAfter = master.getAll(REPLICATE_AFTER); + replicateOnCommit = replicateAfter.contains("commit"); + replicateOnOptimize = !replicateOnCommit && replicateAfter.contains("optimize"); + + if (!replicateOnCommit && !replicateOnOptimize) + { + replicateOnCommit = true; + } + + // if we only want to replicate on optimize, we need the deletion policy to + // save the last optimized commit point. + if (replicateOnOptimize) + { + IndexDeletionPolicyWrapper wrapper = core.getDeletionPolicy(); + IndexDeletionPolicy policy = wrapper == null ? null : wrapper.getWrappedDeletionPolicy(); + if (policy instanceof SolrDeletionPolicy) + { + SolrDeletionPolicy solrPolicy = (SolrDeletionPolicy) policy; + if (solrPolicy.getMaxOptimizedCommitsToKeep() < 1) + { + solrPolicy.setMaxOptimizedCommitsToKeep(1); + } + } + else + { + LOG.warn("Replication can't call setMaxOptimizedCommitsToKeep on " + policy); + } + } + + if (replicateOnOptimize || backupOnOptimize) + { + core.getUpdateHandler() + .registerOptimizeCallback(getEventListener(backupOnOptimize, replicateOnOptimize)); + } + if (replicateOnCommit || backupOnCommit) + { + replicateOnCommit = true; + core.getUpdateHandler().registerCommitCallback(getEventListener(backupOnCommit, replicateOnCommit)); + } + if (replicateAfter.contains("startup")) + { + replicateOnStart = true; + RefCounted s = core.getNewestSearcher(false); + try + { + DirectoryReader reader = s == null ? null : s.get().getIndexReader(); + if (reader != null && reader.getIndexCommit() != null + && reader.getIndexCommit().getGeneration() != 1L) + { + // try { + if (replicateOnOptimize) + { + Collection commits = DirectoryReader.listCommits(reader.directory()); + for (IndexCommit ic : commits) + { + if (ic.getSegmentCount() == 1) + { + if (indexCommitPoint == null || indexCommitPoint.getGeneration() < ic + .getGeneration()) + indexCommitPoint = ic; + } + } + } + else + { + indexCommitPoint = reader.getIndexCommit(); + } + } + + // ensure the writer is init'd so that we have a list of commit points + RefCounted iw = core.getUpdateHandler().getSolrCoreState().getIndexWriter(core); + iw.decref(); + + } + catch (IOException e) + { + LOG.warn("Unable to get IndexCommit on startup", e); + } + finally + { + if (s != null) + s.decref(); + } + } + String reserve = (String) master.get(RESERVE); + if (reserve != null && !reserve.trim().equals("")) + { + reserveCommitDuration = readIntervalMs(reserve); + } + LOG.info("Commits will be reserved for " + reserveCommitDuration); + isMaster = true; + } + } + + // check master or slave is enabled + private boolean isEnabled(NamedList params) + { + if (params == null) + return false; + Object enable = params.get("enable"); + if (enable == null) + return true; + if (enable instanceof String) + return StrUtils.parseBool((String) enable); + return Boolean.TRUE.equals(enable); + } + + /** + * register a closehook + */ + private void registerCloseHook() + { + core.addCloseHook(new CloseHook() + { + @Override + public void preClose(SolrCore core) + { + try + { + if (executorService != null) + executorService.shutdown(); // we don't wait for shutdown - this can deadlock core reload + } + finally + { + if (pollingAlfrescoIndexFetcher != null) + { + pollingAlfrescoIndexFetcher.destroy(); + } + } + if (currentAlfrescoIndexFetcher != null && currentAlfrescoIndexFetcher != pollingAlfrescoIndexFetcher) + { + currentAlfrescoIndexFetcher.destroy(); + } + } + + @Override + public void postClose(SolrCore core) + { + } + }); + + core.addCloseHook(new CloseHook() + { + @Override + public void preClose(SolrCore core) + { + ExecutorUtil.shutdownAndAwaitTermination(restoreExecutor); + if (restoreFuture != null) + { + restoreFuture.cancel(false); + } + } + + @Override + public void postClose(SolrCore core) + { + } + }); + } + + /** + * Register a listener for postcommit/optimize + * + * @param snapshoot do a snapshoot + * @param getCommit get a commitpoint also + * @return an instance of the eventlistener + */ + private SolrEventListener getEventListener(final boolean snapshoot, final boolean getCommit) + { + return new SolrEventListener() + { + @Override + public void init(NamedList args) + {/*no op*/ } + + /** + * This refreshes the latest replicateable index commit and optionally can create Snapshots as well + */ + @Override + public void postCommit() + { + IndexCommit currentCommitPoint = core.getDeletionPolicy().getLatestCommit(); + + if (getCommit) + { + // IndexCommit oldCommitPoint = indexCommitPoint; + indexCommitPoint = currentCommitPoint; + } + if (snapshoot) + { + try + { + int numberToKeep = numberBackupsToKeep; + if (numberToKeep < 1) + { + numberToKeep = Integer.MAX_VALUE; + } + + SnapShooter snapShooter = new SnapShooter(core, null, null); + snapShooter.validateCreateSnapshot(); + snapShooter.createSnapAsync(currentCommitPoint, numberToKeep, (nl) -> snapShootDetails = nl); + } + catch (Exception e) + { + LOG.error("Exception while snapshooting", e); + } + } + } + + @Override + public void newSearcher(SolrIndexSearcher newSearcher, SolrIndexSearcher currentSearcher) + { /*no op*/} + + @Override + public void postSoftCommit() + { + + } + }; + } + + /** + * This class is used to read and send files in the lucene index + */ + private class DirectoryFileStream implements SolrCore.RawWriter + { + protected SolrParams params; + + FastOutputStream fos; + + Long indexGen; + IndexDeletionPolicyWrapper delPolicy; + + String fileName; + String cfileName; + String tlogFileName; + String contentStoreFilename; + String sOffset; + String sLen; + String compress; + boolean useChecksum; + + protected long offset = -1; + int len = -1; + + Checksum checksum; + + private RateLimiter rateLimiter; + + byte[] buf; + + DirectoryFileStream(SolrParams solrParams) + { + params = solrParams; + delPolicy = core.getDeletionPolicy(); + + fileName = validateFilenameOrError(params.get(FILE)); + cfileName = validateFilenameOrError(params.get(CONF_FILE_SHORT)); + tlogFileName = validateFilenameOrError(params.get(TLOG_FILE)); + + sOffset = params.get(OFFSET); + sLen = params.get(LEN); + compress = params.get(COMPRESSION); + useChecksum = params.getBool(CHECKSUM, false); + + indexGen = params.getLong(GENERATION); + if (useChecksum) + { + checksum = new Adler32(); + } + //No throttle if MAX_WRITE_PER_SECOND is not specified + double maxWriteMBPerSec = params.getDouble(MAX_WRITE_PER_SECOND, Double.MAX_VALUE); + rateLimiter = new RateLimiter.SimpleRateLimiter(maxWriteMBPerSec); + } + + // Throw exception on directory traversal attempts + String validateFilenameOrError(String filename) + { + if (filename != null) + { + Path filePath = Paths.get(filename); + filePath.forEach(subpath -> { + if ("..".equals(subpath.toString())) + { + throw new SolrException(ErrorCode.FORBIDDEN, "File name cannot contain .."); + } + }); + if (filePath.isAbsolute()) + { + throw new SolrException(ErrorCode.FORBIDDEN, "File name must be relative"); + } + return filename; + } + else + return null; + } + + void initWrite() throws IOException + { + if (sOffset != null) + offset = Long.parseLong(sOffset); + if (sLen != null) + len = Integer.parseInt(sLen); + if (fileName == null && cfileName == null && tlogFileName == null && contentStoreFilename == null) + { + // no filename do nothing + writeNothingAndFlush(); + } + buf = new byte[(len == -1 || len > PACKET_SZ) ? PACKET_SZ : len]; + + //reserve commit point till write is complete + if (indexGen != null) + { + delPolicy.saveCommitPoint(indexGen); + } + } + + void createOutputStream(OutputStream out) + { + if (Boolean.parseBoolean(compress)) + { + fos = new FastOutputStream(new DeflaterOutputStream(out)); + } + else + { + fos = new FastOutputStream(out); + } + } + + void extendReserveAndReleaseCommitPoint() + { + if (indexGen != null) + { + //Reserve the commit point for another 10s for the next file to be to fetched. + //We need to keep extending the commit reservation between requests so that the replica can fetch + //all the files correctly. + delPolicy.setReserveDuration(indexGen, reserveCommitDuration); + + //release the commit point as the write is complete + delPolicy.releaseCommitPoint(indexGen); + } + + } + + public void write(OutputStream out) throws IOException + { + createOutputStream(out); + + IndexInput in = null; + try + { + initWrite(); + + RefCounted sref = core.getSearcher(); + Directory dir; + try + { + SolrIndexSearcher searcher = sref.get(); + dir = searcher.getIndexReader().directory(); + } + finally + { + sref.decref(); + } + in = dir.openInput(fileName, IOContext.READONCE); + // if offset is mentioned move the pointer to that point + if (offset != -1) + in.seek(offset); + + long filelen = dir.fileLength(fileName); + long maxBytesBeforePause = 0; + + while (true) + { + offset = offset == -1 ? 0 : offset; + int read = (int) Math.min(buf.length, filelen - offset); + in.readBytes(buf, 0, read); + + fos.writeInt(read); + if (useChecksum) + { + checksum.reset(); + checksum.update(buf, 0, read); + fos.writeLong(checksum.getValue()); + } + fos.write(buf, 0, read); + fos.flush(); + LOG.debug("Wrote {} bytes for file {}", offset + read, fileName); + + //Pause if necessary + maxBytesBeforePause += read; + if (maxBytesBeforePause >= rateLimiter.getMinPauseCheckBytes()) + { + rateLimiter.pause(maxBytesBeforePause); + maxBytesBeforePause = 0; + } + if (read != buf.length) + { + writeNothingAndFlush(); + fos.close(); + break; + } + offset += read; + in.seek(offset); + } + } + catch (IOException e) + { + LOG.warn("Exception while writing response for params: " + params, e); + } + finally + { + if (in != null) + { + in.close(); + } + extendReserveAndReleaseCommitPoint(); + } + } + + /** + * Used to write a marker for EOF + */ + protected void writeNothingAndFlush() throws IOException + { + fos.writeInt(0); + fos.flush(); + } + } + + /** + * This is used to read and send files in content store ad a single stream + */ + protected abstract class LocalFsFileStream extends DirectoryFileStream + { + + private File file; + + LocalFsFileStream(SolrParams solrParams) + { + super(solrParams); + this.file = this.initFile(); + } + + protected abstract File initFile(); + + @Override + public void write(OutputStream out) + { + createOutputStream(out); + FileInputStream inputStream = null; + try + { + initWrite(); + + if (file.exists() && file.canRead()) + { + inputStream = new FileInputStream(file); + FileChannel channel = inputStream.getChannel(); + //if offset is mentioned move the pointer to that point + if (offset != -1) + channel.position(offset); + ByteBuffer bb = ByteBuffer.wrap(buf); + + while (true) + { + bb.clear(); + long bytesRead = channel.read(bb); + if (bytesRead <= 0) + { + writeNothingAndFlush(); + fos.close(); + break; + } + fos.writeInt((int) bytesRead); + if (useChecksum) + { + checksum.reset(); + checksum.update(buf, 0, (int) bytesRead); + fos.writeLong(checksum.getValue()); + } + fos.write(buf, 0, (int) bytesRead); + fos.flush(); + } + } + else + { + writeNothingAndFlush(); + } + } + catch (IOException e) + { + LOG.warn("Exception while writing response for params: " + params, e); + } + finally + { + IOUtils.closeQuietly(inputStream); + extendReserveAndReleaseCommitPoint(); + } + } + } + + /** + * This is used to write files in content store. + */ + protected class ContentStoreFilesStream extends DirectoryFileStream + { + ContentStoreFilesStream(SolrParams solrParams) + { + super(solrParams); + } + + @Override + public void write(OutputStream out) throws IOException + { + createOutputStream(out); + String contentStoreRoot = contentStore.getRootLocation(); + try + { + for (String fileName : params.getParams(CONTENT_STORE_FILE_LIST)) + { + File f = new File(contentStoreRoot + fileName); + if (f.exists() && !f.isDirectory()) + { + try + { + writeFile(f, fileName); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + } + } + catch (Exception e) + { + e.printStackTrace(); + } + finally + { + fos.close(); + extendReserveAndReleaseCommitPoint(); + } + } + + @Override + protected void writeNothingAndFlush() throws IOException + { + fos.flush(); + } + + void writeFile(File file, String fileName) throws IOException + { + + buf = new byte[PACKET_SZ]; + if (file.exists() && file.canRead()) + { + FileInputStream inputStream = new FileInputStream(file); + FileChannel channel = inputStream.getChannel(); + //if offset is mentioned move the pointer to that point + if (offset != -1) + channel.position(offset); + ByteBuffer bb = ByteBuffer.wrap(buf); + channel.size(); + + fos.writeInt(fileName.length()); + fos.write(fileName.getBytes()); + fos.writeInt((int) channel.size()); + + if (channel.size() != 0) + { + + while (true) + { + bb.clear(); + long bytesRead = channel.read(bb); + if (bytesRead <= 0) + { + writeNothingAndFlush(); + inputStream.close(); + break; + } + + fos.writeInt((int) bytesRead); + + if (useChecksum) + { + checksum.reset(); + checksum.update(buf, 0, (int) bytesRead); + fos.writeLong(checksum.getValue()); + } + + fos.write(buf, 0, (int) bytesRead); + fos.flush(); + } + } + + } + } + } + + private class LocalFsTlogFileStream extends LocalFsFileStream + { + + LocalFsTlogFileStream(SolrParams solrParams) + { + super(solrParams); + } + + protected File initFile() + { + //if it is a tlog file read from tlog directory + return new File(core.getUpdateHandler().getUpdateLog().getLogDir(), tlogFileName); + } + + } + + private class LocalFsConfFileStream extends LocalFsFileStream + { + LocalFsConfFileStream(SolrParams solrParams) + { + super(solrParams); + } + + protected File initFile() + { + //if it is a conf file read from config directory + return new File(core.getResourceLoader().getConfigDir(), cfileName); + } + } + + private static Integer readIntervalMs(String interval) + { + return (int) TimeUnit.MILLISECONDS.convert(readIntervalNs(interval), TimeUnit.NANOSECONDS); + } + + private static Long readIntervalNs(String interval) + { + if (interval == null) + return null; + int result; + Matcher m = INTERVAL_PATTERN.matcher(interval.trim()); + if (m.find()) + { + String hr = m.group(1); + String min = m.group(2); + String sec = m.group(3); + result = 0; + try + { + if (sec != null && sec.length() > 0) + result += Integer.parseInt(sec); + if (min != null && min.length() > 0) + result += (60 * Integer.parseInt(min)); + if (hr != null && hr.length() > 0) + result += (60 * 60 * Integer.parseInt(hr)); + return TimeUnit.NANOSECONDS.convert(result, TimeUnit.SECONDS); + } + catch (NumberFormatException e) + { + throw new SolrException(ErrorCode.SERVER_ERROR, INTERVAL_ERR_MSG); + } + } + else + { + throw new SolrException(ErrorCode.SERVER_ERROR, INTERVAL_ERR_MSG); + } + } + + private static final String SUCCESS = "success"; + + private static final String FAILED = "failed"; + + private static final String EXCEPTION = "exception"; + + static final String MASTER_URL = "masterUrl"; + + private static final String STATUS = "status"; + + static final String COMMAND = "command"; + + static final String CMD_DETAILS = "details"; + + private static final String CMD_BACKUP = "backup"; + + private static final String CMD_RESTORE = "restore"; + + private static final String CMD_RESTORE_STATUS = "restorestatus"; + + private static final String CMD_FETCH_INDEX = "fetchindex"; + + private static final String CMD_ABORT_FETCH = "abortfetch"; + + static final String CMD_GET_FILE_LIST = "filelist"; + + static final String CMD_GET_FILE = "filecontent"; + + private static final String CMD_DISABLE_POLL = "disablepoll"; + + private static final String CMD_DISABLE_REPL = "disablereplication"; + + private static final String CMD_ENABLE_REPL = "enablereplication"; + + private static final String CMD_ENABLE_POLL = "enablepoll"; + + static final String CMD_INDEX_VERSION = "indexversion"; + + private static final String CMD_SHOW_COMMITS = "commits"; + + private static final String CMD_DELETE_BACKUP = "deletebackup"; + + static final String GENERATION = "generation"; + + static final String CONTENT_STORE_VERSION = "contentstoreversion"; + + static final String OFFSET = "offset"; + + private static final String LEN = "len"; + + static final String FILE = "file"; + + public static final String SIZE = "size"; + + private static final String MAX_WRITE_PER_SECOND = "maxWriteMBPerSec"; + + static final String CONF_FILE_SHORT = "cf"; + + static final String TLOG_FILE = "tlogFile"; + + static final String CHECKSUM = "checksum"; + + static final String ALIAS = "alias"; + + static final String CONF_FILES = "confFiles"; + + static final String TLOG_FILES = "tlogFiles"; + + static final String CONTENT_STORE_FILES = "contentStoreFiles"; + + private static final String REPLICATE_AFTER = "replicateAfter"; + + static final String FILE_STREAM = "filestream"; + + private static final String POLL_INTERVAL = "pollInterval"; + + private static final String INTERVAL_ERR_MSG = "The " + POLL_INTERVAL + " must be in this format 'HH:mm:ss'"; + + private static final Pattern INTERVAL_PATTERN = Pattern.compile("(\\d*?):(\\d*?):(\\d*)"); + + private static final int PACKET_SZ = 1024 * 1024; // 1MB + + private static final String RESERVE = "commitReserveDuration"; + + static final String COMPRESSION = "compression"; + + static final String EXTERNAL = "external"; + + static final String INTERNAL = "internal"; + + private static final String ERR_STATUS = "ERROR"; + + private static final String OK_STATUS = "OK"; + + private static final String NEXT_EXECUTION_AT = "nextExecutionAt"; + + private static final String NUMBER_BACKUPS_TO_KEEP_REQUEST_PARAM = "numberToKeep"; + + private static final String NUMBER_BACKUPS_TO_KEEP_INIT_PARAM = "maxNumberOfBackups"; + + static final String CONTENT_STORE_FILE_LIST = "contentStoreFiles"; + + static final String CMD_CONTENT_STORE_FILES = "cmdContentStoreFiles"; + + static final long NO_INDEX_REPLICATION_REQUIRED = -3; + + /** + * Boolean param for tests that can be specified when using + * {@link #CMD_FETCH_INDEX} to force the current request to block until + * the fetch is complete. NOTE: This param is not advised for + * non-test code, since the the duration of the fetch for non-trivial + * indexes will likeley cause the request to time out. + */ + private static final String WAIT = "wait"; +} diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/OldBackupDirectory.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/OldBackupDirectory.java new file mode 100644 index 000000000..3c188ea2b --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/OldBackupDirectory.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + * + * Modification copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see + * + */ +package org.alfresco.solr.handler; + +import org.apache.solr.handler.SnapShooter; + +import java.net.URI; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +class OldBackupDirectory implements Comparable +{ + private static final Pattern dirNamePattern = Pattern.compile("^snapshot[.](.*)$"); + + private URI basePath; + private String dirName; + private Optional timestamp = Optional.empty(); + + OldBackupDirectory(URI basePath, String dirName) + { + this.dirName = Objects.requireNonNull(dirName); + this.basePath = Objects.requireNonNull(basePath); + Matcher m = dirNamePattern.matcher(dirName); + if (m.find()) + { + try + { + this.timestamp = Optional.of(new SimpleDateFormat(SnapShooter.DATE_FMT, Locale.ROOT).parse(m.group(1))); + } + catch (ParseException e) + { + this.timestamp = Optional.empty(); + } + } + } + + public URI getPath() + { + return this.basePath.resolve(dirName); + } + + String getDirName() + { + return dirName; + } + + public Optional getTimestamp() + { + return timestamp; + } + + @Override + public int compareTo(OldBackupDirectory that) + { + if (this.timestamp.isPresent() && that.timestamp.isPresent()) + { + return that.timestamp.get().compareTo(this.timestamp.get()); + } + // Use absolute value of path in case the time-stamp is missing on either side. + return that.getPath().compareTo(this.getPath()); + } +} diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/SnapShooter.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/SnapShooter.java new file mode 100644 index 000000000..680d915f3 --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/handler/SnapShooter.java @@ -0,0 +1,377 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + * + * Modification copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see + * + */ +package org.alfresco.solr.handler; + +import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.store.Directory; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrException.ErrorCode; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.core.DirectoryFactory.DirContext; +import org.apache.solr.core.IndexDeletionPolicyWrapper; +import org.apache.solr.core.SolrCore; +import org.apache.solr.core.backup.repository.BackupRepository; +import org.apache.solr.core.backup.repository.BackupRepository.PathType; +import org.apache.solr.core.backup.repository.LocalFileSystemRepository; +import org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager; +import org.apache.solr.search.SolrIndexSearcher; +import org.apache.solr.util.RefCounted; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; + +/** + *

Provides functionality equivalent to the snapshooter script

+ * This is no longer used in standard replication. + * + * @since solr 1.4 + */ +public class SnapShooter +{ + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private SolrCore solrCore; + private String snapshotName = null; + private String directoryName = null; + private URI baseSnapDirPath = null; + private URI snapshotDirPath = null; + private BackupRepository backupRepo = null; + private String commitName; // can be null + + @Deprecated + public SnapShooter(SolrCore core, String location, String snapshotName) + { + String snapDirStr; + // Note - This logic is only applicable to the usecase where a shared file-system is exposed via + // local file-system interface (primarily for backwards compatibility). For other use-cases, users + // will be required to specify "location" where the backup should be stored. + if (location == null) + { + snapDirStr = core.getDataDir(); + } + else + { + snapDirStr = core.getCoreDescriptor().getInstanceDir().resolve(location).normalize().toString(); + } + initialize(new LocalFileSystemRepository(), core, Paths.get(snapDirStr).toUri(), snapshotName, null); + } + + SnapShooter(BackupRepository backupRepo, SolrCore core, URI location, String snapshotName, String commitName) + { + initialize(backupRepo, core, location, snapshotName, commitName); + } + + private void initialize(BackupRepository backupRepo, SolrCore core, URI location, String snapshotName, String commitName) + { + this.solrCore = Objects.requireNonNull(core); + this.backupRepo = Objects.requireNonNull(backupRepo); + this.baseSnapDirPath = location; + this.snapshotName = snapshotName; + if (snapshotName != null) + { + directoryName = "snapshot." + snapshotName; + } + else + { + SimpleDateFormat fmt = new SimpleDateFormat(DATE_FMT, Locale.ROOT); + directoryName = "snapshot." + fmt.format(new Date()); + } + this.snapshotDirPath = backupRepo.resolve(location, directoryName); + this.commitName = commitName; + } + + public BackupRepository getBackupRepository() + { + return backupRepo; + } + + /** + * Gets the parent directory of the snapshots. This is the {@code location} + * given in the constructor. + */ + public URI getLocation() + { + return this.baseSnapDirPath; + } + + void validateDeleteSnapshot() + { + Objects.requireNonNull(this.snapshotName); + + boolean dirFound = false; + String[] paths; + try + { + paths = backupRepo.listAll(baseSnapDirPath); + for (String path : paths) + { + if (path.equals(this.directoryName) + && backupRepo.getPathType(baseSnapDirPath.resolve(path)) == PathType.DIRECTORY) + { + dirFound = true; + break; + } + } + if (!dirFound) + { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + "Snapshot " + snapshotName + " cannot be found in directory: " + baseSnapDirPath); + } + } + catch (IOException e) + { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Unable to find snapshot " + snapshotName + " in directory: " + baseSnapDirPath, e); + } + } + + void deleteSnapAsync(final AlfrescoReplicationHandler alfrescoReplicationHandler) + { + new Thread(() -> deleteNamedSnapshot(alfrescoReplicationHandler)).start(); + } + + void validateCreateSnapshot() throws IOException + { + // Note - Removed the current behavior of creating the directory hierarchy. + // Do we really need to provide this support? + if (!backupRepo.exists(baseSnapDirPath)) + { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + " Directory does not exist: " + snapshotDirPath); + } + + if (backupRepo.exists(snapshotDirPath)) + { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + "Snapshot directory already exists: " + snapshotDirPath); + } + } + + public NamedList createSnapshot() throws Exception + { + RefCounted searcher = solrCore.getSearcher(); + try + { + if (commitName != null) + { + SolrSnapshotMetaDataManager snapshotMgr = solrCore.getSnapshotMetaDataManager(); + Optional commit = snapshotMgr.getIndexCommitByName(commitName); + if (commit.isPresent()) + { + return createSnapshot(commit.get()); + } + throw new SolrException(ErrorCode.SERVER_ERROR, + "Unable to find an index commit with name " + commitName + " for core " + solrCore.getName()); + } + else + { + //TODO should we try solrCore.getDeletionPolicy().getLatestCommit() first? + IndexDeletionPolicyWrapper deletionPolicy = solrCore.getDeletionPolicy(); + IndexCommit indexCommit = searcher.get().getIndexReader().getIndexCommit(); + deletionPolicy.saveCommitPoint(indexCommit.getGeneration()); + try + { + return createSnapshot(indexCommit); + } + finally + { + deletionPolicy.releaseCommitPoint(indexCommit.getGeneration()); + } + } + } + finally + { + searcher.decref(); + } + } + + void createSnapAsync(final IndexCommit indexCommit, final int numberToKeep, Consumer result) + { + solrCore.getDeletionPolicy().saveCommitPoint(indexCommit.getGeneration()); + + new Thread(() -> { + try + { + result.accept(createSnapshot(indexCommit)); + } + catch (Exception e) + { + LOG.error("Exception while creating snapshot", e); + NamedList snapShootDetails = new NamedList<>(); + snapShootDetails.add("snapShootException", e.getMessage()); + result.accept(snapShootDetails); + } + finally + { + solrCore.getDeletionPolicy().releaseCommitPoint(indexCommit.getGeneration()); + } + if (snapshotName == null) + { + try + { + deleteOldBackups(numberToKeep); + } + catch (IOException e) + { + LOG.warn("Unable to delete old snapshots ", e); + } + } + }).start(); + + } + + // note: remember to reserve the indexCommit first so it won't get deleted concurrently + private NamedList createSnapshot(final IndexCommit indexCommit) throws Exception + { + LOG.info("Creating backup snapshot " + (snapshotName == null ? "" : snapshotName) + " at " + + baseSnapDirPath); + boolean success = false; + try + { + NamedList details = new NamedList<>(); + details.add("startTime", new Date().toString());//bad; should be Instant.now().toString() + + Collection files = indexCommit.getFileNames(); + Directory dir = solrCore.getDirectoryFactory() + .get(solrCore.getIndexDir(), DirContext.DEFAULT, solrCore.getSolrConfig().indexConfig.lockType); + try + { + for (String fileName : files) + { + backupRepo.copyFileFrom(dir, fileName, snapshotDirPath); + } + } + finally + { + solrCore.getDirectoryFactory().release(dir); + } + + details.add("fileCount", files.size()); + details.add("status", "success"); + details.add("snapshotCompletedAt", new Date().toString());//bad; should be Instant.now().toString() + details.add("snapshotName", snapshotName); + LOG.info("Done creating backup snapshot: " + (snapshotName == null ? "" : snapshotName) + " at " + + baseSnapDirPath); + success = true; + return details; + } + finally + { + if (!success) + { + try + { + backupRepo.deleteDirectory(snapshotDirPath); + } + catch (Exception excDuringDelete) + { + LOG.warn("Failed to delete " + snapshotDirPath + " after snapshot creation failed due to: " + + excDuringDelete); + } + } + } + } + + private void deleteOldBackups(int numberToKeep) throws IOException + { + String[] paths = backupRepo.listAll(baseSnapDirPath); + List dirs = new ArrayList<>(); + for (String f : paths) + { + if (backupRepo.getPathType(baseSnapDirPath.resolve(f)) == PathType.DIRECTORY) + { + OldBackupDirectory obd = new OldBackupDirectory(baseSnapDirPath, f); + if (obd.getTimestamp().isPresent()) + { + dirs.add(obd); + } + } + } + if (numberToKeep > dirs.size() - 1) + { + return; + } + Collections.sort(dirs); + int i = 1; + for (OldBackupDirectory dir : dirs) + { + if (i++ > numberToKeep) + { + backupRepo.deleteDirectory(dir.getPath()); + } + } + } + + protected void deleteNamedSnapshot(AlfrescoReplicationHandler alfrescoReplicationHandler) + { + LOG.info("Deleting snapshot: " + snapshotName); + + NamedList details = new NamedList<>(); + + try + { + URI path = baseSnapDirPath.resolve("snapshot." + snapshotName); + backupRepo.deleteDirectory(path); + + details.add("status", "success"); + details.add("snapshotDeletedAt", new Date().toString()); + + } + catch (IOException e) + { + details.add("status", "Unable to delete snapshot: " + snapshotName); + LOG.warn("Unable to delete snapshot: " + snapshotName, e); + } + + alfrescoReplicationHandler.snapShootDetails = details; + } + + private static final String DATE_FMT = "yyyyMMddHHmmssSSS"; + +} diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/lifecycle/SolrCoreLoadListener.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/lifecycle/SolrCoreLoadListener.java index 0b41cba15..66ed18b9e 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/lifecycle/SolrCoreLoadListener.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/lifecycle/SolrCoreLoadListener.java @@ -35,7 +35,7 @@ import org.alfresco.solr.tracker.CommitTracker; import org.alfresco.solr.tracker.ContentTracker; import org.alfresco.solr.tracker.MetadataTracker; import org.alfresco.solr.tracker.ModelTracker; -import org.alfresco.solr.tracker.SlaveNodeStatePublisher; +import org.alfresco.solr.tracker.SlaveCoreStatePublisher; import org.alfresco.solr.tracker.SolrTrackerScheduler; import org.alfresco.solr.tracker.Tracker; import org.alfresco.solr.tracker.TrackerRegistry; @@ -113,7 +113,7 @@ public class SolrCoreLoadListener extends AbstractSolrEventListener AlfrescoSolrDataModel.getInstance().getDictionaryService(CMISStrictDictionaryService.DEFAULT), AlfrescoSolrDataModel.getInstance().getNamespaceDAO()); - SolrContentStore contentStore = new SolrContentStore(coreContainer.getSolrHome()); + SolrContentStore contentStore = admin.getSolrContentStore(); SolrInformationServer informationServer = new SolrInformationServer(admin, core, repositoryClient, contentStore); coreProperties.putAll(informationServer.getProps()); admin.getInformationServers().put(core.getName(), informationServer); @@ -162,6 +162,7 @@ public class SolrCoreLoadListener extends AbstractSolrEventListener boolean trackersHaveBeenEnabled = Boolean.parseBoolean(coreProperties.getProperty("enable.alfresco.tracking", "true")); boolean owningCoreIsSlave = isSlaveModeEnabledFor(core); + contentStore.toggleReadOnlyMode(owningCoreIsSlave); if (trackerRegistry.hasTrackersForCore(core.getName())) { @@ -182,7 +183,7 @@ public class SolrCoreLoadListener extends AbstractSolrEventListener { LOGGER.info("SearchServices Core Trackers have been explicitly disabled on core \"{}\" through \"enable.alfresco.tracking\" configuration property.", core.getName()); - SlaveNodeStatePublisher statePublisher = new SlaveNodeStatePublisher(false, coreProperties, repositoryClient, core.getName(), informationServer); + SlaveCoreStatePublisher statePublisher = new SlaveCoreStatePublisher(false, coreProperties, repositoryClient, core.getName(), informationServer); trackerRegistry.register(core.getName(), statePublisher); scheduler.schedule(statePublisher, core.getName(), coreProperties); trackers.add(statePublisher); @@ -197,7 +198,7 @@ public class SolrCoreLoadListener extends AbstractSolrEventListener { LOGGER.info("SearchServices Core Trackers have been disabled on core \"{}\" because it is a slave core.", core.getName()); - SlaveNodeStatePublisher statePublisher = new SlaveNodeStatePublisher(false, coreProperties, repositoryClient, core.getName(), informationServer); + SlaveCoreStatePublisher statePublisher = new SlaveCoreStatePublisher(false, coreProperties, repositoryClient, core.getName(), informationServer); trackerRegistry.register(core.getName(), statePublisher); scheduler.schedule(statePublisher, core.getName(), coreProperties); trackers.add(statePublisher); diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/AbstractTracker.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/AbstractTracker.java index d4429f92b..f4d381cd1 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/AbstractTracker.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/AbstractTracker.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2014 Alfresco Software Limited. + * Copyright (C) 2005-2019 Alfresco Software Limited. * * This file is part of Alfresco * @@ -18,6 +18,8 @@ */ package org.alfresco.solr.tracker; +import static java.util.Optional.ofNullable; + import java.lang.invoke.MethodHandles; import java.net.ConnectException; import java.net.SocketTimeoutException; @@ -49,11 +51,10 @@ public abstract class AbstractTracker implements Tracker protected SOLRAPIClient client; InformationServer infoSrv; protected String coreName; - protected StoreRef storeRef; - protected long batchCount; - protected String alfrescoVersion; - protected TrackerStats trackerStats; - protected boolean runPostModelLoadInit = true; + StoreRef storeRef; + long batchCount; + TrackerStats trackerStats; + boolean runPostModelLoadInit = true; private int maxLiveSearchers; private volatile boolean shutdown = false; @@ -63,9 +64,9 @@ public abstract class AbstractTracker implements Tracker protected volatile TrackerState state; protected int shardCount; protected int shardInstance; - protected String shardMethod; + String shardMethod; protected boolean transformContent; - protected String shardTemplate; + String shardTemplate; protected volatile boolean rollback; protected final Type type; @@ -102,12 +103,8 @@ public abstract class AbstractTracker implements Tracker transformContent = Boolean.parseBoolean(p.getProperty("alfresco.index.transformContent", "true")); this.trackerStats = this.infoSrv.getTrackerStats(); - - alfrescoVersion = p.getProperty("alfresco.version", "5.0.0"); this.type = type; - - LOGGER.info("Solr built for Alfresco version: {}", alfrescoVersion); } @@ -183,12 +180,9 @@ public abstract class AbstractTracker implements Tracker if(this.state == null) { - /* - * Set the global state for the tracker here. - */ this.state = getTrackerState(); - LOGGER.debug("##### Setting tracker global state."); - LOGGER.debug("State set: {}", this.state.toString()); + + LOGGER.debug("Global Tracker State set to: {}", this.state.toString()); this.state.setRunning(true); } else @@ -237,12 +231,11 @@ public abstract class AbstractTracker implements Tracker finally { infoSrv.unregisterTrackerThread(); - if(state != null) - { - //During a rollback state is set to null. + ofNullable(state).ifPresent(tstate -> { + // During a rollback state is set to null. state.setRunning(false); state.setCheck(false); - } + }); runLock.release(); } } @@ -284,7 +277,7 @@ public abstract class AbstractTracker implements Tracker /** * Allows time for the scheduled asynchronous tasks to complete */ - protected synchronized void waitForAsynchronous() + synchronized void waitForAsynchronous() { AbstractWorkerRunnable currentRunnable = this.threadHandler.peekHeadReindexWorker(); while (currentRunnable != null) @@ -305,12 +298,12 @@ public abstract class AbstractTracker implements Tracker } } - public int getMaxLiveSearchers() + int getMaxLiveSearchers() { return maxLiveSearchers; } - protected void checkShutdown() + void checkShutdown() { if(shutdown) { @@ -345,20 +338,11 @@ public abstract class AbstractTracker implements Tracker return this.writeLock; } - public Semaphore getRunLock() + Semaphore getRunLock() { return this.runLock; } - /** - * @return Alfresco version Solr was built for - */ - @Override - public String getAlfrescoVersion() - { - return alfrescoVersion; - } - public Properties getProps() { return props; diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/CommitTracker.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/CommitTracker.java index d8a301cf9..375c21ad8 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/CommitTracker.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/CommitTracker.java @@ -151,6 +151,8 @@ public class CommitTracker extends AbstractTracker maintenance(); } + infoSrv.flushContentStore(); + //Do the commit opening the searcher if needed. This will commit all the work done by indexing trackers. //This will return immediately and not wait for searchers to warm //System.out.println("################### Commit:"+openSearcherNeeded); diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/NodeStatePublisher.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/CoreStatePublisher.java similarity index 91% rename from search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/NodeStatePublisher.java rename to search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/CoreStatePublisher.java index d6fd2d78a..6c38fb47a 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/NodeStatePublisher.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/CoreStatePublisher.java @@ -41,6 +41,7 @@ import org.alfresco.service.namespace.QName; import org.alfresco.solr.AlfrescoCoreAdminHandler; import org.alfresco.solr.AlfrescoSolrDataModel; import org.alfresco.solr.InformationServer; +import org.alfresco.solr.NodeReport; import org.alfresco.solr.TrackerState; import org.alfresco.solr.client.SOLRAPIClient; import org.apache.commons.lang3.StringUtils; @@ -59,7 +60,7 @@ import java.util.Properties; * @since 1.5 * @see SEARCH-1752 */ -public abstract class NodeStatePublisher extends AbstractTracker +public abstract class CoreStatePublisher extends AbstractTracker { DocRouter docRouter; private final boolean isMaster; @@ -70,7 +71,7 @@ public abstract class NodeStatePublisher extends AbstractTracker /** The property to use for determining the shard. */ protected Optional shardProperty = Optional.empty(); - NodeStatePublisher( + CoreStatePublisher( boolean isMaster, Properties p, SOLRAPIClient client, @@ -88,12 +89,28 @@ public abstract class NodeStatePublisher extends AbstractTracker docRouter = DocRouterFactory.getRouter(p, ShardMethodEnum.getShardMethod(shardMethod)); } - NodeStatePublisher(Type type) + CoreStatePublisher(Type type) { super(type); this.isMaster = false; } + /** + * Returns information about the {@link org.alfresco.solr.client.Node} associated with the given dbid. + * + * @param dbid the node identifier. + * @return the {@link org.alfresco.solr.client.Node} associated with the given dbid. + */ + public NodeReport checkNode(Long dbid) + { + NodeReport nodeReport = new NodeReport(); + nodeReport.setDbid(dbid); + + this.infoSrv.addCommonNodeReportInfo(nodeReport); + + return nodeReport; + } + private void firstUpdateShardProperty() { shardKey.ifPresent( shardKeyName -> { @@ -222,4 +239,14 @@ public abstract class NodeStatePublisher extends AbstractTracker { return this.docRouter; } + + /** + * Returns true if the hosting core is master or standalone. + * + * @return true if the hosting core is master or standalone. + */ + public boolean isOnMasterOrStandalone() + { + return isMaster; + } } diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/MetadataTracker.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/MetadataTracker.java index f118c523b..3862ebd30 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/MetadataTracker.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/MetadataTracker.java @@ -49,7 +49,7 @@ import org.slf4j.LoggerFactory; * This tracks two things: transactions and metadata nodes * @author Ahmed Owian */ -public class MetadataTracker extends NodeStatePublisher implements Tracker +public class MetadataTracker extends CoreStatePublisher implements Tracker { protected final static Logger log = LoggerFactory.getLogger(MetadataTracker.class); private static final int DEFAULT_TRANSACTION_DOCS_BATCH_SIZE = 100; @@ -888,24 +888,18 @@ public class MetadataTracker extends NodeStatePublisher implements Tracker } } - - - - + @Override public NodeReport checkNode(Long dbid) { - NodeReport nodeReport = new NodeReport(); - nodeReport.setDbid(dbid); + NodeReport nodeReport = super.checkNode(dbid); // In DB - GetNodesParameters parameters = new GetNodesParameters(); parameters.setFromNodeId(dbid); parameters.setToNodeId(dbid); - List dbnodes; try { - dbnodes = client.getNodes(parameters, 1); + List dbnodes = client.getNodes(parameters, 1); if (dbnodes.size() == 1) { Node dbnode = dbnodes.get(0); @@ -915,41 +909,31 @@ public class MetadataTracker extends NodeStatePublisher implements Tracker else { nodeReport.setDbNodeStatus(SolrApiNodeStatus.UNKNOWN); - nodeReport.setDbTx(-1l); + nodeReport.setDbTx(-1L); } } catch (IOException e) { nodeReport.setDbNodeStatus(SolrApiNodeStatus.UNKNOWN); - nodeReport.setDbTx(-2l); + nodeReport.setDbTx(-2L); } catch (JSONException e) { nodeReport.setDbNodeStatus(SolrApiNodeStatus.UNKNOWN); - nodeReport.setDbTx(-3l); + nodeReport.setDbTx(-3L); } catch (AuthenticationException e1) { nodeReport.setDbNodeStatus(SolrApiNodeStatus.UNKNOWN); - nodeReport.setDbTx(-4l); + nodeReport.setDbTx(-4L); } - - this.infoSrv.addCommonNodeReportInfo(nodeReport); return nodeReport; } public NodeReport checkNode(Node node) { - NodeReport nodeReport = new NodeReport(); - nodeReport.setDbid(node.getId()); - - nodeReport.setDbNodeStatus(node.getStatus()); - nodeReport.setDbTx(node.getTxnId()); - - this.infoSrv.addCommonNodeReportInfo(nodeReport); - - return nodeReport; + return checkNode(node.getId()); } public List getFullNodesForDbTransaction(Long txid) diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/SlaveNodeStatePublisher.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/SlaveCoreStatePublisher.java similarity index 72% rename from search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/SlaveNodeStatePublisher.java rename to search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/SlaveCoreStatePublisher.java index 4e7ccf709..71750c53f 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/SlaveNodeStatePublisher.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/SlaveCoreStatePublisher.java @@ -1,3 +1,21 @@ +/* + * Copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ package org.alfresco.solr.tracker; import static org.alfresco.solr.tracker.Tracker.Type.NODE_STATE_PUBLISHER; @@ -14,8 +32,8 @@ import java.util.Properties; /** * Despite belonging to the Tracker ecosystem, this component is actually a publisher, which periodically informs - * Alfresco about the state of the hosting slave node. - * As the name suggests, this worker is scheduled only when the hosting node acts as a slave. + * Alfresco about the state of the hosting slave core. + * As the name suggests, this worker is scheduled only when the owning core acts as a slave. * It allows Solr's master/slave setup to be used with dynamic shard registration. * * In this scenario the slave is polling a "tracking" Solr node. The tracker below calls @@ -27,9 +45,9 @@ import java.util.Properties; * @author Andrea Gazzarini * @since 1.5 */ -public class SlaveNodeStatePublisher extends NodeStatePublisher +public class SlaveCoreStatePublisher extends CoreStatePublisher { - public SlaveNodeStatePublisher( + public SlaveCoreStatePublisher( boolean isMaster, Properties coreProperties, SOLRAPIClient repositoryClient, @@ -61,6 +79,12 @@ public class SlaveNodeStatePublisher extends NodeStatePublisher // Do nothing here } + @Override + public boolean isOnMasterOrStandalone() + { + return false; + } + @Override public boolean hasMaintenance() { diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/Tracker.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/Tracker.java index 05fe0766a..976d52c20 100644 --- a/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/Tracker.java +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/tracker/Tracker.java @@ -32,8 +32,6 @@ public interface Tracker boolean hasMaintenance() throws Exception; Semaphore getWriteLock(); - - String getAlfrescoVersion(); void setShutdown(boolean shutdown); diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/utils/AlfrescoFileUtils.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/utils/AlfrescoFileUtils.java new file mode 100644 index 000000000..dcbf963cf --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/utils/AlfrescoFileUtils.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.solr.utils; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + + +/** + * @author Elia Porciani + */ +public class AlfrescoFileUtils { + + /** + * Check if two directories contains the same files + * + * @param dir + * @param dir2 + * @param extensions Limits the search to the extensions provided + * @param recursive Check recursively in all subdirs + * @return + */ + public static boolean areDirectoryEquals(Path dir, Path dir2, String[] extensions, boolean recursive) { + + Map filesDir1 = FileUtils.listFiles(new File(dir.toUri()), extensions, recursive) + .stream().collect(Collectors.toMap(f -> f.getName(), f -> f)); + Map filesDir2 = FileUtils.listFiles(new File(dir2.toUri()), extensions, recursive) + .stream().collect(Collectors.toMap(f -> f.getName(), f -> f)); + + if (filesDir1.size() != filesDir2.size()) + return false; + + return filesDir1.entrySet().stream().allMatch(e -> { + File fileDir2 = filesDir2.get(e.getKey()); + if (fileDir2 == null) { + return false; + } + try { + byte[] otherBytes = Files.readAllBytes(e.getValue().toPath()); + byte[] thisBytes = Files.readAllBytes(fileDir2.toPath()); + + return (Arrays.equals(otherBytes, thisBytes)); + } catch (IOException ex) { + return false; + } + }); + } +} diff --git a/search-services/alfresco-search/src/main/java/org/alfresco/solr/utils/Utils.java b/search-services/alfresco-search/src/main/java/org/alfresco/solr/utils/Utils.java new file mode 100644 index 000000000..3b586ca11 --- /dev/null +++ b/search-services/alfresco-search/src/main/java/org/alfresco/solr/utils/Utils.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2005-2019 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.solr.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +public abstract class Utils +{ + private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class); + + /** + * Returns the same input collection if that is not null, otherwise a new empty collection. + * Provides a safe way for iterating over a returned collection (which could be null). + * + * @param values the collection. + * @param the collection type. + * @return the same input collection if that is not null, otherwise a new empty collection. + */ + public static Collection notNullOrEmpty(Collection values) + { + return values != null ? values : Collections.emptyList(); + } + + /** + * Converts the given input in an Integer, otherwise it returns null. + * + * @param value the numeric string. + * @return the corresponding Integer or null in case the input is NaN. + */ + public static Integer toIntOrNull(String value) + { + try + { + return Integer.valueOf(value); + } + catch(NumberFormatException nfe) + { + return null; + } + } + + /** + * Silently closes the given {@link Closeable} resource without raising any exception. + * This utility method is specifically useful when we have to close a resource in a lamba statement: since the + * close() method could throw an {@link IOException} the compiler requires an enclosing try / catch block which + * makes the code less readable. + * + *

+ *

+ * + * try { if (resource != null) resource.close } catch (IOException exception) { ... } + * + *

+ *
+ * + * In these contexts a call to this method reduces the amount of code needed: + * + *

+ *

+ * + * silentlyClose(resource); + * + *

+ * + * @param resource the {@link Closeable} resource we want to silently close. + */ + public static void silentyClose(Closeable resource) + { + try + { + if (resource != null) resource.close(); + } + catch(IOException ignore) + { + LOGGER.warn("Unable to properly close the resource instance {}. See the stacktrace below for further details.", resource, ignore); + } + } +} diff --git a/search-services/alfresco-search/src/main/resources/solr/instance/templates/rerank/conf/solrconfig.xml b/search-services/alfresco-search/src/main/resources/solr/instance/templates/rerank/conf/solrconfig.xml index 0f5529850..e89bf5876 100644 --- a/search-services/alfresco-search/src/main/resources/solr/instance/templates/rerank/conf/solrconfig.xml +++ b/search-services/alfresco-search/src/main/resources/solr/instance/templates/rerank/conf/solrconfig.xml @@ -1154,7 +1154,9 @@ https://wiki.apache.org/solr/SolrCloud/ --> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + diff --git a/search-services/alfresco-search/src/test/resources/test-files/master/conf/solrconfig.snippet.randomindexconfig.xml b/search-services/alfresco-search/src/test/resources/test-files/master/conf/solrconfig.snippet.randomindexconfig.xml new file mode 100644 index 000000000..7514aa478 --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/master/conf/solrconfig.snippet.randomindexconfig.xml @@ -0,0 +1,47 @@ + + + + + + + + + ${useCompoundFile:false} + + ${solr.tests.maxBufferedDocs} + ${solr.tests.maxIndexingThreads} + ${solr.tests.ramBufferSizeMB} + + + + 1000 + 10000 + + + ${solr.tests.lockType:single} + diff --git a/search-services/alfresco-search/src/test/resources/test-files/master/conf/solrconfig.xml b/search-services/alfresco-search/src/test/resources/test-files/master/conf/solrconfig.xml new file mode 100644 index 000000000..c05053d6e --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/master/conf/solrconfig.xml @@ -0,0 +1,568 @@ + + + + + + + + + + + + ${solr.data.dir:} + + + + 1000000 + 2000000 + 3000000 + 4000000 + + + + + ${tests.luceneMatchVersion:LUCENE_CURRENT} + + + + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + true + + + + + + 10 + + + + + + + + + + + + + + + + + + + + + commit + schema.xml + + + + + + + + + true + + + + + true + + + + + + dismax + *:* + 0.01 + + text^0.5 features_t^1.0 subject^1.4 title_stemmed^2.0 + + + text^0.2 features_t^1.1 subject^1.4 title_stemmed^2.0 title^1.5 + + + ord(weight)^0.5 recip(rord(iind),1,1000,1000)^0.3 + + + 3<-1 5<-2 6<90% + + 100 + + + + + + + 4 + true + text,name,subject,title,whitetok + + + + + + + 4 + true + text,name,subject,title,whitetok + + + + + + + + + + fingerprint + + + + + + afts + false + false + 5 + 2 + 5 + true + true + 5 + 3 + mltext@m___t@{http://www.alfresco.org/model/content/1.0}title + id + content@s___t@{http://www.alfresco.org/model/content/1.0}content + true + false + + + setLocale + rewriteFacetParameters + query + facet + facet_module + mlt + highlight + stats + debug + clearLocale + rewriteFacetCounts + spellcheck + spellcheckbackcompat + setProcessedDenies + + + + + + + + explicit + 10 + suggest + + + + + setLocale + query + facet + mlt + highlight + stats + debug + clearLocale + + + + + + + + cmis + + + query + facet + mlt + highlight + stats + debug + + + + + + + + + termsComp + + + + + + + + + + + + + + + tvComponent + + + + + + + + + + + + 100 + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + 10 + .,!? + + + + + + + WORD + + + en + US + + + + + + + + + + max-age=30, public + + + + + + + explicit + true + + + + + solr + solrconfig.xml schema.xml admin-extra.html + + + + + + + + + + + + + + + + + + conf/mime_types.csv + + + + 1 + 10 + + + + + + + + text_shingle + + + + + + default + suggest + solr.DirectSolrSpellChecker + + internal + + 0.5 + + 2 + + 1 + + 5 + + 4 + + 0.01 + + + + + + wordbreak + suggest + solr.WordBreakSolrSpellChecker + true + true + 10 + 5 + + + + + + + + + + + + + + + diff --git a/search-services/alfresco-search/src/test/resources/test-files/master/conf/solrcore.properties b/search-services/alfresco-search/src/test/resources/test-files/master/conf/solrcore.properties new file mode 100644 index 000000000..aa07843d0 --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/master/conf/solrcore.properties @@ -0,0 +1,154 @@ +# +# solrcore.properties - used in solrconfig.xml +# + +enable.alfresco.tracking=true + +# +#These are replaced by the admin handler +# +#data.dir.root=DATA_DIR +#data.dir.store=workspace/SpacesStore +#alfresco.stores=workspace://SpacesStore + +# +# Properties loaded during alfresco tracking +# + +alfresco.host=localhost +alfresco.port=8080 +alfresco.port.ssl=8443 +alfresco.baseUrl=/alfresco +alfresco.cron=0/2 * * * * ? * + +#alfresco.index.transformContent=false +#alfresco.ignore.datatype.1=d:content +alfresco.lag=1000 +alfresco.hole.retention=3600000 +# alfresco.hole.check.after is not used yet +# It will reduce the hole checking load +alfresco.hole.check.after=300000 +alfresco.batch.count=1000 + +# encryption + +# none, https +alfresco.secureComms=none + +# ssl +alfresco.encryption.ssl.keystore.type=JCEKS +alfresco.encryption.ssl.keystore.provider= +alfresco.encryption.ssl.keystore.location=ssl.repo.client.keystore +alfresco.encryption.ssl.keystore.passwordFileLocation=ssl-keystore-passwords.properties +alfresco.encryption.ssl.truststore.type=JCEKS +alfresco.encryption.ssl.truststore.provider= +alfresco.encryption.ssl.truststore.location=ssl.repo.client.truststore +alfresco.encryption.ssl.truststore.passwordFileLocation=ssl-truststore-passwords.properties + +# Tracking + +alfresco.corePoolSize=1 +alfresco.maximumPoolSize=-1 +alfresco.keepAliveTime=120 +alfresco.threadPriority=5 +alfresco.threadDaemon=true +alfresco.workQueueSize=-1 + +# HTTP Client + +alfresco.maxTotalConnections=200 +alfresco.maxHostConnections=200 +alfresco.socketTimeout=360000 + +# SOLR caching + +solr.filterCache.size=256 +solr.filterCache.initialSize=128 +solr.queryResultCache.size=1024 +solr.queryResultCache.initialSize=1024 +solr.documentCache.size=1024 +solr.documentCache.initialSize=1024 +solr.queryResultMaxDocsCached=2048 + +solr.authorityCache.size=128 +solr.authorityCache.initialSize=64 +solr.pathCache.size=256 +solr.pathCache.initialSize=128 + +solr.ownerCache.size=128 +solr.ownerCache.initialSize=64 + +solr.readerCache.size=128 +solr.readerCache.initialSize=64 + +solr.deniedCache.size=128 +solr.deniedCache.initialSize=64 + +# SOLR + +solr.maxBooleanClauses=10000 + +# Batch fetch + +alfresco.transactionDocsBatchSize=100 +alfresco.nodeBatchSize=10 +alfresco.changeSetAclsBatchSize=100 +alfresco.aclBatchSize=10 +alfresco.contentReadBatchSize=4000 +alfresco.contentUpdateBatchSize=1000 + +# Warming + +solr.filterCache.autowarmCount=32 +solr.authorityCache.autowarmCount=4 +solr.pathCache.autowarmCount=32 +solr.deniedCache.autowarmCount=0 +solr.readerCache.autowarmCount=0 +solr.ownerCache.autowarmCount=0 +solr.queryResultCache.autowarmCount=4 +solr.documentCache.autowarmCount=512 + +solr.queryResultWindowSize=512 + + +# +# TODO +# +# cross language support +# locale expansion +# logging check report .... +# +# + +alfresco.commitInterval=1000 +alfresco.newSearcherInterval=2000 + +alfresco.doPermissionChecks=true + + +# +# Metadata pulling control +# +alfresco.metadata.skipDescendantDocsForSpecificTypes=false +alfresco.metadata.ignore.datatype.0=cm:person +alfresco.metadata.ignore.datatype.1=app:configurations +alfresco.metadata.skipDescendantDocsForSpecificAspects=false +#alfresco.metadata.ignore.aspect.0= + + +# +# Suggestions +# +solr.suggester.enabled=false +# -1 to disable suggester build throttling +solr.suggester.minSecsBetweenBuilds=3600 + +# +# Limit the maximum text size of transformed content sent to the index - in bytes +# +alfresco.contentStreamLimit=10000000 + +#Sharding default values +shard.instance=1 +shard.count=0 +shard.method=DB_ID diff --git a/search-services/alfresco-search/src/test/resources/test-files/master/conf/stopwords.txt b/search-services/alfresco-search/src/test/resources/test-files/master/conf/stopwords.txt new file mode 100644 index 000000000..6f46b1bf4 --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/master/conf/stopwords.txt @@ -0,0 +1,50 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +a +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +s +such +t +that +the +their +then +there +these +they +this +to +was +will +with \ No newline at end of file diff --git a/search-services/alfresco-search/src/test/resources/test-files/master/conf/synonyms.txt b/search-services/alfresco-search/src/test/resources/test-files/master/conf/synonyms.txt new file mode 100644 index 000000000..74cea68ad --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/master/conf/synonyms.txt @@ -0,0 +1,35 @@ +# The ASF licenses this file to You 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. + +#----------------------------------------------------------------------- +#some test synonym mappings unlikely to appear in real input text +aaa => aaaa +bbb => bbbb1 bbbb2 +ccc => cccc1,cccc2 +a\=>a => b\=>b +a\,a => b\,b +fooaaa,baraaa,bazaaa + +# Some synonym groups specific to this example +GB,gib,gigabyte,gigabytes +MB,mib,megabyte,megabytes +Television, Televisions, TV, TVs +#notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming +#after us won't split it into two words. + +# Synonym mappings can be used for spelling correction too +pixima => pixma + +# Test synonyms +quick,fast,rapid,speedy +brown fox jumped,leaping reynard,springer +lazy,bone idle \ No newline at end of file diff --git a/search-services/alfresco-search/src/test/resources/test-files/slave/conf/README.md b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/README.md new file mode 100644 index 000000000..47c6e62d2 --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/README.md @@ -0,0 +1,14 @@ +The folder contains several Solr configuration files that are used within the test suite. +Note that the folder, although it has the structure that Solr expects (/conf), it is not a complete "core" +folder, because + +- some files are missing (e.g. schema.xml) +- some files are used only in specific tests (e.g. solrconfig-rerank.xml, schema-rerank.xml) + +During the build process, Maven creates a complete core definition under the test build output folder by merging the +configuration of the "Rerank" template (src/main/resources/templates/rerank) together with the content of this folder. + +Note that this folder is copied **after** the template, so duplicates will be overwritten. For example, both folders +have a *solrcore.properties* and a *solrconfig.xml*: the test execution will use are those in this folder (because they +will overwrite the same files in the rerank template). + diff --git a/search-services/alfresco-search/src/test/resources/test-files/slave/conf/schema-rerank.xml b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/schema-rerank.xml new file mode 100644 index 000000000..6cd4159df --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/schema-rerank.xml @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + diff --git a/search-services/alfresco-search/src/test/resources/test-files/slave/conf/solrconfig.snippet.randomindexconfig.xml b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/solrconfig.snippet.randomindexconfig.xml new file mode 100644 index 000000000..7514aa478 --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/solrconfig.snippet.randomindexconfig.xml @@ -0,0 +1,47 @@ + + + + + + + + + ${useCompoundFile:false} + + ${solr.tests.maxBufferedDocs} + ${solr.tests.maxIndexingThreads} + ${solr.tests.ramBufferSizeMB} + + + + 1000 + 10000 + + + ${solr.tests.lockType:single} + diff --git a/search-services/alfresco-search/src/test/resources/test-files/slave/conf/solrconfig.xml b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/solrconfig.xml new file mode 100644 index 000000000..8d92bca34 --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/solrconfig.xml @@ -0,0 +1,567 @@ + + + + + + + + + + + + ${solr.data.dir:} + + + + 1000000 + 2000000 + 3000000 + 4000000 + + + + + ${tests.luceneMatchVersion:LUCENE_CURRENT} + + + + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + true + + + + + + 10 + + + + + + + + + + + + + + + + + + + + + {masterURL} + 00:00:02 + + + + + + + + true + + + + + true + + + + + + dismax + *:* + 0.01 + + text^0.5 features_t^1.0 subject^1.4 title_stemmed^2.0 + + + text^0.2 features_t^1.1 subject^1.4 title_stemmed^2.0 title^1.5 + + + ord(weight)^0.5 recip(rord(iind),1,1000,1000)^0.3 + + + 3<-1 5<-2 6<90% + + 100 + + + + + + + 4 + true + text,name,subject,title,whitetok + + + + + + + 4 + true + text,name,subject,title,whitetok + + + + + + + + + + fingerprint + + + + + + afts + false + false + 5 + 2 + 5 + true + true + 5 + 3 + mltext@m___t@{http://www.alfresco.org/model/content/1.0}title + id + content@s___t@{http://www.alfresco.org/model/content/1.0}content + true + false + + + setLocale + rewriteFacetParameters + query + facet + facet_module + mlt + highlight + stats + debug + clearLocale + rewriteFacetCounts + spellcheck + spellcheckbackcompat + setProcessedDenies + + + + + + + + explicit + 10 + suggest + + + + + setLocale + query + facet + mlt + highlight + stats + debug + clearLocale + + + + + + + + cmis + + + query + facet + mlt + highlight + stats + debug + + + + + + + + + termsComp + + + + + + + + + + + + + + + tvComponent + + + + + + + + + + + + 100 + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + 10 + .,!? + + + + + + + WORD + + + en + US + + + + + + + + + + max-age=30, public + + + + + + + explicit + true + + + + + solr + solrconfig.xml schema.xml admin-extra.html + + + + + + + + + + + + + + + + + + conf/mime_types.csv + + + + 1 + 10 + + + + + + + + text_shingle + + + + + + default + suggest + solr.DirectSolrSpellChecker + + internal + + 0.5 + + 2 + + 1 + + 5 + + 4 + + 0.01 + + + + + + wordbreak + suggest + solr.WordBreakSolrSpellChecker + true + true + 10 + 5 + + + + + + + + + + + + + + + diff --git a/search-services/alfresco-search/src/test/resources/test-files/slave/conf/solrcore.properties b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/solrcore.properties new file mode 100644 index 000000000..a46ace7f4 --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/solrcore.properties @@ -0,0 +1,154 @@ +# +# solrcore.properties - used in solrconfig.xml +# + +enable.alfresco.tracking=false + +# +#These are replaced by the admin handler +# +#data.dir.root=DATA_DIR +#data.dir.store=workspace/SpacesStore +#alfresco.stores=workspace://SpacesStore + +# +# Properties loaded during alfresco tracking +# + +alfresco.host=localhost +alfresco.port=8080 +alfresco.port.ssl=8443 +alfresco.baseUrl=/alfresco +alfresco.cron=0/2 * * * * ? * + +#alfresco.index.transformContent=false +#alfresco.ignore.datatype.1=d:content +alfresco.lag=1000 +alfresco.hole.retention=3600000 +# alfresco.hole.check.after is not used yet +# It will reduce the hole checking load +alfresco.hole.check.after=300000 +alfresco.batch.count=1000 + +# encryption + +# none, https +alfresco.secureComms=none + +# ssl +alfresco.encryption.ssl.keystore.type=JCEKS +alfresco.encryption.ssl.keystore.provider= +alfresco.encryption.ssl.keystore.location=ssl.repo.client.keystore +alfresco.encryption.ssl.keystore.passwordFileLocation=ssl-keystore-passwords.properties +alfresco.encryption.ssl.truststore.type=JCEKS +alfresco.encryption.ssl.truststore.provider= +alfresco.encryption.ssl.truststore.location=ssl.repo.client.truststore +alfresco.encryption.ssl.truststore.passwordFileLocation=ssl-truststore-passwords.properties + +# Tracking + +alfresco.corePoolSize=1 +alfresco.maximumPoolSize=-1 +alfresco.keepAliveTime=120 +alfresco.threadPriority=5 +alfresco.threadDaemon=true +alfresco.workQueueSize=-1 + +# HTTP Client + +alfresco.maxTotalConnections=200 +alfresco.maxHostConnections=200 +alfresco.socketTimeout=360000 + +# SOLR caching + +solr.filterCache.size=256 +solr.filterCache.initialSize=128 +solr.queryResultCache.size=1024 +solr.queryResultCache.initialSize=1024 +solr.documentCache.size=1024 +solr.documentCache.initialSize=1024 +solr.queryResultMaxDocsCached=2048 + +solr.authorityCache.size=128 +solr.authorityCache.initialSize=64 +solr.pathCache.size=256 +solr.pathCache.initialSize=128 + +solr.ownerCache.size=128 +solr.ownerCache.initialSize=64 + +solr.readerCache.size=128 +solr.readerCache.initialSize=64 + +solr.deniedCache.size=128 +solr.deniedCache.initialSize=64 + +# SOLR + +solr.maxBooleanClauses=10000 + +# Batch fetch + +alfresco.transactionDocsBatchSize=100 +alfresco.nodeBatchSize=10 +alfresco.changeSetAclsBatchSize=100 +alfresco.aclBatchSize=10 +alfresco.contentReadBatchSize=4000 +alfresco.contentUpdateBatchSize=1000 + +# Warming + +solr.filterCache.autowarmCount=32 +solr.authorityCache.autowarmCount=4 +solr.pathCache.autowarmCount=32 +solr.deniedCache.autowarmCount=0 +solr.readerCache.autowarmCount=0 +solr.ownerCache.autowarmCount=0 +solr.queryResultCache.autowarmCount=4 +solr.documentCache.autowarmCount=512 + +solr.queryResultWindowSize=512 + + +# +# TODO +# +# cross language support +# locale expansion +# logging check report .... +# +# + +alfresco.commitInterval=1000 +alfresco.newSearcherInterval=2000 + +alfresco.doPermissionChecks=true + + +# +# Metadata pulling control +# +alfresco.metadata.skipDescendantDocsForSpecificTypes=false +alfresco.metadata.ignore.datatype.0=cm:person +alfresco.metadata.ignore.datatype.1=app:configurations +alfresco.metadata.skipDescendantDocsForSpecificAspects=false +#alfresco.metadata.ignore.aspect.0= + + +# +# Suggestions +# +solr.suggester.enabled=false +# -1 to disable suggester build throttling +solr.suggester.minSecsBetweenBuilds=3600 + +# +# Limit the maximum text size of transformed content sent to the index - in bytes +# +alfresco.contentStreamLimit=10000000 + +#Sharding default values +shard.instance=1 +shard.count=0 +shard.method=DB_ID diff --git a/search-services/alfresco-search/src/test/resources/test-files/slave/conf/stopwords.txt b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/stopwords.txt new file mode 100644 index 000000000..6f46b1bf4 --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/stopwords.txt @@ -0,0 +1,50 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +a +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +s +such +t +that +the +their +then +there +these +they +this +to +was +will +with \ No newline at end of file diff --git a/search-services/alfresco-search/src/test/resources/test-files/slave/conf/synonyms.txt b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/synonyms.txt new file mode 100644 index 000000000..74cea68ad --- /dev/null +++ b/search-services/alfresco-search/src/test/resources/test-files/slave/conf/synonyms.txt @@ -0,0 +1,35 @@ +# The ASF licenses this file to You 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. + +#----------------------------------------------------------------------- +#some test synonym mappings unlikely to appear in real input text +aaa => aaaa +bbb => bbbb1 bbbb2 +ccc => cccc1,cccc2 +a\=>a => b\=>b +a\,a => b\,b +fooaaa,baraaa,bazaaa + +# Some synonym groups specific to this example +GB,gib,gigabyte,gigabytes +MB,mib,megabyte,megabytes +Television, Televisions, TV, TVs +#notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming +#after us won't split it into two words. + +# Synonym mappings can be used for spelling correction too +pixima => pixma + +# Test synonyms +quick,fast,rapid,speedy +brown fox jumped,leaping reynard,springer +lazy,bone idle \ No newline at end of file diff --git a/search-services/alfresco-search/src/test/resources/test-files/solrconfig_empty_replication_handler.xml b/search-services/alfresco-search/src/test/resources/test-files/solrconfig_empty_replication_handler.xml index e05ab0538..1c830555f 100644 --- a/search-services/alfresco-search/src/test/resources/test-files/solrconfig_empty_replication_handler.xml +++ b/search-services/alfresco-search/src/test/resources/test-files/solrconfig_empty_replication_handler.xml @@ -15,7 +15,7 @@ - + commit schema.xml diff --git a/search-services/alfresco-search/src/test/resources/test-files/solrconfig_master_disabled_replication_handler.xml b/search-services/alfresco-search/src/test/resources/test-files/solrconfig_master_disabled_replication_handler.xml index 0ed005b25..682674962 100644 --- a/search-services/alfresco-search/src/test/resources/test-files/solrconfig_master_disabled_replication_handler.xml +++ b/search-services/alfresco-search/src/test/resources/test-files/solrconfig_master_disabled_replication_handler.xml @@ -15,7 +15,7 @@ - + false commit diff --git a/search-services/alfresco-search/src/test/resources/test-files/solrconfig_master_replication_handler.xml b/search-services/alfresco-search/src/test/resources/test-files/solrconfig_master_replication_handler.xml index e05ab0538..1c830555f 100644 --- a/search-services/alfresco-search/src/test/resources/test-files/solrconfig_master_replication_handler.xml +++ b/search-services/alfresco-search/src/test/resources/test-files/solrconfig_master_replication_handler.xml @@ -15,7 +15,7 @@ - + commit schema.xml diff --git a/search-services/alfresco-solrclient-lib/pom.xml b/search-services/alfresco-solrclient-lib/pom.xml index b5a64bd65..0d99286ac 100644 --- a/search-services/alfresco-solrclient-lib/pom.xml +++ b/search-services/alfresco-solrclient-lib/pom.xml @@ -22,8 +22,8 @@ - 8.53 - 2.10.0 + 8.72 + 2.10.1 @@ -67,13 +67,13 @@ org.mockito mockito-core - 3.1.0 + 3.2.4 test org.slf4j slf4j-log4j12 - 1.7.29 + 1.7.30 diff --git a/search-services/packaging/pom.xml b/search-services/packaging/pom.xml index 52b9fb06b..49328b95a 100644 --- a/search-services/packaging/pom.xml +++ b/search-services/packaging/pom.xml @@ -111,7 +111,7 @@ ${project.version} libs ${project.build.directory}/solr-libs - **/jackson-dataformat-smile-*.jar,**/asm-3.3.1.jar,**/jackson-core-asl-*.jar,**/jackson-mapper-asl-*.jar,**/dom4j-1.6.1.jar,**/annotations-1.0.0.jar + **/jackson-dataformat-smile-*.jar,**/asm-3.3.1.jar,**/jackson-core-asl-*.jar,**/jackson-mapper-asl-*.jar,**/dom4j-1.6.1.jar,**/annotations-1.0.0.jar,**/woodstox-core-asl-4.4.1.jar @@ -157,7 +157,9 @@ + + diff --git a/search-services/packaging/src/docker/6.x/docker-compose.yml b/search-services/packaging/src/docker/6.x/docker-compose.yml index 1174827d3..ca4268e79 100644 --- a/search-services/packaging/src/docker/6.x/docker-compose.yml +++ b/search-services/packaging/src/docker/6.x/docker-compose.yml @@ -37,6 +37,10 @@ services: search: image: quay.io/alfresco/search-services:${SEARCH_TAG} environment: + #Replication properties + - REPLICATION_TYPE=master + #- REPLICATION_AFTER=commit,startup- SOLR_ALFRESCO_HOST=alfresco + #- REPLICATION_CONFIG_FILES=schema.xml,stopwords.txt- SOLR_ALFRESCO_PORT=8080 #Solr needs to know how to register itself with Alfresco - SOLR_ALFRESCO_HOST=alfresco - SOLR_ALFRESCO_PORT=8080 @@ -51,6 +55,27 @@ services: - ENABLE_SPELLCHECK=${SEARCH_ENABLE_SPELLCHECK} ports: - 8083:8983 #Browser port + #search_slave: + # image: quay.io/alfresco/search-services:${SEARCH_TAG} + # environment: + # #Replication properties + # - REPLICATION_TYPE=slave + # - REPLICATION_MASTER_HOST=search + # - REPLICATION_MASTER_PORT=8983 + # #- REPLICATION_MASTER_PROTOCOL=http + # #- REPLICATION_CORE_NAME=alfresco + # #- REPLICATION_POLL_INTERVAL=00:00:60 + # #Solr needs to know how to register itself with Alfresco + # - SOLR_ALFRESCO_HOST=alfresco + # - SOLR_ALFRESCO_PORT=8080 + # #Alfresco needs to know how to call solr + # - SOLR_SOLR_HOST=search + # - SOLR_SOLR_PORT=8983 + # #Create the default alfresco and archive cores + # - SOLR_CREATE_ALFRESCO_DEFAULTS=alfresco,archive + # ports: + # - 8084:8983 #Browser port + activemq: image: alfresco/alfresco-activemq:5.15.6 ports: diff --git a/search-services/packaging/src/docker/Dockerfile b/search-services/packaging/src/docker/Dockerfile index 852223ecf..ea60bca76 100644 --- a/search-services/packaging/src/docker/Dockerfile +++ b/search-services/packaging/src/docker/Dockerfile @@ -1,6 +1,6 @@ # Alfresco Search Services ${project.version} Docker Image -FROM alfresco/alfresco-base-java:11.0.1-openjdk-centos-7-6784d76a7b81 +FROM alfresco/alfresco-base-java:11.0.1-openjdk-centos-7-7a6031154417 LABEL creator="Gethin James" maintainer="Alfresco Search Services Team" ENV DIST_DIR /opt/alfresco-search-services diff --git a/search-services/packaging/src/docker/search_config_setup.sh b/search-services/packaging/src/docker/search_config_setup.sh index 40ad069e1..fb7636649 100644 --- a/search-services/packaging/src/docker/search_config_setup.sh +++ b/search-services/packaging/src/docker/search_config_setup.sh @@ -1,5 +1,48 @@ #!/bin/bash set -e +# By default its going to deploy "Master" setup configuration with "REPLICATION_TYPE=master". +# Slave replica service can be enabled using "REPLICATION_TYPE=slave" environment value. + +SOLR_CONFIG_FILE=$PWD/solrhome/templates/rerank/conf/solrconfig.xml +if [[ $REPLICATION_TYPE == "master" ]]; then + + findStringMaster='/' + replaceStringMaster="\n\t \n" + if [[ $REPLICATION_AFTER == "" ]]; then + REPLICATION_AFTER=commit + fi + for i in $(echo $REPLICATION_AFTER | sed "s/,/ /g") + do + replaceStringMaster+="\t\t"$i"<\/str> \n" + done + if [[ ! -z "$REPLICATION_CONFIG_FILES" ]]; then + replaceStringMaster+="\t\t$REPLICATION_CONFIG_FILES<\/str> \n" + fi + replaceStringMaster+="\t<\/lst>" + sed -i "s/$findStringMaster/$findStringMaster$replaceStringMaster/g" $SOLR_CONFIG_FILE +fi +if [[ $REPLICATION_TYPE == "slave" ]]; then + if [[ $REPLICATION_MASTER_PROTOCOL == "" ]]; then + REPLICATION_MASTER_PROTOCOL=http + fi + if [[ $REPLICATION_MASTER_HOST == "" ]]; then + REPLICATION_MASTER_HOST=localhost + fi + if [[ $REPLICATION_MASTER_PORT == "" ]]; then + REPLICATION_MASTER_PORT=8083 + fi + if [[ $REPLICATION_CORE_NAME == "" ]]; then + REPLICATION_CORE_NAME=alfresco + fi + if [[ $REPLICATION_POLL_INTERVAL == "" ]]; then + REPLICATION_POLL_INTERVAL=00:00:30 + fi + sed -i 's//\ + \ + '$REPLICATION_MASTER_PROTOCOL':\/\/'$REPLICATION_MASTER_HOST':'$REPLICATION_MASTER_PORT'\/solr\/'$REPLICATION_CORE_NAME'<\/str>\ + '$REPLICATION_POLL_INTERVAL'<\/str>\ + <\/lst>/g' $SOLR_CONFIG_FILE +fi SOLR_IN_FILE=$PWD/solr.in.sh diff --git a/search-services/packaging/src/main/resources/licenses/3rd-party/Apache-like-XPP.txt b/search-services/packaging/src/main/resources/licenses/3rd-party/BSDlike-XPP.txt similarity index 89% rename from search-services/packaging/src/main/resources/licenses/3rd-party/Apache-like-XPP.txt rename to search-services/packaging/src/main/resources/licenses/3rd-party/BSDlike-XPP.txt index fb11965fa..e4553dcaf 100644 --- a/search-services/packaging/src/main/resources/licenses/3rd-party/Apache-like-XPP.txt +++ b/search-services/packaging/src/main/resources/licenses/3rd-party/BSDlike-XPP.txt @@ -1,58 +1,58 @@ -LICENSE FOR THE Extreme! Lab PullParser ------------------------------------------------------------------------- - -Copyright © 2002 The Trustees of Indiana University. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1) All redistributions of source code must retain the above - copyright notice, the list of authors in the original source - code, this list of conditions and the disclaimer listed in this - license; - -2) All redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the disclaimer - listed in this license in the documentation and/or other - materials provided with the distribution; - -3) Any documentation included with all redistributions must include - the following acknowledgement: - - "This product includes software developed by the Indiana - University Extreme! Lab. For further information please visit - http://www.extreme.indiana.edu/" - - Alternatively, this acknowledgment may appear in the software - itself, and wherever such third-party acknowledgments normally - appear. - -4) The name "Indiana Univeristy" and "Indiana Univeristy - Extreme! Lab" shall not be used to endorse or promote - products derived from this software without prior written - permission from Indiana University. For written permission, - please contact http://www.extreme.indiana.edu/. - -5) Products derived from this software may not use "Indiana - Univeristy" name nor may "Indiana Univeristy" appear in their name, - without prior written permission of the Indiana University. - -Indiana University provides no reassurances that the source code -provided does not infringe the patent or any other intellectual -property rights of any other entity. Indiana University disclaims any -liability to any recipient for claims brought by any other entity -based on infringement of intellectual property rights or otherwise. - -LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH -NO WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA -UNIVERSITY GIVES NO WARRANTIES AND MAKES NO REPRESENTATION THAT -SOFTWARE IS FREE OF INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR -OTHER PROPRIETARY RIGHTS.  INDIANA UNIVERSITY MAKES NO WARRANTIES THAT -SOFTWARE IS FREE FROM "BUGS", "VIRUSES", "TROJAN HORSES", "TRAP -DOORS", "WORMS", OR OTHER HARMFUL CODE. LICENSEE ASSUMES THE ENTIRE -RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR ASSOCIATED MATERIALS, -AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION GENERATED USING -SOFTWARE. - +LICENSE FOR THE Extreme! Lab +------------------------------------------------------------------------ + +Copyright © 2003 The Trustees of Indiana University. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1) All redistributions of source code must retain the above + copyright notice, the list of authors in the original source + code, this list of conditions and the disclaimer listed in this + license; + +2) All redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the disclaimer + listed in this license in the documentation and/or other + materials provided with the distribution; + +3) Any documentation included with all redistributions must include + the following acknowledgement: + + "This product includes software developed by the Indiana + University Extreme! Lab. For further information please visit + http://www.extreme.indiana.edu/" + + Alternatively, this acknowledgment may appear in the software + itself, and wherever such third-party acknowledgments normally + appear. + +4) The name "Indiana University" and "Indiana University + Extreme! Lab" shall not be used to endorse or promote + products derived from this software without prior written + permission from Indiana University. For written permission, + please contact http://www.extreme.indiana.edu/. + +5) Products derived from this software may not use "Indiana + University" name nor may "Indiana University" appear in their name, + without prior written permission of the Indiana University. + +Indiana University provides no reassurances that the source code +provided does not infringe the patent or any other intellectual +property rights of any other entity. Indiana University disclaims any +liability to any recipient for claims brought by any other entity +based on infringement of intellectual property rights or otherwise. + +LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH +NO WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA +UNIVERSITY GIVES NO WARRANTIES AND MAKES NO REPRESENTATION THAT +SOFTWARE IS FREE OF INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR +OTHER PROPRIETARY RIGHTS.  INDIANA UNIVERSITY MAKES NO WARRANTIES THAT +SOFTWARE IS FREE FROM "BUGS", "VIRUSES", "TROJAN HORSES", "TRAP +DOORS", "WORMS", OR OTHER HARMFUL CODE. LICENSEE ASSUMES THE ENTIRE +RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR ASSOCIATED MATERIALS, +AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION GENERATED USING +SOFTWARE. + diff --git a/search-services/packaging/src/main/resources/licenses/notice.txt b/search-services/packaging/src/main/resources/licenses/notice.txt index 12e46175c..9932d0653 100644 --- a/search-services/packaging/src/main/resources/licenses/notice.txt +++ b/search-services/packaging/src/main/resources/licenses/notice.txt @@ -22,8 +22,8 @@ antlr-3.5.2.jar http://www.antlr.org/ jaxen-1.2.0.jar http://www.cafeconleche.org/jaxen/ -=== Apache variant License === -xpp3-1.1.3_8.jar http://www.extreme.indiana.edu/xgws/xsoap/xpp/ +=== BSD variant License === +xpp3-1.1.4c.jar http://www.extreme.indiana.edu/dist/java-repository/xpp3/licenses/LICENSE.txt === JSON === @@ -32,43 +32,42 @@ json-20160212.jar http://code.google.com/p/json-simple/ === Apache 2.0 === xml-resolver-1.2.jar https://github.com/FasterXML/jackson -neethi-3.0.3.jar http://ws.apache.org/commons/neethi/ +neethi-3.1.1.jar http://ws.apache.org/commons/neethi/ commons-logging-1.2.jar http://jakarta.apache.org/commons/ commons-lang3-3.9.jar http://jakarta.apache.org/commons/ mybatis-3.3.0.jar http://www.mybatis.org/ chemistry-opencmis-commons-impl-1.1.0.jar http://chemistry.apache.org/ chemistry-opencmis-commons-api-1.1.0.jar http://chemistry.apache.org/ -xmlschema-core-2.2.1.jar http://ws.apache.org/commons/XmlSchema/ +xmlschema-core-2.2.3.jar http://ws.apache.org/commons/XmlSchema/ HikariCP-java7-2.4.13.jar https://github.com/brettwooldridge/HikariCP -cxf-core-3.0.12.jar https://cxf.apache.org/ -cxf-rt-bindings-soap-3.0.12.jar https://cxf.apache.org/ -cxf-rt-bindings-xml-3.0.12.jar https://cxf.apache.org/ -cxf-rt-databinding-jaxb-3.0.12.jar https://cxf.apache.org/ -cxf-rt-frontend-jaxws-3.0.12.jar https://cxf.apache.org/ -cxf-rt-frontend-simple-3.0.12.jar https://cxf.apache.org/ -cxf-rt-transports-http-3.0.12.jar https://cxf.apache.org/ -cxf-rt-ws-addr-3.0.12.jar https://cxf.apache.org/ -cxf-rt-ws-policy-3.0.12.jar https://cxf.apache.org/ -cxf-rt-wsdl-3.0.12.jar https://cxf.apache.org/ +cxf-core-3.2.5.jar https://cxf.apache.org/ +cxf-rt-bindings-soap-3.2.5.jar https://cxf.apache.org/ +cxf-rt-bindings-xml-3.2.5.jar https://cxf.apache.org/ +cxf-rt-databinding-jaxb-3.2.5.jar https://cxf.apache.org/ +cxf-rt-frontend-jaxws-3.2.5.jar https://cxf.apache.org/ +cxf-rt-frontend-simple-3.2.5.jar https://cxf.apache.org/ +cxf-rt-transports-http-3.2.5.jar https://cxf.apache.org/ +cxf-rt-ws-addr-3.2.5.jar https://cxf.apache.org/ +cxf-rt-ws-policy-3.2.5.jar https://cxf.apache.org/ +cxf-rt-wsdl-3.2.5.jar https://cxf.apache.org/ mybatis-spring-1.2.5.jar http://www.mybatis.org/ chemistry-opencmis-server-support-1.0.0.jar http://chemistry.apache.org/ chemistry-opencmis-server-bindings-1.0.0.jar http://chemistry.apache.org/ -quartz-2.3.1.jar http://quartz-scheduler.org/ -jackson-core-2.10.0.jar https://github.com/FasterXML/jackson -jackson-annotations-2.10.0.jar https://github.com/FasterXML/jackson -jackson-databind-2.10.0.jar https://github.com/FasterXML/jackson +quartz-2.3.2.jar http://quartz-scheduler.org/ +jackson-core-2.10.1.jar https://github.com/FasterXML/jackson +jackson-annotations-2.10.1.jar https://github.com/FasterXML/jackson +jackson-databind-2.10.1.jar https://github.com/FasterXML/jackson commons-httpclient-3.1-HTTPCLIENT-1265.jar http://jakarta.apache.org/commons/ -spring-aop-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ -spring-beans-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ -spring-context-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ -spring-context-support-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ -spring-core-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ -spring-expression-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ -spring-jcl-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ -spring-jdbc-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ -spring-orm-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ -spring-tx-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ -spring-web-5.1.8.RELEASE.jar http://projects.spring.io/spring-framework/ +spring-aop-5.2.2.RELEASE.jar http://projects.spring.io/spring-framework/ +spring-beans-5.2.2.RELEASE.jar http://projects.spring.io/spring-framework/ +spring-context-5.2.2.RELEASE.jar http://projects.spring.io/spring-framework/ +spring-context-support-5.2.2.RELEASE.jar http://projects.spring.io/spring-framework/ +spring-core-5.2.2.RELEASE.jar http://projects.spring.io/spring-framework/ +spring-expression-5.2.2.RELEASE.jar http://projects.spring.io/spring-framework/ +spring-jdbc-5.2.2.RELEASE.jar http://projects.spring.io/spring-framework/ +spring-orm-5.2.2.RELEASE.jar http://projects.spring.io/spring-framework/ +spring-tx-5.2.2.RELEASE.jar http://projects.spring.io/spring-framework/ +spring-web-5.2.2.RELEASE.jar http://projects.spring.io/spring-framework/ xercesImpl-2.12.0-alfresco-patched-20191004.jar http://xerces.apache.org/xerces2-j guessencoding-1.4.jar http://docs.codehaus.org/display/GUESSENC/ xml-apis-1.4.01.jar https://github.com/FasterXML/jackson @@ -87,6 +86,7 @@ jetty-servlets-9.3.27.v20190418.jar https://www.eclipse.org/jetty/licenses.html jetty-util-9.3.27.v20190418.jar https://www.eclipse.org/jetty/licenses.html jetty-webapp-9.3.27.v20190418.jar https://www.eclipse.org/jetty/licenses.html jetty-xml-9.3.27.v20190418.jar https://www.eclipse.org/jetty/licenses.html +woodstox-core-5.0.3.jar https://github.com/FasterXML/woodstox === CDDL 1.0 === @@ -115,8 +115,8 @@ asm-commons-5.1.jar aspectjrt-1.8.0.jar attributes-binder-1.3.1.jar avatica-core-1.9.0.jar -bcmail-jdk15-1.45.jar -bcprov-jdk15-1.45.jar +bcmail-jdk15on-1.47.jar +bcprov-jdk15on-1.47.jar boilerpipe-1.1.0.jar caffeine-2.4.0.jar calcite-core-1.11.0.jar @@ -131,7 +131,7 @@ commons-compiler-2.7.6.jar commons-compress-1.14.jar commons-configuration-1.6.jar commons-exec-1.3.jar -commons-fileupload-1.3.2.jar +commons-fileupload-1.3.3.jar commons-io-2.5.jar commons-lang-2.6.jar commons-math3-3.4.1.jar @@ -168,28 +168,28 @@ jul-to-slf4j-1.7.7.jar juniversalchardet-1.0.3.jar langdetect-1.1-20120112.jar log4j-1.2.17.jar -lucene-analyzers-common-6.6.5-patched.1.jar -lucene-analyzers-icu-6.6.5-patched.1.jar -lucene-analyzers-kuromoji-6.6.5-patched.1.jar -lucene-analyzers-morfologik-6.6.5-patched.1.jar -lucene-analyzers-phonetic-6.6.5-patched.1.jar -lucene-analyzers-smartcn-6.6.5-patched.1.jar -lucene-analyzers-stempel-6.6.5-patched.1.jar -lucene-backward-codecs-6.6.5-patched.1.jar -lucene-classification-6.6.5-patched.1.jar -lucene-codecs-6.6.5-patched.1.jar -lucene-core-6.6.5-patched.1.jar -lucene-expressions-6.6.5-patched.1.jar -lucene-grouping-6.6.5-patched.1.jar -lucene-highlighter-6.6.5-patched.1.jar -lucene-join-6.6.5-patched.1.jar -lucene-memory-6.6.5-patched.1.jar -lucene-misc-6.6.5-patched.1.jar -lucene-queries-6.6.5-patched.1.jar -lucene-queryparser-6.6.5-patched.1.jar -lucene-sandbox-6.6.5-patched.1.jar -lucene-spatial-extras-6.6.5-patched.1.jar -lucene-suggest-6.6.5-patched.1.jar +lucene-analyzers-common-6.6.5-patched.2.jar +lucene-analyzers-icu-6.6.5-patched.2.jar +lucene-analyzers-kuromoji-6.6.5-patched.2.jar +lucene-analyzers-morfologik-6.6.5-patched.2.jar +lucene-analyzers-phonetic-6.6.5-patched.2.jar +lucene-analyzers-smartcn-6.6.5-patched.2.jar +lucene-analyzers-stempel-6.6.5-patched.2.jar +lucene-backward-codecs-6.6.5-patched.2.jar +lucene-classification-6.6.5-patched.2.jar +lucene-codecs-6.6.5-patched.2.jar +lucene-core-6.6.5-patched.2.jar +lucene-expressions-6.6.5-patched.2.jar +lucene-grouping-6.6.5-patched.2.jar +lucene-highlighter-6.6.5-patched.2.jar +lucene-join-6.6.5-patched.2.jar +lucene-memory-6.6.5-patched.2.jar +lucene-misc-6.6.5-patched.2.jar +lucene-queries-6.6.5-patched.2.jar +lucene-queryparser-6.6.5-patched.2.jar +lucene-sandbox-6.6.5-patched.2.jar +lucene-spatial-extras-6.6.5-patched.2.jar +lucene-suggest-6.6.5-patched.2.jar metadata-extractor-2.9.1.jar metrics-core-3.2.2.jar metrics-ganglia-3.2.2.jar @@ -213,11 +213,11 @@ rome-1.5.1.jar simple-xml-2.7.1.jar slf4j-api-1.7.7.jar slf4j-log4j12-1.7.7.jar -solr-analysis-extras-6.6.5-patched.1.jar -solr-clustering-6.6.5-patched.1.jar -solr-core-6.6.5-patched.1.jar -solr-langid-6.6.5-patched.1.jar -solr-solrj-6.6.5-patched.1.jar +solr-analysis-extras-6.6.5-patched.2.jar +solr-clustering-6.6.5-patched.2.jar +solr-core-6.6.5-patched.2.jar +solr-langid-6.6.5-patched.2.jar +solr-solrj-6.6.5-patched.2.jar spatial4j-0.6.jar start.jar stax2-api-3.1.4.jar @@ -229,7 +229,6 @@ tika-parsers-1.16.jar tika-xmp-1.16.jar vorbis-java-core-0.8.jar vorbis-java-tika-0.8.jar -woodstox-core-asl-4.4.1.jar xmlbeans-2.6.0.jar xmpcore-5.1.2.jar xz-1.6.jar diff --git a/search-services/pom.xml b/search-services/pom.xml index b7d8355e4..2473ef45d 100644 --- a/search-services/pom.xml +++ b/search-services/pom.xml @@ -14,7 +14,8 @@ pom Alfresco Solr Search parent - 1.7.29 + 1.7.30 + 3.2.5 alfresco-solrclient-lib