22
33import java .io .File ;
44import java .io .IOException ;
5+ import java .io .OutputStream ;
56import java .net .InetSocketAddress ;
7+ import java .nio .charset .StandardCharsets ;
8+ import java .nio .file .Files ;
69import java .util .ArrayList ;
710import java .util .List ;
811import java .util .UUID ;
1518import org .junit .jupiter .api .AfterEach ;
1619import org .junit .jupiter .api .Test ;
1720
21+ import static org .junit .jupiter .api .Assertions .assertArrayEquals ;
1822import static org .junit .jupiter .api .Assertions .assertEquals ;
1923import static org .junit .jupiter .api .Assertions .assertFalse ;
24+ import static org .junit .jupiter .api .Assertions .assertNotEquals ;
2025import static org .junit .jupiter .api .Assertions .assertNotNull ;
26+ import static org .junit .jupiter .api .Assertions .assertNull ;
2127import static org .junit .jupiter .api .Assertions .assertThrows ;
2228import static org .junit .jupiter .api .Assertions .assertTrue ;
2329
@@ -34,16 +40,30 @@ void cleanUp() {
3440 filesToDelete .forEach (File ::delete );
3541 }
3642
43+ @ Test
44+ void asyncDownloadWorkerThreadsAreNamedDaemons () {
45+ Thread first = JdbcJarUtils .ASYNC_DOWNLOAD_THREAD_FACTORY .newThread (() -> { });
46+ Thread second = JdbcJarUtils .ASYNC_DOWNLOAD_THREAD_FACTORY .newThread (() -> { });
47+
48+ assertTrue (first .isDaemon ());
49+ assertTrue (second .isDaemon ());
50+ assertTrue (first .getName ().startsWith ("chat2db-jdbc-download-" ));
51+ assertTrue (second .getName ().startsWith ("chat2db-jdbc-download-" ));
52+ assertNotEquals (first .getName (), second .getName ());
53+ }
54+
3755 @ Test
3856 void asyncConnectionFailureIsReportedWithoutLeavingAPartialFile () throws Exception {
3957 String fileName = uniqueFileName ();
4058 File output = outputFile (fileName );
4159 CountDownLatch completion = new CountDownLatch (1 );
4260 AtomicReference <IOException > failure = new AtomicReference <>();
61+ AtomicReference <Thread > callbackThread = new AtomicReference <>();
4362
4463 JdbcJarUtils .asyncDownload (
4564 "http://user:password@127.0.0.1:1/" + fileName + "?token=secret" ,
4665 error -> {
66+ callbackThread .set (Thread .currentThread ());
4767 failure .set (error );
4868 completion .countDown ();
4969 });
@@ -53,6 +73,131 @@ void asyncConnectionFailureIsReportedWithoutLeavingAPartialFile() throws Excepti
5373 assertFalse (failure .get ().getMessage ().contains ("user:password" ));
5474 assertFalse (failure .get ().getMessage ().contains ("token=secret" ));
5575 assertFalse (output .exists ());
76+ assertNotNull (callbackThread .get ());
77+ assertTrue (callbackThread .get ().isDaemon ());
78+ assertTrue (callbackThread .get ().getName ().startsWith ("chat2db-jdbc-download-" ));
79+ }
80+
81+ @ Test
82+ void asyncDownloadPublishesOnlyCompleteFiles () throws Exception {
83+ String fileName = uniqueFileName ();
84+ File output = outputFile (fileName );
85+ byte [] content = "complete-jdbc-driver" .getBytes (StandardCharsets .UTF_8 );
86+ CountDownLatch headersSent = new CountDownLatch (1 );
87+ CountDownLatch releaseBody = new CountDownLatch (1 );
88+ server = HttpServer .create (new InetSocketAddress ("127.0.0.1" , 0 ), 0 );
89+ server .createContext ("/" + fileName , exchange -> {
90+ exchange .sendResponseHeaders (200 , content .length );
91+ headersSent .countDown ();
92+ try (OutputStream responseBody = exchange .getResponseBody ()) {
93+ try {
94+ if (!releaseBody .await (10 , TimeUnit .SECONDS )) {
95+ return ;
96+ }
97+ } catch (InterruptedException e ) {
98+ Thread .currentThread ().interrupt ();
99+ return ;
100+ }
101+ responseBody .write (content );
102+ }
103+ });
104+ server .start ();
105+ CountDownLatch completion = new CountDownLatch (1 );
106+ AtomicReference <IOException > failure = new AtomicReference <>();
107+
108+ JdbcJarUtils .asyncDownload (
109+ "http://127.0.0.1:" + server .getAddress ().getPort () + "/" + fileName ,
110+ error -> {
111+ failure .set (error );
112+ completion .countDown ();
113+ });
114+
115+ File partialFile = null ;
116+ try {
117+ assertTrue (headersSent .await (10 , TimeUnit .SECONDS ));
118+ partialFile = awaitPartialFile (output );
119+ assertNotNull (partialFile );
120+ filesToDelete .add (partialFile );
121+ assertFalse (output .exists ());
122+ } finally {
123+ releaseBody .countDown ();
124+ }
125+ assertTrue (completion .await (10 , TimeUnit .SECONDS ));
126+ assertNull (failure .get ());
127+ assertArrayEquals (content , Files .readAllBytes (output .toPath ()));
128+ assertFalse (partialFile .exists ());
129+ }
130+
131+ @ Test
132+ void asyncFailureDoesNotDeletePreviouslyPublishedDriver () throws Exception {
133+ String fileName = uniqueFileName ();
134+ File output = outputFile (fileName );
135+ byte [] content = "complete-jdbc-driver" .getBytes (StandardCharsets .UTF_8 );
136+ server = HttpServer .create (new InetSocketAddress ("127.0.0.1" , 0 ), 0 );
137+ server .createContext ("/" + fileName , exchange -> {
138+ if ("fail=true" .equals (exchange .getRequestURI ().getRawQuery ())) {
139+ exchange .sendResponseHeaders (503 , -1 );
140+ exchange .close ();
141+ return ;
142+ }
143+ exchange .sendResponseHeaders (200 , content .length );
144+ try (OutputStream responseBody = exchange .getResponseBody ()) {
145+ responseBody .write (content );
146+ }
147+ });
148+ server .start ();
149+ String url = "http://127.0.0.1:" + server .getAddress ().getPort () + "/" + fileName ;
150+
151+ assertNull (awaitAsyncDownload (url ));
152+ assertArrayEquals (content , Files .readAllBytes (output .toPath ()));
153+
154+ assertNotNull (awaitAsyncDownload (url + "?fail=true" ));
155+ assertArrayEquals (content , Files .readAllBytes (output .toPath ()));
156+ }
157+
158+ @ Test
159+ void interruptedResponseRemovesPartialFile () throws Exception {
160+ String fileName = uniqueFileName ();
161+ File output = outputFile (fileName );
162+ byte [] partialContent = "partial" .getBytes (StandardCharsets .UTF_8 );
163+ server = HttpServer .create (new InetSocketAddress ("127.0.0.1" , 0 ), 0 );
164+ server .createContext ("/" + fileName , exchange -> {
165+ exchange .sendResponseHeaders (200 , partialContent .length + 10 );
166+ try (OutputStream responseBody = exchange .getResponseBody ()) {
167+ responseBody .write (partialContent );
168+ responseBody .flush ();
169+ }
170+ });
171+ server .start ();
172+
173+ IOException failure = awaitAsyncDownload (
174+ "http://127.0.0.1:" + server .getAddress ().getPort () + "/" + fileName );
175+
176+ assertNotNull (failure );
177+ assertFalse (output .exists ());
178+ assertNull (findPartialFile (output ));
179+ }
180+
181+ @ Test
182+ void stalePartialFilesAreCleanedWithoutRemovingFreshDownloads () throws Exception {
183+ File directory = new File (JdbcDriverConstants .DRIVER_LIB_PATH );
184+ File stale = new File (directory , "jdbc-stale-" + UUID .randomUUID () + ".part" );
185+ File fresh = new File (directory , "jdbc-fresh-" + UUID .randomUUID () + ".part" );
186+ File matchingDirectory = new File (directory , "jdbc-directory-" + UUID .randomUUID () + ".part" );
187+ filesToDelete .add (stale );
188+ filesToDelete .add (fresh );
189+ filesToDelete .add (matchingDirectory );
190+ Files .writeString (stale .toPath (), "stale" , StandardCharsets .UTF_8 );
191+ Files .writeString (fresh .toPath (), "fresh" , StandardCharsets .UTF_8 );
192+ assertTrue (matchingDirectory .mkdir ());
193+ assertTrue (stale .setLastModified (System .currentTimeMillis () - TimeUnit .DAYS .toMillis (2 )));
194+ assertTrue (matchingDirectory .setLastModified (System .currentTimeMillis () - TimeUnit .DAYS .toMillis (2 )));
195+
196+ JdbcJarUtils .cleanupStalePartialFiles (directory );
197+
198+ assertFalse (stale .exists ());
199+ assertTrue (fresh .exists ());
200+ assertTrue (matchingDirectory .exists ());
56201 }
57202
58203 @ Test
@@ -123,4 +268,34 @@ private File outputFile(String fileName) {
123268 filesToDelete .add (output );
124269 return output ;
125270 }
271+
272+ private File awaitPartialFile (File output ) throws InterruptedException {
273+ long deadline = System .nanoTime () + TimeUnit .SECONDS .toNanos (10 );
274+ do {
275+ File partialFile = findPartialFile (output );
276+ if (partialFile != null ) {
277+ return partialFile ;
278+ }
279+ Thread .sleep (10 );
280+ } while (System .nanoTime () < deadline );
281+ return null ;
282+ }
283+
284+ private File findPartialFile (File output ) {
285+ String prefix = "jdbc-" + output .getName () + "-" ;
286+ File [] partialFiles = output .getParentFile ().listFiles (
287+ (directory , name ) -> name .startsWith (prefix ) && name .endsWith (".part" ));
288+ return partialFiles == null || partialFiles .length == 0 ? null : partialFiles [0 ];
289+ }
290+
291+ private IOException awaitAsyncDownload (String url ) throws Exception {
292+ CountDownLatch completion = new CountDownLatch (1 );
293+ AtomicReference <IOException > failure = new AtomicReference <>();
294+ JdbcJarUtils .asyncDownload (url , error -> {
295+ failure .set (error );
296+ completion .countDown ();
297+ });
298+ assertTrue (completion .await (10 , TimeUnit .SECONDS ));
299+ return failure .get ();
300+ }
126301}
0 commit comments