Skip to content

Commit 2ee2ff3

Browse files
Fix connection pool exhaustion, statement leaks, and texture API error handling (#733)
* [FIX] Fix connection pool exhaustion, statement leaks, and texture API error handling - Fix deadlock: pass existing Connection to loadPermissions() instead of acquiring new ones from pool - Fix PreparedStatement leaks in MySQLCoreProvider: wrap all statements in try-with-resources - Optimize HWID lookup: add filtered SQL pre-query before full table scan fallback - Fix SimpleError deserialization: preserve HTTP status code when error response is empty - Add retry with backoff for texture API 429 (rate limit) responses - Remove synchronized bottleneck from getConnection() in MySQLSourceConfig/PostgreSQLSourceConfig - Fix getUsersByHardwareInfo iteration bug (isLast -> next) * [FIX] Replace blocking Thread.sleep retry with async CompletableFuture retry in JsonTextureProvider - Add sendAsync() to HttpRequester for non-blocking HTTP requests - Rewrite texture retry using CompletableFuture.delayedExecutor() instead of Thread.sleep() - No handler threads blocked during retry backoff on 429 rate limit --------- Co-authored-by: MrLeonardosVoid <i.serikov@void0.dev>
1 parent 5d021b5 commit 2ee2ff3

File tree

7 files changed

+233
-125
lines changed

7 files changed

+233
-125
lines changed

components/launchserver/src/main/java/pro/gravit/launchserver/HttpRequester.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.net.http.HttpClient;
1111
import java.net.http.HttpRequest;
1212
import java.time.Duration;
13+
import java.util.concurrent.CompletableFuture;
1314

1415
public class HttpRequester {
1516
private transient final HttpClient httpClient = HttpClient.newBuilder().build();
@@ -67,6 +68,10 @@ public <T> HttpHelper.HttpOptional<T, SimpleError> send(HttpRequest request, Typ
6768
return HttpHelper.send(httpClient, request, makeEH(type));
6869
}
6970

71+
public <T> CompletableFuture<HttpHelper.HttpOptional<T, SimpleError>> sendAsync(HttpRequest request, Type type) {
72+
return HttpHelper.sendAsync(httpClient, request, makeEH(type));
73+
}
74+
7075

7176
public static class SimpleErrorHandler<T> implements HttpHelper.HttpJsonErrorHandler<T, SimpleError> {
7277
private final Type type;
@@ -78,7 +83,16 @@ private SimpleErrorHandler(Type type) {
7883
@Override
7984
public HttpHelper.HttpOptional<T, SimpleError> applyJson(JsonElement response, int statusCode) {
8085
if (statusCode < 200 || statusCode >= 300) {
81-
return new HttpHelper.HttpOptional<>(null, Launcher.gsonManager.gson.fromJson(response, SimpleError.class), statusCode);
86+
SimpleError error = null;
87+
try {
88+
error = Launcher.gsonManager.gson.fromJson(response, SimpleError.class);
89+
} catch (Exception ignored) {
90+
}
91+
if (error == null || (error.error == null && error.code == 0)) {
92+
error = new SimpleError("HTTP " + statusCode);
93+
error.code = statusCode;
94+
}
95+
return new HttpHelper.HttpOptional<>(null, error, statusCode);
8296
}
8397
if (type == Void.class) {
8498
return new HttpHelper.HttpOptional<>(null, null, statusCode);

components/launchserver/src/main/java/pro/gravit/launchserver/auth/MySQLSourceConfig.java

Lines changed: 56 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public final class MySQLSourceConfig implements AutoCloseable, SQLSourceConfig {
3939
private boolean useHikari;
4040

4141
// Cache
42-
private transient DataSource source;
42+
private transient volatile DataSource source;
4343
private transient boolean hikari;
4444

4545

@@ -69,59 +69,64 @@ public synchronized void close() {
6969
}
7070

7171

72-
public synchronized Connection getConnection() throws SQLException {
73-
if (source == null) { // New data source
74-
MysqlDataSource mysqlSource = new MysqlDataSource();
75-
mysqlSource.setCharacterEncoding("UTF-8");
76-
77-
// Prep statements cache
78-
mysqlSource.setPrepStmtCacheSize(250);
79-
mysqlSource.setPrepStmtCacheSqlLimit(2048);
80-
mysqlSource.setCachePrepStmts(true);
81-
mysqlSource.setUseServerPrepStmts(true);
82-
83-
// General optimizations
84-
mysqlSource.setCacheServerConfiguration(true);
85-
mysqlSource.setUseLocalSessionState(true);
86-
mysqlSource.setRewriteBatchedStatements(true);
87-
mysqlSource.setMaintainTimeStats(false);
88-
mysqlSource.setUseUnbufferedInput(false);
89-
mysqlSource.setUseReadAheadInput(false);
90-
mysqlSource.setUseSSL(useSSL);
91-
mysqlSource.setVerifyServerCertificate(verifyCertificates);
92-
// Set credentials
93-
mysqlSource.setServerName(address);
94-
mysqlSource.setPortNumber(port);
95-
mysqlSource.setUser(username);
96-
mysqlSource.setPassword(password);
97-
mysqlSource.setDatabaseName(database);
98-
mysqlSource.setTcpNoDelay(true);
99-
if (timezone != null) mysqlSource.setServerTimezone(timezone);
100-
hikari = false;
101-
// Try using HikariCP
102-
source = mysqlSource;
103-
if (useHikari) {
104-
try {
105-
Class.forName("com.zaxxer.hikari.HikariDataSource");
106-
hikari = true; // Used for shutdown. Not instanceof because of possible classpath error
107-
HikariConfig hikariConfig = new HikariConfig();
108-
hikariConfig.setDataSource(mysqlSource);
109-
hikariConfig.setPoolName(poolName);
110-
hikariConfig.setMinimumIdle(1);
111-
hikariConfig.setMaximumPoolSize(MAX_POOL_SIZE);
112-
hikariConfig.setConnectionTestQuery("SELECT 1");
113-
hikariConfig.setConnectionTimeout(1000);
114-
hikariConfig.setLeakDetectionThreshold(2000);
115-
hikariConfig.setMaxLifetime(hikariMaxLifetime);
116-
// Set HikariCP pool
117-
// Replace source with hds
118-
source = new HikariDataSource(hikariConfig);
119-
} catch (ClassNotFoundException ignored) {
120-
logger.debug("HikariCP isn't in classpath for '{}'", poolName);
72+
public Connection getConnection() throws SQLException {
73+
DataSource ds = source;
74+
if (ds == null) {
75+
synchronized (this) {
76+
ds = source;
77+
if (ds == null) {
78+
ds = initDataSource();
79+
source = ds;
12180
}
12281
}
82+
}
83+
return ds.getConnection();
84+
}
12385

86+
private DataSource initDataSource() throws SQLException {
87+
MysqlDataSource mysqlSource = new MysqlDataSource();
88+
mysqlSource.setCharacterEncoding("UTF-8");
89+
90+
mysqlSource.setPrepStmtCacheSize(250);
91+
mysqlSource.setPrepStmtCacheSqlLimit(2048);
92+
mysqlSource.setCachePrepStmts(true);
93+
mysqlSource.setUseServerPrepStmts(true);
94+
95+
mysqlSource.setCacheServerConfiguration(true);
96+
mysqlSource.setUseLocalSessionState(true);
97+
mysqlSource.setRewriteBatchedStatements(true);
98+
mysqlSource.setMaintainTimeStats(false);
99+
mysqlSource.setUseUnbufferedInput(false);
100+
mysqlSource.setUseReadAheadInput(false);
101+
mysqlSource.setUseSSL(useSSL);
102+
mysqlSource.setVerifyServerCertificate(verifyCertificates);
103+
mysqlSource.setServerName(address);
104+
mysqlSource.setPortNumber(port);
105+
mysqlSource.setUser(username);
106+
mysqlSource.setPassword(password);
107+
mysqlSource.setDatabaseName(database);
108+
mysqlSource.setTcpNoDelay(true);
109+
if (timezone != null) mysqlSource.setServerTimezone(timezone);
110+
hikari = false;
111+
DataSource result = mysqlSource;
112+
if (useHikari) {
113+
try {
114+
Class.forName("com.zaxxer.hikari.HikariDataSource");
115+
hikari = true;
116+
HikariConfig hikariConfig = new HikariConfig();
117+
hikariConfig.setDataSource(mysqlSource);
118+
hikariConfig.setPoolName(poolName);
119+
hikariConfig.setMinimumIdle(1);
120+
hikariConfig.setMaximumPoolSize(MAX_POOL_SIZE);
121+
hikariConfig.setConnectionTestQuery("SELECT 1");
122+
hikariConfig.setConnectionTimeout(1000);
123+
hikariConfig.setLeakDetectionThreshold(2000);
124+
hikariConfig.setMaxLifetime(hikariMaxLifetime);
125+
result = new HikariDataSource(hikariConfig);
126+
} catch (ClassNotFoundException ignored) {
127+
logger.debug("HikariCP isn't in classpath for '{}'", poolName);
128+
}
124129
}
125-
return source.getConnection();
130+
return result;
126131
}
127132
}

components/launchserver/src/main/java/pro/gravit/launchserver/auth/PostgreSQLSourceConfig.java

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public final class PostgreSQLSourceConfig implements AutoCloseable, SQLSourceCon
3333
private final long hikariMaxLifetime = MINUTES.toMillis(30); // 30 minutes
3434

3535
// Cache
36-
private transient DataSource source;
36+
private transient volatile DataSource source;
3737
private transient boolean hikari;
3838

3939
@Override
@@ -43,43 +43,48 @@ public synchronized void close() {
4343
}
4444
}
4545

46-
public synchronized Connection getConnection() throws SQLException {
47-
if (source == null) { // New data source
48-
PGSimpleDataSource postgresqlSource = new PGSimpleDataSource();
49-
50-
// Set credentials
51-
postgresqlSource.setServerNames(addresses);
52-
postgresqlSource.setPortNumbers(ports);
53-
postgresqlSource.setUser(username);
54-
postgresqlSource.setPassword(password);
55-
postgresqlSource.setDatabaseName(database);
46+
public Connection getConnection() throws SQLException {
47+
DataSource ds = source;
48+
if (ds == null) {
49+
synchronized (this) {
50+
ds = source;
51+
if (ds == null) {
52+
ds = initDataSource();
53+
source = ds;
54+
}
55+
}
56+
}
57+
return ds.getConnection();
58+
}
5659

57-
// Try using HikariCP
58-
source = postgresqlSource;
60+
private DataSource initDataSource() {
61+
PGSimpleDataSource postgresqlSource = new PGSimpleDataSource();
5962

60-
//noinspection Duplicates
61-
try {
62-
Class.forName("com.zaxxer.hikari.HikariDataSource");
63-
hikari = true; // Used for shutdown. Not instanceof because of possible classpath error
63+
postgresqlSource.setServerNames(addresses);
64+
postgresqlSource.setPortNumbers(ports);
65+
postgresqlSource.setUser(username);
66+
postgresqlSource.setPassword(password);
67+
postgresqlSource.setDatabaseName(database);
6468

65-
// Set HikariCP pool
66-
HikariDataSource hikariSource = new HikariDataSource();
67-
hikariSource.setDataSource(source);
69+
hikari = false;
70+
DataSource result = postgresqlSource;
71+
try {
72+
Class.forName("com.zaxxer.hikari.HikariDataSource");
73+
hikari = true;
6874

69-
// Set pool settings
70-
hikariSource.setPoolName(poolName);
71-
hikariSource.setMinimumIdle(0);
72-
hikariSource.setMaximumPoolSize(MAX_POOL_SIZE);
73-
hikariSource.setIdleTimeout(SECONDS.toMillis(TIMEOUT));
74-
hikariSource.setMaxLifetime(hikariMaxLifetime);
75+
HikariDataSource hikariSource = new HikariDataSource();
76+
hikariSource.setDataSource(postgresqlSource);
77+
hikariSource.setPoolName(poolName);
78+
hikariSource.setMinimumIdle(0);
79+
hikariSource.setMaximumPoolSize(MAX_POOL_SIZE);
80+
hikariSource.setIdleTimeout(SECONDS.toMillis(TIMEOUT));
81+
hikariSource.setMaxLifetime(hikariMaxLifetime);
7582

76-
// Replace source with hds
77-
source = hikariSource;
78-
logger.info("HikariCP pooling enabled for '{}'", poolName);
79-
} catch (ClassNotFoundException ignored) {
80-
logger.warn("HikariCP isn't in classpath for '{}'", poolName);
81-
}
83+
result = hikariSource;
84+
logger.info("HikariCP pooling enabled for '{}'", poolName);
85+
} catch (ClassNotFoundException ignored) {
86+
logger.warn("HikariCP isn't in classpath for '{}'", poolName);
8287
}
83-
return source.getConnection();
88+
return result;
8489
}
8590
}

components/launchserver/src/main/java/pro/gravit/launchserver/auth/core/AbstractSQLCoreProvider.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ private SQLUser queryUser(String sql, String param) throws SQLException {
504504
try (ResultSet rs = s.executeQuery()) {
505505
SQLUser user = constructUser(rs);
506506
if (user != null) {
507-
user.permissions = loadPermissions(user.uuid.toString());
507+
user.permissions = loadPermissions(c, user.uuid.toString());
508508
}
509509
return user;
510510
}
@@ -557,18 +557,23 @@ public boolean isRolesEnabled() {
557557
}
558558

559559
public ClientPermissions loadPermissions(String uuid) throws SQLException {
560+
try (Connection c = getSQLConfig().getConnection()) {
561+
return loadPermissions(c, uuid);
562+
}
563+
}
564+
565+
public ClientPermissions loadPermissions(Connection c, String uuid) throws SQLException {
560566
List<String> roles = isRolesEnabled()
561-
? queryStringColumn(queryRolesByUserUUID, uuid, rolesNameColumn)
567+
? queryStringColumn(c, queryRolesByUserUUID, uuid, rolesNameColumn)
562568
: List.of();
563569
List<String> perms = isPermissionsEnabled()
564-
? queryStringColumn(queryPermissionsByUUIDSQL, uuid, permissionsPermissionColumn)
570+
? queryStringColumn(c, queryPermissionsByUUIDSQL, uuid, permissionsPermissionColumn)
565571
: List.of();
566572
return new ClientPermissions(roles, perms);
567573
}
568574

569-
private List<String> queryStringColumn(String sql, String param, String column) throws SQLException {
570-
try (Connection c = getSQLConfig().getConnection();
571-
PreparedStatement s = c.prepareStatement(sql)) {
575+
private List<String> queryStringColumn(Connection c, String sql, String param, String column) throws SQLException {
576+
try (PreparedStatement s = c.prepareStatement(sql)) {
572577
s.setString(1, param);
573578
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
574579
try (ResultSet rs = s.executeQuery()) {

0 commit comments

Comments
 (0)