Skip to content

Extract arrow-base plugin - #21465

Merged
rishabhmaurya merged 28 commits into
opensearch-project:mainfrom
bowenlan-amzn:experiment/arrow-base-plugin
May 17, 2026
Merged

Extract arrow-base plugin#21465
rishabhmaurya merged 28 commits into
opensearch-project:mainfrom
bowenlan-amzn:experiment/arrow-base-plugin

Conversation

@bowenlan-amzn

@bowenlan-amzn bowenlan-amzn commented May 4, 2026

Copy link
Copy Markdown
Member

Description

Apache Arrow's JVM classes have object identity tied to (classloader, FQN). If two plugins each bundle their own copy of arrow-vector, a VectorSchemaRoot produced by one can't be consumed by the other — different classloaders, different classes, ClassCastException at the handoff. This PR introduces arrow-base as the single place Arrow lives: every Arrow-using plugin declares extendedPlugins = ['arrow-base'] and shares its classloader.

flowchart TD
  base[arrow-base<br/>bundles Arrow + Netty]
  flight[arrow-flight-rpc]
  analytics[analytics-engine]
  parquet[parquet-data-format]
  dfusion[analytics-backend-datafusion]
  flight --> base
  analytics --> base
  parquet --> base
  dfusion --> analytics
Loading

ArrowAllocatorService

The previous static ArrowAllocatorProvider is replaced with a proper node-level service. ArrowBasePlugin provides DefaultArrowAllocatorService — one root allocator per node, child allocators for each consumer.

PluginComponentRegistry

Plugins are initialized in dependency order (topologically sorted by extendedPlugins). This PR introduces PluginComponentRegistry — a typed lookup that accumulates components as each plugin initializes. Later plugins discover services from their dependencies at createComponents() time without Guice or deferred resolution.

FlightStreamPlugin and AnalyticsPlugin obtain ArrowAllocatorService from the registry directly. ArrowBasePlugin also provides a Guice binding via createGuiceModules() for @Inject consumers (DefaultPlanExecutor, transport actions).

Plugin close order

Node.close() now closes plugins in reverse dependency order, so child allocators are released before the root. This eliminates the need for defensive exception swallowing on shutdown.

Testing

Integration tests using FlightStreamPlugin must declare it in additionalNodePlugins() with extendedPlugins = [ArrowBasePlugin] so the test framework sorts initialization correctly (the standard nodePlugins() API doesn't preserve dependency metadata).

Non-goals

arrow-c-data stays in parquet (FFM-only). Flight-internal types stay in flight — only types used by ≥2 plugins move.

Related Issues

N/A

Check List

  • Functionality includes testing.
  • API changes companion pull request created, if applicable.
  • Public documentation issue/PR created, if applicable.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

@bowenlan-amzn
bowenlan-amzn force-pushed the experiment/arrow-base-plugin branch from d557143 to 35e224a Compare May 4, 2026 05:09
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 35e224a.

PathLineSeverityDescription
plugins/arrow-base/src/main/plugin-metadata/plugin-security.policy35highRuntimePermission wildcard '*' grants all Java runtime permissions. The comment says 'setContextClassLoader' but that is the actions field (ignored for RuntimePermission), not the name — the granted name is '*', matching every RuntimePermission. This is a blanket privilege escalation far beyond any Arrow or Netty requirement.
plugins/arrow-base/src/main/plugin-metadata/plugin-security.policy27highSocketPermission '*' with accept,connect,listen,resolve grants unrestricted inbound and outbound network access to every host and port. No Arrow memory-management or Netty allocator operation requires listening or accepting connections. This enables arbitrary data exfiltration or backdoor listener from a plugin classpath.
plugins/arrow-base/build.gradle22highNew dependency on org.apache.arrow:arrow-vector, arrow-format, arrow-memory-core, arrow-memory-netty, arrow-memory-netty-buffer-patch introduced in a new plugin module. Per mandatory rule, all new dependency additions must be flagged for maintainer verification of artifact authenticity.
plugins/arrow-base/build.gradle33highNew dependency on org.checkerframework:checker-qual:3.44.0 added with a pinned version literal (not via a versions catalog variable). This annotation processor runs at compile time and is a supply-chain risk; maintainers should verify the artifact hash and source.
plugins/arrow-flight-rpc/src/main/java/org/opensearch/arrow/flight/transport/FlightTransport.java147mediumFlightTransport now creates its root allocator as a child of ArrowAllocatorProvider with limit Long.MAX_VALUE, previously it used Integer.MAX_VALUE with a fresh RootAllocator. Sharing a root allocator across plugins means memory exhaustion in one plugin can affect all others; this is architecturally unusual and warrants review.
plugins/arrow-base/src/main/java/org/opensearch/arrow/transport/ArrowAllocatorProvider.java37mediumRootAllocator is created with Long.MAX_VALUE (effectively unbounded) and shared statically across the JVM. A malicious or buggy plugin calling newChildAllocator with Long.MAX_VALUE could exhaust off-heap direct memory with no cap, enabling a denial-of-service on the node.
plugins/arrow-base/src/main/plugin-metadata/plugin-security.policy29mediumGrants modifyThreadGroup and modifyThread RuntimePermissions. Combined with the wildcard SocketPermission and broad reflection permissions also granted in this policy, these permissions together form a nearly complete privilege set that could be exploited by malicious code loaded into the arrow-base classloader.

The table above displays the top 10 most important findings.

Total: 7 | Critical: 0 | High: 4 | Medium: 3 | Low: 0


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

@bowenlan-amzn bowenlan-amzn added the skip-diff-analyzer Maintainer to skip code-diff-analyzer check, after reviewing issues in AI analysis. label May 4, 2026
@bowenlan-amzn
bowenlan-amzn force-pushed the experiment/arrow-base-plugin branch from 35e224a to 2ef1707 Compare May 4, 2026 16:41
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 6ebbe9d)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Possible Resource Leak

If newChildAllocator is called after close(), the child allocator is created from a closed root. Arrow's behavior on closed allocators is undefined — it may throw, return a broken allocator, or silently succeed. The service does not track whether it has been closed, so callers can invoke newChildAllocator post-close and receive an allocator that may fail unpredictably during buffer operations.

public BufferAllocator newChildAllocator(String name, long limit) {
    return root.newChildAllocator(name, 0, limit);
}
Possible Issue

The code reverses the plugin list and then adds it to toClose, but the original toClose.add(plugin) loop at lines 2205-2207 already added plugins in forward order. This results in plugins being closed twice — once in forward order from the loop, then again in reverse order from the reversed list. The first close may succeed, but the second will attempt to close already-closed plugins, potentially throwing exceptions or leaving resources in an inconsistent state.

// their dependencies (e.g. child allocators close before the root allocator owner).
List<Plugin> pluginsToClose = new ArrayList<>(pluginsService.filterPlugins(Plugin.class));
Collections.reverse(pluginsToClose);
toClose.addAll(pluginsToClose);
Possible Issue

The receive-side constructor claims ownership via claimOwnership() but does not close the StreamInput. If the input holds resources beyond the Arrow batch (e.g., network handles, temp buffers), they leak. The contract is unclear whether the caller or the response owns the input's lifecycle after construction.

protected ArrowBatchResponse(StreamInput in) throws IOException {
    super(in);
    if (in instanceof ArrowStreamInput arrowIn) {
        this.batchRoot = arrowIn.getRoot();
        arrowIn.claimOwnership();
    } else {
        throw new IllegalStateException(
            "ArrowBatchResponse decoded from a non-Arrow StreamInput ("
                + (in == null ? "null" : in.getClass().getName())
                + "). Wrapping handlers around ArrowBatchResponseHandler must forward "
                + "TransportResponseHandler#skipsDeserialization()."
        );
    }
}
Possible Issue

After seal(), both register and getComponent throw IllegalStateException because components is set to null. However, the error message for getComponent says "lookups are no longer allowed," but the real issue is that the registry has been sealed and discarded. If a plugin legitimately needs to look up a component after initialization (e.g., during a later lifecycle hook), this design forces it to cache the component, which may not be feasible for all use cases. The seal mechanism is overly restrictive if the registry is only meant to prevent registration after initialization, not lookups.

public <T> Optional<T> getComponent(Class<T> type) {
    if (components == null) {
        throw new IllegalStateException("PluginComponentRegistry is sealed; lookups are no longer allowed");
    }
    for (Object component : components) {
        if (type.isInstance(component)) {
            return Optional.of((T) component);
        }
    }
    return Optional.empty();
}

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 6ebbe9d

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Handle unexpected exceptions during close

Catching only IllegalStateException may miss other exceptions during allocator
closure. Consider catching a broader exception type or re-throwing after logging to
ensure unexpected errors during shutdown are not silently suppressed.

plugins/arrow-base/src/main/java/org/opensearch/arrow/memory/DefaultArrowAllocatorService.java [54-62]

 @Override
 public void close() {
     try {
         root.close();
     } catch (IllegalStateException e) {
-        // Outstanding child allocators remain open — likely a consumer plugin that didn't
-        // clean up. Log at warn so the leak is visible without crashing shutdown.
         logger.warn("Arrow root allocator closed with outstanding children: {}", e.getMessage());
+    } catch (Exception e) {
+        logger.error("Unexpected error closing Arrow root allocator", e);
+        throw e;
     }
 }
Suggestion importance[1-10]: 6

__

Why: Catching broader exceptions during close() improves robustness by ensuring unexpected errors are logged and potentially re-thrown. This prevents silent failures during shutdown, which is important for maintainability and debugging.

Low
Validate component is not null

Registering a null component will cause issues during lookup. Add a null check to
reject null components at registration time, preventing silent failures later.

server/src/main/java/org/opensearch/plugins/DefaultPluginComponentRegistry.java [36-41]

 public void register(Object component) {
     if (components == null) {
         throw new IllegalStateException("PluginComponentRegistry is sealed; registration is no longer allowed");
     }
+    if (component == null) {
+        throw new IllegalArgumentException("Cannot register null component");
+    }
     components.add(component);
 }
Suggestion importance[1-10]: 5

__

Why: Adding a null check at registration time prevents silent failures during lookup and improves API robustness. While not critical, it enhances code quality by failing fast with a clear error message.

Low
Remove redundant null check

The null check for in is redundant because super(in) would have already thrown a
NullPointerException if in were null. Remove the null check from the error message
to simplify the code and avoid misleading diagnostics.

plugins/arrow-base/src/main/java/org/opensearch/arrow/transport/ArrowBatchResponse.java [87-99]

 protected ArrowBatchResponse(StreamInput in) throws IOException {
     super(in);
     if (in instanceof ArrowStreamInput arrowIn) {
         this.batchRoot = arrowIn.getRoot();
         arrowIn.claimOwnership();
     } else {
         throw new IllegalStateException(
             "ArrowBatchResponse decoded from a non-Arrow StreamInput ("
-                + (in == null ? "null" : in.getClass().getName())
+                + in.getClass().getName()
                 + "). Wrapping handlers around ArrowBatchResponseHandler must forward "
                 + "TransportResponseHandler#skipsDeserialization()."
         );
     }
 }
Suggestion importance[1-10]: 4

__

Why: The null check for in is indeed redundant since super(in) would throw NullPointerException first. However, the impact is minimal—it only simplifies error messaging slightly without affecting correctness or security.

Low
Use safer list access method

Accessing getFirst() on the field vectors list without checking if it's empty can
throw NoSuchElementException. Although there's an empty check, the error message
suggests the root is empty when vectors exist but the list is empty. Verify the
check covers all edge cases or add a more specific validation.

plugins/arrow-flight-rpc/src/main/java/org/opensearch/arrow/flight/transport/VectorStreamInput.java [68-82]

 static VectorStreamInput forNativeArrow(VectorSchemaRoot streamRoot, NamedWriteableRegistry registry) {
-    if (streamRoot.getFieldVectors().isEmpty()) {
+    List<FieldVector> fieldVectors = streamRoot.getFieldVectors();
+    if (fieldVectors.isEmpty()) {
         throw new IllegalArgumentException("Cannot create native Arrow input from empty stream root");
     }
     VectorSchemaRoot consumerRoot = VectorSchemaRoot.create(
         streamRoot.getSchema(),
-        streamRoot.getFieldVectors().getFirst().getAllocator()
+        fieldVectors.get(0).getAllocator()
     );
     try {
         VectorTransfer.transferRoot(streamRoot, consumerRoot);
     } catch (Throwable t) {
         consumerRoot.close();
         throw t;
     }
     return new NativeArrow(consumerRoot, registry);
 }
Suggestion importance[1-10]: 3

__

Why: The suggestion to use get(0) instead of getFirst() is a minor style improvement. The empty check already guards against NoSuchElementException, so the change has minimal impact on correctness or safety.

Low

Previous suggestions

Suggestions up to commit 6f70664
CategorySuggestion                                                                                                                                    Impact
Possible issue
Validate null before instanceof check

The null check for in is performed after attempting instanceof, which will never be
true for null. Move the null check before the instanceof check to avoid misleading
error messages and ensure proper validation order.

plugins/arrow-base/src/main/java/org/opensearch/arrow/transport/ArrowBatchResponse.java [87-100]

 protected ArrowBatchResponse(StreamInput in) throws IOException {
     super(in);
+    if (in == null) {
+        throw new IllegalStateException("ArrowBatchResponse decoded from a null StreamInput");
+    }
     if (in instanceof ArrowStreamInput arrowIn) {
         this.batchRoot = arrowIn.getRoot();
         arrowIn.claimOwnership();
     } else {
         throw new IllegalStateException(
             "ArrowBatchResponse decoded from a non-Arrow StreamInput ("
-                + (in == null ? "null" : in.getClass().getName())
+                + in.getClass().getName()
                 + "). Wrapping handlers around ArrowBatchResponseHandler must forward "
                 + "TransportResponseHandler#skipsDeserialization()."
         );
     }
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the null check for in occurs after the instanceof check, which will never be true for null. Moving the null check before instanceof improves clarity and ensures proper validation order. However, the current code already handles null correctly in the error message, so this is a minor improvement in code structure rather than a critical bug fix.

Medium
General
Handle broader exception types during close

Catching only IllegalStateException may miss other critical exceptions during
allocator closure. Consider catching broader exceptions or at minimum logging the
full exception with stack trace to aid debugging of resource leaks.

plugins/arrow-base/src/main/java/org/opensearch/arrow/memory/DefaultArrowAllocatorService.java [54-62]

 @Override
 public void close() {
     try {
         root.close();
     } catch (IllegalStateException e) {
         // Outstanding child allocators remain open — likely a consumer plugin that didn't
         // clean up. Log at warn so the leak is visible without crashing shutdown.
-        logger.warn("Arrow root allocator closed with outstanding children: {}", e.getMessage());
+        logger.warn("Arrow root allocator closed with outstanding children", e);
+    } catch (Exception e) {
+        logger.error("Unexpected error closing Arrow root allocator", e);
     }
 }
Suggestion importance[1-10]: 5

__

Why: The suggestion to catch broader exceptions and log the full stack trace is reasonable for debugging resource leaks. However, the current implementation intentionally catches only IllegalStateException to handle the specific case of outstanding child allocators without crashing shutdown. Adding a catch-all for other exceptions could be useful, but the improvement is moderate since the current approach is already defensive and logs the issue at warn level.

Low
Suggestions up to commit 4de448a
CategorySuggestion                                                                                                                                    Impact
General
Ensure registry sealing on exception

If a plugin's createComponents throws an exception, the registry remains unsealed
and partially populated. This could lead to inconsistent state if the exception is
caught and handled elsewhere. Consider wrapping the loop in a try-finally block to
ensure seal() is always called, or handle exceptions to clean up the registry state.

server/src/main/java/org/opensearch/node/Node.java [1152-1174]

 final DefaultPluginComponentRegistry pluginComponentRegistry = new DefaultPluginComponentRegistry();
 final List<Object> pluginComponents = new ArrayList<>();
-for (Plugin p : pluginsService.filterPlugins(Plugin.class)) {
-    Collection<Object> components = p.createComponents(
-        ...
-        pluginComponentRegistry
-    );
-    for (Object component : components) {
-        pluginComponentRegistry.register(component);
+try {
+    for (Plugin p : pluginsService.filterPlugins(Plugin.class)) {
+        Collection<Object> components = p.createComponents(
+            ...
+            pluginComponentRegistry
+        );
+        for (Object component : components) {
+            pluginComponentRegistry.register(component);
+        }
+        pluginComponents.addAll(components);
     }
-    pluginComponents.addAll(components);
+} finally {
+    pluginComponentRegistry.seal();
 }
-pluginComponentRegistry.seal();
Suggestion importance[1-10]: 8

__

Why: The suggestion addresses a potential issue where an exception during plugin initialization could leave the registry unsealed. Wrapping the loop in a try-finally block ensures seal() is always called, improving robustness. This is a valid and important improvement.

Medium
Reorder null check before usage

The null check for in is performed after attempting to use it in the instanceof
check and super(in) call. If in is null, super(in) will fail before the error
message can mention it. Move the null check before any usage of in to provide a
clearer error path.

plugins/arrow-base/src/main/java/org/opensearch/arrow/transport/ArrowBatchResponse.java [87-100]

 protected ArrowBatchResponse(StreamInput in) throws IOException {
     super(in);
-    if (in instanceof ArrowStreamInput arrowIn) {
-        this.batchRoot = arrowIn.getRoot();
-        arrowIn.claimOwnership();
-    } else {
+    if (!(in instanceof ArrowStreamInput)) {
         throw new IllegalStateException(
             "ArrowBatchResponse decoded from a non-Arrow StreamInput ("
                 + (in == null ? "null" : in.getClass().getName())
                 + "). Wrapping handlers around ArrowBatchResponseHandler must forward "
                 + "TransportResponseHandler#skipsDeserialization()."
         );
     }
-}
+    ArrowStreamInput arrowIn = (ArrowStreamInput) in;
+    this.batchRoot = arrowIn.getRoot();
+    arrowIn.claimOwnership();
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the null check for in occurs after super(in) is called, which would fail if in is null. However, the improved code still calls super(in) before the null check, so the issue is not fully resolved. The suggestion is valid but the improved code does not accurately reflect the fix.

Medium
Validate component is not null

Registering a null component will succeed but cause getComponent to fail with a
NullPointerException when checking type.isInstance(component). Validate that
component is not null before adding it to the registry to fail fast at registration
time.

server/src/main/java/org/opensearch/plugins/DefaultPluginComponentRegistry.java [36-41]

 public void register(Object component) {
     if (components == null) {
         throw new IllegalStateException("PluginComponentRegistry is sealed; registration is no longer allowed");
     }
+    if (component == null) {
+        throw new IllegalArgumentException("Cannot register null component");
+    }
     components.add(component);
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that registering a null component could cause issues later in getComponent. Adding a null check at registration time provides fail-fast behavior, which is a good practice. This is a valid improvement for robustness.

Medium
Add null check in close

The close() method does not check if root is already closed or null. If close() is
called multiple times or if initialization failed, this could throw an exception.
Add a null check and idempotency guard to make close() safe to call multiple times.

plugins/arrow-base/src/main/java/org/opensearch/arrow/memory/DefaultArrowAllocatorService.java [50-53]

 @Override
 public void close() {
-    root.close();
+    if (root != null) {
+        root.close();
+    }
 }
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that close() could fail if root is null or already closed. Adding a null check makes the method idempotent and safer. However, the root is initialized in the constructor and should not be null under normal circumstances, so the impact is moderate.

Low
Suggestions up to commit 0dbde84
CategorySuggestion                                                                                                                                    Impact
Possible issue
Ensure registry sealing on failure

If a plugin's createComponents() throws an exception, the registry will be left
unsealed and partially populated. Wrap the loop in a try-finally block to ensure
seal() is called even if component creation fails, preventing resource leaks and
enforcing the sealed state invariant.

server/src/main/java/org/opensearch/node/Node.java [1152-1174]

 final DefaultPluginComponentRegistry pluginComponentRegistry = new DefaultPluginComponentRegistry();
 final List<Object> pluginComponents = new ArrayList<>();
-for (Plugin p : pluginsService.filterPlugins(Plugin.class)) {
-    Collection<Object> components = p.createComponents(
-        client,
-        clusterService,
-        threadPool,
-        resourceWatcherService,
-        scriptService,
-        xContentRegistry,
-        environment,
-        nodeEnvironment,
-        namedWriteableRegistry,
-        clusterModule.getIndexNameExpressionResolver(),
-        repositoriesServiceReference::get,
-        pluginComponentRegistry
-    );
-    for (Object component : components) {
-        pluginComponentRegistry.register(component);
+try {
+    for (Plugin p : pluginsService.filterPlugins(Plugin.class)) {
+        Collection<Object> components = p.createComponents(
+            client,
+            clusterService,
+            threadPool,
+            resourceWatcherService,
+            scriptService,
+            xContentRegistry,
+            environment,
+            nodeEnvironment,
+            namedWriteableRegistry,
+            clusterModule.getIndexNameExpressionResolver(),
+            repositoriesServiceReference::get,
+            pluginComponentRegistry
+        );
+        for (Object component : components) {
+            pluginComponentRegistry.register(component);
+        }
+        pluginComponents.addAll(components);
     }
-    pluginComponents.addAll(components);
+} finally {
+    pluginComponentRegistry.seal();
 }
-pluginComponentRegistry.seal();
Suggestion importance[1-10]: 8

__

Why: This is a valid concern about resource management. If createComponents() throws an exception, the registry remains unsealed, which could lead to inconsistent state. The try-finally block ensures seal() is always called, preventing potential resource leaks and enforcing the sealed state invariant.

Medium
General
Reject null component registration

Registering a null component will cause getComponent() to throw NullPointerException
when checking type.isInstance(component). Add a null check to reject null components
at registration time with a clear error message.

server/src/main/java/org/opensearch/plugins/DefaultPluginComponentRegistry.java [36-41]

 public void register(Object component) {
     if (components == null) {
         throw new IllegalStateException("PluginComponentRegistry is sealed; registration is no longer allowed");
     }
+    if (component == null) {
+        throw new IllegalArgumentException("Cannot register null component");
+    }
     components.add(component);
 }
Suggestion importance[1-10]: 7

__

Why: Adding a null check at registration time provides fail-fast behavior with a clear error message, rather than allowing a NullPointerException later during getComponent() lookup. This improves debuggability and API robustness.

Medium
Remove redundant null check

The null check for in is redundant because the code already dereferences in in the
super(in) call, which would throw a NullPointerException before reaching the null
check in the error message. Remove the null check from the error message to avoid
confusion.

plugins/arrow-base/src/main/java/org/opensearch/arrow/transport/ArrowBatchResponse.java [87-99]

 protected ArrowBatchResponse(StreamInput in) throws IOException {
     super(in);
     if (in instanceof ArrowStreamInput arrowIn) {
         this.batchRoot = arrowIn.getRoot();
         arrowIn.claimOwnership();
     } else {
         throw new IllegalStateException(
             "ArrowBatchResponse decoded from a non-Arrow StreamInput ("
-                + (in == null ? "null" : in.getClass().getName())
+                + in.getClass().getName()
                 + "). Wrapping handlers around ArrowBatchResponseHandler must forward "
                 + "TransportResponseHandler#skipsDeserialization()."
         );
     }
 }
Suggestion importance[1-10]: 5

__

Why: The null check for in in the error message is indeed redundant since super(in) would throw NullPointerException first. However, this is a minor code clarity improvement with low impact on functionality.

Low
Suggestions up to commit 0f7c161
CategorySuggestion                                                                                                                                    Impact
Possible issue
Ensure registry sealing on failure

If a plugin's createComponents throws an exception, the registry remains unsealed
and partially populated. Wrap the loop in a try-finally block to ensure seal() is
called even if component creation fails, preventing resource leaks and enforcing the
sealed state invariant.

server/src/main/java/org/opensearch/node/Node.java [1152-1174]

 final DefaultPluginComponentRegistry pluginComponentRegistry = new DefaultPluginComponentRegistry();
 final List<Object> pluginComponents = new ArrayList<>();
-for (Plugin p : pluginsService.filterPlugins(Plugin.class)) {
-    Collection<Object> components = p.createComponents(
-        client,
-        clusterService,
-        threadPool,
-        resourceWatcherService,
-        scriptService,
-        xContentRegistry,
-        environment,
-        nodeEnvironment,
-        namedWriteableRegistry,
-        clusterModule.getIndexNameExpressionResolver(),
-        repositoriesServiceReference::get,
-        pluginComponentRegistry
-    );
-    for (Object component : components) {
-        pluginComponentRegistry.register(component);
+try {
+    for (Plugin p : pluginsService.filterPlugins(Plugin.class)) {
+        Collection<Object> components = p.createComponents(
+            client,
+            clusterService,
+            threadPool,
+            resourceWatcherService,
+            scriptService,
+            xContentRegistry,
+            environment,
+            nodeEnvironment,
+            namedWriteableRegistry,
+            clusterModule.getIndexNameExpressionResolver(),
+            repositoriesServiceReference::get,
+            pluginComponentRegistry
+        );
+        for (Object component : components) {
+            pluginComponentRegistry.register(component);
+        }
+        pluginComponents.addAll(components);
     }
-    pluginComponents.addAll(components);
+} finally {
+    pluginComponentRegistry.seal();
 }
-pluginComponentRegistry.seal();
Suggestion importance[1-10]: 7

__

Why: This is a valid concern. If createComponents throws an exception, the registry remains unsealed and partially populated, which could lead to resource leaks or inconsistent state. Wrapping in try-finally ensures seal() is always called, improving robustness.

Medium
General
Remove redundant null check

The null check for in is redundant because super(in) would have already thrown a
NullPointerException if in were null. Remove the null check from the error message
to simplify the code and avoid confusion.

plugins/arrow-base/src/main/java/org/opensearch/arrow/transport/ArrowBatchResponse.java [87-99]

 protected ArrowBatchResponse(StreamInput in) throws IOException {
     super(in);
     if (in instanceof ArrowStreamInput arrowIn) {
         this.batchRoot = arrowIn.getRoot();
         arrowIn.claimOwnership();
     } else {
         throw new IllegalStateException(
             "ArrowBatchResponse decoded from a non-Arrow StreamInput ("
-                + (in == null ? "null" : in.getClass().getName())
+                + in.getClass().getName()
                 + "). Wrapping handlers around ArrowBatchResponseHandler must forward "
                 + "TransportResponseHandler#skipsDeserialization()."
         );
     }
 }
Suggestion importance[1-10]: 4

__

Why: The null check for in is indeed redundant since super(in) would throw NullPointerException first. However, the impact is minimal—it only simplifies the error message slightly and does not affect correctness or functionality.

Low
Validate allocator creation success

The RootAllocator is created but never checked for successful initialization. If the
privileged action fails or the allocator cannot be created, subsequent operations
will fail with unclear errors. Add validation after creation to fail fast with a
clear message.

plugins/arrow-base/src/main/java/org/opensearch/arrow/memory/DefaultArrowAllocatorService.java [30-33]

-public final class DefaultArrowAllocatorService implements ArrowAllocatorService, Closeable {
+public DefaultArrowAllocatorService() {
+    this.root = AccessController.doPrivileged((PrivilegedAction<RootAllocator>) () -> new RootAllocator(Long.MAX_VALUE));
+    if (this.root == null) {
+        throw new IllegalStateException("Failed to create Arrow RootAllocator");
+    }
+}
 
-    private final RootAllocator root;
-
-    /** Creates a new service with an unbounded root; child allocators carry their own limits. */
-    public DefaultArrowAllocatorService() {
-        this.root = AccessController.doPrivileged((PrivilegedAction<RootAllocator>) () -> new RootAllocator(Long.MAX_VALUE));
-    }
-
Suggestion importance[1-10]: 2

__

Why: The RootAllocator constructor will throw an exception if it fails, so this.root will never be null. The suggested null check is unnecessary and adds no value. The suggestion misunderstands how the constructor behaves.

Low
Suggestions up to commit a8a779c
CategorySuggestion                                                                                                                                    Impact
Possible issue
Ensure registry is sealed on exception

If a plugin's createComponents throws an exception, the registry will not be sealed,
potentially leaving it in an inconsistent state. Wrap the loop in a try-finally
block to ensure seal() is always called, preventing subsequent misuse.

server/src/main/java/org/opensearch/node/Node.java [1152-1174]

 final DefaultPluginComponentRegistry pluginComponentRegistry = new DefaultPluginComponentRegistry();
 final List<Object> pluginComponents = new ArrayList<>();
-for (Plugin p : pluginsService.filterPlugins(Plugin.class)) {
-    Collection<Object> components = p.createComponents(
-        client,
-        clusterService,
-        threadPool,
-        resourceWatcherService,
-        scriptService,
-        xContentRegistry,
-        environment,
-        nodeEnvironment,
-        namedWriteableRegistry,
-        clusterModule.getIndexNameExpressionResolver(),
-        repositoriesServiceReference::get,
-        pluginComponentRegistry
-    );
-    for (Object component : components) {
-        pluginComponentRegistry.register(component);
+try {
+    for (Plugin p : pluginsService.filterPlugins(Plugin.class)) {
+        Collection<Object> components = p.createComponents(
+            client,
+            clusterService,
+            threadPool,
+            resourceWatcherService,
+            scriptService,
+            xContentRegistry,
+            environment,
+            nodeEnvironment,
+            namedWriteableRegistry,
+            clusterModule.getIndexNameExpressionResolver(),
+            repositoriesServiceReference::get,
+            pluginComponentRegistry
+        );
+        for (Object component : components) {
+            pluginComponentRegistry.register(component);
+        }
+        pluginComponents.addAll(components);
     }
-    pluginComponents.addAll(components);
+} finally {
+    pluginComponentRegistry.seal();
 }
-pluginComponentRegistry.seal();
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that if createComponents throws an exception, the registry won't be sealed, leaving it in an inconsistent state. Wrapping the loop in a try-finally block to ensure seal() is always called is a good defensive practice that prevents potential misuse of the registry.

Medium
General
Remove redundant null check

The null check for in is redundant because super(in) would already throw a
NullPointerException if in were null. Remove the null check from the error message
to simplify the code.

plugins/arrow-base/src/main/java/org/opensearch/arrow/transport/ArrowBatchResponse.java [87-99]

 protected ArrowBatchResponse(StreamInput in) throws IOException {
     super(in);
     if (in instanceof ArrowStreamInput arrowIn) {
         this.batchRoot = arrowIn.getRoot();
         arrowIn.claimOwnership();
     } else {
         throw new IllegalStateException(
             "ArrowBatchResponse decoded from a non-Arrow StreamInput ("
-                + (in == null ? "null" : in.getClass().getName())
+                + in.getClass().getName()
                 + "). Wrapping handlers around ArrowBatchResponseHandler must forward "
                 + "TransportResponseHandler#skipsDeserialization()."
         );
     }
 }
Suggestion importance[1-10]: 4

__

Why: The suggestion is technically correct that super(in) would throw if in is null, making the null check in the error message redundant. However, the explicit null check in the error message improves clarity for debugging, so removing it offers only a minor code simplification with minimal impact.

Low
Handle privileged action exceptions

If RootAllocator construction throws an exception inside the privileged action, the
exception will be wrapped in a PrivilegedActionException or similar, obscuring the
root cause. Consider handling or documenting this behavior to aid debugging.

plugins/arrow-base/src/main/java/org/opensearch/arrow/memory/DefaultArrowAllocatorService.java [30-33]

 @SuppressWarnings("removal")
 public final class DefaultArrowAllocatorService implements ArrowAllocatorService, Closeable {
 
     private final RootAllocator root;
 
     /** Creates a new service with an unbounded root; child allocators carry their own limits. */
     public DefaultArrowAllocatorService() {
-        this.root = AccessController.doPrivileged((PrivilegedAction<RootAllocator>) () -> new RootAllocator(Long.MAX_VALUE));
+        try {
+            this.root = AccessController.doPrivileged((PrivilegedAction<RootAllocator>) () -> new RootAllocator(Long.MAX_VALUE));
+        } catch (Exception e) {
+            throw new IllegalStateException("Failed to create RootAllocator", e);
+        }
     }
Suggestion importance[1-10]: 3

__

Why: The suggestion raises a valid concern about exception handling in privileged actions, but the proposed solution is not ideal. PrivilegedAction does not throw checked exceptions, so the try-catch around doPrivileged is unnecessary. If RootAllocator construction fails, the exception will propagate directly without wrapping. The suggestion's impact is minimal and the proposed code change doesn't add significant value.

Low

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

❌ Gradle check result for 2ef1707: 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?

@bowenlan-amzn
bowenlan-amzn force-pushed the experiment/arrow-base-plugin branch from 2ef1707 to f9b5e24 Compare May 4, 2026 17:35
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit f9b5e24

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

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

@bowenlan-amzn
bowenlan-amzn force-pushed the experiment/arrow-base-plugin branch from f9b5e24 to 3d4ea11 Compare May 4, 2026 18:14
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 3d4ea11

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

❕ Gradle check result for 3d4ea11: UNSTABLE

Please review all flaky tests that succeeded after retry and create an issue if one does not already exist to track the flaky failure.

@codecov

codecov Bot commented May 4, 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.58%. Comparing base (75b6e82) to head (6ebbe9d).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...rch/arrow/flight/transport/FlightStreamPlugin.java 0.00% 3 Missing ⚠️
...e/stream/TransportNativeArrowStreamDataAction.java 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main   #21465      +/-   ##
============================================
+ Coverage     73.56%   73.58%   +0.02%     
+ Complexity    74882    74867      -15     
============================================
  Files          5994     5992       -2     
  Lines        339592   339581      -11     
  Branches      48948    48949       +1     
============================================
+ Hits         249811   249892      +81     
+ Misses        69893    69801      -92     
  Partials      19888    19888              

☔ 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.

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit d011fc8

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

✅ Gradle check result for d011fc8: SUCCESS

@bowenlan-amzn
bowenlan-amzn force-pushed the experiment/arrow-base-plugin branch from d011fc8 to fa5a63f Compare May 6, 2026 15:45
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit fa5a63f

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

✅ Gradle check result for fa5a63f: SUCCESS

@bowenlan-amzn
bowenlan-amzn force-pushed the experiment/arrow-base-plugin branch from fa5a63f to 348719d Compare May 6, 2026 17:09
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 348719d

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

❌ Gradle check result for 348719d: 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?

@bowenlan-amzn
bowenlan-amzn force-pushed the experiment/arrow-base-plugin branch from 348719d to e5cad8b Compare May 6, 2026 18:58
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit e5cad8b

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

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

@bowenlan-amzn
bowenlan-amzn force-pushed the experiment/arrow-base-plugin branch from e5cad8b to a9f67da Compare May 6, 2026 20:02
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit a9f67da

Plugins are initialized in topological order (dependencies first), but
Node.close() previously closed them in the same forward order. This
means a dependency plugin (e.g. arrow-base owning the root allocator)
could close before its dependents (e.g. analytics-engine holding child
allocators), causing spurious IllegalStateExceptions.

Reverse the plugin list before closing so dependents release resources
before their dependencies. This matches the standard
construct-in-order / destroy-in-reverse convention.

With ordering now guaranteed, remove the defensive catch in
AnalyticsSearchService.close() that was masking potential buffer leaks.

Signed-off-by: bowenlan-amzn <bowenlan23@gmail.com>
QueryContext constructors now require ArrowAllocatorService as the last
parameter. Provide a stub implementation in the test.

Signed-off-by: bowenlan-amzn <bowenlan23@gmail.com>
Same fix as other coordinator QA tests — AnalyticsPlugin requires
ArrowAllocatorService from the registry at createComponents() time.

Signed-off-by: bowenlan-amzn <bowenlan23@gmail.com>
Signed-off-by: bowenlan-amzn <bowenlan23@gmail.com>
…rService

Replace removed ArrowAllocatorProvider with a test-local RootAllocator
for stub batch production, and add ArrowBasePlugin to node plugins so
AnalyticsPlugin's registry lookup succeeds.

Signed-off-by: bowenlan-amzn <bowenlan23@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 0dbde84

Fixes CoordinatorSingleNodeTopologyIT and CoordinatorTwoNodeTopologyIT
which inherit plugin setup from this base class.

Signed-off-by: bowenlan-amzn <bowenlan23@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 4de448a

…locators

If a consumer plugin fails to close its child allocator before node
shutdown, the root allocator throws IllegalStateException. This
surfaces the bug clearly in logs while allowing graceful shutdown.

Signed-off-by: bowenlan-amzn <bowenlan23@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 6f70664

@github-actions

Copy link
Copy Markdown
Contributor

❕ Gradle check result for 6f70664: UNSTABLE

Please review all flaky tests that succeeded after retry and create an issue if one does not already exist to track the flaky failure.

@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 6ebbe9d

@github-actions

Copy link
Copy Markdown
Contributor

❌ Gradle check result for 6ebbe9d: null

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

❌ Gradle check result for 6ebbe9d: 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

❌ Gradle check result for 6ebbe9d: 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

✅ Gradle check result for 6ebbe9d: SUCCESS

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

Labels

skip-diff-analyzer Maintainer to skip code-diff-analyzer check, after reviewing issues in AI analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants