Skip to content

OAK-12249: lazy ES index provisioning — skip creation for empty reindex - #3085

Open
bhabegger wants to merge 3 commits into
apache:trunkfrom
oak-indexing:OAK-12249-lazy-provisioning
Open

OAK-12249: lazy ES index provisioning — skip creation for empty reindex#3085
bhabegger wants to merge 3 commits into
apache:trunkfrom
oak-indexing:OAK-12249-lazy-provisioning

Conversation

@bhabegger

Copy link
Copy Markdown
Contributor

Summary

  • Defers provisionIndex() from the ElasticIndexWriter constructor to the first updateDocument(), deleteDocumentTree(), or deleteDocument() call when both FT_OAK-12249 and FT_OAK-12248 are enabled
  • A reindex that produces zero documents never creates an Elasticsearch index or alias, eliminating the empty-index problem described in OAK-12249
  • ensureProvisioned() handles the incremental-write-after-empty-reindex case: if an alias does not exist when the first document arrives, it creates a new backing index with a fresh seed and points the alias at it

Dependency on OAK-12248

OAK-12248 (#2950, graceful 404 handling) has merged to trunk. This branch is rebased on top of it.

The runtime dependency remains: isLazyProvisioningActive() returns true only when both FT_OAK-12249 and FT_OAK-12248 are enabled. Enabling FT_OAK-12249 alone falls back to eager provisioning and logs a WARN.

Tests

Three new unit tests in ElasticIndexWriterTest:

  • lazyProvisioning_requiresGraceful404Toggle — asserts lazy provisioning is inactive when OAK-12248 toggle is off
  • emptyReindex_doesNotCreateEsIndex — verifies no ES index is created during construction when no documents are written
  • nonEmptyReindex_provisionsOnFirstDocument — verifies provisionIndex() is called on the first updateDocument() and not before

All 11 tests in ElasticIndexWriterTest pass; full oak-search-elastic suite (514 tests) passes.

Note

Supersedes #2955, opened from #2955 (bhabegger/jackrabbit-oak), moved to this fork to pick up CI.

Jira

https://issues.apache.org/jira/browse/OAK-12249

When FT_OAK-12249 and FT_OAK-12248 are both enabled, ElasticIndexWriter
defers provisionIndex() from the constructor to the first updateDocument()
or deleteDocuments() call. A reindex that produces zero documents never
creates an Elasticsearch index or alias, eliminating the empty-index
problem described in OAK-12249.

Deployment order is enforced at runtime: isLazyProvisioningActive() returns
true only when both toggles are on. Enabling FT_OAK-12249 alone logs a WARN
and falls back to eager provisioning, preventing 404 errors on query paths
that lack graceful 404 handling.

ensureProvisioned() handles the incremental-write-after-empty-reindex case:
if an alias does not exist when the first document arrives, it creates a new
backing index with a fresh seed and points the alias at it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bhabegger
bhabegger force-pushed the OAK-12249-lazy-provisioning branch from a6f718a to 2f564a4 Compare August 24, 2026 08:38
…ments

A reindex under lazy provisioning that matches zero documents never invoked
the writer supplier, so LazyElasticIndexWriter.close() had no way to flip
or remove an alias. On a never-provisioned index this is correct (nothing to
clean up), but on an already-provisioned, populated index it left the old,
stale backing index fully aliased and queryable indefinitely -- silently
serving pre-reindex content with no signal anything was wrong, since
REINDEX_COMPLETION_TIMESTAMP also isn't written in this path.

Adds ElasticIndexWriter.unaliasIfProvisioned(), called from
LazyElasticIndexWriter.close() whenever the delegate was never created: it
removes the alias and deletes the backing index if one exists, no-op
otherwise. Proven with a new testcontainers-based
LazyProvisioningReindexITTest against a real Elasticsearch cluster,
following TDD (confirmed RED against the prior behavior before this fix).
ElasticIndexWriterFactory#newInstance's return type widened from the
concrete ElasticIndexWriter to the FulltextIndexWriter interface -- a
binary-incompatible, major change to the exported
elastic.index package. Without a package-info.java, this package's
declared OSGi version tracked the bundle's own version (2.5.0), which
only satisfies a minor bump, failing the maven-bundle-plugin baseline
check ("Version increase required ... suggested 3.0.0").

The same auto-tracking also drags elastic.query and elastic.util up to
2.5.0 even though neither package's API changed at all, triggering
"Excessive version increase" / "no changes detected" warnings.

Adds package-info.java per OSGi semantic versioning to pin each
package's version independently of the bundle version: elastic.index
to 3.0.0 (major, matches the actual breaking change), elastic.query
and elastic.util to 2.4.1 (bnd requires at least a micro bump the first
time a package gains an explicit declared version, even with no API
change).

@fabriziofortino fabriziofortino left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic LGTM. Just a few improvements.

Comment on lines 53 to +55
@Override
public ElasticIndexWriter getWriter() {
return (ElasticIndexWriter) super.getWriter();
public FulltextIndexWriter<ElasticDocument> getWriter() {
return super.getWriter();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is actually no need for that. We could remove it

private final boolean requiresProvisioning;
private final String indexName;
private final ElasticRetryPolicy retryPolicy;
private final NodeBuilder definitionBuilder;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only needed in the constructor. Can we remove it?

if (delegate == null) {
LOG.info("Reindex produced no documents — skipping ES index creation (OAK-12249)");
definitionBuilder.setProperty(ElasticIndexDefinition.PROP_REQUIRES_PROVISIONING, true);
ElasticIndexWriter.unaliasIfProvisioned(elasticConnection, indexDefinition);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about moving unaliasIfProvisioned in this class (no need to be static)?

* See the License for the specific language governing permissions and
* limitations under the License.
*/
@Version("2.4.1")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +50 to +64
@ClassRule
public static final ElasticConnectionRule elasticRule = new ElasticConnectionRule();

private ElasticConnection connection;
private ElasticIndexTracker indexTracker;
private NodeStore nodeStore;

@Before
public void setup() {
this.connection = elasticRule.useDocker() ?
elasticRule.getElasticConnectionForDocker() :
elasticRule.getElasticConnectionFromString();
this.indexTracker = new ElasticIndexTracker(connection, new ElasticMetricHandler(StatisticsProvider.NOOP));
this.nodeStore = new MemoryNodeStore(INITIAL_CONTENT);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should extend ElasticAbstractQueryTest and remove this whole block

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants