Skip to content

Commit cd3f650

Browse files
committed
Retire SQL workers even when connection-pool cleanup fails
1 parent 9ac3286 commit cd3f650

3 files changed

Lines changed: 76 additions & 6 deletions

File tree

‎SimpleAPI/src/main/java/com/bencodez/simpleapi/sql/mysql/MySQL.java‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,17 @@ public boolean connect(MysqlConfig config, boolean emitDiagnostics) {
126126
* Close all connections and the data source.
127127
*/
128128
public void disconnect() {
129-
if (connectionManager != null) {
130-
connectionManager.close();
131-
}
132-
if (threadPool != null) {
133-
threadPool.shutdown();
134-
}
129+
try {
130+
if (connectionManager != null) connectionManager.close();
131+
} catch (RuntimeException | Error failure) {
132+
// Pool cleanup and worker retirement are independent responsibilities.
133+
try { if (threadPool != null) threadPool.shutdown(); }
134+
catch (RuntimeException | Error cleanup) {
135+
if (cleanup != failure) failure.addSuppressed(cleanup);
136+
}
137+
throw failure;
138+
}
139+
if (threadPool != null) threadPool.shutdown();
135140
}
136141

137142
/**
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.bencodez.simpleapi.sql.mysql;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
import static org.mockito.Mockito.*;
5+
6+
import java.sql.SQLException;
7+
import java.util.concurrent.ExecutorService;
8+
import java.util.concurrent.Executors;
9+
import java.util.stream.Stream;
10+
import org.junit.jupiter.api.Test;
11+
12+
class MySQLDisconnectTest {
13+
static Stream<Throwable[]> failures() {
14+
return Stream.of(
15+
pair(null, null),
16+
pair(new IllegalStateException("pool close"), null),
17+
pair(new LinkageError("pool close"), null),
18+
pair(null, new IllegalStateException("worker shutdown")),
19+
pair(null, new LinkageError("worker shutdown")),
20+
pair(new IllegalStateException("pool close"), new LinkageError("worker shutdown")),
21+
pair(new LinkageError("pool close"), new IllegalStateException("worker shutdown")));
22+
}
23+
private static Throwable[] pair(Throwable pool, Throwable worker) { return new Throwable[] {pool, worker}; }
24+
@Test void everyCleanupIsAttemptedAndPrimaryFailureIsPreserved() throws Exception {
25+
for (Throwable[] failures : failures().toList()) checkCleanup(failures[0], failures[1]);
26+
}
27+
private void checkCleanup(Throwable poolFailure, Throwable workerFailure) throws Exception {
28+
var worker = mock(ExecutorService.class); var manager = mock(ConnectionManager.class);
29+
if (poolFailure != null) doThrow(poolFailure).when(manager).close();
30+
if (workerFailure != null) doThrow(workerFailure).when(worker).shutdown();
31+
try (var executors = mockStatic(Executors.class)) {
32+
executors.when(() -> Executors.newFixedThreadPool(anyInt())).thenReturn(worker);
33+
var mysql = mysql();
34+
var field = MySQL.class.getDeclaredField("connectionManager"); field.setAccessible(true); field.set(mysql, manager);
35+
Throwable primary = poolFailure == null ? workerFailure : poolFailure;
36+
if (primary == null) assertDoesNotThrow(mysql::disconnect);
37+
else {
38+
Throwable thrown = assertThrows(primary.getClass(), mysql::disconnect);
39+
assertSame(primary, thrown);
40+
if (poolFailure != null && workerFailure != null)
41+
assertArrayEquals(new Throwable[] { workerFailure }, thrown.getSuppressed());
42+
else assertEquals(0, thrown.getSuppressed().length);
43+
}
44+
verify(manager).close(); verify(worker).shutdown();
45+
}
46+
}
47+
@Test void disconnectBeforeConnectAndRepeatedDisconnectAreSafe() {
48+
var mysql = mysql();
49+
assertDoesNotThrow(mysql::disconnect); assertDoesNotThrow(mysql::disconnect);
50+
assertTrue(mysql.getThreadPool().isShutdown());
51+
}
52+
private static MySQL mysql() {
53+
return new MySQL(1) {
54+
public void debug(SQLException failure) { fail("Unexpected diagnostics"); }
55+
public void debug(String message) { fail("Unexpected diagnostics"); }
56+
public void severe(String message) { fail("Unexpected diagnostics"); }
57+
};
58+
}
59+
}

‎docs/sql-initialization-lifecycle.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ and invoke `init()` after construction. The legacy non-deferred constructors
1717
still initialize the table using subclass schema hooks; subclasses whose hooks
1818
need instance fields should use the existing deferred constructor. Successful
1919
external `MySQL.connect(config)` calls retain their debug diagnostics.
20+
21+
Disconnect attempts both connection-pool closure and worker shutdown. A pool-close
22+
failure remains the primary exception; any independent worker-shutdown failure is
23+
attached as suppressed evidence. Worker retirement is still attempted after failed
24+
pool cleanup, including during aborted table construction. Normal shutdown remains
25+
graceful and does not interrupt accepted worker tasks.

0 commit comments

Comments
 (0)