Skip to content

LeafReader should not remove SubReaderWrappers incase IndexWriter encounters a non aborting Exception#20193

Merged
Bukhtawar merged 1 commit intoopensearch-project:mainfrom
RS146BIJAY:derived-source
Jan 16, 2026
Merged

LeafReader should not remove SubReaderWrappers incase IndexWriter encounters a non aborting Exception#20193
Bukhtawar merged 1 commit intoopensearch-project:mainfrom
RS146BIJAY:derived-source

Conversation

@RS146BIJAY
Copy link
Contributor

@RS146BIJAY RS146BIJAY commented Dec 9, 2025

Description

In OpenSearch, during recovery flow, documents are queried using a DirectoryReader to be replayed as operations during translog replay. Now incase OpenSearch IndexWriter encounters any non aborted exception, underlying segments can contain both hard and soft deleted documents. In this case, OpenSearch unwraps a LeafReader and uses it for querying documents which are replayed as operation for Translog replay.

Since this unwrapping process will remove all wrappers from LeafReader, it becomes an issue for features like DerivedSource where we rely on a DerivedSourceDirectoryReader wrapper over LeafReader to extract source for a document. For Derived Source enabled index, with wrapper removed, a MissingHistoryOperationsException will be thrown during Translog replay as source field will be null for the documents, failing recovery.

Related Issues

#19851

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 9, 2025

📝 Walkthrough

Walkthrough

Select LeafReader base when wrapping for live-doc handling to respect derived-source readers; propagate ScriptService into MapperService test factories; add context-aware scripting test helpers and new derived-source snapshot tests; update changelog.

Changes

Cohort / File(s) Summary
Lucene reader wrapping
server/src/main/java/org/opensearch/common/lucene/Lucene.java
Import DerivedSourceLeafReader; add isDerivedSourceEnabled(LeafReader) helper; in DirectoryReaderWithAllLiveDocs.wrap choose the base for LeafReaderWithLiveDocs as either the original leaf or the underlying SegmentReader based on derived-source support.
Engine tests — derived-source snapshots & wrappers
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java
Add contextAwareEnabled parameter to createEngineConfigWithMapperSupplierForDerivedSource; add multiple derived-source snapshot tests and context-aware variants; introduce FilterDirectoryReader/FilterLeafReader wrappers and update call sites to pass the new flag.
MapperService test wiring
test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java
Add overloads of newMapperService(...) that accept a ScriptService and propagate it into MapperService construction (replace previous null scriptService usage).
Test framework — context-aware scripting helpers
test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
Add createMapperServiceForContextAwareIndex(), ContextAwareCustomScriptPlugin, createEngineForWrapper(...), and testDocumentWithGroupingCriteria() to support context-aware scripting in tests (includes duplicated declarations noted in diff).
Changelog
CHANGELOG.md
Add Unreleased 3.x Fixed entry about LeafReader not removing SubReaderWrappers when IndexWriter encounters a non-aborting exception.

Sequence Diagram(s)

sequenceDiagram
  participant DirReader as DirectoryReaderWithAllLiveDocs
  participant Leaf as LeafReader
  participant Unwrap as (Filter)LeafReader unwrapping
  participant Segment as SegmentReader
  participant Live as LeafReaderWithLiveDocs

  DirReader->>Leaf: iterate leaves
  alt isDerivedSourceEnabled(Leaf) == true
    Leaf-->>DirReader: derived-source enabled
    DirReader->>Live: wrap using original Leaf as base
  else
    Leaf->>Unwrap: unwrap FilterLeafReader(s) if present
    Unwrap-->>Segment: obtain underlying SegmentReader
    DirReader->>Live: wrap using SegmentReader as base
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: preventing unwrapping of SubReaderWrappers when IndexWriter encounters non-aborting exceptions, which is the core fix for the LeafReader handling issue.
Description check ✅ Passed The description provides comprehensive context about the recovery flow issue, explains the problem with DerivedSource wrappers being removed, and links to the related issue, covering all essential information.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java (1)

85-112: LGTM!

The ScriptService is now correctly propagated to the MapperService constructor, enabling script-based field derivation in tests.

For consistency, consider whether newMapperServiceWithHelperAnalyzer (lines 114-140) should also support ScriptService injection. Currently it passes null, which may be intentional if that variant doesn't require scripting support.

server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

9137-9215: contextAwareEnabled isn’t actually honored for mapper wiring (and a couple robustness nits).

  • final MapperService mapperService = createMapperServiceForContextAwareIndex(); is unconditional (Line 9184), so callers passing false don’t actually get the non-context-aware mapper path.
  • Minor: prefer if (contextAwareEnabled) over == true; consider using a string "-1" for index.refresh_interval to match typical time-value parsing.
Proposed fix
-        if (contextAwareEnabled == true) {
+        if (contextAwareEnabled) {
             settingBuilder.put(IndexSettings.INDEX_CONTEXT_AWARE_ENABLED_SETTING.getKey(), true);
@@
         IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetadata);
@@
-        final MapperService mapperService = createMapperServiceForContextAwareIndex();
+        final MapperService mapperService = contextAwareEnabled ? createMapperServiceForContextAwareIndex() : createMapperService();
         mapperService.merge("_doc", new CompressedXContent(mapping.toString()), MapperService.MergeReason.MAPPING_UPDATE);

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3c0f7e and 4f9ec4c.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
  • server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java
  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-13T17:40:27.167Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:27.167Z
Learning: Avoid capturing or evaluating a supplier (e.g., this::defaultCodec) upfront when passing it to a registry during object construction. If registries may replace defaults during iteration (as in EnginePlugin.getAdditionalCodecs), pass the supplier itself and only resolve it at use time. This ensures dynamic behavior is preserved during initialization and prevents premature binding of defaults in codecs/registry setup. This pattern should apply to similar initialization paths in Java server code where registries may mutate defaults during construction.

Applied to files:

  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
📚 Learning: 2026-01-13T17:40:34.780Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:34.780Z
Learning: In OpenSearch's CodecService, when registries are processed during construction (via EnginePlugin::getAdditionalCodecs), the defaultCodec supplier passed to CodecRegistry.getCodecs() must remain dynamic (this::defaultCodec) rather than captured upfront, because registries can replace the default codec during iteration. The contract is that registries should not invoke the supplier during getCodecs() execution since CodecService is still initializing.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
📚 Learning: 2025-12-02T22:44:14.799Z
Learnt from: prudhvigodithi
Repo: opensearch-project/OpenSearch PR: 20112
File: server/src/internalClusterTest/java/org/opensearch/search/slice/SearchSliceIT.java:73-81
Timestamp: 2025-12-02T22:44:14.799Z
Learning: In OpenSearch integration tests extending OpenSearchIntegTestCase, using `LuceneTestCase.SuppressCodecs("*")` triggers special handling that selects a random production codec from the CODECS array, while `SuppressCodecs("Asserting")` or other specific codec suppressions still allow Lucene's default codec randomization which may include the asserting codec. Use `SuppressCodecs("*")` when you need to completely avoid asserting codecs (e.g., for cross-thread StoredFieldsReader usage) while maintaining production codec test coverage.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🧬 Code graph analysis (2)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (2)
server/src/main/java/org/opensearch/common/lucene/index/DerivedSourceLeafReader.java (1)
  • DerivedSourceLeafReader (25-68)
server/src/main/java/org/opensearch/index/codec/CriteriaBasedCodec.java (1)
  • CriteriaBasedCodec (26-72)
test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (2)
server/src/main/java/org/opensearch/script/ScriptModule.java (1)
  • ScriptModule (54-127)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (1)
  • Lucene (115-1017)
🪛 LanguageTool
CHANGELOG.md

[grammar] ~35-~35: Use a hyphen to join words.
Context: ...pers incase IndexWriter encounters a non aborting Exception ([#20193](https://git...

(QB_NEW_EN_HYPHEN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: gradle-check
  • GitHub Check: Analyze (java)
  • GitHub Check: assemble (25, ubuntu-24.04-arm)
  • GitHub Check: assemble (21, ubuntu-latest)
  • GitHub Check: precommit (25, windows-latest)
  • GitHub Check: assemble (21, ubuntu-24.04-arm)
  • GitHub Check: assemble (25, windows-latest)
  • GitHub Check: assemble (25, ubuntu-latest)
  • GitHub Check: precommit (21, macos-15)
  • GitHub Check: precommit (21, ubuntu-24.04-arm)
  • GitHub Check: assemble (21, windows-latest)
  • GitHub Check: precommit (21, windows-latest)
  • GitHub Check: precommit (25, macos-15-intel)
  • GitHub Check: precommit (25, macos-15)
  • GitHub Check: precommit (25, ubuntu-latest)
  • GitHub Check: detect-breaking-change
  • GitHub Check: precommit (25, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, ubuntu-latest)
  • GitHub Check: precommit (21, macos-15-intel)
  • GitHub Check: precommit (21, windows-2025, true)
🔇 Additional comments (10)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (3)

87-87: LGTM!

The import for DerivedSourceLeafReader is correctly added and properly positioned among the other OpenSearch imports.


955-959: Good fix for preserving Derived Source wrapper.

The conditional logic correctly preserves the DerivedSourceLeafReader wrapper when derived source is enabled, while maintaining the existing behavior (wrapping segmentReader) for other cases. This targeted fix addresses the recovery failure without impacting non-derived-source indices.


966-984: Implementation correctly detects Derived Source wrappers.

The recursive unwrapping logic properly traverses the FilterLeafReader chain to detect DerivedSourceLeafReader. The base cases are well-defined and the default false return is a safe fallback for unknown reader types.

As discussed in prior reviews, while a marker interface (PreserveOnUnwrap) would provide better extensibility, the current approach directly addresses the immediate issue for Derived Source. If additional wrappers require similar handling in the future, consider refactoring to a marker interface pattern.

test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java (1)

65-83: LGTM! Clean backward-compatible API extension.

The overload chain correctly maintains backward compatibility by delegating from the 4-arg to 5-arg to 6-arg method, with null as the default for ScriptService. This allows existing tests to continue working while enabling script injection for new Derived Source tests.

server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

49-51: New Lucene wrapper imports look appropriate for the added wrapper-preservation tests.

test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (5)

65-65: LGTM!

The new imports are all necessary for the added test utilities (exception handling in wrapper, script plugin infrastructure, and context-aware mapper service creation).

Also applies to: 74-74, 117-117, 129-135, 149-149, 152-152


398-402: LGTM!

Simple helper method that follows the existing pattern of similar test document creation methods in this class.


717-752: LGTM!

The method correctly implements wrapper-based engine creation following the existing createEngine patterns. The use of IndexShard.wrapSearcher to apply the DirectoryReader wrapper aligns with the PR objective of preserving wrappers like DerivedSourceDirectoryReader during recovery scenarios.


1665-1691: LGTM!

The method correctly sets up a MapperService with context-aware indexing enabled and proper script infrastructure. This provides the necessary test foundation for derived source scenarios where script-based grouping criteria are needed.


1697-1726: LGTM!

The ContextAwareCustomScriptPlugin follows the established pattern for test script plugins in the OpenSearch codebase. The use of "painless" as the language name allows seamless integration with standard script execution paths, and the provided scripts (ctx.op='delete' and String.valueOf(grouping_criteria)) are appropriate for testing context-aware document handling.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.40.5)
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (2)

8314-8318: Helper-based EngineConfig construction for derived source looks correct but can be tightened

The refactor to createEngineConfigWithMapperSupplierForDerivedSource(Store, boolean) and its call sites (contextAwareEnabled=false for the existing tests and true for the context-aware variant) is consistent: settings toggle INDEX_DERIVED_SOURCE_SETTING and, when requested, INDEX_CONTEXT_AWARE_ENABLED_SETTING, and the mapping defines a stored value field with an optional context_aware_grouping block.

Two small follow-ups to consider:

  • Keep MapperService and IndexSettings aligned: createEngineConfigWithMapperSupplierForDerivedSource builds a fresh IndexSettings instance but createMapperServiceForContextAwareIndex() likely uses a default set of settings. If that factory doesn’t already honor the derived‑source and context‑aware flags, consider wiring it to the same IndexSettings (or a Settings instance derived from it) to avoid subtle divergence between the mapper and engine config.
  • Reduce duplication in mapping construction: the value field definition is duplicated between the two branches. A tiny helper that builds the base _doc.properties.value object and optionally attaches context_aware_grouping would keep the mapping definition DRY and easier to evolve.

Functionally this looks fine; these are mainly maintainability and consistency tweaks.

Also applies to: 8548-8552, 8843-8922


8377-8538: Context-aware derived source snapshot test is sound; consider asserting on derived _source too

The new testNewChangesSnapshotWithDeleteAndUpdateWithDerivedSourceAndContextAwareEnabled exercises a good mix of inserts, deletes, and updates with derived source + context-aware enabled, and validates:

  • all operations are present in the newChangesSnapshot range,
  • deletes truly remove documents, and
  • updates are visible via stored value content.

Since the underlying bug here is about keeping the derived-source wrappers around in Lucene (so _source is non-null and correct), you may want to also assert on the Translog.Index payload for at least some operations, e.g. comparing indexOp.source().utf8ToString() against an expected JSON representation for created/updated docs. That would more directly guard the contract that LuceneChangesSnapshot continues to surface the derived source correctly in the context-aware path.

If you decide to add such an assertion, you can mirror the style used in testNewChangesSnapshotWithDerivedSource to avoid duplication.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f8d381 and 854b133.

📒 Files selected for processing (4)
  • server/src/main/java/org/opensearch/common/lucene/Lucene.java (1 hunks)
  • server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (4 hunks)
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java (3 hunks)
  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-02T22:44:14.799Z
Learnt from: prudhvigodithi
Repo: opensearch-project/OpenSearch PR: 20112
File: server/src/internalClusterTest/java/org/opensearch/search/slice/SearchSliceIT.java:73-81
Timestamp: 2025-12-02T22:44:14.799Z
Learning: In OpenSearch integration tests extending OpenSearchIntegTestCase, using `LuceneTestCase.SuppressCodecs("*")` triggers special handling that selects a random production codec from the CODECS array, while `SuppressCodecs("Asserting")` or other specific codec suppressions still allow Lucene's default codec randomization which may include the asserting codec. Use `SuppressCodecs("*")` when you need to completely avoid asserting codecs (e.g., for cross-thread StoredFieldsReader usage) while maintaining production codec test coverage.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🧬 Code graph analysis (1)
test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (1)
server/src/main/java/org/opensearch/script/ScriptModule.java (1)
  • ScriptModule (54-127)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: gradle-check
  • GitHub Check: detect-breaking-change
  • GitHub Check: precommit (25, windows-latest)
  • GitHub Check: precommit (25, macos-15)
  • GitHub Check: precommit (21, windows-2025, true)
  • GitHub Check: precommit (25, ubuntu-latest)
  • GitHub Check: precommit (25, ubuntu-24.04-arm)
  • GitHub Check: Analyze (java)
  • GitHub Check: precommit (25, macos-15-intel)
  • GitHub Check: precommit (21, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, macos-15)
  • GitHub Check: assemble (25, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, macos-15-intel)
  • GitHub Check: precommit (21, ubuntu-latest)
  • GitHub Check: precommit (21, windows-latest)
  • GitHub Check: assemble (25, windows-latest)
  • GitHub Check: assemble (21, ubuntu-24.04-arm)
  • GitHub Check: assemble (25, ubuntu-latest)
  • GitHub Check: assemble (21, windows-latest)
  • GitHub Check: assemble (21, ubuntu-latest)
🔇 Additional comments (9)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (1)

934-955: Using the original leaf here correctly preserves wrapper readers (fixing derived‑source behavior)

Switching the delegate from segmentReader to leaf when constructing LeafReaderWithLiveDocs keeps any existing LeafReader wrappers (e.g., derived‑source / context‑aware readers) in the chain while still applying hardLiveDocs and the corrected numDocs computed from the underlying SegmentReader. This also makes the behavior consistent with the hardLiveDocs == null branch, which already wrapped leaf.

Assuming our wrappers are doc‑ID preserving (as they should be for LeafReader leaves of a DirectoryReader), this looks like the right fix and should avoid dropping SubReaderWrapper state during recovery after non‑aborting exceptions.

server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

8753-8753: Locking the context-aware feature flag on these tests is appropriate

Adding @LockFeatureFlag(CONTEXT_AWARE_MIGRATION_EXPERIMENTAL_FLAG) to the composite IndexWriter failure tests matches their reliance on INDEX_CONTEXT_AWARE_ENABLED_SETTING and ensures they won’t silently start running with the wrong behavior when the feature flag is toggled.

Just double-check that the LockFeatureFlag annotation type is correctly imported/visible to this class (I don’t see a new import in this diff), so these tests continue to compile once the feature is wired in.

Also applies to: 8798-8798

test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java (2)

65-82: LGTM! Clean method overloading for ScriptService injection.

The delegation pattern maintains backward compatibility while enabling ScriptService injection for context-aware index testing.


85-112: LGTM! ScriptService properly propagated to MapperService constructor.

The ScriptService parameter is correctly passed through to the final MapperService instantiation.

test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (5)

126-132: LGTM! Necessary imports for scripting infrastructure.

These imports support the new ContextAwareCustomScriptPlugin and createMapperServiceForContextAwareIndex() method.


396-400: LGTM! Minimal test helper for grouping criteria documents.

The method follows the existing pattern of test document helpers and provides a focused utility for context-aware index tests.


1626-1655: LGTM! Properly configured MapperService factory for context-aware index testing.

The method correctly wires up the ScriptModule, ScriptService, and MapperService with the context-aware index setting enabled. This enables testing scenarios that require derived source functionality.


1661-1690: LGTM! Well-structured mock script plugin for testing.

The ContextAwareCustomScriptPlugin correctly implements the ScriptPlugin interface and provides the necessary mock scripts for context-aware index testing. Using "painless" as the script language name appropriately mimics production behavior in tests.


146-166: Verify the toList import is used in this file.

The Collection and HashMap imports are clearly used by the new code. However, the toList static import on line 166 requires verification to confirm it is actually used somewhere in this file, as it is not visible in the provided code snippet.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
CHANGELOG.md (1)

103-103: Minor typo in changelog entry.

The entry reads "incase" but should be "in case" (two words).

Apply this diff to fix the typo:

-- LeafReader should not remove SubReaderWrappers incase IndexWriter encounters a non aborting Exception ([#20193](https://github.com/opensearch-project/OpenSearch/pull/20193))
+- LeafReader should not remove SubReaderWrappers in case IndexWriter encounters a non aborting Exception ([#20193](https://github.com/opensearch-project/OpenSearch/pull/20193))
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (3)

8313-8374: Avoid double-closing Store when using try-with-resources plus IOUtils.close

In these three tests you wrap Store in try-with-resources and also pass the same store into IOUtils.close(engine, store) in the inner finally. That results in two close attempts for the same Store instance.

While OpenSearch Store is generally defensive about repeated closes, this pattern is unnecessary and can hide real close-time issues behind swallowed AlreadyClosedExceptions.

Consider only closing engine in the inner finally and letting try-with-resources handle the Store, e.g.:

-        try (Store store = createStore()) {
+        try (Store store = createStore()) {
             EngineConfig engineConfig = createEngineConfigWithMapperSupplierForDerivedSource(store, false);
             InternalEngine engine = null;
             try {
                 engine = createEngine(engineConfig);
                 // ...
             } finally {
-                IOUtils.close(engine, store);
+                IOUtils.close(engine);
             }
         }

(and similarly for the other two tests).

Also applies to: 8385-8536, 8547-8698


8377-8538: Reduce duplication between derived-source delete/update snapshot tests

testNewChangesSnapshotWithDeleteAndUpdateWithDerivedSourceAndContextAwareEnabled and testNewChangesSnapshotWithDeleteAndUpdateWithDerivedSource share almost all of their structure: randomized index/delete/update history, operations bookkeeping, and snapshot assertions, differing mainly in the contextAwareEnabled flag and which document factory is used.

To keep these tests easier to maintain, consider extracting a shared helper like:

private void assertNewChangesSnapshotWithDeleteAndUpdateDerivedSource(boolean contextAwareEnabled) { ... }

and have each test call it with true/false. That way any future bugfix to the scenario only needs to be updated in one place.

Also applies to: 8540-8700


8843-8922: Clarify mapper service choice in createEngineConfigWithMapperSupplierForDerivedSource

This helper always uses createMapperServiceForContextAwareIndex() regardless of the contextAwareEnabled flag, and only toggles index settings/mapping (extra context_aware_grouping block + INDEX_CONTEXT_AWARE_ENABLED_SETTING) based on that flag.

If createMapperServiceForContextAwareIndex() has additional behavior or assumptions specific to context-aware indices, using it for the non-context-aware path may unintentionally change the baseline derived-source tests’ behavior compared to the old implementation.

Two suggestions:

  • Either guard the mapper-service choice on the flag (using the “normal” mapper service for contextAwareEnabled == false, and the context-aware variant only when true), or
  • Add a brief comment explaining that this helper intentionally reuses the context-aware mapper service even when contextAwareEnabled is false, so future readers don’t assume it’s a bug.

This is mostly about clarity and avoiding surprises in future refactors of the mapper creation path.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 854b133 and 8f33904.

📒 Files selected for processing (5)
  • CHANGELOG.md (1 hunks)
  • server/src/main/java/org/opensearch/common/lucene/Lucene.java (1 hunks)
  • server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (4 hunks)
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java (3 hunks)
  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-02T22:44:14.799Z
Learnt from: prudhvigodithi
Repo: opensearch-project/OpenSearch PR: 20112
File: server/src/internalClusterTest/java/org/opensearch/search/slice/SearchSliceIT.java:73-81
Timestamp: 2025-12-02T22:44:14.799Z
Learning: In OpenSearch integration tests extending OpenSearchIntegTestCase, using `LuceneTestCase.SuppressCodecs("*")` triggers special handling that selects a random production codec from the CODECS array, while `SuppressCodecs("Asserting")` or other specific codec suppressions still allow Lucene's default codec randomization which may include the asserting codec. Use `SuppressCodecs("*")` when you need to completely avoid asserting codecs (e.g., for cross-thread StoredFieldsReader usage) while maintaining production codec test coverage.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🧬 Code graph analysis (1)
test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (2)
test/framework/src/main/java/org/opensearch/script/MockScriptEngine.java (1)
  • MockScriptEngine (74-825)
server/src/main/java/org/opensearch/script/ScriptModule.java (1)
  • ScriptModule (54-127)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: gradle-check
  • GitHub Check: Analyze (java)
  • GitHub Check: assemble (21, ubuntu-24.04-arm)
  • GitHub Check: detect-breaking-change
  • GitHub Check: assemble (25, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, windows-2025, true)
  • GitHub Check: assemble (25, windows-latest)
  • GitHub Check: assemble (21, windows-latest)
  • GitHub Check: assemble (25, ubuntu-latest)
  • GitHub Check: assemble (21, ubuntu-latest)
  • GitHub Check: precommit (25, macos-15-intel)
  • GitHub Check: precommit (25, ubuntu-24.04-arm)
  • GitHub Check: precommit (25, macos-15)
  • GitHub Check: precommit (25, windows-latest)
  • GitHub Check: precommit (21, ubuntu-latest)
  • GitHub Check: precommit (21, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, windows-latest)
  • GitHub Check: precommit (25, ubuntu-latest)
  • GitHub Check: precommit (21, macos-15-intel)
  • GitHub Check: precommit (21, macos-15)
🔇 Additional comments (6)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (1)

954-954: LGTM! Core fix correctly preserves wrapper chain.

This change is the essential fix for the issue. By passing leaf (the original reader with wrappers intact) instead of segmentReader (the unwrapped SegmentReader), the code now preserves SubReaderWrappers like DerivedSourceDirectoryReader. This prevents the MissingHistoryOperationsException during translog replay when derived source is enabled, as the wrapper needed to extract document source is no longer removed.

test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (4)

126-132: LGTM! Imports correctly support new test infrastructure.

The added imports (Plugin, ScriptPlugin, MockScriptEngine, ScriptContext, ScriptEngine, ScriptModule, ScriptService, Collection, HashMap, toList) are all used by the new test infrastructure for context-aware scripting support.

Also applies to: 146-146, 149-149, 166-166


396-400: LGTM! Test helper method follows established patterns.

The testDocumentWithGroupingCriteria() method is a straightforward test helper that creates a document with grouping criteria set, consistent with other document creation helpers in this file.


1626-1655: LGTM! Test infrastructure for context-aware indexing.

The createMapperServiceForContextAwareIndex() method provides essential test infrastructure for validating context-aware indexing scenarios with derived source. The method correctly:

  • Enables context-aware settings via INDEX_CONTEXT_AWARE_ENABLED_SETTING
  • Sets up a ScriptService with the custom test plugin
  • Configures the MapperService appropriately for testing

This supports validation of the fix for preserving SubReaderWrappers.


1661-1690: LGTM! Test script plugin implementation is appropriate.

The ContextAwareCustomScriptPlugin provides mock script behavior for testing context-aware functionality. The implementation:

  • Correctly extends Plugin and implements ScriptPlugin
  • Provides appropriate mock scripts for testing ("ctx.op='delete'" and "String.valueOf(grouping_criteria)")
  • Uses MockScriptEngine with the "painless" language identifier
  • Follows established patterns for test script plugins
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

8307-8375: Derived-source snapshot happy-path coverage looks good

This test exercises newChangesSnapshot over a fully derived-source index and asserts that each Translog.Index operation has a non-null, correctly-shaped source ({"value":"test"}) and that seqNos are contiguous from 0..N-1. That directly guards the regression from the linked issue and provides good safety around the unwrap/wrapper behavior.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 9, 2025

❌ Gradle check result for 8f33904: FAILURE

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

@RS146BIJAY RS146BIJAY force-pushed the derived-source branch 3 times, most recently from da8dfc4 to cb859ee Compare December 30, 2025 14:39
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

8944-9022: createEngineConfigWithMapperSupplierForDerivedSource(...) creates a MapperService with mismatched settings.

The call to createMapperServiceForContextAwareIndex() creates an IndexMetadata with hardcoded settings (context-aware enabled, but no derived-source setting), while the EngineConfig is constructed with indexSettings that includes both INDEX_DERIVED_SOURCE_SETTING and INDEX_CONTEXT_AWARE_ENABLED_SETTING (conditionally). This mismatch means the MapperService does not reflect the full configuration intended for the engine. Consider:

  • Make the MapperService creation conditional on contextAwareEnabled (use createMapperServiceForContextAwareIndex() when true, otherwise createMapperService())
  • Ideally, pass the newly constructed indexSettings into the mapper service factory to ensure consistency

Optionally, specify "lang": "painless" in the script block for clarity, though Painless is the default for inline mapping scripts.

♻️ Duplicate comments (3)
CHANGELOG.md (1)

35-35: Minor typographical and grammatical corrections needed.

The changelog entry has two minor issues:

  • "incase" should be "in case" (two words)
  • "non aborting" should be "non-aborting" (hyphenated compound adjective)
🔧 Suggested fix
-- LeafReader should not remove SubReaderWrappers incase IndexWriter encounters a non aborting Exception ([`#20193`](https://github.com/opensearch-project/OpenSearch/pull/20193))
+- LeafReader should not remove SubReaderWrappers in case IndexWriter encounters a non-aborting Exception ([`#20193`](https://github.com/opensearch-project/OpenSearch/pull/20193))
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (2)

8641-8801: Same: validate derived source from the snapshot, not via engine.get(...).

This non-context-aware test still validates “updated” docs via engine.get(...), which can hide issues where Translog.Index#source() returned by newChangesSnapshot() is null/incorrect.

Sketch change (same idea as the context-aware variant)
                 try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", 0, operations.size() - 1, true, true)) {
                     int count = 0;
+                    final int updatesStartSeqNo = numDocs + deletedDocs.size();
                     Translog.Operation operation;
                     while ((operation = snapshot.next()) != null) {
                         if (operation instanceof Translog.Index) {
                             Translog.Index indexOp = (Translog.Index) operation;
                             String docId = indexOp.id();
-                            if (updatedDocs.contains(docId)) {
-                                // Verify updated content using get
-                                try (
-                                    Engine.GetResult get = engine.get(
-                                        new Engine.Get(true, true, docId, newUid(docId)),
-                                        engine::acquireSearcher
-                                    )
-                                ) {
-                                    assertTrue("Document " + docId + " should exist", get.exists());
-                                    StoredFields storedFields = get.docIdAndVersion().reader.storedFields();
-                                    org.apache.lucene.document.Document document = storedFields.document(get.docIdAndVersion().docId);
-                                    assertEquals(
-                                        "Document " + docId + " should have updated value",
-                                        "updated",
-                                        document.getField("value").stringValue()
-                                    );
-                                }
-                            }
+                            assertNotNull("snapshot index op must have derived source", indexOp.source());
+                            final boolean isUpdateOp = indexOp.seqNo() >= updatesStartSeqNo;
+                            if (isUpdateOp) {
+                                assertThat(indexOp.source().utf8ToString(), containsString("\"updated\""));
+                                assertTrue("update op docId should be in updated set", updatedDocs.contains(docId));
+                            } else {
+                                assertThat(indexOp.source().utf8ToString(), containsString("\"test\""));
+                            }
                         } else if (operation instanceof Translog.Delete) {

Also applies here: avoid closing the try-with-resources store again in finally.


8478-8639: Snapshot test should assert Translog.Index#source() directly (currently can miss the regression).

The “updated doc” verification uses engine.get(...) + stored fields, which can still pass even if newChangesSnapshot() produced a Translog.Index with missing/stale source (the bug this PR is about). This is the same concern raised previously.

Suggested direction: validate per-op derived source from the snapshot
-                // Test snapshot with all operations
+                // Test snapshot with all operations
                 try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", 0, operations.size() - 1, true, true)) {
                     int count = 0;
+                    final int updatesStartSeqNo = numDocs + deletedDocs.size();
                     Translog.Operation operation;
                     while ((operation = snapshot.next()) != null) {
                         if (operation instanceof Translog.Index) {
                             Translog.Index indexOp = (Translog.Index) operation;
                             String docId = indexOp.id();
-                            if (updatedDocs.contains(docId)) {
-                                // Verify updated content using get
-                                try (
-                                    Engine.GetResult get = engine.get(
-                                        new Engine.Get(true, true, docId, newUid(docId)),
-                                        engine::acquireSearcher
-                                    )
-                                ) {
-                                    assertTrue("Document " + docId + " should exist", get.exists());
-                                    StoredFields storedFields = get.docIdAndVersion().reader.storedFields();
-                                    org.apache.lucene.document.Document document = storedFields.document(get.docIdAndVersion().docId);
-                                    assertEquals(
-                                        "Document " + docId + " should have updated value",
-                                        "updated",
-                                        document.getField("value").stringValue()
-                                    );
-                                }
-                            }
+                            assertNotNull("snapshot index op must have derived source", indexOp.source());
+                            final boolean isUpdateOp = indexOp.seqNo() >= updatesStartSeqNo;
+                            if (isUpdateOp) {
+                                assertThat(indexOp.source().utf8ToString(), containsString("\"updated\""));
+                                assertTrue("update op docId should be in updated set", updatedDocs.contains(docId));
+                            } else {
+                                assertThat(indexOp.source().utf8ToString(), containsString("\"test\""));
+                            }
                         } else if (operation instanceof Translog.Delete) {
                             String docId = ((Translog.Delete) operation).id();
                             assertTrue("Document " + docId + " should be in deleted set", deletedDocs.contains(docId));
@@
                         count++;
                     }

Also: this method has the same try-with-resources + IOUtils.close(engine, store) double-close/shadowing pattern as the other test—consider switching to IOUtils.close(engine) only (let try-with-resources close store).

🧹 Nitpick comments (1)
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

8408-8476: Make derived-source assertion less brittle + avoid double-closing the local Store.

  • assertEquals("{\"value\":\"test\"}", indexOp.source().utf8ToString()) is fragile (ordering/whitespace/extra fields). Prefer asserting non-null and matching the field value more loosely (or parse JSON).
  • The Store store in try-with-resources is also closed in finally (IOUtils.close(engine, store)), which is risky for ref-counted resources and makes failures harder to diagnose. Also, local store/engine shadow the test class fields.
Proposed tweak (within this method)
-        try (Store store = createStore()) {
+        try (Store store = createStore()) {
             EngineConfig engineConfig = createEngineConfigWithMapperSupplierForDerivedSource(store, false);
             InternalEngine engine = null;
             try {
                 engine = createEngine(engineConfig);
@@
                     while ((operation = snapshot.next()) != null) {
@@
                         Translog.Index indexOp = (Translog.Index) operation;
-                        assertEquals(
-                            "Document " + indexOp.id() + " should have updated value",
-                            "{\"value\":\"test\"}",
-                            indexOp.source().utf8ToString()
-                        );
+                        assertNotNull("derived source must not be null", indexOp.source());
+                        assertThat(indexOp.source().utf8ToString(), containsString("\"value\""));
+                        assertThat(indexOp.source().utf8ToString(), containsString("\"test\""));
                         count++;
                     }
@@
             } finally {
-                IOUtils.close(engine, store);
+                IOUtils.close(engine);
             }
         }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6f0ba9 and be52eec.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
  • server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java
  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-13T17:40:27.167Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:27.167Z
Learning: In OpenSearch's CodecService, when registries are processed during construction (via EnginePlugin::getAdditionalCodecs), the defaultCodec supplier passed to CodecRegistry.getCodecs() must remain dynamic (this::defaultCodec) rather than captured upfront, because registries can replace the default codec during iteration. The contract is that registries should not invoke the supplier during getCodecs() execution since CodecService is still initializing.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
📚 Learning: 2025-12-02T22:44:14.799Z
Learnt from: prudhvigodithi
Repo: opensearch-project/OpenSearch PR: 20112
File: server/src/internalClusterTest/java/org/opensearch/search/slice/SearchSliceIT.java:73-81
Timestamp: 2025-12-02T22:44:14.799Z
Learning: In OpenSearch integration tests extending OpenSearchIntegTestCase, using `LuceneTestCase.SuppressCodecs("*")` triggers special handling that selects a random production codec from the CODECS array, while `SuppressCodecs("Asserting")` or other specific codec suppressions still allow Lucene's default codec randomization which may include the asserting codec. Use `SuppressCodecs("*")` when you need to completely avoid asserting codecs (e.g., for cross-thread StoredFieldsReader usage) while maintaining production codec test coverage.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
📚 Learning: 2026-01-13T17:40:27.167Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:27.167Z
Learning: Avoid capturing or evaluating a supplier (e.g., this::defaultCodec) upfront when passing it to a registry during object construction. If registries may replace defaults during iteration (as in EnginePlugin.getAdditionalCodecs), pass the supplier itself and only resolve it at use time. This ensures dynamic behavior is preserved during initialization and prevents premature binding of defaults in codecs/registry setup. This pattern should apply to similar initialization paths in Java server code where registries may mutate defaults during construction.

Applied to files:

  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
🧬 Code graph analysis (2)
test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (1)
server/src/main/java/org/opensearch/script/ScriptModule.java (1)
  • ScriptModule (54-127)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (2)
server/src/main/java/org/opensearch/common/lucene/index/DerivedSourceLeafReader.java (1)
  • DerivedSourceLeafReader (25-68)
server/src/main/java/org/opensearch/index/codec/CriteriaBasedCodec.java (1)
  • CriteriaBasedCodec (26-72)
🪛 LanguageTool
CHANGELOG.md

[grammar] ~35-~35: Use a hyphen to join words.
Context: ...pers incase IndexWriter encounters a non aborting Exception ([#20193](https://git...

(QB_NEW_EN_HYPHEN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: gradle-check
  • GitHub Check: assemble (25, windows-latest)
  • GitHub Check: assemble (21, ubuntu-24.04-arm)
  • GitHub Check: assemble (21, windows-latest)
  • GitHub Check: assemble (25, ubuntu-latest)
  • GitHub Check: assemble (25, ubuntu-24.04-arm)
  • GitHub Check: assemble (21, ubuntu-latest)
  • GitHub Check: precommit (21, ubuntu-24.04-arm)
  • GitHub Check: precommit (25, windows-latest)
  • GitHub Check: precommit (25, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, macos-15-intel)
  • GitHub Check: precommit (21, windows-2025, true)
  • GitHub Check: precommit (21, ubuntu-latest)
  • GitHub Check: precommit (21, macos-15)
  • GitHub Check: precommit (25, macos-15-intel)
  • GitHub Check: precommit (25, ubuntu-latest)
  • GitHub Check: precommit (25, macos-15)
  • GitHub Check: precommit (21, windows-latest)
  • GitHub Check: detect-breaking-change
  • GitHub Check: Analyze (java)
🔇 Additional comments (8)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (2)

955-959: LGTM! Conditional wrapping preserves derived source functionality.

The logic correctly preserves the original LeafReader (with DerivedSourceLeafReader wrapper intact) when derived source is enabled, ensuring the source field can be properly reconstructed during translog replay. When derived source is not enabled, it falls back to the previous behavior using the unwrapped SegmentReader.


966-980: Recursive unwrapping logic is correct.

The helper method properly traverses the reader wrapper chain to detect DerivedSourceLeafReader. The recursive handling of both FilterLeafReader and FilterCodecReader ensures complete coverage of potential wrapper compositions.

One consideration: the method could potentially encounter deep nesting in edge cases, but typical reader chains are shallow, so this is not a practical concern.

test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java (2)

65-83: LGTM! Clean method overloading pattern.

The refactoring maintains backward compatibility by providing overloads that default scriptService to null, while enabling new test scenarios to inject a ScriptService when needed for context-aware indexing tests.


85-112: LGTM! ScriptService properly wired into MapperService.

The ScriptService parameter is correctly propagated through the method chain and passed to the MapperService constructor, enabling script-based functionality in tests.

test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (4)

126-132: LGTM! Required imports for scripting test infrastructure.

The new imports support the ContextAwareCustomScriptPlugin and createMapperServiceForContextAwareIndex additions.


395-399: LGTM! Simple test utility method.

Creates a minimal document with grouping criteria for context-aware indexing tests.


1625-1651: LGTM! Properly wires context-aware scripting for tests.

The method correctly:

  1. Configures IndexSettings with INDEX_CONTEXT_AWARE_ENABLED_SETTING enabled
  2. Creates a ScriptService via ScriptModule with the custom plugin
  3. Passes the ScriptService to MapperTestUtils.newMapperService

This enables comprehensive testing of context-aware derived-source scenarios.


1657-1686: LGTM! Well-structured mock script plugin for tests.

The ContextAwareCustomScriptPlugin properly implements ScriptPlugin and provides mock scripts for testing context-aware indexing scenarios. The use of "painless" as the script language name ensures compatibility with production code paths that expect this language.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@github-actions
Copy link
Contributor

✅ Gradle check result for be52eec: SUCCESS

@codecov
Copy link

codecov bot commented Jan 14, 2026

Codecov Report

❌ Patch coverage is 81.81818% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.37%. Comparing base (f6c78d7) to head (4f9ec4c).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
...main/java/org/opensearch/common/lucene/Lucene.java 81.81% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main   #20193      +/-   ##
============================================
+ Coverage     73.32%   73.37%   +0.04%     
+ Complexity    71862    71848      -14     
============================================
  Files          5793     5792       -1     
  Lines        328644   328632      -12     
  Branches      47313    47309       -4     
============================================
+ Hits         240990   241118     +128     
+ Misses        68324    68199     -125     
+ Partials      19330    19315      -15     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

9137-9214: Pass IndexSettings to mapper initialization to ensure it matches the engine config's settings.

The createMapperServiceForContextAwareIndex() helper is parameterless and hardcoded to create settings with only INDEX_CONTEXT_AWARE_ENABLED_SETTING=true, but this method builds indexSettings with both INDEX_DERIVED_SOURCE_SETTING=true and INDEX_CONTEXT_AWARE_ENABLED_SETTING=true. The MapperService created will not reflect the derived-source configuration, causing the test to run without actually exercising the intended scenario.

Additionally, line 9201 initializes CodecService with config.getIndexSettings() instead of the locally built indexSettings, risking unintended drift.

Suggested fix:

  • Refactor createMapperServiceForContextAwareIndex() to accept IndexSettings as a parameter, or create a new overload
  • Update line 9201 to use indexSettings instead of config.getIndexSettings()
🤖 Fix all issues with AI agents
In `@server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java`:
- Around line 8658-8683: The test’s custom FilterDirectoryReader used in
createEngineForWrapper currently returns the raw DirectoryReader from
doWrapDirectoryReader, which strips the wrapper on reopen and weakens the
wrapper-preservation test; modify the anonymous FilterDirectoryReader
implementation of doWrapDirectoryReader to re-wrap the incoming DirectoryReader
(apply the same FilterDirectoryReader wrapper logic used on initial creation,
i.e., return a new FilterDirectoryReader(in, same SubReaderWrapper) that
preserves getReaderCacheHelper behavior) so that wrappers survive reopen/refresh
and the test actually exercises wrapper retention.
♻️ Duplicate comments (2)
CHANGELOG.md (1)

35-35: Minor typographical corrections needed.

The changelog entry has two minor issues:

  • "incase" should be "in case" (two words)
  • "non aborting" should be "non-aborting" (hyphenated compound adjective)
🔧 Suggested fix
-- LeafReader should not remove SubReaderWrappers incase IndexWriter encounters a non aborting Exception ([`#20193`](https://github.com/opensearch-project/OpenSearch/pull/20193))
+- LeafReader should not remove SubReaderWrappers in case IndexWriter encounters a non-aborting Exception ([`#20193`](https://github.com/opensearch-project/OpenSearch/pull/20193))
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

8578-8639: Assert on Translog.Index#source() (not engine.get(...)) so the test actually covers derived-source snapshot correctness.

Right now, “updated” verification uses engine.get(...) + stored fields, which can still pass even if newChangesSnapshot(...) returns a Translog.Index with missing/stale source() (the regression you’re targeting). Also, docs that were later updated will have multiple Translog.Index ops in the snapshot; using engine.get(...) reads only the latest state and can’t validate per-op source.

Proposed change (validate snapshot op source directly)
                 while ((operation = snapshot.next()) != null) {
                     if (operation instanceof Translog.Index) {
                         Translog.Index indexOp = (Translog.Index) operation;
                         String docId = indexOp.id();
-                        if (updatedDocs.contains(docId)) {
-                            // Verify updated content using get
-                            try (
-                                Engine.GetResult get = engine.get(
-                                    new Engine.Get(true, true, docId, newUid(docId)),
-                                    engine::acquireSearcher
-                                )
-                            ) {
-                                assertTrue("Document " + docId + " should exist", get.exists());
-                                StoredFields storedFields = get.docIdAndVersion().reader.storedFields();
-                                org.apache.lucene.document.Document document = storedFields.document(get.docIdAndVersion().docId);
-                                assertEquals(
-                                    "Document " + docId + " should have updated value",
-                                    "updated",
-                                    document.getField("value").stringValue()
-                                );
-                            }
-                        }
+                        assertNotNull("snapshot index op must have derived source", indexOp.source());
+                        final Map<String, Object> m =
+                            XContentHelper.convertToMap(indexOp.source(), false, MediaTypeRegistry.JSON).v2();
+
+                        // Distinguish original index vs update by version (original: 0..numDocs-1, update: >= numDocs + numDocsToDelete)
+                        final boolean isUpdateOp = indexOp.version() >= (long) numDocs + (long) numDocsToDelete;
+                        assertEquals("value mismatch for docId=" + docId, isUpdateOp ? "updated" : "test", m.get("value"));
                     } else if (operation instanceof Translog.Delete) {
                         String docId = ((Translog.Delete) operation).id();
                         assertTrue("Document " + docId + " should be in deleted set", deletedDocs.contains(docId));
                         // Verify document is actually deleted
                         try (
                             Engine.GetResult get = engine.get(
                                 new Engine.Get(true, false, docId, newUid(docId)),
                                 engine::acquireSearcher
                             )
                         ) {
                             assertFalse("Document " + docId + " should not exist", get.exists());
                         }
                     }
                     count++;
                 }

Also applies to: 8766-8827

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be52eec and a17a9a5.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
  • server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java
  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-13T17:40:27.167Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:27.167Z
Learning: In OpenSearch's CodecService, when registries are processed during construction (via EnginePlugin::getAdditionalCodecs), the defaultCodec supplier passed to CodecRegistry.getCodecs() must remain dynamic (this::defaultCodec) rather than captured upfront, because registries can replace the default codec during iteration. The contract is that registries should not invoke the supplier during getCodecs() execution since CodecService is still initializing.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
📚 Learning: 2025-12-02T22:44:14.799Z
Learnt from: prudhvigodithi
Repo: opensearch-project/OpenSearch PR: 20112
File: server/src/internalClusterTest/java/org/opensearch/search/slice/SearchSliceIT.java:73-81
Timestamp: 2025-12-02T22:44:14.799Z
Learning: In OpenSearch integration tests extending OpenSearchIntegTestCase, using `LuceneTestCase.SuppressCodecs("*")` triggers special handling that selects a random production codec from the CODECS array, while `SuppressCodecs("Asserting")` or other specific codec suppressions still allow Lucene's default codec randomization which may include the asserting codec. Use `SuppressCodecs("*")` when you need to completely avoid asserting codecs (e.g., for cross-thread StoredFieldsReader usage) while maintaining production codec test coverage.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
📚 Learning: 2026-01-13T17:40:27.167Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:27.167Z
Learning: Avoid capturing or evaluating a supplier (e.g., this::defaultCodec) upfront when passing it to a registry during object construction. If registries may replace defaults during iteration (as in EnginePlugin.getAdditionalCodecs), pass the supplier itself and only resolve it at use time. This ensures dynamic behavior is preserved during initialization and prevents premature binding of defaults in codecs/registry setup. This pattern should apply to similar initialization paths in Java server code where registries may mutate defaults during construction.

Applied to files:

  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
🧬 Code graph analysis (1)
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)
server/src/main/java/org/opensearch/common/lucene/index/DerivedSourceDirectoryReader.java (1)
  • DerivedSourceDirectoryReader (25-60)
🪛 LanguageTool
CHANGELOG.md

[grammar] ~35-~35: Use a hyphen to join words.
Context: ...pers incase IndexWriter encounters a non aborting Exception ([#20193](https://git...

(QB_NEW_EN_HYPHEN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: gradle-check
  • GitHub Check: precommit (25, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, macos-15)
  • GitHub Check: precommit (21, windows-2025, true)
  • GitHub Check: precommit (25, macos-15)
  • GitHub Check: precommit (21, windows-latest)
  • GitHub Check: precommit (25, ubuntu-latest)
  • GitHub Check: precommit (25, macos-15-intel)
  • GitHub Check: precommit (21, macos-15-intel)
  • GitHub Check: precommit (25, windows-latest)
  • GitHub Check: precommit (21, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, ubuntu-latest)
  • GitHub Check: detect-breaking-change
  • GitHub Check: assemble (21, windows-latest)
  • GitHub Check: assemble (21, ubuntu-24.04-arm)
  • GitHub Check: assemble (25, ubuntu-24.04-arm)
  • GitHub Check: assemble (25, windows-latest)
  • GitHub Check: assemble (21, ubuntu-latest)
  • GitHub Check: assemble (25, ubuntu-latest)
  • GitHub Check: Analyze (java)
🔇 Additional comments (11)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (3)

87-87: LGTM!

Import is correctly added for DerivedSourceLeafReader to support the derived source detection logic.


955-959: LGTM! Core fix correctly preserves wrapper chain for derived source.

This conditional correctly addresses the issue: when DerivedSourceLeafReader is in the wrapper chain, the original leaf is used (preserving the wrapper), otherwise the segmentReader is used (existing behavior). This ensures the source field can be reconstructed during translog replay.


966-984: LGTM! Helper method correctly detects derived source in wrapper chain.

The method properly handles the recursive unwrapping of FilterLeafReader to detect DerivedSourceLeafReader. The Javadoc accurately explains why FilterCodecReader is not recursively handled (it cannot wrap DerivedSourceLeafReader since the latter is an IndexReader, not a CodecReader).

test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (5)

44-46: LGTM!

All new imports are necessary for the new test utilities: FilterDirectoryReader/FilterLeafReader for wrapper handling, OpenSearchException for error handling in createEngineForWrapper, script-related imports for context-aware indexing tests, and CheckedFunction/Collection/HashMap for utility method signatures.

Also applies to: 67-67, 76-76, 119-119, 131-137, 151-151, 154-154


400-404: LGTM!

Clean helper method for creating test documents with grouping criteria.


719-753: LGTM!

The createEngineForWrapper method properly creates an engine with custom DirectoryReader wrapping capability. This enables tests to exercise the derived source wrapper behavior. The pattern follows existing createEngine methods while adding the wrapper hook via IndexShard.wrapSearcher.


1666-1692: LGTM!

The createMapperServiceForContextAwareIndex method properly wires up a MapperService with ScriptService support for context-aware indexing tests. The setup correctly enables the INDEX_CONTEXT_AWARE_ENABLED_SETTING and uses the custom ContextAwareCustomScriptPlugin for script execution.


1698-1727: LGTM!

The ContextAwareCustomScriptPlugin properly implements ScriptPlugin with mock scripts for testing context-aware indexing. The scripts for delete operation and grouping criteria are appropriately simple for test purposes.

test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java (3)

51-51: LGTM!

Import for ScriptService is correctly added to support the new overloads.


65-83: LGTM!

Clean refactoring that maintains backward compatibility: the original 4-parameter overload now delegates to the new 5-parameter overload with null for scriptService. Tests that don't need script support continue to work unchanged.


85-112: LGTM!

The method signature is updated to accept ScriptService and correctly passes it to the MapperService constructor at line 110. This enables context-aware indexing tests that require script execution capability.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
CHANGELOG.md (1)

35-35: Minor typographical corrections needed.

The entry has two minor issues:

  • "incase" → "in case" (two words)
  • "non aborting" → "non-aborting" (hyphenated compound adjective)
🔎 Suggested correction
-- LeafReader should not remove SubReaderWrappers incase IndexWriter encounters a non aborting Exception ([`#20193`](https://github.com/opensearch-project/OpenSearch/pull/20193))
+- LeafReader should not remove SubReaderWrappers in case IndexWriter encounters a non-aborting Exception ([`#20193`](https://github.com/opensearch-project/OpenSearch/pull/20193))
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (2)

8483-8644: Snapshot test likely misses the regression: assert Translog.Index#source() directly (not just engine.get()).

The “updated” assertions go through engine.get(...), so the test can pass even if engine.newChangesSnapshot(...) returned an index op with a null/stale source() (the bug this PR is about). Consider asserting ((Translog.Index) operation).source() is non-null and matches expected JSON for both the initial and updated ops (e.g., distinguish by operation.seqNo() / operation.version()).


8658-8683: Wrapper-preservation test is weakened: doWrapDirectoryReader returning in drops wrappers on reopen.

If the goal is to prove wrappers (including derived-source) survive refresh/reopen and aren’t stripped by unwrap logic, doWrapDirectoryReader(...) should re-wrap in (similar to DerivedSourceDirectoryReader#doWrapDirectoryReader), otherwise the wrapper can silently disappear and the test won’t exercise the intended behavior.

🧹 Nitpick comments (1)
test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (1)

719-753: Consider extracting duplicate store initialization logic.

The store initialization logic (lines 720-731) is duplicated from createEngine() (lines 761-773). Consider extracting this into a private helper method to improve maintainability.

Also, minor formatting: there are extra blank lines at lines 732 and 746-747 that could be removed for consistency.

♻️ Suggested refactoring approach
private void initializeStoreIfNeeded(EngineConfig config) throws IOException {
    final Store store = config.getStore();
    final Directory directory = store.directory();
    if (Lucene.indexExists(directory) == false) {
        store.createEmpty(config.getIndexSettings().getIndexVersionCreated().luceneVersion);
        final String translogUuid = Translog.createEmptyTranslog(
            config.getTranslogConfig().getTranslogPath(),
            SequenceNumbers.NO_OPS_PERFORMED,
            shardId,
            primaryTerm.get()
        );
        store.associateIndexWithNewTranslog(translogUuid);
    }
}

Then use this helper in both createEngineForWrapper() and createEngine().

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a17a9a5 and 290d88d.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
  • server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java
  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-13T17:40:27.167Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:27.167Z
Learning: In OpenSearch's CodecService, when registries are processed during construction (via EnginePlugin::getAdditionalCodecs), the defaultCodec supplier passed to CodecRegistry.getCodecs() must remain dynamic (this::defaultCodec) rather than captured upfront, because registries can replace the default codec during iteration. The contract is that registries should not invoke the supplier during getCodecs() execution since CodecService is still initializing.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
📚 Learning: 2025-12-02T22:44:14.799Z
Learnt from: prudhvigodithi
Repo: opensearch-project/OpenSearch PR: 20112
File: server/src/internalClusterTest/java/org/opensearch/search/slice/SearchSliceIT.java:73-81
Timestamp: 2025-12-02T22:44:14.799Z
Learning: In OpenSearch integration tests extending OpenSearchIntegTestCase, using `LuceneTestCase.SuppressCodecs("*")` triggers special handling that selects a random production codec from the CODECS array, while `SuppressCodecs("Asserting")` or other specific codec suppressions still allow Lucene's default codec randomization which may include the asserting codec. Use `SuppressCodecs("*")` when you need to completely avoid asserting codecs (e.g., for cross-thread StoredFieldsReader usage) while maintaining production codec test coverage.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🧬 Code graph analysis (2)
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (2)
server/src/main/java/org/opensearch/common/lucene/index/DerivedSourceDirectoryReader.java (1)
  • DerivedSourceDirectoryReader (25-60)
server/src/main/java/org/opensearch/index/engine/Engine.java (1)
  • Operation (1502-1602)
test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (3)
test/framework/src/main/java/org/opensearch/script/MockScriptEngine.java (1)
  • MockScriptEngine (74-825)
server/src/main/java/org/opensearch/script/ScriptModule.java (1)
  • ScriptModule (54-127)
test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java (1)
  • MapperTestUtils (63-161)
🪛 LanguageTool
CHANGELOG.md

[grammar] ~35-~35: Use a hyphen to join words.
Context: ...pers incase IndexWriter encounters a non aborting Exception ([#20193](https://git...

(QB_NEW_EN_HYPHEN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: gradle-check
  • GitHub Check: precommit (25, macos-15-intel)
  • GitHub Check: precommit (25, macos-15)
  • GitHub Check: precommit (21, ubuntu-latest)
  • GitHub Check: precommit (21, macos-15)
  • GitHub Check: precommit (21, windows-latest)
  • GitHub Check: precommit (21, windows-2025, true)
  • GitHub Check: precommit (25, ubuntu-latest)
  • GitHub Check: precommit (21, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, macos-15-intel)
  • GitHub Check: precommit (25, windows-latest)
  • GitHub Check: precommit (25, ubuntu-24.04-arm)
  • GitHub Check: Analyze (java)
  • GitHub Check: assemble (25, windows-latest)
  • GitHub Check: assemble (21, windows-latest)
  • GitHub Check: assemble (21, ubuntu-latest)
  • GitHub Check: assemble (21, ubuntu-24.04-arm)
  • GitHub Check: assemble (25, ubuntu-24.04-arm)
  • GitHub Check: assemble (25, ubuntu-latest)
  • GitHub Check: detect-breaking-change
🔇 Additional comments (4)
test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (3)

400-404: LGTM!

Simple and clear test utility method that follows the existing pattern of document creation helpers.


1666-1692: LGTM!

The method properly configures a context-aware MapperService with ScriptService wiring, following the existing pattern of createMapperService(). The use of ContextAwareCustomScriptPlugin and the new MapperTestUtils.newMapperService overload is appropriate.


1698-1727: Verify the intentional use of "painless" as the script language name.

The NAME constant is set to "painless", which is the same identifier as the actual Painless scripting engine. This appears intentional for mimicking Painless behavior in tests with MockScriptEngine, but please confirm this won't cause conflicts if tests are run with the actual Painless plugin loaded.

test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java (1)

65-111: LGTM!

Clean, backward-compatible refactoring that adds ScriptService injection capability while preserving existing API contracts:

  • 4-arg version delegates to 5-arg with null (preserves backward compatibility)
  • 5-arg version provides the new entry point for ScriptService injection
  • 6-arg version properly wires the scriptService to MapperService constructor

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@github-actions
Copy link
Contributor

❌ Gradle check result for 290d88d: FAILURE

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

8834-8994: Same snapshot assertion gap exists in the non-context-aware derived-source test.
For the bug this PR targets, validating Translog.Index#source() is the key signal; validating via engine.get() can mask the regression.

♻️ Duplicate comments (4)
CHANGELOG.md (1)

35-35: Minor typographical corrections needed.

The changelog entry has minor issues: "incase" should be "in case" (two words), and "non aborting" should be "non-aborting" (hyphenated compound adjective).

🔧 Suggested fix
-- LeafReader should not remove SubReaderWrappers incase IndexWriter encounters a non aborting Exception ([`#20193`](https://github.com/opensearch-project/OpenSearch/pull/20193))
+- LeafReader should not remove SubReaderWrappers in case IndexWriter encounters a non-aborting Exception ([`#20193`](https://github.com/opensearch-project/OpenSearch/pull/20193))
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (3)

9137-9215: EngineConfig factory: avoid unconditional context-aware MapperService + ensure config consistency.

  • createMapperServiceForContextAwareIndex() is used even when contextAwareEnabled is false.
  • CodecService (and potentially TranslogConfig) are built from config.getIndexSettings() while the EngineConfig is built with a different indexSettings instance—this can make the test setup internally inconsistent/brittle.
Proposed change (conditional mapper service + align CodecService with the new IndexSettings)
-        if (contextAwareEnabled == true) {
+        if (contextAwareEnabled) {
             settingBuilder.put(IndexSettings.INDEX_CONTEXT_AWARE_ENABLED_SETTING.getKey(), true);
             mapping = XContentFactory.jsonBuilder()
                 .startObject()
                 .startObject("_doc")
                 .startObject("properties")
                 .startObject("value")
                 .field("type", "text")
                 .field("store", true)
                 .endObject()
                 .endObject()
                 .startObject("context_aware_grouping")
                 .array("fields", "value")
                 .startObject("script")
                 .field("source", "String.valueOf(grouping_criteria)")
                 .endObject()
                 .endObject()
                 .endObject()
                 .endObject();
         } else {
             mapping = XContentFactory.jsonBuilder()
                 .startObject()
                 .startObject("_doc")
                 .startObject("properties")
                 .startObject("value")
                 .field("type", "text")
                 .field("store", true)
                 .endObject()
                 .endObject()
                 .endObject()
                 .endObject();
         }
@@
-        final MapperService mapperService = createMapperServiceForContextAwareIndex();
+        final MapperService mapperService = contextAwareEnabled ? createMapperServiceForContextAwareIndex() : createMapperService();
         mapperService.merge("_doc", new CompressedXContent(mapping.toString()), MapperService.MergeReason.MAPPING_UPDATE);
@@
-            .codecService(new CodecService(null, config.getIndexSettings(), logger))
+            .codecService(new CodecService(null, indexSettings, logger))

8480-8641: Snapshot assertions should validate Translog.Index#source() directly (not engine.get(...)).
Right now the “updated” verification can still pass even if newChangesSnapshot(...) returns index ops with missing/stale source().

Proposed change (assert snapshot content rather than engine state)
                 while ((operation = snapshot.next()) != null) {
                     if (operation instanceof Translog.Index) {
                         Translog.Index indexOp = (Translog.Index) operation;
                         String docId = indexOp.id();
+                        assertNotNull("snapshot index op must have source (derived)", indexOp.source());
                         if (updatedDocs.contains(docId)) {
-                            // Verify updated content using get
-                            try (
-                                Engine.GetResult get = engine.get(
-                                    new Engine.Get(true, true, docId, newUid(docId)),
-                                    engine::acquireSearcher
-                                )
-                            ) {
-                                assertTrue("Document " + docId + " should exist", get.exists());
-                                StoredFields storedFields = get.docIdAndVersion().reader.storedFields();
-                                org.apache.lucene.document.Document document = storedFields.document(get.docIdAndVersion().docId);
-                                assertEquals(
-                                    "Document " + docId + " should have updated value",
-                                    "updated",
-                                    document.getField("value").stringValue()
-                                );
-                            }
+                            final Map<String, Object> m =
+                                XContentHelper.convertToMap(indexOp.source(), false, MediaTypeRegistry.JSON).v2();
+                            assertEquals("updated", m.get("value"));
                         }
                     } else if (operation instanceof Translog.Delete) {
                         String docId = ((Translog.Delete) operation).id();
                         assertTrue("Document " + docId + " should be in deleted set", deletedDocs.contains(docId));

8643-8832: FilterDirectoryReader#doWrapDirectoryReader returning in likely drops the wrapper on reopen (weakening the test).
If the goal is to ensure SubReaderWrappers survive and aren’t stripped by an unwrap, the wrapper needs to be re-applied on reopen; also getReaderCacheHelper() should typically delegate to the current in, not the originally-captured reader.

Proposed change (re-wrap on reopen + delegate cache helper correctly)
-                engine = createEngineForWrapper(
-                    engineConfig,
-                    reader -> new FilterDirectoryReader(reader, new FilterDirectoryReader.SubReaderWrapper() {
+                final FilterDirectoryReader.SubReaderWrapper subReaderWrapper = new FilterDirectoryReader.SubReaderWrapper() {
                         `@Override`
                         public LeafReader wrap(LeafReader reader) {
                             return new FilterLeafReader(reader) {
                                 `@Override`
                                 public CacheHelper getCoreCacheHelper() {
                                     return in.getCoreCacheHelper();
                                 }
 
                                 `@Override`
                                 public CacheHelper getReaderCacheHelper() {
                                     return in.getReaderCacheHelper();
                                 }
                             };
                         }
-                    }) {
+                };
+
+                engine = createEngineForWrapper(
+                    engineConfig,
+                    reader -> new FilterDirectoryReader(reader, subReaderWrapper) {
                         `@Override`
                         protected DirectoryReader doWrapDirectoryReader(DirectoryReader in) throws IOException {
-                            return in;
+                            return new FilterDirectoryReader(in, subReaderWrapper) {
+                                `@Override`
+                                protected DirectoryReader doWrapDirectoryReader(DirectoryReader in) throws IOException {
+                                    return this;
+                                }
+
+                                `@Override`
+                                public CacheHelper getReaderCacheHelper() {
+                                    return in.getReaderCacheHelper();
+                                }
+                            };
                         }
 
                         `@Override`
                         public CacheHelper getReaderCacheHelper() {
-                            return reader.getReaderCacheHelper();
+                            return in.getReaderCacheHelper();
                         }
                     }
                 );
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 290d88d and c13f01f.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
  • server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java
  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-13T17:40:27.167Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:27.167Z
Learning: Avoid capturing or evaluating a supplier (e.g., this::defaultCodec) upfront when passing it to a registry during object construction. If registries may replace defaults during iteration (as in EnginePlugin.getAdditionalCodecs), pass the supplier itself and only resolve it at use time. This ensures dynamic behavior is preserved during initialization and prevents premature binding of defaults in codecs/registry setup. This pattern should apply to similar initialization paths in Java server code where registries may mutate defaults during construction.

Applied to files:

  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
📚 Learning: 2026-01-13T17:40:27.167Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:27.167Z
Learning: In OpenSearch's CodecService, when registries are processed during construction (via EnginePlugin::getAdditionalCodecs), the defaultCodec supplier passed to CodecRegistry.getCodecs() must remain dynamic (this::defaultCodec) rather than captured upfront, because registries can replace the default codec during iteration. The contract is that registries should not invoke the supplier during getCodecs() execution since CodecService is still initializing.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
📚 Learning: 2025-12-02T22:44:14.799Z
Learnt from: prudhvigodithi
Repo: opensearch-project/OpenSearch PR: 20112
File: server/src/internalClusterTest/java/org/opensearch/search/slice/SearchSliceIT.java:73-81
Timestamp: 2025-12-02T22:44:14.799Z
Learning: In OpenSearch integration tests extending OpenSearchIntegTestCase, using `LuceneTestCase.SuppressCodecs("*")` triggers special handling that selects a random production codec from the CODECS array, while `SuppressCodecs("Asserting")` or other specific codec suppressions still allow Lucene's default codec randomization which may include the asserting codec. Use `SuppressCodecs("*")` when you need to completely avoid asserting codecs (e.g., for cross-thread StoredFieldsReader usage) while maintaining production codec test coverage.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🧬 Code graph analysis (2)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (2)
server/src/main/java/org/opensearch/common/lucene/index/DerivedSourceLeafReader.java (1)
  • DerivedSourceLeafReader (25-68)
server/src/main/java/org/opensearch/index/codec/CriteriaBasedCodec.java (1)
  • CriteriaBasedCodec (26-72)
test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (1)
test/framework/src/main/java/org/opensearch/script/MockScriptEngine.java (1)
  • MockScriptEngine (74-825)
🪛 LanguageTool
CHANGELOG.md

[grammar] ~35-~35: Use a hyphen to join words.
Context: ...pers incase IndexWriter encounters a non aborting Exception ([#20193](https://git...

(QB_NEW_EN_HYPHEN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: gradle-check
  • GitHub Check: precommit (25, windows-latest)
  • GitHub Check: precommit (21, windows-2025, true)
  • GitHub Check: precommit (25, macos-15-intel)
  • GitHub Check: precommit (25, ubuntu-24.04-arm)
  • GitHub Check: precommit (25, ubuntu-latest)
  • GitHub Check: precommit (21, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, macos-15)
  • GitHub Check: precommit (25, macos-15)
  • GitHub Check: precommit (21, macos-15-intel)
  • GitHub Check: precommit (21, windows-latest)
  • GitHub Check: precommit (21, ubuntu-latest)
  • GitHub Check: detect-breaking-change
  • GitHub Check: assemble (21, ubuntu-24.04-arm)
  • GitHub Check: assemble (21, ubuntu-latest)
  • GitHub Check: assemble (25, ubuntu-latest)
  • GitHub Check: assemble (21, windows-latest)
  • GitHub Check: assemble (25, windows-latest)
  • GitHub Check: assemble (25, ubuntu-24.04-arm)
  • GitHub Check: Analyze (java)
🔇 Additional comments (7)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (2)

955-959: LGTM - Conditional wrapping preserves DerivedSourceLeafReader when needed.

The change correctly preserves the original leaf reader (with its DerivedSourceLeafReader wrapper intact) when derived source is enabled, while maintaining the previous behavior of using segmentReader otherwise. This ensures the source field can be properly reconstructed during translog replay for indices with Derived Source enabled.


966-984: Well-structured recursive detection of DerivedSourceLeafReader.

The method correctly traverses the reader chain to detect whether DerivedSourceLeafReader is present. The Javadoc appropriately explains why FilterCodecReader doesn't need special handling (it cannot wrap DerivedSourceLeafReader since that's an IndexReader, not a CodecReader).

The recursive approach via FilterLeafReader.getDelegate() correctly handles arbitrarily nested wrappers.

test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (4)

398-402: LGTM - Simple test utility method.

The method correctly creates a document with grouping criteria set for context-aware indexing tests.


717-752: LGTM - Test utility for creating engine with custom searcher wrapper.

The method correctly creates an InternalEngine that wraps searchers with a custom DirectoryReader wrapper. This enables testing of derived source behavior during translog replay. The exception handling appropriately wraps IOException in OpenSearchException for the test context.

Minor: There's an extra blank line at line 733 before the closing brace.


1665-1691: LGTM - Proper test setup for context-aware indexing with script support.

The method correctly configures a MapperService for context-aware index testing by:

  1. Enabling INDEX_CONTEXT_AWARE_ENABLED_SETTING
  2. Creating a ScriptService with the custom ContextAwareCustomScriptPlugin
  3. Using the MapperTestUtils.newMapperService overload that accepts a ScriptService

1697-1726: LGTM - Well-structured test script plugin for context-aware testing.

The ContextAwareCustomScriptPlugin follows the established pattern for test script plugins in OpenSearch:

  • Uses MockScriptEngine with the "painless" language name for compatibility with production scripts
  • Provides necessary scripts for context-aware grouping tests (ctx.op='delete' and String.valueOf(grouping_criteria))
  • Properly implements empty defaults for nonDeterministicPluginScripts() and pluginContextCompilers()
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

8416-8477: Good: derived-source snapshot test now uses the derived-source-aware EngineConfig factory.
This keeps the test wiring aligned with the newer context-aware toggle.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@github-actions
Copy link
Contributor

❌ Gradle check result for c13f01f: FAILURE

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (1)

8416-8476: Avoid double-closing store inside a try (Store store = ...) block.

The inner finally { IOUtils.close(engine, store); } will close store early, and then the outer try-with-resources will close it again. Prefer only closing engine in the inner finally.

Proposed fix
-            } finally {
-                IOUtils.close(engine, store);
-            }
+            } finally {
+                IOUtils.close(engine);
+            }

Also, asserting indexOp.source().utf8ToString() equals a hardcoded JSON string is a bit brittle; consider parsing and comparing the map (or at least asserting it contains "value":"test").

🤖 Fix all issues with AI agents
In `@server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java`:
- Around line 8488-8640: The test double-closes Store by using try (Store store
= createStore()) and then calling IOUtils.close(engine, store) in the finally
block; remove Store from explicit close there and only close engine (or let
try-with-resources close store) so ownership is not conflicted—specifically,
update the finally block that calls IOUtils.close(engine, store) to call
IOUtils.close(engine) (or IOUtils.close(engine) while leaving the
try-with-resources for Store intact) and ensure engine is null-checked before
closing.
- Around line 8651-8831: The test double-closes the Store by using
try-with-resources for Store (Store store = createStore()) and then passing both
engine and store to IOUtils.close(engine, store) in the finally; change the
finally to only close the engine (IOUtils.close(engine)) so the
try-with-resources still closes store automatically, leaving the engine variable
closed explicitly (reference symbols: Store store = createStore(), engine,
IOUtils.close(engine, store)).
♻️ Duplicate comments (5)
CHANGELOG.md (1)

35-35: Typographical corrections needed.

This has already been flagged: "incase" should be "in case" and "non aborting" should be "non-aborting" (hyphenated compound adjective).

server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (4)

8841-8992: Same snapshot-assertion gap and store double-close pattern repeats here.
This test also verifies “updated” docs via engine.get(...) instead of snapshot op source(), and closes store twice.


8480-8641: Snapshot assertions don’t validate Translog.Index#source() (can miss the regression).
The “updated” verification uses engine.get(...) + stored fields, which can still pass even if engine.newChangesSnapshot(...) returned a Translog.Index with null/wrong source().

Action: assert ((Translog.Index) operation).source() is non-null and matches "test" vs "updated" based on op identity (e.g., seqNo/version).


8651-8683: doWrapDirectoryReader returning in likely drops the wrapper on reopen (weakens wrapper-preservation test).
If the intent is to prove wrappers survive refresh/reopen and aren’t removed by unwrapping, the wrapper should be re-applied in doWrapDirectoryReader.


9137-9186: Use the non-context-aware MapperService when contextAwareEnabled == false.
Right now it always uses createMapperServiceForContextAwareIndex(), which can make the “contextAware disabled” test setup diverge from production wiring.

Proposed fix
-        final MapperService mapperService = createMapperServiceForContextAwareIndex();
+        final MapperService mapperService = contextAwareEnabled ? createMapperServiceForContextAwareIndex() : createMapperService();
🧹 Nitpick comments (1)
test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (1)

717-752: Engine factory with custom searcher wrapper enables derived source testing.

The implementation correctly wraps the searcher using IndexShard.wrapSearcher, which is the same mechanism used in production code. The IOException to OpenSearchException conversion is appropriate since acquireSearcher declares EngineException.

Minor style observation: there's an extra blank line at line 733.

🧹 Optional: Remove extra blank line
             store.associateIndexWithNewTranslog(translogUuid);
-
         }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c13f01f and 50d61ec.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
  • server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java
  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/framework/src/main/java/org/opensearch/index/MapperTestUtils.java
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-13T17:40:34.780Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:34.780Z
Learning: In OpenSearch's CodecService, when registries are processed during construction (via EnginePlugin::getAdditionalCodecs), the defaultCodec supplier passed to CodecRegistry.getCodecs() must remain dynamic (this::defaultCodec) rather than captured upfront, because registries can replace the default codec during iteration. The contract is that registries should not invoke the supplier during getCodecs() execution since CodecService is still initializing.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
📚 Learning: 2025-12-02T22:44:14.799Z
Learnt from: prudhvigodithi
Repo: opensearch-project/OpenSearch PR: 20112
File: server/src/internalClusterTest/java/org/opensearch/search/slice/SearchSliceIT.java:73-81
Timestamp: 2025-12-02T22:44:14.799Z
Learning: In OpenSearch integration tests extending OpenSearchIntegTestCase, using `LuceneTestCase.SuppressCodecs("*")` triggers special handling that selects a random production codec from the CODECS array, while `SuppressCodecs("Asserting")` or other specific codec suppressions still allow Lucene's default codec randomization which may include the asserting codec. Use `SuppressCodecs("*")` when you need to completely avoid asserting codecs (e.g., for cross-thread StoredFieldsReader usage) while maintaining production codec test coverage.

Applied to files:

  • test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java
📚 Learning: 2026-01-13T17:40:27.167Z
Learnt from: reta
Repo: opensearch-project/OpenSearch PR: 20411
File: server/src/main/java/org/opensearch/index/codec/CodecService.java:112-133
Timestamp: 2026-01-13T17:40:27.167Z
Learning: Avoid capturing or evaluating a supplier (e.g., this::defaultCodec) upfront when passing it to a registry during object construction. If registries may replace defaults during iteration (as in EnginePlugin.getAdditionalCodecs), pass the supplier itself and only resolve it at use time. This ensures dynamic behavior is preserved during initialization and prevents premature binding of defaults in codecs/registry setup. This pattern should apply to similar initialization paths in Java server code where registries may mutate defaults during construction.

Applied to files:

  • server/src/main/java/org/opensearch/common/lucene/Lucene.java
🧬 Code graph analysis (1)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (2)
server/src/main/java/org/opensearch/common/lucene/index/DerivedSourceLeafReader.java (1)
  • DerivedSourceLeafReader (25-68)
server/src/main/java/org/opensearch/index/codec/CriteriaBasedCodec.java (1)
  • CriteriaBasedCodec (26-72)
🪛 LanguageTool
CHANGELOG.md

[grammar] ~35-~35: Use a hyphen to join words.
Context: ...pers incase IndexWriter encounters a non aborting Exception ([#20193](https://git...

(QB_NEW_EN_HYPHEN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: gradle-check
  • GitHub Check: assemble (21, windows-latest)
  • GitHub Check: precommit (21, windows-latest)
  • GitHub Check: detect-breaking-change
  • GitHub Check: precommit (21, windows-2025, true)
  • GitHub Check: assemble (25, ubuntu-24.04-arm)
  • GitHub Check: precommit (25, ubuntu-latest)
  • GitHub Check: precommit (25, macos-15)
  • GitHub Check: assemble (21, ubuntu-24.04-arm)
  • GitHub Check: assemble (25, ubuntu-latest)
  • GitHub Check: precommit (21, macos-15)
  • GitHub Check: precommit (21, ubuntu-latest)
  • GitHub Check: precommit (21, macos-15-intel)
  • GitHub Check: assemble (25, windows-latest)
  • GitHub Check: precommit (25, macos-15-intel)
  • GitHub Check: precommit (25, ubuntu-24.04-arm)
  • GitHub Check: assemble (21, ubuntu-latest)
  • GitHub Check: precommit (21, ubuntu-24.04-arm)
  • GitHub Check: precommit (25, windows-latest)
  • GitHub Check: Analyze (java)
🔇 Additional comments (9)
server/src/main/java/org/opensearch/common/lucene/Lucene.java (3)

87-87: Import addition looks appropriate.

The import is needed for the instanceof check in the new isDerivedSourceEnabled helper method.


955-959: Conditional wrapping correctly preserves the DerivedSourceLeafReader wrapper.

When derived source is enabled, using leaf (the original reader) instead of segmentReader preserves the wrapper chain. This ensures that DerivedSourceLeafReader.storedFields() remains accessible during translog replay, fixing the null source field issue.


965-984: Recursive wrapper detection logic is sound.

The method correctly traverses the FilterLeafReader chain to detect DerivedSourceLeafReader. The Javadoc accurately explains that FilterCodecReader cannot wrap DerivedSourceLeafReader since it only accepts CodecReader delegates, while DerivedSourceLeafReader extends SequentialStoredFieldsLeafReader (a FilterLeafReader).

The default return false at line 983 is a safe fallback for unexpected reader types.

test/framework/src/main/java/org/opensearch/index/engine/EngineTestCase.java (4)

65-65: New imports support the test infrastructure additions.

The imports for OpenSearchException, CheckedFunction, IndexShard, script-related classes, and collection types are all utilized by the new test helper methods.


398-402: Simple test helper for documents with grouping criteria.

Follows the pattern of existing testDocument* helper methods.


1665-1691: MapperService factory for context-aware index testing.

The method correctly wires up the ScriptService with ContextAwareCustomScriptPlugin and configures the index settings with INDEX_CONTEXT_AWARE_ENABLED_SETTING. This enables testing of derived source scenarios that require script evaluation.


1697-1726: Context-aware script plugin provides test scripts for derived source scenarios.

The plugin correctly implements ScriptPlugin with a MockScriptEngine backend. The two registered scripts (ctx.op='delete' and String.valueOf(grouping_criteria)) cover the test cases needed for context-aware indexing functionality.

server/src/test/java/org/opensearch/index/engine/InternalEngineTests.java (2)

49-52: New Lucene filter imports look appropriate for the wrapper-preservation tests.


9190-9214: This concern is unfounded. While the code does mix settings objects, CodecService does not use the IndexSettings parameter when mapperService is null. Since the test passes null as the mapper service, CodecService only instantiates basic Lucene103Codec instances that have no dependency on IndexSettings. The choice between config.getIndexSettings() and the new indexSettings has zero functional impact in this context.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@github-actions
Copy link
Contributor

❌ Gradle check result for 50d61ec: FAILURE

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

@RS146BIJAY RS146BIJAY force-pushed the derived-source branch 3 times, most recently from 9809a69 to d3c0f7e Compare January 15, 2026 04:29
@github-actions
Copy link
Contributor

❌ Gradle check result for d3c0f7e: FAILURE

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

…ounters a non aborting Exception

Signed-off-by: RS146BIJAY <rishavsagar4b1@gmail.com>
@github-actions
Copy link
Contributor

✅ Gradle check result for 4f9ec4c: SUCCESS

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments