-
Notifications
You must be signed in to change notification settings - Fork 3k
Changes to handle session context handle close #21519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
bharath-techie
merged 2 commits into
opensearch-project:main
from
bharath-techie:close-ctx
May 7, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
86 changes: 86 additions & 0 deletions
86
...-framework/src/main/java/org/opensearch/analytics/backend/jni/ConsumableNativeHandle.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /* | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| */ | ||
|
|
||
| package org.opensearch.analytics.backend.jni; | ||
|
|
||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
|
|
||
| /** | ||
| * Specialisation of {@link NativeHandle} for pointers whose ownership is transferred to the | ||
| * native side by a specific FFM call (for example, Rust's {@code Box::from_raw} inside a | ||
| * consuming function). After the consuming call the native resource is freed internally; | ||
| * calling the matching {@code close_X} entry a second time would be a double-free, while | ||
| * not calling it on the error path would leak. | ||
| * | ||
| * <p>The bridge method that performs the consuming FFM call must invoke | ||
| * {@link #markConsumed()} after the downcall returns (typically in a {@code finally} block). | ||
| * This: | ||
| * <ul> | ||
| * <li>flips an internal flag so the inherited {@link #doClose()} short-circuits;</li> | ||
| * <li>eagerly closes the Java wrapper — the pointer is removed from LIVE_HANDLES in | ||
| * {@link NativeHandle}, subsequent {@link #getPointer()} calls | ||
| * throw, and {@link NativeHandle#validatePointer(long, String) validatePointer} rejects | ||
| * the now-dangling pointer value.</li> | ||
| * </ul> | ||
| * | ||
| * <p>On paths where the consuming call never happened (pre-dispatch Java error, aborted flow, | ||
| * Cleaner-at-GC fallback), {@link #doClose()} delegates to {@link #doCloseNative()} which | ||
| * subclasses implement to free the native resource via the appropriate {@code close_X} FFM entry. | ||
| * | ||
| * <p>{@link #markConsumed()} is idempotent and safe to call after {@link #close()}. | ||
| */ | ||
| public abstract class ConsumableNativeHandle extends NativeHandle { | ||
|
|
||
| /** | ||
| * Set once the native side has taken ownership of {@link #ptr} via the consuming FFM call. | ||
| * When {@code true}, {@link #doClose()} skips the call to {@link #doCloseNative()} to avoid | ||
| * a double-free. | ||
| */ | ||
| private final AtomicBoolean consumed = new AtomicBoolean(false); | ||
|
|
||
| protected ConsumableNativeHandle(long ptr) { | ||
| super(ptr); | ||
| } | ||
|
|
||
| /** | ||
| * Marks this handle as having had its native pointer consumed by the bridge's | ||
| * ownership-transferring FFM call, then closes the Java wrapper. See the class javadoc | ||
| * for the full contract and typical call pattern. | ||
| */ | ||
| public final void markConsumed() { | ||
| consumed.set(true); | ||
| close(); | ||
| } | ||
|
|
||
| /** | ||
| * @return {@code true} if {@link #markConsumed()} has been called. | ||
| */ | ||
| protected final boolean isConsumed() { | ||
| return consumed.get(); | ||
| } | ||
|
|
||
| /** | ||
| * Template method: short-circuits to a no-op when {@link #isConsumed()} is {@code true} | ||
| * (the native side already freed the resource), otherwise delegates to | ||
| * {@link #doCloseNative()}. Marked {@code final} so subclasses cannot bypass the guard. | ||
| */ | ||
| @Override | ||
| protected final void doClose() { | ||
| if (isConsumed()) { | ||
| return; | ||
| } | ||
| doCloseNative(); | ||
| } | ||
|
|
||
| /** | ||
| * Releases the native resource via the appropriate {@code close_X} FFM entry. | ||
| * Called by {@link #doClose()} only when the handle has <b>not</b> been marked consumed, | ||
| * i.e. on the error / never-executed path. Must be safe to call at most once per pointer. | ||
| */ | ||
| protected abstract void doCloseNative(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
...ework/src/test/java/org/opensearch/analytics/backend/jni/ConsumableNativeHandleTests.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| /* | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| */ | ||
|
|
||
| package org.opensearch.analytics.backend.jni; | ||
|
|
||
| import org.opensearch.test.OpenSearchTestCase; | ||
|
|
||
| import java.util.concurrent.atomic.AtomicInteger; | ||
|
|
||
| /** | ||
| * Tests for {@link ConsumableNativeHandle}'s ownership-transfer contract. | ||
| * | ||
| * <p>The class guards against two specific failure modes: | ||
| * <ul> | ||
| * <li><b>Double-free</b>: the Rust side consumed the pointer via | ||
| * {@code Box::from_raw}, then the Java-side {@code close()} calls | ||
| * {@code df_close_X} which tries to free the same memory again.</li> | ||
| * <li><b>Leak</b>: the consuming FFM call never dispatched (pre-invoke | ||
| * Java failure, aborted flow), so the Java wrapper is responsible for | ||
| * calling {@code df_close_X} exactly once.</li> | ||
| * </ul> | ||
| * | ||
| * <p>Both paths rely on the {@code doCloseNative()} callback being invoked | ||
| * exactly zero or one times, never twice. These tests nail that contract | ||
| * down with a counting subclass so a future change to | ||
| * {@link ConsumableNativeHandle} that accidentally re-introduces a | ||
| * double-close will fail loudly. | ||
| * | ||
| * <p>Reference: the real subclass | ||
| * {@code org.opensearch.be.datafusion.nativelib.SessionContextHandle} is used | ||
| * from {@code DatafusionContext#close()} and | ||
| * {@code DataFusionSessionState#close()} — both paths can reach | ||
| * {@code close()} on the same instance, so idempotency is load-bearing. | ||
| */ | ||
| public class ConsumableNativeHandleTests extends OpenSearchTestCase { | ||
|
|
||
| /** | ||
| * Counts calls to {@link #doCloseNative()} so tests can assert exact | ||
| * invocation counts. | ||
| */ | ||
| private static final class CountingHandle extends ConsumableNativeHandle { | ||
| final AtomicInteger nativeCloses = new AtomicInteger(0); | ||
|
|
||
| CountingHandle(long ptr) { | ||
| super(ptr); | ||
| } | ||
|
|
||
| @Override | ||
| protected void doCloseNative() { | ||
| nativeCloses.incrementAndGet(); | ||
| } | ||
| } | ||
|
|
||
| // ---- close() without consumption ------------------------------------ | ||
|
|
||
| public void testCloseWithoutConsumeCallsNativeOnce() { | ||
| CountingHandle handle = new CountingHandle(100L); | ||
| handle.close(); | ||
| assertEquals("doCloseNative should run once on the never-consumed path", 1, handle.nativeCloses.get()); | ||
| } | ||
|
|
||
| public void testDoubleCloseWithoutConsumeStillCallsNativeOnce() { | ||
| CountingHandle handle = new CountingHandle(101L); | ||
| handle.close(); | ||
| handle.close(); | ||
| assertEquals("close() must be idempotent — second call is a no-op", 1, handle.nativeCloses.get()); | ||
| } | ||
|
|
||
| // ---- markConsumed() ownership-transferred path ---------------------- | ||
|
|
||
| public void testMarkConsumedSkipsNativeClose() { | ||
| CountingHandle handle = new CountingHandle(200L); | ||
| handle.markConsumed(); | ||
| assertEquals( | ||
| "markConsumed() must not call doCloseNative — the native side already freed the pointer", | ||
| 0, | ||
| handle.nativeCloses.get() | ||
| ); | ||
| } | ||
|
|
||
| public void testCloseAfterMarkConsumedIsNoOp() { | ||
| CountingHandle handle = new CountingHandle(201L); | ||
| handle.markConsumed(); | ||
| handle.close(); | ||
| assertEquals( | ||
| "An explicit close() after markConsumed() must remain a no-op — otherwise Rust's Box::from_raw would be followed by a second free", | ||
| 0, | ||
| handle.nativeCloses.get() | ||
| ); | ||
| } | ||
|
|
||
| public void testMarkConsumedAfterCloseDoesNotRunNativeTwice() { | ||
| // Order reversed from the normal happy path. The bridge always calls | ||
| // markConsumed() after the FFM downcall returns, but the test ensures | ||
| // that even if some future caller inverted the sequence, the native | ||
| // close is never invoked twice. | ||
| CountingHandle handle = new CountingHandle(202L); | ||
| handle.close(); | ||
| assertEquals(1, handle.nativeCloses.get()); | ||
| handle.markConsumed(); | ||
| assertEquals("markConsumed() after close() must not trigger another native close", 1, handle.nativeCloses.get()); | ||
| } | ||
|
|
||
| public void testMarkConsumedIsIdempotent() { | ||
| CountingHandle handle = new CountingHandle(203L); | ||
| handle.markConsumed(); | ||
| handle.markConsumed(); | ||
| handle.close(); | ||
| assertEquals(0, handle.nativeCloses.get()); | ||
| } | ||
|
|
||
| // ---- State observation --------------------------------------------- | ||
|
|
||
| public void testGetPointerAfterMarkConsumedThrows() { | ||
| CountingHandle handle = new CountingHandle(300L); | ||
| handle.markConsumed(); | ||
| // markConsumed() closes the Java wrapper eagerly; subsequent getPointer | ||
| // should refuse to hand out the now-dangling value. | ||
| expectThrows(IllegalStateException.class, handle::getPointer); | ||
| } | ||
|
|
||
| public void testIsLivePointerFalseAfterMarkConsumed() { | ||
| CountingHandle handle = new CountingHandle(301L); | ||
| assertTrue(NativeHandle.isLivePointer(301L)); | ||
| handle.markConsumed(); | ||
| assertFalse( | ||
| "markConsumed() must remove the pointer from the live registry so validatePointer rejects it on a stale re-use", | ||
| NativeHandle.isLivePointer(301L) | ||
| ); | ||
| } | ||
|
|
||
| public void testValidatePointerAfterMarkConsumedThrows() { | ||
| CountingHandle handle = new CountingHandle(302L); | ||
| handle.markConsumed(); | ||
| expectThrows(IllegalStateException.class, () -> NativeHandle.validatePointer(302L, "consumed")); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this to avoid bigger changes ? Ideally we should not give a default for close to FORCE child classes to implement and then they no-op on their end.