diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java index ad3fca090f9..b17ed2e09bd 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticIndexDefinition.java @@ -87,6 +87,13 @@ public class ElasticIndexDefinition extends IndexDefinition { */ public static final String PROP_INDEX_NAME_SEED = ":nameSeed"; + /** + * Hidden property written when a lazy reindex (OAK-12249) produced zero documents and left no + * ES index or alias. Signals the next incremental-write cycle to provision the index on demand. + * Cleared once provisioning completes. + */ + public static final String PROP_REQUIRES_PROVISIONING = ":requiresProvisioning"; + /** * Hidden property to store similarity tags */ @@ -268,6 +275,10 @@ public String getIndexAlias() { return indexAlias; } + public boolean requiresProvisioning() { + return getOptionalValue(getDefinitionNodeState(), PROP_REQUIRES_PROVISIONING, false); + } + public Map> getPropertiesByName() { return propertiesByName; } 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 89e64b2693e..977358cd012 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 @@ -231,6 +231,9 @@ private void activate(BundleContext bundleContext, Config config) { oakRegs.add(whiteboard.register(FeatureToggle.class, new FeatureToggle(ElasticIndexStatistics.FT_OAK_12248, ElasticIndexStatistics.FT_OAK_12248_ENABLE), emptyMap())); + oakRegs.add(whiteboard.register(FeatureToggle.class, + new FeatureToggle(ElasticIndexEditorProvider.FT_OAK_12249, ElasticIndexEditorProvider.FT_OAK_12249_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/index/ElasticIndexEditorContext.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexEditorContext.java index 4e7f68cf958..fedf7688338 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexEditorContext.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexEditorContext.java @@ -23,6 +23,7 @@ import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition; import org.apache.jackrabbit.oak.plugins.index.search.spi.editor.DocumentMaker; import org.apache.jackrabbit.oak.plugins.index.search.spi.editor.FulltextIndexEditorContext; +import org.apache.jackrabbit.oak.plugins.index.search.spi.editor.FulltextIndexWriter; import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.apache.jackrabbit.oak.spi.state.NodeState; import org.jetbrains.annotations.Nullable; @@ -50,8 +51,8 @@ public DocumentMaker newDocumentMaker(IndexDefinition.IndexingR } @Override - public ElasticIndexWriter getWriter() { - return (ElasticIndexWriter) super.getWriter(); + public FulltextIndexWriter getWriter() { + return super.getWriter(); } @Override diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexEditorProvider.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexEditorProvider.java index d116495c74c..afac843258c 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexEditorProvider.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexEditorProvider.java @@ -23,6 +23,7 @@ import org.apache.jackrabbit.oak.plugins.index.IndexingContext; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticConnection; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition; +import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexStatistics; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexTracker; import org.apache.jackrabbit.oak.plugins.index.search.ExtractedTextCache; import org.apache.jackrabbit.oak.plugins.index.search.spi.editor.FulltextIndexEditor; @@ -32,6 +33,8 @@ import org.apache.jackrabbit.oak.spi.state.NodeState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; @@ -41,6 +44,8 @@ public class ElasticIndexEditorProvider implements IndexEditorProvider { + private static final Logger LOG = LoggerFactory.getLogger(ElasticIndexEditorProvider.class); + private final ElasticIndexTracker indexTracker; private final ElasticConnection elasticConnection; private final ExtractedTextCache extractedTextCache; @@ -57,6 +62,32 @@ public class ElasticIndexEditorProvider implements IndexEditorProvider { */ public static final AtomicBoolean FT_OAK_12206_DISABLE = new AtomicBoolean(false); + public static final String FT_OAK_12249 = "FT_OAK-12249"; + /** + * When {@code true} AND {@link ElasticIndexStatistics#FT_OAK_12248_ENABLE} is also {@code true}, + * Elasticsearch index provisioning is deferred to the first {@code updateDocument()} or + * {@code deleteDocuments()} call. A reindex that produces no documents never creates an ES + * index or alias. + * + *

Requires {@code FT_OAK-12248} (graceful 404 handling) to be enabled first. Enabling + * lazy provisioning without graceful 404 handling would cause unhandled ES 404 errors on every + * query against an empty-reindexed index. {@link #isLazyProvisioningActive()} enforces this + * dependency at runtime. + * + *

Disabled by default. + */ + public static final AtomicBoolean FT_OAK_12249_ENABLE = new AtomicBoolean(false); + + /** + * Returns {@code true} when lazy provisioning is active. Requires both this toggle and + * {@link ElasticIndexStatistics#FT_OAK_12248_ENABLE} to be {@code true}. The combined check + * enforces the deployment order: graceful 404 handling (OAK-12248) must be on before lazy + * provisioning (OAK-12249) can take effect. + */ + public static boolean isLazyProvisioningActive() { + return FT_OAK_12249_ENABLE.get() && ElasticIndexStatistics.FT_OAK_12248_ENABLE.get(); + } + private final boolean OAK_INDEX_ELASTIC_WRITER_DISABLE = Boolean.getBoolean(OAK_INDEX_ELASTIC_WRITER_DISABLE_KEY); public ElasticIndexEditorProvider(@NotNull ElasticIndexTracker indexTracker, diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriter.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriter.java index 893dd792f7a..beb90f9f7cd 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriter.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriter.java @@ -62,46 +62,46 @@ class ElasticIndexWriter implements FulltextIndexWriter { private final ElasticConnection elasticConnection; private final ElasticIndexDefinition indexDefinition; private final ElasticBulkProcessorHandler bulkProcessorHandler; - private final boolean reindex; + private final boolean requiresProvisioning; private final String indexName; private final ElasticRetryPolicy retryPolicy; + private final NodeBuilder definitionBuilder; ElasticIndexWriter(@NotNull ElasticIndexTracker indexTracker, @NotNull ElasticConnection elasticConnection, @NotNull ElasticIndexDefinition indexDefinition, @NotNull NodeBuilder definitionBuilder, - boolean reindex, CommitInfo commitInfo, + boolean requiresProvisioning, CommitInfo commitInfo, ElasticBulkProcessorHandler bulkProcessorHandler, ElasticRetryPolicy retryPolicy) { this.indexTracker = indexTracker; this.elasticConnection = elasticConnection; this.indexDefinition = indexDefinition; - this.reindex = reindex; + this.requiresProvisioning = requiresProvisioning; this.bulkProcessorHandler = bulkProcessorHandler; this.retryPolicy = retryPolicy; + this.definitionBuilder = definitionBuilder; - // We don't use stored index definitions with elastic. Every time a new writer gets created we - // use the actual index name (based on the current seed) while reindexing, or the alias (pointing to the - // old index until the new one gets enabled) during incremental reindexing - if (this.reindex) { + if (requiresProvisioning) { + // Full provisioning: generate a seed-based backing index, create it in ES, and prepare + // for alias flip on close(). Applies to both a standard reindex and an incremental write + // arriving after a lazy reindex that produced zero documents (OAK-12249). try { //TODO we should observe changes under inference config path. InferenceConfig.reInitialize(); - // refresh inference config on any index reindex. long seed = indexDefinition.indexNameSeed == 0L ? UUID.randomUUID().getMostSignificantBits() : indexDefinition.indexNameSeed; - // merge gets called on node store later in the indexing flow definitionBuilder.setProperty(ElasticIndexDefinition.PROP_INDEX_NAME_SEED, seed); - // let's store the current mapping version in the index definition definitionBuilder.setProperty(ElasticIndexDefinition.PROP_INDEX_MAPPING_VERSION, ElasticIndexDefinition.MAPPING_VERSION.toString()); - + definitionBuilder.removeProperty(ElasticIndexDefinition.PROP_REQUIRES_PROVISIONING); indexName = ElasticIndexNameHelper. getRemoteIndexName(elasticConnection.getIndexPrefix(), indexDefinition.getIndexPath(), seed); - provisionIndex(); } catch (IOException e) { throw new IllegalStateException("Unable to provision index", e); } - } else indexName = indexDefinition.getIndexAlias(); + } else { + indexName = indexDefinition.getIndexAlias(); + } boolean waitForESAcknowledgement = true; PropertyState async = indexDefinition.getDefinitionNodeState().getProperty("async"); if (async != null) { @@ -132,14 +132,15 @@ class ElasticIndexWriter implements FulltextIndexWriter { @NotNull ElasticIndexDefinition indexDefinition, @NotNull ElasticBulkProcessorHandler bulkProcessorHandler, @NotNull ElasticRetryPolicy retryPolicy, - boolean reindex) { + boolean requiresProvisioning) { this.indexTracker = indexTracker; this.elasticConnection = elasticConnection; this.indexDefinition = indexDefinition; this.bulkProcessorHandler = bulkProcessorHandler; this.indexName = indexDefinition.getIndexAlias(); this.retryPolicy = retryPolicy; - this.reindex = reindex; + this.requiresProvisioning = requiresProvisioning; + this.definitionBuilder = null; } @Override @@ -155,7 +156,7 @@ public void updateDocument(String path, ElasticDocument doc) throws IOException AND InferenceIndexConfig is NOOP ) */ - if (reindex + if (requiresProvisioning || (!indexDefinition.isExternallyModifiable() && !InferenceConfig.getInstance().isInferenceEnabled() && (InferenceIndexConfig.NOOP.equals(InferenceConfig.getInstance().getInferenceIndexConfig(jcrIndexName))))) { @@ -190,8 +191,7 @@ public void deleteDocuments(String path) throws IOException { @Override public boolean close(long timestamp) throws IOException { boolean updateStatus = bulkProcessorHandler.flushIndex(indexName); - if (reindex) { - // if we are closing a writer in reindex mode, it means we need to open the new index for queries + if (requiresProvisioning) { this.enableIndex(); } if (updateStatus) { @@ -220,43 +220,47 @@ private void saveMetrics() { private void provisionIndex() throws IOException { final ElasticsearchIndicesClient esClient = elasticConnection.getClient().indices(); - // check if index already exists if (esClient.exists(i -> i.index(indexName)).value()) { LOG.info("Index {} already exists. Skip index provision", indexName); return; } + createIndex(indexName); + } + /** + * Builds a {@link CreateIndexRequest} for {@code backingIndexName} and submits it to + * Elasticsearch, with debug logging and idempotent handling of concurrent-creation races. + */ + private void createIndex(String backingIndexName) throws IOException { + final ElasticsearchIndicesClient esClient = elasticConnection.getClient().indices(); CreateIndexRequest request; try { - request = ElasticIndexHelper.createIndexRequest(indexName, indexDefinition); + request = ElasticIndexHelper.createIndexRequest(backingIndexName, indexDefinition); } catch (Exception e) { - LOG.error("Failed to create index {}: {}", indexName, e.toString()); + LOG.error("Failed to create index {}: {}", backingIndexName, e.toString()); throw e; } if (LOG.isDebugEnabled()) { int old = JsonpUtils.maxToStringLength(); try { - // temporarily increase the length, to avoid truncation JsonpUtils.maxToStringLength(1_000_000); LOG.debug("Creating Index with request {}", request); } finally { JsonpUtils.maxToStringLength(old); } } - // create the new index try { final CreateIndexResponse response = esClient.create(request); - LOG.info("Created index {}. Response acknowledged: {}", indexName, response.acknowledged()); - checkResponseAcknowledgement(response, "Create index call not acknowledged for index " + indexName); + LOG.info("Created index {}. Response acknowledged: {}", backingIndexName, response.acknowledged()); + checkResponseAcknowledgement(response, "Create index call not acknowledged for index " + backingIndexName); } catch (ElasticsearchException ese) { - // We already check index existence as first thing in this method, if we get here it means we have got into - // a conflict (eg: multiple cluster nodes provision concurrently). - // Elasticsearch does not have a CREATE IF NOT EXIST, need to inspect exception + // We already check index existence as first thing in provisionIndex(); if we get here it + // means a concurrent cluster node raced us. Elasticsearch has no CREATE IF NOT EXISTS: // https://github.com/elastic/elasticsearch/issues/19862 if (ese.status() == 400 && ese.getMessage().contains("resource_already_exists_exception")) { - LOG.warn("Index {} already exists. Ignoring error", indexName); + LOG.warn("Index {} already exists. Ignoring error", backingIndexName); } else { - LOG.warn("Failed to create index {}", indexName, ese); + LOG.warn("Failed to create index {}", backingIndexName, ese); StringBuilder sb = new StringBuilder(); int old = JsonpUtils.maxToStringLength(); try { diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriterFactory.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriterFactory.java index 59bb06c855c..9ef94cd9988 100644 --- a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriterFactory.java +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriterFactory.java @@ -20,6 +20,7 @@ import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexTracker; import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition; +import org.apache.jackrabbit.oak.plugins.index.search.spi.editor.FulltextIndexWriter; import org.apache.jackrabbit.oak.plugins.index.search.spi.editor.FulltextIndexWriterFactory; import org.apache.jackrabbit.oak.spi.commit.CommitInfo; import org.apache.jackrabbit.oak.spi.state.NodeBuilder; @@ -43,8 +44,8 @@ public ElasticIndexWriterFactory(@NotNull ElasticConnection elasticConnection, @ } @Override - public ElasticIndexWriter newInstance(IndexDefinition definition, NodeBuilder definitionBuilder, - CommitInfo commitInfo, boolean reindex) { + public FulltextIndexWriter newInstance(IndexDefinition definition, NodeBuilder definitionBuilder, + CommitInfo commitInfo, boolean reindex) { if (!(definition instanceof ElasticIndexDefinition)) { throw new IllegalArgumentException("IndexDefinition must be of type ElasticsearchIndexDefinition " + "instead of " + definition.getClass().getName()); @@ -52,6 +53,21 @@ public ElasticIndexWriter newInstance(IndexDefinition definition, NodeBuilder de ElasticIndexDefinition esDefinition = (ElasticIndexDefinition) definition; - return new ElasticIndexWriter(indexTracker, elasticConnection, esDefinition, definitionBuilder, reindex, commitInfo, bulkProcessorHandler, retryPolicy); + // requiresProvisioning=true for a standard reindex, or when a prior lazy reindex produced + // zero documents and set PROP_REQUIRES_PROVISIONING in the node store. + boolean requiresProvisioning = reindex || esDefinition.requiresProvisioning(); + + if (requiresProvisioning && ElasticIndexEditorProvider.isLazyProvisioningActive()) { + // OAK-12249: defer provisioning to the first write, whether this is a reindex or an + // incremental cycle after an empty lazy reindex. If no documents arrive the supplier is + // never called, PROP_REQUIRES_PROVISIONING is re-written, and the next cycle retries. + return new LazyElasticIndexWriter( + () -> new ElasticIndexWriter(indexTracker, elasticConnection, esDefinition, + definitionBuilder, true, commitInfo, bulkProcessorHandler, retryPolicy), + definitionBuilder); + } + + return new ElasticIndexWriter(indexTracker, elasticConnection, esDefinition, + definitionBuilder, requiresProvisioning, commitInfo, bulkProcessorHandler, retryPolicy); } } diff --git a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/LazyElasticIndexWriter.java b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/LazyElasticIndexWriter.java new file mode 100644 index 00000000000..1ba36388e3e --- /dev/null +++ b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/LazyElasticIndexWriter.java @@ -0,0 +1,79 @@ +/* + * 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. + */ +package org.apache.jackrabbit.oak.plugins.index.elastic.index; + +import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition; +import org.apache.jackrabbit.oak.plugins.index.search.spi.editor.FulltextIndexWriter; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.function.Supplier; + +/** + * A {@link FulltextIndexWriter} proxy that defers creation of the real {@link ElasticIndexWriter} + * to the first {@link #updateDocument} or {@link #deleteDocuments} call (OAK-12249). + * + *

If {@link #close} is called before any document is written — i.e. the reindex produced zero + * documents — the supplier is never invoked, so no Elasticsearch index or alias is created. + * Instead, {@link ElasticIndexDefinition#PROP_REQUIRES_PROVISIONING} is set on the definition + * node so the next incremental-write cycle provisions the index on demand. + * + *

The supplier is expected to create and provision the index as a side effect of construction + * (as {@link ElasticIndexWriter} does). Thread safety is not required: Oak calls each writer + * instance from a single thread. + */ +class LazyElasticIndexWriter implements FulltextIndexWriter { + private static final Logger LOG = LoggerFactory.getLogger(LazyElasticIndexWriter.class); + + private final Supplier writerSupplier; + private final NodeBuilder definitionBuilder; + private ElasticIndexWriter delegate; + + LazyElasticIndexWriter(Supplier writerSupplier, NodeBuilder definitionBuilder) { + this.writerSupplier = writerSupplier; + this.definitionBuilder = definitionBuilder; + } + + @Override + public void updateDocument(String path, ElasticDocument doc) throws IOException { + getOrCreate().updateDocument(path, doc); + } + + @Override + public void deleteDocuments(String path) throws IOException { + getOrCreate().deleteDocuments(path); + } + + @Override + public boolean close(long timestamp) throws IOException { + if (delegate == null) { + LOG.info("Reindex produced no documents — skipping ES index creation (OAK-12249)"); + definitionBuilder.setProperty(ElasticIndexDefinition.PROP_REQUIRES_PROVISIONING, true); + return false; + } + return delegate.close(timestamp); + } + + private ElasticIndexWriter getOrCreate() { + if (delegate == null) { + delegate = writerSupplier.get(); + } + return delegate; + } +} diff --git a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriterTest.java b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriterTest.java index e644f2a456f..030f7261f65 100644 --- a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriterTest.java +++ b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/index/ElasticIndexWriterTest.java @@ -16,15 +16,21 @@ */ package org.apache.jackrabbit.oak.plugins.index.elastic.index; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.DeleteByQueryRequest; +import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.core.DeleteByQueryRequest; import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse; import co.elastic.clients.util.ObjectBuilder; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticConnection; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexDefinition; +import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexStatistics; import org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexTracker; import org.apache.jackrabbit.oak.plugins.index.elastic.query.inference.InferenceConfig; +import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -37,6 +43,7 @@ import java.time.LocalDate; import java.util.Arrays; import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import static org.apache.jackrabbit.oak.plugins.index.elastic.ElasticTestUtils.randomString; @@ -44,6 +51,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.number.OrderingComparison.lessThan; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -95,6 +103,8 @@ public void setUp() throws IOException { @After public void tearDown() throws Exception { closeable.close(); + ElasticIndexStatistics.FT_OAK_12248_ENABLE.set(false); + ElasticIndexEditorProvider.FT_OAK_12249_ENABLE.set(false); } @Test @@ -187,4 +197,53 @@ public void ft_oak_12206_toggleShouldBeRemoved() { LocalDate.now().isBefore(LocalDate.of(2027, 5, 6))); } + // --- OAK-12249: lazy provisioning tests --- + + @Test + public void lazyProvisioning_requiresGraceful404Toggle() { + // OAK-12249 alone must not activate lazy provisioning — OAK-12248 is the hard dependency. + ElasticIndexStatistics.FT_OAK_12248_ENABLE.set(false); + ElasticIndexEditorProvider.FT_OAK_12249_ENABLE.set(true); + + assertFalse("Lazy provisioning must be inactive when graceful 404 handling is off", + ElasticIndexEditorProvider.isLazyProvisioningActive()); + } + + @Test + public void emptyReindex_supplierNeverCalled() throws IOException { + // GIVEN: a LazyElasticIndexWriter whose supplier records whether it was invoked + AtomicBoolean supplierCalled = new AtomicBoolean(false); + NodeBuilder definitionBuilder = EmptyNodeState.EMPTY_NODE.builder(); + LazyElasticIndexWriter lazyWriter = new LazyElasticIndexWriter(() -> { + supplierCalled.set(true); + return indexWriter; + }, definitionBuilder); + + // WHEN: closed without writing any documents + lazyWriter.close(System.currentTimeMillis()); + + // THEN: supplier was never called — no ElasticIndexWriter created, no ES index provisioned + assertFalse("Supplier must not be called when no documents are written", supplierCalled.get()); + } + + @Test + public void nonEmptyReindex_supplierCalledOnFirstWrite() throws IOException { + // GIVEN: a LazyElasticIndexWriter whose supplier records when it is invoked + AtomicBoolean supplierCalled = new AtomicBoolean(false); + NodeBuilder definitionBuilder = EmptyNodeState.EMPTY_NODE.builder(); + LazyElasticIndexWriter lazyWriter = new LazyElasticIndexWriter(() -> { + supplierCalled.set(true); + return indexWriter; + }, definitionBuilder); + + // Supplier not yet called before any write + assertFalse(supplierCalled.get()); + + // WHEN: first document written + lazyWriter.updateDocument("/foo", new ElasticDocument("/foo")); + + // THEN: supplier was called — ElasticIndexWriter (and its ES index) created on first write + assertTrue("Supplier must be called on the first write", supplierCalled.get()); + } + }