Skip to content

Commit 0dc05b3

Browse files
committed
force gc to verify native holds reference to object
1 parent cbf7b14 commit 0dc05b3

1 file changed

Lines changed: 92 additions & 10 deletions

File tree

src/test/java/software/amazon/awssdk/crt/test/WriteDataTest.java

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.net.URI;
2121
import java.nio.ByteBuffer;
2222
import java.nio.charset.StandardCharsets;
23+
import java.lang.ref.WeakReference;
2324
import java.util.Arrays;
2425
import java.util.concurrent.CompletableFuture;
2526
import java.util.concurrent.TimeUnit;
@@ -66,14 +67,15 @@ public void testHttp2WriteData() throws Exception {
6667
skipIfLocalhostUnavailable();
6768

6869
URI uri = new URI(HOST + ":" + H2_TLS_PORT);
69-
byte[] payload = "hello from writeData".getBytes(StandardCharsets.UTF_8);
70+
String expectedBody = "hello from writeData";
71+
int expectedLen = expectedBody.getBytes(StandardCharsets.UTF_8).length;
7072

7173
HttpHeader[] headers = new HttpHeader[]{
7274
new HttpHeader(":method", "PUT"),
7375
new HttpHeader(":path", "/echo"),
7476
new HttpHeader(":scheme", "https"),
7577
new HttpHeader(":authority", uri.getHost()),
76-
new HttpHeader("content-length", Integer.toString(payload.length)),
78+
new HttpHeader("content-length", Integer.toString(expectedLen)),
7779
};
7880
Http2Request request = new Http2Request(headers, null);
7981

@@ -109,7 +111,33 @@ public void onResponseComplete(HttpStreamBase stream, int errorCode) {
109111

110112
try (Http2Stream stream = conn.makeRequest(request, streamHandler, true)) {
111113
stream.activate();
112-
stream.writeData(payload, true).get(5, TimeUnit.SECONDS);
114+
115+
/*
116+
* Issue the write from a helper that:
117+
* 1) allocates the byte[] locally, so once the helper returns the
118+
* caller's stack has no reference to it,
119+
* 2) captures only a WeakReference to the array in its inner callback
120+
* (not the byte[] itself) so the lambda doesn't accidentally become
121+
* a Java-side GC root,
122+
* 3) performs the "is the native GlobalRef still holding the array?"
123+
* assertion INSIDE the write-completion callback -- which is
124+
* guaranteed by the native code to run BEFORE native cleanup
125+
* (Release + DeleteGlobalRef) in s_write_data_complete.
126+
*
127+
* If NewGlobalRef on callback_data were missing or dropped early, the
128+
* array would be unreachable from Java-land at the moment the callback
129+
* fires, System.gc() inside the callback would clear the WeakReference,
130+
* and the helper would complete the returned future exceptionally.
131+
*
132+
* Caveat: GetByteArrayElements may pin the array, and on some JVMs a
133+
* pin can also keep the object GC-reachable. A pass here therefore
134+
* proves "native side is keeping the array alive somehow" rather than
135+
* strictly "GlobalRef is what is keeping it alive". Combined with the
136+
* echo-body-matches assertion below (which would fail on any data
137+
* corruption from premature release), this covers the interesting cases.
138+
*/
139+
CompletableFuture<Void> writeFuture = issueWriteAndAssertReachable(stream, expectedBody);
140+
writeFuture.get(5, TimeUnit.SECONDS);
113141
reqCompleted.get(60, TimeUnit.SECONDS);
114142
}
115143
}
@@ -119,15 +147,59 @@ public void onResponseComplete(HttpStreamBase stream, int errorCode) {
119147
Assert.assertEquals(200, response.statusCode);
120148
// /echo returns JSON: {"body": "<sent>", "bytes": <len>}
121149
String body = response.getBody();
122-
Assert.assertTrue("Response should contain sent body: " + body,
123-
body.contains("\"body\": \"hello from writeData\""));
150+
Assert.assertTrue("Response should contain sent body intact: " + body,
151+
body.contains("\"body\": \"" + expectedBody + "\""));
124152
Assert.assertTrue("Response should contain byte count: " + body,
125-
body.contains("\"bytes\": " + payload.length));
153+
body.contains("\"bytes\": " + expectedLen));
126154

127155
shutdownComplete.get(60, TimeUnit.SECONDS);
128156
CrtResource.waitForNoResources();
129157
}
130158

159+
/**
160+
* Allocates the payload locally and issues writeData. Inside the write-completion
161+
* callback (which runs on the event-loop thread BEFORE native cleanup -- see
162+
* s_write_data_complete in http_request_response.c), forces GC and asserts that a
163+
* WeakReference to the payload is still live. A pass means the native side is keeping
164+
* the array reachable from the JVM's point of view for the duration of the async write,
165+
* which is what the NewGlobalRef stored on callback_data is there to guarantee.
166+
*
167+
* The lambda captures {@code weak} and {@code future}, never {@code payload} directly,
168+
* so the lambda itself does not become a Java-side strong root for the array. Once this
169+
* method returns, the local {@code payload} is out of scope and the only thing
170+
* reachable from Java-land is whatever the native layer holds.
171+
*/
172+
private CompletableFuture<Void> issueWriteAndAssertReachable(HttpStreamBase stream, String body) {
173+
byte[] payload = body.getBytes(StandardCharsets.UTF_8);
174+
WeakReference<byte[]> weak = new WeakReference<>(payload);
175+
CompletableFuture<Void> future = new CompletableFuture<>();
176+
177+
stream.writeData(payload, true, (errorCode) -> {
178+
try {
179+
/* We're inside CallVoidMethod on the event-loop thread; native cleanup has
180+
* not yet run, so the native GlobalRef is still holding the array. Trigger
181+
* a collection attempt and verify the WeakReference is still live. */
182+
System.gc();
183+
if (weak.get() == null) {
184+
future.completeExceptionally(new AssertionError(
185+
"byte[] was reclaimed while native should still hold a GlobalRef on it "
186+
+ "(NewGlobalRef on callback_data missing or dropped early?)"));
187+
return;
188+
}
189+
if (errorCode != 0) {
190+
future.completeExceptionally(new RuntimeException(
191+
"writeData failed with errorCode=" + errorCode));
192+
} else {
193+
future.complete(null);
194+
}
195+
} catch (Throwable t) {
196+
future.completeExceptionally(t);
197+
}
198+
});
199+
200+
return future;
201+
}
202+
131203
@Test
132204
public void testHttp2WriteDataEndStreamOnly() throws Exception {
133205
skipIfAndroid();
@@ -199,11 +271,12 @@ public void testHttp1WriteData() throws Exception {
199271
skipIfLocalhostUnavailable();
200272

201273
URI uri = new URI(HOST + ":" + H1_TLS_PORT);
202-
byte[] payload = "hello from writeData h1".getBytes(StandardCharsets.UTF_8);
274+
String expectedBody = "hello from writeData h1";
275+
int expectedLen = expectedBody.getBytes(StandardCharsets.UTF_8).length;
203276

204277
HttpHeader[] headers = new HttpHeader[]{
205278
new HttpHeader("Host", uri.getHost()),
206-
new HttpHeader("Content-Length", Integer.toString(payload.length)),
279+
new HttpHeader("Content-Length", Integer.toString(expectedLen)),
207280
};
208281
HttpRequest request = new HttpRequest("PUT", "/echo", headers, null);
209282

@@ -239,7 +312,16 @@ public void onResponseComplete(HttpStreamBase stream, int errorCode) {
239312
// Use the unified makeRequest with useManualDataWrites=true
240313
try (HttpStreamBase stream = conn.makeRequest(request, streamHandler, true)) {
241314
stream.activate();
242-
stream.writeData(payload, true).get(5, TimeUnit.SECONDS);
315+
316+
/*
317+
* Same weak-ref-inside-write-callback assertion as testHttp2WriteData --
318+
* proves that the H1 writeData path also keeps the byte[] reachable
319+
* from the JVM for the duration of the async write. Helper is shared
320+
* (takes HttpStreamBase), so this exercises the exact same JNI code path
321+
* in http_request_response.c (httpStreamBaseWriteData) as H2.
322+
*/
323+
CompletableFuture<Void> writeFuture = issueWriteAndAssertReachable(stream, expectedBody);
324+
writeFuture.get(5, TimeUnit.SECONDS);
243325
reqCompleted.get(60, TimeUnit.SECONDS);
244326
}
245327
}
@@ -250,7 +332,7 @@ public void onResponseComplete(HttpStreamBase stream, int errorCode) {
250332
// H1 /echo returns JSON: {"data": "<sent>"}
251333
String body = response.getBody();
252334
Assert.assertTrue("Response should contain sent data: " + body,
253-
body.contains("\"data\": \"hello from writeData h1\""));
335+
body.contains("\"data\": \"" + expectedBody + "\""));
254336

255337
shutdownComplete.get(60, TimeUnit.SECONDS);
256338
CrtResource.waitForNoResources();

0 commit comments

Comments
 (0)