diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexProviderService.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexProviderService.java index fbc3fc794f0..89e64b2693e 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexProviderService.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexProviderService.java @@ -25,6 +25,7 @@ import org.apache.jackrabbit.oak.plugins.index.elastic.index.ElasticIndexEditorProvider; import org.apache.jackrabbit.oak.plugins.index.elastic.index.ElasticRetryPolicy; import org.apache.jackrabbit.oak.plugins.index.elastic.query.ElasticIndexProvider; +import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexStatistics; import org.apache.jackrabbit.oak.plugins.index.elastic.query.inference.InferenceConfig; import org.apache.jackrabbit.oak.plugins.index.elastic.query.inference.InferenceConstants; import org.apache.jackrabbit.oak.plugins.index.elastic.query.inference.InferenceMBeanImpl; @@ -227,6 +228,9 @@ private void activate(BundleContext bundleContext, Config config) { oakRegs.add(whiteboard.register(FeatureToggle.class, new FeatureToggle(ElasticConnection.FT_OAK_12234, ElasticConnection.FT_OAK_12234_DISABLE), emptyMap())); + oakRegs.add(whiteboard.register(FeatureToggle.class, + new FeatureToggle(ElasticIndexStatistics.FT_OAK_12248, ElasticIndexStatistics.FT_OAK_12248_ENABLE), + emptyMap())); if (System.getProperty(QueryEngineSettings.OAK_INFERENCE_ENABLED) != null) { this.isInferenceEnabled = Boolean.parseBoolean(System.getProperty(QueryEngineSettings.OAK_INFERENCE_ENABLED)); } else { diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatistics.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatistics.java index bdeab1c898e..387b840277a 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatistics.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatistics.java @@ -22,7 +22,9 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; import co.elastic.clients.elasticsearch._types.query_dsl.Query; import org.apache.jackrabbit.oak.cache.api.CacheBuilder; @@ -33,6 +35,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import co.elastic.clients.elasticsearch._types.Bytes; import co.elastic.clients.elasticsearch.cat.indices.IndicesRecord; @@ -53,6 +57,17 @@ */ public class ElasticIndexStatistics implements IndexStatistics { + private static final Logger LOG = LoggerFactory.getLogger(ElasticIndexStatistics.class); + + public static final String FT_OAK_12248 = "FT_OAK-12248"; + /** + * When {@code true}, a 404 from Elasticsearch (alias not found) is treated as an expected + * empty-index condition: statistics return 0, the query returns an empty cursor, and INFO is + * logged instead of ERROR. Requires OAK-12249 lazy provisioning to be meaningful. + * Disabled by default. + */ + public static final AtomicBoolean FT_OAK_12248_ENABLE = new AtomicBoolean(false); + private static final String MAX_SIZE = "oak.elastic.statsMaxSize"; private static final Long MAX_SIZE_DEFAULT = 10000L; private static final String EXPIRE_SECONDS = "oak.elastic.statsExpireSeconds"; @@ -64,7 +79,6 @@ public class ElasticIndexStatistics implements IndexStatistics { private final ElasticIndexDefinition indexDefinition; private final LoadingCache countCache; private final LoadingCache statsCache; - ElasticIndexStatistics(@NotNull ElasticConnection elasticConnection, @NotNull ElasticIndexDefinition indexDefinition) { this(elasticConnection, indexDefinition, null, null); @@ -89,7 +103,7 @@ public class ElasticIndexStatistics implements IndexStatistics { */ @Override public int numDocs() { - return countCache.get(new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias())); + return getOrRefetchDocCount(null, null); } /** @@ -98,10 +112,7 @@ public int numDocs() { */ @Override public int getDocCountFor(String field) { - String elasticField = ElasticIndexUtils.fieldName(field); - return countCache.get( - new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias(), elasticField, null) - ); + return getOrRefetchDocCount(ElasticIndexUtils.fieldName(field), null); } /** @@ -109,9 +120,11 @@ public int getDocCountFor(String field) { * {@code ElasticIndexDefinition}. */ public int getDocCountFor(Query query) { - return countCache.get( - new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias(), null, query) - ); + return getOrRefetchDocCount(null, query); + } + + private int getOrRefetchDocCount(@Nullable String field, @Nullable Query query) { + return countCache.get(new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias(), field, query)); } /** @@ -119,9 +132,7 @@ public int getDocCountFor(Query query) { * {@code ElasticIndexDefinition}. */ public long primaryStoreSize() { - return statsCache.get( - new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias()) - ).primaryStoreSize; + return getOrRefetchStats().primaryStoreSize; } /** @@ -129,18 +140,14 @@ public long primaryStoreSize() { * primary shards and replica shards. */ public long storeSize() { - return statsCache.get( - new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias()) - ).storeSize; + return getOrRefetchStats().storeSize; } /** * Returns the creation date for the remote index bound to the {@code ElasticIndexDefinition}. */ public long creationDate() { - return statsCache.get( - new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias()) - ).creationDate; + return getOrRefetchStats().creationDate; } /** @@ -148,9 +155,7 @@ public long creationDate() { * {@code ElasticIndexDefinition}. This document count includes hidden nested documents. */ public int luceneNumDocs() { - return statsCache.get( - new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias()) - ).luceneDocsCount; + return getOrRefetchStats().luceneDocsCount; } /** @@ -158,9 +163,11 @@ public int luceneNumDocs() { * {@code ElasticIndexDefinition}. This document count includes hidden nested documents. */ public int luceneNumDeletedDocs() { - return statsCache.get( - new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias()) - ).luceneDocsDeleted; + return getOrRefetchStats().luceneDocsDeleted; + } + + private StatsResponse getOrRefetchStats() { + return statsCache.get(new StatsRequestDescriptor(elasticConnection, indexDefinition.getIndexAlias())); } static LoadingCache setupCountCache(long maxSize, long expireSeconds, long refreshSeconds, @Nullable Clock clock) { @@ -195,7 +202,15 @@ static class CountCacheLoader implements CacheLoader { + assertTrue(exists(index)); + assertEquals(2, countDocuments(index)); + }); + return index; + } + + private void deleteAlias(Tree index) throws Exception { + esConnection.getClient().indices().delete(i -> i + .index(getElasticIndexDefinition(index).getIndexAlias() + "*")); + } + + @Test + public void queryOnMissingAlias_withToggleOff_failsWithTraversalError() throws Exception { + Tree index = provisionIndex(); + deleteAlias(index); + + List results = executeQuery(QUERY, SQL2); + + assertEquals(1, results.size()); + assertTrue("Expected traversal-fail error when toggle is off and alias is missing", + results.get(0).contains("Traversal")); + } + + @Test + public void queryOnMissingAlias_withToggleOn_returnsEmpty() throws Exception { + Tree index = provisionIndex(); + deleteAlias(index); + ElasticIndexStatistics.FT_OAK_12248_ENABLE.set(true); + + List results = executeQuery(QUERY, SQL2); + + assertEquals("Expected empty result set when alias is missing and toggle is on", + 0, results.size()); + } +} diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatisticsTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatisticsTest.java index 89491e72f65..d65f835dc9a 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatisticsTest.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexStatisticsTest.java @@ -17,6 +17,7 @@ package org.apache.jackrabbit.oak.plugins.index.elastic; import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; import co.elastic.clients.elasticsearch._types.query_dsl.Query; import co.elastic.clients.elasticsearch.core.CountRequest; import co.elastic.clients.elasticsearch.core.CountResponse; @@ -66,6 +67,7 @@ public void setUp() { @After public void releaseMocks() throws Exception { closeable.close(); + ElasticIndexStatistics.FT_OAK_12248_ENABLE.set(false); } @Test @@ -142,4 +144,45 @@ public void cachedStatistics() throws Exception { assertEquals(5000, indexStatistics.getDocCountFor(Query.of(qf -> qf.matchAll(mf -> mf.boost(100F))))); verify(elasticClientMock, times(5)).count(any(CountRequest.class)); } + + @Test + public void numDocsReturnsZeroOn404WithToggleEnabled() throws Exception { + ElasticIndexStatistics.FT_OAK_12248_ENABLE.set(true); + LoadingCache cache = + ElasticIndexStatistics.setupCountCache(100, 10 * 60, 60, null); + ElasticIndexStatistics stats = + new ElasticIndexStatistics(elasticConnectionMock, indexDefinitionMock, cache, null); + + ElasticsearchException notFound = mock(ElasticsearchException.class); + when(notFound.status()).thenReturn(404); + when(elasticClientMock.count(any(CountRequest.class))).thenThrow(notFound); + + assertEquals(0, stats.numDocs()); + verify(elasticClientMock).count(any(CountRequest.class)); + } + + @Test + public void numDocsRecoverAfterCacheExpiry() throws Exception { + ElasticIndexStatistics.FT_OAK_12248_ENABLE.set(true); + Clock.Virtual clock = new Clock.Virtual(); + LoadingCache cache = + ElasticIndexStatistics.setupCountCache(100, 10 * 60, 60, clock); + ElasticIndexStatistics stats = + new ElasticIndexStatistics(elasticConnectionMock, indexDefinitionMock, cache, null); + + ElasticsearchException notFound = mock(ElasticsearchException.class); + when(notFound.status()).thenReturn(404); + CountResponse countResponse = mock(CountResponse.class); + when(countResponse.count()).thenReturn(5L); + when(elasticClientMock.count(any(CountRequest.class))) + .thenThrow(notFound) + .thenReturn(countResponse); + + assertEquals(0, stats.numDocs()); + + clock.waitFor(Duration.ofMinutes(11)); + + assertEquals(5, stats.numDocs()); + verify(elasticClientMock, times(2)).count(any(CountRequest.class)); + } }