Skip to content

test(integ-test): fix three multi-shard integration-test defects - #5722

Merged
RyanL1997 merged 4 commits into
opensearch-project:mainfrom
mengweieric:fix/multishard-test-defects-main
Aug 26, 2026
Merged

test(integ-test): fix three multi-shard integration-test defects#5722
RyanL1997 merged 4 commits into
opensearch-project:mainfrom
mengweieric:fix/multishard-test-defects-main

Conversation

@mengweieric

@mengweieric mengweieric commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Description

Three integration-test defects that only surface when the test index has more than one primary shard. All three are test-side: every changed file is under src/test, and no production code is touched.

These have three different mechanisms, not one:

Fix Why multiple shards break it
Beer fixture mapping Dynamic-mapping race: which value reaches the coordinator's mapping update first varies with shard count
Max-out settings leak A test that fails leaves a transient cluster setting behind, poisoning later tests
Rex row-0 assertions With one shard an unordered query returns a stable incidental order that the assertion relies on; multi-shard merging changes it

1. Beer fixture has no mapping

BEER is registered with a null mapping, so the index relies on dynamic mapping. LastEditorUserId holds 18 integers and 7 "-1" strings, and whichever value reaches the coordinator's mapping update first decides the inferred type. That order varies with shard count, so the bulk load partially fails:

Real failure, e.g. CalciteMatchPhrasePrefixIT::required_parameters:

java.lang.IllegalStateException: Bulk load into [opensearch-sql_test_index_beer] had item failures
(errors=true). First failures:
  doc#1: {"reason":"mapper [LastEditorUserId] cannot be changed from type [text] to [long]",
          "type":"illegal_argument_exception"}
  doc#22: {"reason":"mapper [LastEditorUserId] cannot be changed from type [text] to [long]", ...}

Suites then fail for a reason unrelated to what they assert — several merely count beer rows. In a full 5-shard run this accounts for 22 failures; I directly re-verified 13 of them across the relevance and pagination suites I reran, and all 13 now pass.

Pinned as long; every string value is numerically coercible. No test references the field, so the change is semantically inert, and all other fields stay dynamic so text/keyword behaviour is unchanged.

2. Subsearch max-out settings leak on failure

setSubsearchMaxOut / setJoinSubsearchMaxOut write transient cluster settings, and 14 tests across five suites previously reset them only on the success path — 8 in CalcitePPLExistsSubqueryIT, 3 in CalcitePPLInSubqueryIT, and 1 each in CalcitePPLJoinIT, CalcitePPLScalarSubqueryIT and CalciteExplainIT. When an assertion failed, the reset did not run and the limit leaked. The leaked values are 5, 2, 1, 0 and -1, so a leak either caps later subsearches or silently makes them unlimited.

Cleanup is now centralized in PPLIntegTestCase. The setter methods mark each setting dirty before the cluster update, and an inherited JUnit @After resets any dirty setting after the test. Both resets are attempted even if one fails, with cleanup exceptions preserved. Tests keep their explicit success-path resets, while the centralized teardown provides the failure-path safety net without duplicating 14 try/finally blocks.

These are two independent settings, and each contaminates its own family:

  • testJoinSubsearchMaxOut leaks PPL_JOIN_SUBSEARCH_MAXOUT=5, after which later join tests return exactly 5 rows.
  • testSubsearchMaxOut leaks PPL_SUBSEARCH_MAXOUT=1, after which later IN-subquery tests collapse to exactly 1 row.

Both fail on a 5-shard run, so both leak, and the combined effect looks like a distributed-execution defect rather than two leaked settings.

Real failures — the two primaries, then a representative cascade of each:

CalcitePPLJoinIT::testJoinSubsearchMaxOut       AssertionError: expected:<10> but was:<15>   (primary)
CalcitePPLJoinIT::testJoinWithFieldList         AssertionError: expected:<6>  but was:<5>    (capped by the leak)
CalcitePPLInSubqueryIT::testFilterInSubquery    AssertionError: expected:<5>  but was:<1>    (capped by the leak)
5 shards before 5 shards after
CalcitePPLJoinIT 19 fail 3
CalcitePPLInSubqueryIT 8 fail 1

Those 4 remaining failures are left red on purpose — they are genuine multi-shard failures this PR does not attempt to fix, and were previously buried under the cascade. testJoinSubsearchMaxOut (expects 10, gets 15) and testSubsearchMaxOut (which row survives max=1) need a separate look.

The second query in testJoinSubsearchMaxOut is intentional: it runs after the explicit mid-test reset, verifies the default was restored, and expects 15 rows.

3. Rex tests assert on datarows[0]

Six tests read row 0 of an unsorted result, so on a multi-shard index row 0 is a different document:

testRexBasicFieldExtraction   1 shard: amberduke@pyrami.com   5 shards: nanettebates@quility.com
testRexChainedCommands        1 shard: Amber, A               5 shards: Nanette, N
testRexWithFiltering          1 shard: 880 Holmes Lane        5 shards: 789 Madison Street

Real failures:

testRexBasicFieldExtraction       ComparisonFailure: expected:<[amberduke@pyrami].com>
                                                     but was:<[nanettebates@quility].com>
testRexChainedCommands            ComparisonFailure: expected:<[Amber]> but was:<[Nanette]>
testRexNestedCaptureGroupsBugFix  AssertionError:    expected:<amberduke> but was:<null>

The null in the last one is the pattern legitimately not matching the document that happened to land first — not an extraction failure.

Why this is a test defect and not an engine bug: the row-count assertion passes at both shard counts (1000/1000), so no rows are lost or duplicated; and asserting concat(user,'@',domain) = email holds for 1000/1000 rows at both 1 and 5 shards, so extraction is correct on every row. Only the assumption that row 0 is a particular document is wrong.

Two are worse than order-dependent: testRexNestedCaptureGroupsBugFix restricts the domain to (pyrami|gmail|yahoo) — matching exactly 1 of 1000 documents — then takes head 1, so unless scan order puts that document first every capture is null and its second half calls getString on a null. That half was also wrapped in a datarows-not-empty conditional, which would let a filtering or rex regression pass vacuously; it now asserts the row is present.

Fixed by filtering to account_number = 1 so each assertion identifies its document, keeping an unfiltered query where cardinality was covered. Expected values are unchanged — adding a sort instead would have required rewriting three of them, since amberduke is account_number 1 while the minimum is 0.

Testing

integTestRemote against the same cluster at both shard counts, verifying the modified tests did not simply trade one failure mode for another:

1 shard 5 shards
touched suites 224 tests, 0 failures 224 tests, 4 failures — all the deliberate residue above
CalciteRexCommandIT pass 18/18, pushdown and no-pushdown routes

No expected value was changed to match multi-shard output.

Check List

  • New functionality includes testing.
  • Commits are signed per the DCO using --signoff or -s.

Remaining items are not applicable: this changes integration-test fixtures and assertions only, and adds no functionality or user-facing behaviour.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

The beer fixture is registered with a null mapping, so the index relies on
dynamic mapping. LastEditorUserId holds 18 integers and 7 "-1" strings, and
whichever value reaches the coordinator's mapping update first decides the
inferred type. With more than one shard that order varies, and the bulk load
partially fails:

  mapper [LastEditorUserId] cannot be changed from type [text] to [long]

Every suite using the fixture then fails for an unrelated reason -- 22 tests in
a 5-shard run, including relevance and pagination suites that merely count beer
rows.

Pin only that field as long; every string value is numerically coercible, and
the remaining fields stay on dynamic mapping so text/keyword behaviour for the
relevance suites is unchanged.

Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 3b6bb52)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 Multiple PR themes

Sub-PR theme: Fix beer fixture dynamic mapping race

Relevant files:

  • integ-test/src/test/resources/indexDefinitions/beer_index_mapping.json
  • integ-test/src/test/java/org/opensearch/sql/legacy/SQLIntegTestCase.java

Sub-PR theme: Add cleanup for subsearch settings leak

Relevant files:

  • integ-test/src/test/java/org/opensearch/sql/ppl/PPLIntegTestCase.java

Sub-PR theme: Fix rex test row-order assumptions

Relevant files:

  • integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteRexCommandIT.java

⚡ Recommended focus areas for review

Exception Swallowing

In resetModifiedSubsearchSettings(), if both resetSubsearchMaxOut() and resetJoinSubsearchMaxOut() throw exceptions, the first exception's stack trace is lost when the second is added as suppressed. The original exception context may be needed for debugging cleanup failures in test teardown.

public void resetModifiedSubsearchSettings() throws IOException {
  IOException failure = null;
  try {
    if (subsearchMaxOutDirty) {
      resetSubsearchMaxOut();
    }
  } catch (IOException e) {
    failure = e;
  }

  try {
    if (joinSubsearchMaxOutDirty) {
      resetJoinSubsearchMaxOut();
    }
  } catch (IOException e) {
    if (failure == null) {
      failure = e;
    } else {
      failure.addSuppressed(e);
    }
  }

  if (failure != null) {
    throw failure;
  }
}

@mengweieric mengweieric added the testing Related to improving software testing label Aug 25, 2026
@mengweieric
mengweieric force-pushed the fix/multishard-test-defects-main branch from 6700e67 to 626b2c1 Compare August 25, 2026 20:23
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 626b2c1

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 3b6bb52

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Reset dirty flags after cleanup

The @After method doesn't reset the dirty flags before throwing exceptions. If an
exception occurs during reset, the flags remain true, causing subsequent tests to
attempt redundant resets. Reset the flags immediately after calling the reset
methods to ensure proper cleanup state.

integ-test/src/test/java/org/opensearch/sql/ppl/PPLIntegTestCase.java [463-489]

 @After
 public void resetModifiedSubsearchSettings() throws IOException {
   IOException failure = null;
   try {
     if (subsearchMaxOutDirty) {
       resetSubsearchMaxOut();
+      subsearchMaxOutDirty = false;
     }
   } catch (IOException e) {
     failure = e;
   }
 
   try {
     if (joinSubsearchMaxOutDirty) {
       resetJoinSubsearchMaxOut();
+      joinSubsearchMaxOutDirty = false;
     }
   } catch (IOException e) {
     if (failure == null) {
       failure = e;
     } else {
       failure.addSuppressed(e);
     }
   }
 
   if (failure != null) {
     throw failure;
   }
 }
Suggestion importance[1-10]: 3

__

Why: The suggestion identifies a potential issue where subsearchMaxOutDirty and joinSubsearchMaxOutDirty flags aren't reset if exceptions occur. However, the resetSubsearchMaxOut() and resetJoinSubsearchMaxOut() methods already set these flags to false (lines 444, 460), so the suggested changes are redundant. The score is low because while the concern is valid, the implementation already handles it correctly.

Low

Previous suggestions

Suggestions up to commit 5467473
CategorySuggestion                                                                                                                                    Impact
General
Avoid variable name reuse

The second query execution after the try-finally block reuses the variable name
actual and executes the same query. This appears to be testing behavior after reset,
but the variable is already declared. Either remove the duplicate query or use a
different variable name like actualAfterReset.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJoinIT.java [1126-1143]

 setJoinSubsearchMaxOut(5);
 try {
   JSONObject actual =
       executeQuery(
           String.format(
               "source=%s | where country = 'Canada' | join type=inner max=0 country %s",
               TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
   verifyNumOfRows(actual, 10);
 } finally {
   resetJoinSubsearchMaxOut();
 }
-JSONObject actual =
+JSONObject actualAfterReset =
     executeQuery(
         String.format(
             "source=%s | where country = 'Canada' | join type=inner max=0 country %s",
             TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-verifyNumOfRows(actual, 15);
+verifyNumOfRows(actualAfterReset, 15);
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that the variable actual is reused after the try-finally block. While this is valid Java (the second declaration shadows the first), using a distinct variable name like actualAfterReset would improve code clarity and make the intent of testing post-reset behavior more explicit.

Low
Suggestions up to commit 626b2c1
CategorySuggestion                                                                                                                                    Impact
General
Avoid variable name shadowing

The second query execution after the try-finally block reuses the variable name
actual and expects 15 rows, but resetJoinSubsearchMaxOut() has already been called.
This appears to be testing the default behavior, but the variable shadowing and lack
of clear separation makes the test intent unclear and potentially fragile.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJoinIT.java [1126-1143]

 setJoinSubsearchMaxOut(5);
 try {
   JSONObject actual =
       executeQuery(
           String.format(
               "source=%s | where country = 'Canada' | join type=inner max=0 country %s",
               TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
   verifyNumOfRows(actual, 10);
 } finally {
   resetJoinSubsearchMaxOut();
 }
-JSONObject actual =
+JSONObject actualDefault =
     executeQuery(
         String.format(
             "source=%s | where country = 'Canada' | join type=inner max=0 country %s",
             TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION));
-verifyNumOfRows(actual, 15);
+verifyNumOfRows(actualDefault, 15);
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies variable name reuse that could reduce code clarity. However, the second query appears to verify default behavior after reset, which is a valid test pattern. Renaming to actualDefault would improve readability but has limited impact on correctness.

Low

setSubsearchMaxOut and setJoinSubsearchMaxOut write transient cluster settings,
and 14 tests across five suites reset them only on the success path. When an
assertion fails the reset never runs and the limit leaks into every later test in
the run. The leaked values are 5, 2, 1, 0 and -1, so a leak either caps later
subsearches or silently makes them unlimited.

These are two independent settings, and each contaminates its own family:

  * testJoinSubsearchMaxOut leaks PPL_JOIN_SUBSEARCH_MAXOUT=5, after which later
    join tests return exactly 5 rows.
  * testSubsearchMaxOut leaks PPL_SUBSEARCH_MAXOUT=1, after which later
    IN-subquery tests collapse to exactly 1 row.

Both fail on a 5-shard run, so both leak, and the combined effect looks like a
distributed-execution defect rather than two leaked settings.

Move every reset into a finally block. No assertion or expected value changes.
The query after the finally in testJoinSubsearchMaxOut is intentional -- it
verifies the default was restored and expects 15 rows.

Signed-off-by: Eric Wei <menwe@amazon.com>
Six rex tests assert on datarows[0] of an unsorted result. Row order is
unspecified, so on a multi-shard index row 0 is a different document and the
assertions fail while the extraction itself is correct.

Two are worse than order-dependent. testRexNestedCaptureGroupsBugFix restricts
the domain to (pyrami|gmail|yahoo) -- a pattern matching exactly 1 of the 1000
documents -- then takes `head 1`, so unless scan order puts that document first
every capture is null and its second half calls getString on a null. That half
was also wrapped in a datarows-not-empty conditional, which would let a
filtering or rex regression pass vacuously; it now asserts the row is present.

Filter to account_number = 1 so each assertion identifies its document, keeping
an unfiltered query where cardinality was covered. Expected values are unchanged.

Verified that extraction is correct for every row -- concat(user,'@',domain) =
email holds 1000/1000 at both 1 and 5 shards -- so narrowing the assertion hides
no extraction defect.

Signed-off-by: Eric Wei <menwe@amazon.com>
@mengweieric
mengweieric force-pushed the fix/multishard-test-defects-main branch from 626b2c1 to 5467473 Compare August 25, 2026 20:33
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 5467473

Comment thread integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteExplainIT.java Outdated
Track subsearch and join max-out mutations in PPLIntegTestCase and reset dirty settings from an inherited JUnit teardown. Mark settings dirty before update so partially applied changes are still cleaned up, and attempt both resets while preserving cleanup failures.\n\nRestore individual tests to their simple success-path resets; the centralized teardown now provides the failure-path safety net.

Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 3b6bb52

@RyanL1997
RyanL1997 merged commit a0788ec into opensearch-project:main Aug 26, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testing Related to improving software testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants