Skip to content

Fix dynamic native-memory settings that are accepted but not applied (rebalancer interval/thresholds, spill limit)#22303

Open
gaurav-amz wants to merge 13 commits into
opensearch-project:mainfrom
gaurav-amz:fix/spill-limit-validator-cross-scope
Open

Fix dynamic native-memory settings that are accepted but not applied (rebalancer interval/thresholds, spill limit)#22303
gaurav-amz wants to merge 13 commits into
opensearch-project:mainfrom
gaurav-amz:fix/spill-limit-validator-cross-scope

Conversation

@gaurav-amz

@gaurav-amz gaurav-amz commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Fix 1 — Changing the rebalancer interval silently turned the rebalancer off

Setting: native.allocator.rebalance.interval_seconds (arrow-base)

updateRebalanceInterval() first called cancelRebalanceTask(), which sets the rebalancer and rebalancerScheduler fields to null. It then tried to reschedule — but guarded that reschedule on those same fields being non-null. They were just nulled, so the reschedule branch was unreachable dead code.

The visible effect: you could PUT a new interval and get a 200, but the rebalancer never ran again. It stayed off until someone toggled the enabled flag or restarted the node.

Fix: rebuild the rebalancer through startRebalancer(), using the allocator and budget supplier that were captured at startup. An interval of 0 still means "disabled", as before.

Fix 2 — The rebalancer ignored thresholds set in opensearch.yml

Settings: native.allocator.rebalance.{pressure_threshold, idle_threshold, shrink_factor} (arrow-base)

startRebalancer() built the NativeMemoryRebalancer using getDefault(Settings.EMPTY) for these three thresholds. Settings.EMPTY has nothing in it, so this always returned the hard-coded defaults — any value you put in opensearch.yml was thrown away at boot, and a disable/enable cycle reset live values back to defaults.

Fix: seed the thresholds from the live ClusterSettings (which includes both opensearch.yml values and any applied dynamic overrides) instead of from an empty settings object.

Fix 3 — A valid spill limit was rejected on a node where spill is actually enabled

Setting: datafusion.spill_memory_limit_bytes (analytics-backend-datafusion)

This setting's validator checks the requested limit against the spill volume's real capacity. To find the volume, it depends on datafusion.spill_directory. The catch: spill_directory is NodeScope + Final, so it lives only in opensearch.yml and never appears in cluster state.

When you do a dynamic PUT _cluster/settings, OpenSearch validates each setting scope in isolation, so the validator's dependency on spill_directory came back empty — even on a node where spill is fully configured. The validator then assumed "spill is disabled" and rejected any non-zero limit with a misleading "spill_directory is unset" error.

Fix: capture the node-scoped spill directory once at startup, and have the validator fall back to it when the dependency map is empty. Now the capacity check runs against the real configured volume, so valid limits are accepted and only genuinely-too-large values are rejected (with the correct "exceeds spill volume capacity" message).


Related Issues

N/A — internal audit findings.

Check List

  • All tests pass.
  • New functionality includes testing.
  • Commits are signed per the DCO using --signoff.

…ry on dynamic update

datafusion.spill_memory_limit_bytes is a Dynamic cluster setting whose
SpillLimitValidator depends on datafusion.spill_directory. But spill_directory
is NodeScope+Final, so it lives only in opensearch.yml and never reaches cluster
state. During a PUT _cluster/settings the framework validates each scope in
isolation (SettingsUpdater#updateSettings) against a cluster-state-only view, so
the dependency resolves empty even on a node where spill is configured and
enabled. The validator then took the "spill disabled" branch and rejected every
non-zero limit with "...is non-zero but datafusion.spill_directory is unset",
making the cap effectively unsettable via the API on a spill-enabled node.

Capture the node-scoped spill directory at startup (createComponents) into a
static field and have SpillLimitValidator fall back to it when the dependency
map is empty. The volume-capacity check now runs against the real configured
directory on a dynamic update: values within the volume are accepted, values
exceeding it are rejected with the existing capacity message. Genuine
spill-disabled nodes (no directory anywhere) still accept any value as an inert
no-op, preserving the startup behavior from opensearch-project#22275.

Adds tests for both the dynamic-update accept and reject paths, plus a tearDown
that resets the static field between tests.

Signed-off-by: snghsvn <snghsvn@amazon.com>
…eed configured thresholds

Two defects in the native-allocator rebalancer settings:

1. native.allocator.rebalance.interval_seconds (dynamic) permanently STOPPED the
   rebalancer instead of changing its cadence. updateRebalanceInterval called
   cancelRebalanceTask() — which nulls `rebalancer` and `rebalancerScheduler` —
   then guarded the reschedule on those same fields being non-null, so the
   scheduleAtFixedRate branch was unreachable dead code. A PUT of the interval
   was accepted (200) but silently disabled rebalancing until enable was toggled
   or the node restarted. Fix: rebuild via startRebalancer using the allocator and
   budget supplier captured in buildAllocator; interval 0 still means "disabled".

2. native.allocator.rebalancer.{pressure,idle,shrink}_threshold were ignored at
   startup. startRebalancer constructed NativeMemoryRebalancer with
   getDefault(Settings.EMPTY) for all three thresholds, so values set in
   opensearch.yml were ignored at boot and a disable/enable cycle reset any
   applied dynamic values to defaults. Fix: seed from the live ClusterSettings
   (yml + applied overrides). The dynamic update consumers were already correct.

Adds package-private test accessors (current rebalancer, task-scheduled flag,
interval-update driver, threshold getters) and unit tests covering both fixes.

Signed-off-by: snghsvn <snghsvn@amazon.com>
…allocator

parquet.native.pool.{write,merge}.min are dynamic settings, but their update
consumers were only registered in the allocator-present branch of
ParquetDataFormatPlugin. In the no-allocator branch only the two MAX consumers
were wired, so a PUT of write/merge pool min was accepted (200) yet silently
dropped.

Pool-min is a virtual-pool floor enforced by the arrow-base allocator's
rebalancer; the raw Rust pools (RustBridge) have only a max, so there is
genuinely nothing to apply a min to without the allocator. Rather than wire a
non-existent Rust min setter, register the MIN consumers in the no-allocator
branch to log a warning that the update has no effect on this node — making the
no-op observable instead of silent, mirroring the spill-limit restart warning.

Signed-off-by: snghsvn <snghsvn@amazon.com>
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit ae040cb)

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 rebalancer interval/threshold dynamic updates in arrow-base

Relevant files:

  • plugins/arrow-base/src/main/java/org/opensearch/arrow/allocator/ArrowBasePlugin.java
  • plugins/arrow-base/src/main/java/org/opensearch/arrow/allocator/NativeMemoryRebalancer.java
  • plugins/arrow-base/src/test/java/org/opensearch/arrow/allocator/ArrowBasePluginTests.java

Sub-PR theme: Fix spill limit validator cross-scope dependency

Relevant files:

  • sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionPlugin.java
  • sandbox/plugins/analytics-backend-datafusion/src/test/java/org/opensearch/be/datafusion/DataFusionPluginSettingsTests.java

Sub-PR theme: Stabilize spill e2e test by forcing single partition

Relevant files:

  • sandbox/plugins/analytics-backend-datafusion/rust/src/spill_e2e_test.rs

⚡ Recommended focus areas for review

Potential Enable Race

The REBALANCER_ENABLED_SETTING consumer now calls startRebalancer(allocator, budgetSupplier, this.rebalanceIntervalSeconds) on enable. If the interval field was never updated after startup and REBALANCER_ENABLED_SETTING.get(settings) was false at boot, rebalanceIntervalSeconds is still seeded correctly in buildAllocator. However, if the enable consumer fires before buildAllocator completes its field seeding in some test/lifecycle path, rebalancerAllocator/rebalancerBudgetSupplier could be null. Verify this ordering is guaranteed since the consumer is registered after the fields are set — appears safe but worth confirming for edge cases where enabled=true is applied dynamically before allocator wiring finishes.

cs.addSettingsUpdateConsumer(REBALANCER_ENABLED_SETTING, enabled -> {
    if (enabled == false) {
        cancelRebalanceTask();
        allocator.resetAllPoolsToMax();
    } else {
        startRebalancer(allocator, budgetSupplier, this.rebalanceIntervalSeconds);
    }
});
Static Field Across Nodes

configuredSpillDir is a static volatile field. In integration tests or embedded scenarios where multiple DataFusionPlugin instances could coexist in the same JVM (e.g., multi-node internal test clusters), the last-written value wins for all instances, potentially causing the validator to reference a directory that does not belong to the node being validated. Consider whether the current test infrastructure exercises this or if scoping this per-instance would be safer.

static volatile String configuredSpillDir = "";

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to ae040cb

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Avoid restarting a disabled rebalancer

updateRebalanceInterval unconditionally tears down and rebuilds the rebalancer even
when the rebalancer is currently disabled (via REBALANCER_ENABLED_SETTING=false).
This will spuriously start a rebalancer that the operator has disabled. Gate the
rebuild on the enabled state.

plugins/arrow-base/src/main/java/org/opensearch/arrow/allocator/ArrowBasePlugin.java [464-480]

 private synchronized void updateRebalanceInterval(long newInterval) {
-    // Track the live interval so the enable/disable consumer can rebuild with the right cadence
-    // without a stale clusterSettings.get().
     this.rebalanceIntervalSeconds = newInterval;
-    // cancelRebalanceTask() nulls rebalancer and rebalancerScheduler, so we cannot reschedule
-    // the old task — we must rebuild via startRebalancer (which seeds thresholds from the live
-    // fields). A new interval of 0 disables rebalancing: tear down and leave it stopped.
+    // Only reschedule if a rebalancer is currently active; otherwise just record the interval
+    // so a later enable picks it up.
+    if (rebalancer == null && rebalancerScheduler == null) {
+        return;
+    }
     cancelRebalanceTask();
     if (newInterval <= 0) {
         return;
     }
     ArrowNativeAllocator a = this.rebalancerAllocator;
     Supplier<Long> budget = this.rebalancerBudgetSupplier;
     if (a != null && budget != null) {
         startRebalancer(a, budget, newInterval);
     }
 }
Suggestion importance[1-10]: 7

__

Why: Valid concern: updateRebalanceInterval will rebuild the rebalancer even when it was explicitly disabled via REBALANCER_ENABLED_SETTING=false, causing a spurious restart. The suggested gate on current rebalancer state prevents this behavioral bug.

Medium
General
Document or fix static state sharing

A static volatile field will be shared across all DataFusionPlugin instances in the
same JVM (e.g. integration tests running multiple nodes in one process). If one node
has spill configured and another does not, the validator may apply the wrong node's
directory for capacity checks. Consider making this an instance field and having the
validator reference the specific plugin instance, or clearly document the
single-node-per-JVM assumption.

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionPlugin.java [132]

+// Node-scoped; static because SpillLimitValidator is a static inner class. Assumes a single
+// DataFusionPlugin instance per JVM (production nodes; integration test clusters should be
+// aware that all nodes share this value).
 static volatile String configuredSpillDir = "";
Suggestion importance[1-10]: 4

__

Why: The concern about static state sharing across plugin instances in multi-node integration tests is reasonable, but the improved code only adds a comment rather than fixing the issue. Low-to-moderate impact since production nodes have one plugin instance per JVM.

Low

Previous suggestions

Suggestions up to commit cce1014
CategorySuggestion                                                                                                                                    Impact
General
Serialize enable/disable with other transitions

The enable/disable consumer is not synchronized, but it mutates
rebalancer/rebalancerScheduler via cancelRebalanceTask()/startRebalancer() which are
synchronized on this. If a concurrent updateRebalanceInterval runs between
cancelRebalanceTask() and startRebalancer() inside the enable branch, or if enable
and disable races overlap, state can become inconsistent (e.g. an orphan scheduler).
Synchronize the whole consumer body on this to serialize with the other lifecycle
transitions.

plugins/arrow-base/src/main/java/org/opensearch/arrow/allocator/ArrowBasePlugin.java [323-330]

 cs.addSettingsUpdateConsumer(REBALANCER_ENABLED_SETTING, enabled -> {
-    if (enabled == false) {
-        cancelRebalanceTask();
-        allocator.resetAllPoolsToMax();
-    } else {
-        startRebalancer(allocator, budgetSupplier, this.rebalanceIntervalSeconds);
+    synchronized (this) {
+        if (enabled == false) {
+            cancelRebalanceTask();
+            allocator.resetAllPoolsToMax();
+        } else {
+            startRebalancer(allocator, budgetSupplier, this.rebalanceIntervalSeconds);
+        }
     }
 });
Suggestion importance[1-10]: 5

__

Why: The observation is reasonable: the enable/disable consumer calls synchronized methods but is not itself synchronized, which could allow interleaving with updateRebalanceInterval. However, since cancelRebalanceTask and startRebalancer are individually synchronized and cluster settings consumers typically run serially on the cluster applier thread, the practical impact is limited.

Low
Suggestions up to commit e613fdb
CategorySuggestion                                                                                                                                    Impact
Possible issue
Avoid re-enabling rebalancer on interval change

updateRebalanceInterval unconditionally rebuilds the rebalancer even when it was
disabled via REBALANCER_ENABLED_SETTING=false. This will silently re-enable
rebalancing whenever the interval is changed on a node where the operator has
explicitly disabled it. Guard the restart by also checking the enabled setting (or
remember a "disabled" flag) before calling startRebalancer.

plugins/arrow-base/src/main/java/org/opensearch/arrow/allocator/ArrowBasePlugin.java [408-424]

 private synchronized void updateRebalanceInterval(long newInterval) {
-    // Track the live interval so the enable/disable consumer can rebuild with the right cadence
-    // without a stale clusterSettings.get().
     this.rebalanceIntervalSeconds = newInterval;
-    // cancelRebalanceTask() nulls rebalancer and rebalancerScheduler, so we cannot reschedule
-    // the old task — we must rebuild via startRebalancer (which seeds thresholds from the live
-    // fields). A new interval of 0 disables rebalancing: tear down and leave it stopped.
     cancelRebalanceTask();
     if (newInterval <= 0) {
+        return;
+    }
+    if (clusterSettings != null && clusterSettings.get(REBALANCER_ENABLED_SETTING) == false) {
         return;
     }
     ArrowNativeAllocator a = this.rebalancerAllocator;
     Supplier<Long> budget = this.rebalancerBudgetSupplier;
     if (a != null && budget != null) {
         startRebalancer(a, budget, newInterval);
     }
 }
Suggestion importance[1-10]: 7

__

Why: Valid concern: updateRebalanceInterval does not check REBALANCER_ENABLED_SETTING, so changing the interval on a node where rebalancing was explicitly disabled would silently restart it. This is a real behavioral bug, though severity is moderate.

Medium
General
Static state shared across plugin instances

configuredSpillDir is a static field but is populated in instance method
createComponents. In test environments (or any process with multiple plugin
instances/nodes) the last initialized node will overwrite the value for all others,
and stale values can leak across tests despite the tearDown reset elsewhere.
Consider making it an instance field and binding the validator via a non-static
inner class, or document/enforce the single-node assumption explicitly.

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionPlugin.java [583-588]

 String spillDir = DATAFUSION_SPILL_DIRECTORY.get(settings);
-// Capture the node-scoped spill directory so SpillLimitValidator can fall back to it on a
-// dynamic PUT _cluster/settings, where the Final/NodeScope spill_directory dependency is
-// absent from cluster state. Set before reading the spill limit below so the validator
-// invoked by DATAFUSION_SPILL_MEMORY_LIMIT.get(settings) sees the configured directory.
+// Single-JVM/single-node assumption: this static is shared across all plugin instances in
+// the JVM. Acceptable in production (one node per JVM) but fragile under multi-node tests.
 configuredSpillDir = spillDir == null ? "" : spillDir;
 long spillMemoryLimit = DATAFUSION_SPILL_MEMORY_LIMIT.get(settings);
Suggestion importance[1-10]: 4

__

Why: The observation about static field state leakage is legitimate for tests/multi-instance scenarios, but the PR already documents the single-node-per-JVM assumption and resets in tearDown. The suggested improved_code only adds a comment without an actual fix.

Low
Suggestions up to commit e613fdb
CategorySuggestion                                                                                                                                    Impact
Possible issue
Avoid re-enabling a disabled rebalancer

updateRebalanceInterval unconditionally rebuilds the rebalancer when newInterval >
0, even if the rebalancer was previously disabled via
REBALANCER_ENABLED_SETTING=false. This will silently re-enable rebalancing against
the operator's explicit disable. Guard the rebuild on the current enabled state so a
disabled rebalancer stays disabled across interval changes.

plugins/arrow-base/src/main/java/org/opensearch/arrow/allocator/ArrowBasePlugin.java [408-424]

 private synchronized void updateRebalanceInterval(long newInterval) {
-    // Track the live interval so the enable/disable consumer can rebuild with the right cadence
-    // without a stale clusterSettings.get().
     this.rebalanceIntervalSeconds = newInterval;
-    // cancelRebalanceTask() nulls rebalancer and rebalancerScheduler, so we cannot reschedule
-    // the old task — we must rebuild via startRebalancer (which seeds thresholds from the live
-    // fields). A new interval of 0 disables rebalancing: tear down and leave it stopped.
     cancelRebalanceTask();
     if (newInterval <= 0) {
         return;
     }
     ArrowNativeAllocator a = this.rebalancerAllocator;
     Supplier<Long> budget = this.rebalancerBudgetSupplier;
-    if (a != null && budget != null) {
+    if (a != null && budget != null && REBALANCER_ENABLED_SETTING.get(clusterSettings.getNodeSettings())) {
         startRebalancer(a, budget, newInterval);
     }
 }
Suggestion importance[1-10]: 7

__

Why: Valid concern: if rebalancing was disabled via REBALANCER_ENABLED_SETTING=false, a subsequent interval change would rebuild and re-enable it. However, the exact improved_code references clusterSettings.getNodeSettings() which may not be available; the underlying issue is real and worth addressing.

Medium
General
Document static-field multi-node hazard

configuredSpillDir is a static field but createComponents is an instance method that
can run per-plugin-instance (notably in tests with multiple nodes in a single JVM).
If two nodes configure different spill directories, the last writer wins and
validators on other nodes will probe the wrong volume. Consider documenting this
single-JVM-node assumption or routing the fallback through a per-node mechanism.

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionPlugin.java [582-588]

 long memoryPoolLimit = DATAFUSION_MEMORY_POOL_LIMIT.get(settings);
 String spillDir = DATAFUSION_SPILL_DIRECTORY.get(settings);
-// Capture the node-scoped spill directory so SpillLimitValidator can fall back to it on a
-// dynamic PUT _cluster/settings, where the Final/NodeScope spill_directory dependency is
-// absent from cluster state. Set before reading the spill limit below so the validator
-// invoked by DATAFUSION_SPILL_MEMORY_LIMIT.get(settings) sees the configured directory.
+// NOTE: static field — assumes a single node per JVM. Multi-node test harnesses sharing a
+// JVM will see the last writer's value; the validator probes that one volume for all nodes.
 configuredSpillDir = spillDir == null ? "" : spillDir;
 long spillMemoryLimit = DATAFUSION_SPILL_MEMORY_LIMIT.get(settings);
Suggestion importance[1-10]: 3

__

Why: The suggestion only adds a documentation comment about a static field's multi-node JVM hazard, which is a minor improvement and not functionally impactful.

Low
Suggestions up to commit b025b72
CategorySuggestion                                                                                                                                    Impact
General
Synchronize enable/disable consumer for atomicity

The enable/disable consumer is not synchronized, while startRebalancer and
cancelRebalanceTask are synchronized on this. However, the consumer reads
this.rebalanceIntervalSeconds (a volatile) and could race with
updateRebalanceInterval. More critically, if the interval consumer fires before the
enable consumer in the same apply cycle, ordering across consumers is fine, but
consider also synchronizing this lambda to ensure atomic enable/disable transitions
and consistent visibility with updateRebalanceInterval.

plugins/arrow-base/src/main/java/org/opensearch/arrow/allocator/ArrowBasePlugin.java [323-330]

 cs.addSettingsUpdateConsumer(REBALANCER_ENABLED_SETTING, enabled -> {
-    if (enabled == false) {
-        cancelRebalanceTask();
-        allocator.resetAllPoolsToMax();
-    } else {
-        startRebalancer(allocator, budgetSupplier, this.rebalanceIntervalSeconds);
+    synchronized (this) {
+        if (enabled == false) {
+            cancelRebalanceTask();
+            allocator.resetAllPoolsToMax();
+        } else {
+            startRebalancer(allocator, budgetSupplier, this.rebalanceIntervalSeconds);
+        }
     }
 });
Suggestion importance[1-10]: 4

__

Why: Adding synchronized (this) around the enable/disable lambda may improve atomicity with updateRebalanceInterval, but the inner methods are already synchronized and rebalanceIntervalSeconds is volatile, so the actual race risk is minor. Cluster settings consumers also typically run serialized.

Low
Distinguish absent vs explicitly-empty dependency

Falling back to configuredSpillDir when the dependency value is non-null but empty
changes the original validation semantics: previously an explicit empty
spill_directory in dependencies meant "spill disabled". Now an explicit empty value
gets overridden by the captured startup value. Distinguish the "dependency absent"
case (null) from "dependency explicitly empty" (treated as disabled) to preserve the
explicit-disable signal.

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionPlugin.java [158-165]

 String dir = (String) dependencies.get(DATAFUSION_SPILL_DIRECTORY);
-if (dir == null || dir.isEmpty()) {
-    // The dependency resolves empty during a dynamic PUT _cluster/settings because
-    // spill_directory is NodeScope+Final and never appears in cluster state. Fall back
-    // to the directory captured from node settings at startup so a spill-enabled node
-    // still gets its capacity check (rather than blindly accepting any value).
+if (dir == null) {
+    // Dependency absent (dynamic PUT _cluster/settings path: spill_directory is
+    // NodeScope+Final, so not in cluster state). Fall back to startup-captured value.
     dir = configuredSpillDir;
 }
Suggestion importance[1-10]: 3

__

Why: The suggestion raises a semantic question, but since spill_directory is NodeScope+Final it cannot be explicitly set to empty via a dynamic PUT, so the distinction between null and empty has limited practical effect. The PR's behavior is reasonable for the documented use case.

Low
Suggestions up to commit 5c2e80d
CategorySuggestion                                                                                                                                    Impact
General
Warn when enabling with non-positive interval

When the rebalancer is enabled via a combined PUT that also changes the interval,
the consumer ordering may invoke this enable-consumer before the interval consumer
updates this.rebalanceIntervalSeconds. Read the live field via the same defensive
pattern (or also accept that interval consumer will rebuild). More importantly, if
the user enables the rebalancer with rebalanceIntervalSeconds <= 0 (disabled state),
startRebalancer will silently no-op; consider logging or seeding a default to avoid
an enabled-but-not-running state.

plugins/arrow-base/src/main/java/org/opensearch/arrow/allocator/ArrowBasePlugin.java [323-330]

 cs.addSettingsUpdateConsumer(REBALANCER_ENABLED_SETTING, enabled -> {
     if (enabled == false) {
         cancelRebalanceTask();
         allocator.resetAllPoolsToMax();
     } else {
-        startRebalancer(allocator, budgetSupplier, this.rebalanceIntervalSeconds);
+        long interval = this.rebalanceIntervalSeconds;
+        if (interval <= 0) {
+            logger.warn("Rebalancer enabled but interval is {}s; rebalancer will not run", interval);
+            return;
+        }
+        startRebalancer(allocator, budgetSupplier, interval);
     }
 });
Suggestion importance[1-10]: 3

__

Why: Adding a warning log for enabled-but-non-positive interval is a minor UX improvement; it does not fix a correctness issue since startRebalancer already no-ops safely.

Low
Avoid static state leaking across instances

configuredSpillDir is a non-final static field written from createComponents. If
multiple plugin instances are constructed (e.g., across tests in the same JVM, or
hypothetical multi-node test fixtures), the last writer wins and validators on other
instances will use the wrong directory. Consider making the validator reference the
plugin instance, or document/assert single-instance usage to avoid
cross-contamination beyond test tearDown.

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionPlugin.java [587-588]

+// Single-instance plugin: static is acceptable, but reset on close() to avoid leaking
+// across plugin lifecycles in tests/embedded usage.
 configuredSpillDir = spillDir == null ? "" : spillDir;
 long spillMemoryLimit = DATAFUSION_SPILL_MEMORY_LIMIT.get(settings);
Suggestion importance[1-10]: 3

__

Why: The concern about static state is valid in theory, but the improved_code only adds a comment without any functional change, providing minimal value.

Low

@github-actions

Copy link
Copy Markdown
Contributor

❌ Gradle check result for 749f33c: 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?

@gaurav-amz gaurav-amz closed this Jun 24, 2026
@gaurav-amz gaurav-amz reopened this Jun 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 749f33c

…terSettings.get()

The rebalancer rebuild path (startRebalancer, and the enable consumer) re-read
sibling settings via clusterSettings.get(PRESSURE/IDLE/SHRINK_THRESHOLD) and
get(REBALANCE_INTERVAL). During a PUT _cluster/settings apply cycle, get()
resolves against lastSettingsApplied, which the framework only swaps in AFTER
all update consumers have run — so a single PUT that changes the interval (or
toggles enabled) together with a threshold rebuilt the rebalancer with the
STALE pre-PUT threshold. It only self-corrected because the threshold consumers
happen to be registered after the interval consumer and ran later in the same
batch — correctness by incidental ordering, not by design.

Replace the cross-setting get() calls with volatile fields: pressureThreshold,
idleThreshold, shrinkFactor, rebalanceIntervalSeconds. Seeded from node settings
at startup (buildAllocator — not mid-cycle, so reading `settings` is correct)
and kept current by the same update consumers. startRebalancer and the enable
consumer now seed from these fields, so a combined PUT rebuilds with the correct
new values regardless of consumer ordering.

Adds testCombinedIntervalAndThresholdPutSeedsRebuiltRebalancerWithNewThreshold,
which drives a real applySettings() cycle changing interval+threshold together
and asserts the rebuilt rebalancer carries the new threshold.

Signed-off-by: snghsvn <snghsvn@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 151172f

@github-actions

Copy link
Copy Markdown
Contributor

❌ Gradle check result for 151172f: 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?

@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 4ecff02

…e aggregate partition

group_by_spills_to_disk_and_returns_correct_results failed intermittently in CI
with "Failed to reserve memory for sort during spill: ResourcesExhausted ... for
GroupedHashAggregateStream[1]".

Root cause: the test ran target_partitions=2, so two GroupedHashAggregateStreams
shared one 12 MiB DynamicLimitPool. Below the 16 MiB RSS gate that pool is pure
greedy/first-come-first-serve (== GreedyMemoryPool). When a partition spills it
frees its own state and then reserves ~32 KiB of sort headroom (datafusion
row_hash.rs `spill()`); if a sibling partition has already greedily filled the
pool, that 32 KiB reservation fails and the spill errors. Whether partition 0
fills the pool before partition 1 grabs its headroom depends on thread
scheduling, so the failure was nondeterministic. DataFusion's own docs say to use
FairSpillPool (not greedy) when multiple operators all spill, and its aggregate
spill tests do exactly that.

Set target_partitions=1: a single aggregator frees its own state before reserving
sort headroom, so there is no cross-partition starvation and spill is
deterministic. The ~500k-group hash table still dwarfs the 12 MiB pool, so the
test still forces a real spill through the production pool to disk and verifies
result correctness — exactly what it exists to prove.

Signed-off-by: snghsvn <snghsvn@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit dfd1e12

@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 69b9d10

… native allocator"

This reverts commit 749f33c.

Signed-off-by: snghsvn <snghsvn@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit deefeb4

@gaurav-amz gaurav-amz closed this Jun 24, 2026
@gaurav-amz gaurav-amz reopened this Jun 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit deefeb4

@github-actions

Copy link
Copy Markdown
Contributor

✅ Gradle check result for deefeb4: SUCCESS

@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.35%. Comparing base (b2bc553) to head (ae040cb).

Files with missing lines Patch % Lines
...rg/opensearch/arrow/allocator/ArrowBasePlugin.java 80.95% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main   #22303      +/-   ##
============================================
- Coverage     73.45%   73.35%   -0.10%     
+ Complexity    76195    76157      -38     
============================================
  Files          6076     6076              
  Lines        345767   345785      +18     
  Branches      49761    49762       +1     
============================================
- Hits         253968   253647     -321     
- Misses        71531    71913     +382     
+ Partials      20268    20225      -43     

☔ View full report in Codecov by Harness.
📢 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.

@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 5c2e80d

@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b025b72

@github-actions

Copy link
Copy Markdown
Contributor

✅ Gradle check result for b025b72: SUCCESS

@gaurav-amz gaurav-amz marked this pull request as ready for review June 25, 2026 05:18
@gaurav-amz gaurav-amz requested a review from a team as a code owner June 25, 2026 05:18
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit e613fdb

@gaurav-amz gaurav-amz closed this Jun 25, 2026
@gaurav-amz gaurav-amz reopened this Jun 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit e613fdb

@gaurav-amz gaurav-amz changed the title Fix dynamic native-memory settings that are accepted but not applied (rebalancer interval/thresholds, spill limit, parquet pool-min) Fix dynamic native-memory settings that are accepted but not applied (rebalancer interval/thresholds, spill limit) Jun 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Gradle check result for e613fdb: SUCCESS

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit cce1014

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

✅ Gradle check result for cce1014: SUCCESS

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit ae040cb

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

✅ Gradle check result for ae040cb: SUCCESS

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant