1818
1919package org .apache .cassandra .sidecar .client ;
2020
21+ import java .io .IOException ;
22+ import java .io .InputStream ;
23+ import java .nio .file .Path ;
24+ import java .nio .file .StandardCopyOption ;
25+ import java .util .AbstractMap .SimpleEntry ;
26+ import java .util .ArrayList ;
27+ import java .util .List ;
28+ import java .util .concurrent .TimeUnit ;
29+
2130import org .junit .jupiter .api .AfterAll ;
31+ import org .junit .jupiter .api .AfterEach ;
2232import org .junit .jupiter .api .BeforeAll ;
33+ import org .junit .jupiter .api .BeforeEach ;
2334import org .junit .jupiter .api .Test ;
35+ import org .junit .jupiter .api .io .TempDir ;
2436
37+ import io .vertx .core .Future ;
2538import io .vertx .core .Vertx ;
2639import io .vertx .core .buffer .Buffer ;
40+ import io .vertx .core .file .AsyncFile ;
2741import io .vertx .ext .web .client .HttpRequest ;
42+ import io .vertx .ext .web .client .HttpResponse ;
43+ import okhttp3 .mockwebserver .MockResponse ;
44+ import okhttp3 .mockwebserver .MockWebServer ;
45+ import org .apache .cassandra .sidecar .client .request .RequestExecutorTest ;
2846
47+ import static io .netty .handler .codec .http .HttpResponseStatus .INTERNAL_SERVER_ERROR ;
48+ import static io .netty .handler .codec .http .HttpResponseStatus .OK ;
49+ import static java .nio .file .Files .copy ;
2950import static org .apache .cassandra .sidecar .common .http .SidecarHttpHeaderNames .AUTH_ROLE ;
3051import static org .assertj .core .api .Assertions .assertThat ;
52+ import static org .assertj .core .api .Assertions .assertThatThrownBy ;
3153import static org .mockito .Mockito .mock ;
3254import static org .mockito .Mockito .when ;
3355
3759public class VertxHttpClientTest
3860{
3961 private static Vertx vertx ;
62+ private MockWebServer mockServer ;
63+ private HttpClientConfig config ;
64+ private SidecarInstanceImpl sidecarInstance ;
4065
41- @ BeforeAll
42- public static void setUp ()
66+ @ BeforeEach
67+ public void setUp () throws IOException
4368 {
4469 vertx = Vertx .vertx ();
70+ mockServer = new MockWebServer ();
71+ mockServer .start ();
72+
73+ config = new HttpClientConfig .Builder <>()
74+ .ssl (false )
75+ .timeoutMillis (30000 )
76+ .build ();
77+ sidecarInstance = RequestExecutorTest .newSidecarInstance (mockServer );
4578 }
4679
47- @ AfterAll
48- public static void tearDown ()
80+ @ AfterEach
81+ public void tearDown () throws IOException
4982 {
50- vertx .close ();
83+ if (mockServer != null )
84+ {
85+ mockServer .shutdown ();
86+ }
87+ if (vertx != null )
88+ {
89+ vertx .close ();
90+ }
5191 }
5292
5393 @ Test
@@ -74,4 +114,116 @@ private HttpClientConfig.Builder<?> httpClientConfigBuilder()
74114 .timeoutMillis (100 )
75115 .idleTimeoutMillis (100 );
76116 }
117+
118+ @ Test
119+ void testUploadSSTableClosesFile (@ TempDir Path tempDirectory ) throws Exception
120+ {
121+ runTestScenario (tempDirectory ,
122+ new MockResponse ().setResponseCode (OK .code ()),
123+ new ExposeAsyncFileVertxHttpClient (vertx , config ));
124+ }
125+
126+ @ Test
127+ void testUploadClosesFileOnUploadFailure (@ TempDir Path tempDirectory ) throws Exception
128+ {
129+ runTestScenario (tempDirectory ,
130+ new MockResponse ().setResponseCode (INTERNAL_SERVER_ERROR .code ()),
131+ new ExposeAsyncFileVertxHttpClient (vertx , config ));
132+ }
133+
134+ @ Test
135+ void testMultipleUploadClosesAllFiles (@ TempDir Path tempDirectory ) throws Exception
136+ {
137+ mockServer .enqueue (new MockResponse ().setResponseCode (OK .code ()));
138+ mockServer .enqueue (new MockResponse ().setResponseCode (OK .code ()));
139+ mockServer .enqueue (new MockResponse ().setResponseCode (OK .code ()));
140+
141+ Path fileToUpload = prepareFile (tempDirectory );
142+
143+ ExposeAsyncFileVertxHttpClient httpClient = new ExposeAsyncFileVertxHttpClient (vertx , config );
144+
145+ // Upload the same file 3 times (simulating multiple file uploads)
146+ for (int i = 0 ; i < 3 ; i ++)
147+ {
148+ HttpRequest <Buffer > vertxRequest = httpClient .webClient .put (mockServer .getPort (),
149+ mockServer .getHostName (),
150+ "/upload/test" + i );
151+ httpClient .executeUploadFileInternal (sidecarInstance , vertxRequest , fileToUpload .toString ())
152+ .get (30 , TimeUnit .SECONDS );
153+ }
154+
155+ assertThat (mockServer .getRequestCount ()).isEqualTo (3 );
156+ assertThat (httpClient .capturedFiles ).hasSize (3 );
157+
158+ // Give async file close operations time to complete
159+ Thread .sleep (100 );
160+
161+ // Verify all the files are closed by attempting to call .end() which should throw IllegalStateException
162+ for (AsyncFile file : httpClient .capturedFiles )
163+ {
164+ assertThatThrownBy (file ::end )
165+ .isInstanceOf (IllegalStateException .class )
166+ .hasMessageContaining ("File handle is closed" );
167+ }
168+ }
169+
170+ private void runTestScenario (Path tempDirectory ,
171+ MockResponse mockResponse ,
172+ ExposeAsyncFileVertxHttpClient httpClient ) throws Exception
173+ {
174+ mockServer .enqueue (mockResponse );
175+
176+ Path fileToUpload = prepareFile (tempDirectory );
177+ HttpRequest <Buffer > vertxRequest = httpClient .webClient .put (mockServer .getPort (),
178+ mockServer .getHostName (),
179+ "/upload/test" );
180+
181+ httpClient .executeUploadFileInternal (sidecarInstance , vertxRequest , fileToUpload .toString ())
182+ .get (30 , TimeUnit .SECONDS );
183+
184+ assertThat (mockServer .getRequestCount ()).isEqualTo (1 );
185+
186+ // Give async file close operation time to complete
187+ Thread .sleep (100 );
188+
189+ // Verify file is closed by attempting to call .end() which should throw IllegalStateException
190+ assertThat (httpClient .capturedFiles ).hasSize (1 );
191+ assertThatThrownBy (() -> httpClient .capturedFiles .get (0 ).end ())
192+ .isInstanceOf (IllegalStateException .class )
193+ .hasMessageContaining ("File handle is closed" );
194+ }
195+
196+ /**
197+ * Class that extends from {@link VertxHttpClient} for testing purposes and holds a reference to the
198+ * {@link AsyncFile} to ensure that the file has been closed.
199+ */
200+ static class ExposeAsyncFileVertxHttpClient extends VertxHttpClient
201+ {
202+ List <AsyncFile > capturedFiles = new ArrayList <>();
203+
204+ ExposeAsyncFileVertxHttpClient (Vertx vertx , HttpClientConfig config )
205+ {
206+ super (vertx , config );
207+ }
208+
209+ @ Override
210+ protected Future <HttpResponse <Buffer >> sendFileStream (HttpRequest <Buffer > vertxRequest ,
211+ SimpleEntry <Long , AsyncFile > pair ,
212+ String filename )
213+ {
214+ capturedFiles .add (pair .getValue ());
215+ return super .sendFileStream (vertxRequest , pair , filename );
216+ }
217+ }
218+
219+ private Path prepareFile (Path tempDirectory ) throws IOException
220+ {
221+ Path fileToUpload = tempDirectory .resolve ("nb-1-big-TOC.txt" );
222+ try (InputStream inputStream = getClass ().getClassLoader ().getResourceAsStream ("sstables/nb-1-big-TOC.txt" ))
223+ {
224+ assertThat (inputStream ).isNotNull ();
225+ copy (inputStream , fileToUpload , StandardCopyOption .REPLACE_EXISTING );
226+ }
227+ return fileToUpload ;
228+ }
77229}
0 commit comments