Skip to content

Commit 112f081

Browse files
committed
make connection pool configurable
1 parent c61f801 commit 112f081

6 files changed

Lines changed: 192 additions & 2 deletions

File tree

DOCS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ You can override this behavior by explicitly passing in the API key and secret a
5555
var properties = new Properties();
5656
properties.put(DefaultClient.API_KEY_PROP_NAME, "<api-key>");
5757
properties.put(DefaultClient.API_SECRET_PROP_NAME, "<api-secret>");
58+
properties.put(DefaultClient.CONNECTION_POOL_MAX_IDLE_CONNECTIONS_PROP_NAME, "20");
5859
var client = new DefaultClient(properties);
60+
client.setConnectionPool(20, Duration.ofSeconds(59));
5961
DefaultClient.setInstance(client);
6062
```
6163

@@ -1896,4 +1898,4 @@ Import.createImport(createUrlResponse.getPath(), Import.ImportMode.Upsert);
18961898
```java
18971899
// signature comes from the HTTP header x-signature
18981900
boolean valid = App.verifyWebhook(body, signature)
1899-
```
1901+
```

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ To configure the SDK you need to provide required properties
132132
| io.getstream.chat.apiSecret | STREAM_SECRET | - | Yes |
133133
| io.getstream.chat.timeout | STREAM_CHAT_TIMEOUT | 10000 | No |
134134
| io.getstream.chat.url | STREAM_CHAT_URL | https://chat.stream-io-api.com | No |
135+
| io.getstream.chat.connectionPool.maxIdleConnections | STREAM_CHAT_CONNECTION_POOL_MAX_IDLE_CONNECTIONS | 5 | No |
136+
| io.getstream.chat.connectionPool.keepAliveDurationMs | STREAM_CHAT_CONNECTION_POOL_KEEP_ALIVE_DURATION_MS | 59000 | No |
135137

136138
You can also use your own CDN by creating an implementation of FileHandler and setting it this way
137139

@@ -141,6 +143,20 @@ Message.fileHandlerClass = MyFileHandler.class
141143

142144
All setup must be done prior to any request to the API.
143145

146+
You can also tune the underlying OkHttp connection pool explicitly:
147+
148+
```java
149+
var properties = new Properties();
150+
properties.put(DefaultClient.API_KEY_PROP_NAME, "<api-key>");
151+
properties.put(DefaultClient.API_SECRET_PROP_NAME, "<api-secret>");
152+
properties.put(DefaultClient.CONNECTION_POOL_MAX_IDLE_CONNECTIONS_PROP_NAME, "20");
153+
properties.put(DefaultClient.CONNECTION_POOL_KEEP_ALIVE_DURATION_PROP_NAME, "59000");
154+
155+
var client = new DefaultClient(properties);
156+
client.setConnectionPool(20, Duration.ofSeconds(59));
157+
DefaultClient.setInstance(client);
158+
```
159+
144160
## Print Chat app configuration
145161

146162
<table>

src/main/java/io/getstream/chat/java/services/framework/Client.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public interface Client {
1919

2020
void setTimeout(@NotNull Duration timeoutDuration);
2121

22+
void setConnectionPool(int maxIdleConnections, @NotNull Duration keepAliveDuration);
23+
2224
static Client getInstance() {
2325
return DefaultClient.getInstance();
2426
}

src/main/java/io/getstream/chat/java/services/framework/DefaultClient.java

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ public class DefaultClient implements Client {
3333
public static final String API_TIMEOUT_PROP_NAME = "io.getstream.chat.timeout";
3434
public static final String API_URL_PROP_NAME = "io.getstream.chat.url";
3535
public static final String X_STREAM_EXT_PROP_NAME = "io.getstream.chat.xStreamExt";
36+
public static final String CONNECTION_POOL_MAX_IDLE_CONNECTIONS_PROP_NAME =
37+
"io.getstream.chat.connectionPool.maxIdleConnections";
38+
public static final String CONNECTION_POOL_KEEP_ALIVE_DURATION_PROP_NAME =
39+
"io.getstream.chat.connectionPool.keepAliveDurationMs";
3640

3741
private static final String API_DEFAULT_URL = "https://chat.stream-io-api.com";
42+
private static final int DEFAULT_MAX_IDLE_CONNECTIONS = 5;
43+
private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 59_000L;
3844
private static volatile DefaultClient defaultInstance;
3945
@NotNull private final String apiSecret;
4046
@NotNull private final String apiKey;
@@ -98,7 +104,7 @@ public DefaultClient(
98104
private OkHttpClient buildOkHttpClient() {
99105
OkHttpClient.Builder httpClient =
100106
new OkHttpClient.Builder()
101-
.connectionPool(new ConnectionPool(5, 59, TimeUnit.SECONDS))
107+
.connectionPool(buildConnectionPool(extendedProperties))
102108
.callTimeout(getStreamChatTimeout(extendedProperties), TimeUnit.MILLISECONDS);
103109
httpClient.interceptors().clear();
104110

@@ -191,6 +197,24 @@ public void setTimeout(@NotNull Duration timeoutDuration) {
191197
this.serviceFactory = serviceFactoryBuilder.apply(retrofit);
192198
}
193199

200+
@Override
201+
public void setConnectionPool(int maxIdleConnections, @NotNull Duration keepAliveDuration) {
202+
if (maxIdleConnections < 0) {
203+
throw new IllegalArgumentException("maxIdleConnections must be >= 0");
204+
}
205+
if (keepAliveDuration.isNegative()) {
206+
throw new IllegalArgumentException("keepAliveDuration must be >= 0");
207+
}
208+
209+
extendedProperties.setProperty(
210+
CONNECTION_POOL_MAX_IDLE_CONNECTIONS_PROP_NAME, Integer.toString(maxIdleConnections));
211+
extendedProperties.setProperty(
212+
CONNECTION_POOL_KEEP_ALIVE_DURATION_PROP_NAME,
213+
Long.toString(keepAliveDuration.toMillis()));
214+
this.retrofit = buildRetrofitClient(buildOkHttpClient());
215+
this.serviceFactory = serviceFactoryBuilder.apply(retrofit);
216+
}
217+
194218
private static @NotNull String jwtToken(String apiSecret) {
195219
Key signingKey =
196220
new SecretKeySpec(
@@ -233,6 +257,24 @@ private static Properties extendProperties(Properties properties) {
233257
canformedProperties.put(API_TIMEOUT_PROP_NAME, envTimeout);
234258
}
235259

260+
var envConnectionPoolMaxIdleConnections =
261+
env.getOrDefault(
262+
"STREAM_CHAT_CONNECTION_POOL_MAX_IDLE_CONNECTIONS",
263+
System.getProperty("STREAM_CHAT_CONNECTION_POOL_MAX_IDLE_CONNECTIONS"));
264+
if (envConnectionPoolMaxIdleConnections != null) {
265+
canformedProperties.put(
266+
CONNECTION_POOL_MAX_IDLE_CONNECTIONS_PROP_NAME, envConnectionPoolMaxIdleConnections);
267+
}
268+
269+
var envConnectionPoolKeepAliveDuration =
270+
env.getOrDefault(
271+
"STREAM_CHAT_CONNECTION_POOL_KEEP_ALIVE_DURATION_MS",
272+
System.getProperty("STREAM_CHAT_CONNECTION_POOL_KEEP_ALIVE_DURATION_MS"));
273+
if (envConnectionPoolKeepAliveDuration != null) {
274+
canformedProperties.put(
275+
CONNECTION_POOL_KEEP_ALIVE_DURATION_PROP_NAME, envConnectionPoolKeepAliveDuration);
276+
}
277+
236278
var envApiUrl = env.getOrDefault("STREAM_CHAT_URL", System.getProperty("STREAM_CHAT_URL"));
237279
if (envApiUrl != null) {
238280
canformedProperties.put(API_URL_PROP_NAME, envApiUrl);
@@ -255,6 +297,37 @@ private static long getStreamChatTimeout(@NotNull Properties properties) {
255297
return Long.parseLong(timeout.toString());
256298
}
257299

300+
private static @NotNull ConnectionPool buildConnectionPool(@NotNull Properties properties) {
301+
return new ConnectionPool(
302+
getConnectionPoolMaxIdleConnections(properties),
303+
getConnectionPoolKeepAliveDurationMs(properties),
304+
TimeUnit.MILLISECONDS);
305+
}
306+
307+
private static int getConnectionPoolMaxIdleConnections(@NotNull Properties properties) {
308+
var maxIdleConnections =
309+
properties.getOrDefault(
310+
CONNECTION_POOL_MAX_IDLE_CONNECTIONS_PROP_NAME, DEFAULT_MAX_IDLE_CONNECTIONS);
311+
int parsedMaxIdleConnections = Integer.parseInt(maxIdleConnections.toString());
312+
if (parsedMaxIdleConnections < 0) {
313+
throw new IllegalArgumentException(
314+
CONNECTION_POOL_MAX_IDLE_CONNECTIONS_PROP_NAME + " must be >= 0");
315+
}
316+
return parsedMaxIdleConnections;
317+
}
318+
319+
private static long getConnectionPoolKeepAliveDurationMs(@NotNull Properties properties) {
320+
var keepAliveDuration =
321+
properties.getOrDefault(
322+
CONNECTION_POOL_KEEP_ALIVE_DURATION_PROP_NAME, DEFAULT_KEEP_ALIVE_DURATION_MS);
323+
long parsedKeepAliveDuration = Long.parseLong(keepAliveDuration.toString());
324+
if (parsedKeepAliveDuration < 0) {
325+
throw new IllegalArgumentException(
326+
CONNECTION_POOL_KEEP_ALIVE_DURATION_PROP_NAME + " must be >= 0");
327+
}
328+
return parsedKeepAliveDuration;
329+
}
330+
258331
private static String getStreamChatBaseUrl(@NotNull Properties properties) {
259332
var url = properties.getOrDefault(API_URL_PROP_NAME, API_DEFAULT_URL);
260333
return url.toString();

src/main/java/io/getstream/chat/java/services/framework/UserClient.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,15 @@ public UserClient(Client delegate, String userToken) {
6969
public void setTimeout(@NotNull Duration timeoutDuration) {
7070
delegate.setTimeout(timeoutDuration);
7171
}
72+
73+
/**
74+
* Sets the HTTP connection pool configuration on the underlying client.
75+
*
76+
* @param maxIdleConnections the maximum number of idle connections to keep in the pool
77+
* @param keepAliveDuration how long idle connections should be kept alive
78+
*/
79+
@Override
80+
public void setConnectionPool(int maxIdleConnections, @NotNull Duration keepAliveDuration) {
81+
delegate.setConnectionPool(maxIdleConnections, keepAliveDuration);
82+
}
7283
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package io.getstream.chat.java;
2+
3+
import io.getstream.chat.java.services.framework.DefaultClient;
4+
import java.lang.reflect.Field;
5+
import java.time.Duration;
6+
import java.util.Properties;
7+
import okhttp3.ConnectionPool;
8+
import okhttp3.OkHttpClient;
9+
import org.junit.jupiter.api.Assertions;
10+
import org.junit.jupiter.api.DisplayName;
11+
import org.junit.jupiter.api.Test;
12+
import retrofit2.Retrofit;
13+
14+
public class DefaultClientConfigurationTest {
15+
16+
@Test
17+
@DisplayName("DefaultClient uses configured connection pool properties")
18+
void givenConnectionPoolProperties_whenCreatingClient_thenUsesConfiguredPool() {
19+
var properties = baseProperties();
20+
properties.put(DefaultClient.CONNECTION_POOL_MAX_IDLE_CONNECTIONS_PROP_NAME, "20");
21+
properties.put(DefaultClient.CONNECTION_POOL_KEEP_ALIVE_DURATION_PROP_NAME, "120000");
22+
23+
var client = new DefaultClient(properties);
24+
var pool = getConnectionPool(client);
25+
26+
Assertions.assertEquals(20, readIntField(poolDelegate(pool), "maxIdleConnections"));
27+
Assertions.assertEquals(
28+
Duration.ofMinutes(2).toNanos(), readLongField(poolDelegate(pool), "keepAliveDurationNs"));
29+
}
30+
31+
@Test
32+
@DisplayName("DefaultClient can update connection pool at runtime")
33+
void whenSettingConnectionPool_thenRebuildsClientWithNewPool() {
34+
var client = new DefaultClient(baseProperties());
35+
36+
client.setConnectionPool(15, Duration.ofSeconds(30));
37+
38+
var pool = getConnectionPool(client);
39+
Assertions.assertEquals(15, readIntField(poolDelegate(pool), "maxIdleConnections"));
40+
Assertions.assertEquals(
41+
Duration.ofSeconds(30).toNanos(), readLongField(poolDelegate(pool), "keepAliveDurationNs"));
42+
}
43+
44+
private static Properties baseProperties() {
45+
var properties = new Properties();
46+
properties.put(DefaultClient.API_KEY_PROP_NAME, "test-key");
47+
properties.put(DefaultClient.API_SECRET_PROP_NAME, "test-secret");
48+
return properties;
49+
}
50+
51+
private static ConnectionPool getConnectionPool(DefaultClient client) {
52+
Retrofit retrofit = (Retrofit) readField(client, "retrofit");
53+
OkHttpClient okHttpClient = (OkHttpClient) readField(retrofit, "callFactory");
54+
return okHttpClient.connectionPool();
55+
}
56+
57+
private static Object poolDelegate(ConnectionPool pool) {
58+
return readField(pool, "delegate");
59+
}
60+
61+
private static int readIntField(Object target, String fieldName) {
62+
return (int) readField(target, fieldName);
63+
}
64+
65+
private static long readLongField(Object target, String fieldName) {
66+
return (long) readField(target, fieldName);
67+
}
68+
69+
private static Object readField(Object target, String fieldName) {
70+
Class<?> type = target.getClass();
71+
while (type != null) {
72+
try {
73+
Field field = type.getDeclaredField(fieldName);
74+
field.setAccessible(true);
75+
return field.get(target);
76+
} catch (NoSuchFieldException ignored) {
77+
type = type.getSuperclass();
78+
} catch (IllegalAccessException ex) {
79+
throw new IllegalStateException(ex);
80+
}
81+
}
82+
83+
throw new IllegalStateException(
84+
String.format("Field '%s' not found on %s", fieldName, target.getClass().getName()));
85+
}
86+
}

0 commit comments

Comments
 (0)