2020import java .net .URI ;
2121import java .nio .ByteBuffer ;
2222import java .nio .charset .StandardCharsets ;
23+ import java .lang .ref .WeakReference ;
2324import java .util .Arrays ;
2425import java .util .concurrent .CompletableFuture ;
2526import 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