Skip to content

Commit d0e0969

Browse files
authored
fix: Fix lifecycle of shared clients obtained from EtcdClusterConfig (#58)
Motivation A bug was reported where an invalid client was returned from EtcdClusterConfig#getClient() after a client had previously been obtained and closed. This turned out to be due to some incorrect logic in how "released" ref-counted clients are recycled. Modifications - Fix the non-blocking logic in EtcdClientConfig#getClient() for obtaining a shared ref-counted client - in particular ensure that retain() is called on preexisting client instances from the cache. - Add unit test Result Client caching/sharing in EtcdClusterConfig works as intended
1 parent 016bc5e commit d0e0969

2 files changed

Lines changed: 46 additions & 14 deletions

File tree

src/main/java/com/ibm/etcd/client/config/EtcdClusterConfig.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@
2525
import java.io.InputStreamReader;
2626
import java.nio.charset.StandardCharsets;
2727
import java.security.cert.CertificateException;
28-
import java.util.ArrayList;
29-
import java.util.List;
30-
import java.util.Objects;
31-
import java.util.Properties;
32-
import java.util.Set;
28+
import java.util.*;
3329
import java.util.concurrent.ExecutionException;
3430
import java.util.regex.Matcher;
3531
import java.util.regex.Pattern;
@@ -276,6 +272,7 @@ public static EtcdClusterConfig fromJsonFileOrSimple(String fileOrSimpleString)
276272
}
277273

278274
private static final Cache<CacheKey, EtcdClient> clientCache = CacheBuilder.newBuilder().weakValues()
275+
// Not a problem to call close() more than once on the underlying clients
279276
.<CacheKey, EtcdClient>removalListener(rn -> EtcdClient.Internal.cleanup(rn.getValue())).build();
280277

281278
public static EtcdClient getClient(EtcdClusterConfig config) throws IOException, CertificateException {
@@ -285,15 +282,24 @@ public static EtcdClient getClient(EtcdClusterConfig config) throws IOException,
285282
while (true) {
286283
EtcdClient client = clientCache.getIfPresent(key);
287284
if (client == null) {
288-
EtcdClient[] maybeNew = new EtcdClient[1];
289-
client = clientCache.get(key, () -> maybeNew[0] = config.newClient());
290-
// if client != maybeNew[0] then we got a pre-existing client
291-
if (client != maybeNew[0] && !EtcdClient.Internal.retain(client)) {
292-
clientCache.invalidate(key);
293-
continue;
285+
final Deque<EtcdClient> maybeNew = new ArrayDeque<>(1);
286+
try {
287+
client = clientCache.get(key, () -> {
288+
EtcdClient newClient = config.newClient();
289+
maybeNew.add(newClient);
290+
return newClient;
291+
});
292+
if (client == maybeNew.peekLast()) { // else we got a preexisting client
293+
return maybeNew.pollLast();
294+
}
295+
} finally {
296+
maybeNew.stream().forEach(EtcdClient::close);
294297
}
295298
}
296-
return client;
299+
if (EtcdClient.Internal.retain(client)) {
300+
return client;
301+
}
302+
clientCache.invalidate(key); // Client is closed, discard and try again
297303
}
298304
} catch (ExecutionException ee) {
299305
Throwables.throwIfInstanceOf(ee.getCause(), IOException.class);

src/test/java/com/ibm/etcd/client/JsonConfigTest.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
package com.ibm.etcd.client;
1717

1818
import static com.ibm.etcd.client.KeyUtils.bs;
19-
import static org.junit.Assert.assertEquals;
20-
import static org.junit.Assert.assertTrue;
19+
import static org.junit.Assert.*;
2120

2221
import java.nio.charset.StandardCharsets;
2322

@@ -85,6 +84,33 @@ public void testSslCaCertConfig_noClientCert() throws Exception { // should fail
8584
runBasicTests(EtcdClusterConfig.fromJson(json));
8685
}
8786

87+
@Test
88+
public void testLifecycle() throws Exception {
89+
// Test shared client reuse/recreation
90+
ByteSource json = makeJson("http://localhost:2379", null, null, null);
91+
EtcdClusterConfig config = EtcdClusterConfig.fromJson(json);
92+
runBasicTests(config);
93+
runBasicTests(config);
94+
EtcdClient anotherClient = config.getClient();
95+
runBasicTests(config);
96+
assert !anotherClient.isClosed();
97+
anotherClient.close();
98+
assert anotherClient.isClosed();
99+
100+
EtcdClient c1 = config.getClient(), c2 = config.getClient();
101+
assertSame(c1, c2);
102+
c2.close();
103+
EtcdClient c3 = config.getClient();
104+
assertSame(c1, c3);
105+
c1.close();
106+
assert !c1.isClosed();
107+
c3.close();
108+
assert c1.isClosed();
109+
110+
EtcdClient c4 = config.getClient();
111+
assertNotSame(c1, c4);
112+
}
113+
88114
void runBasicTests(EtcdClusterConfig config) throws Exception {
89115
try (EtcdClient client = config.getClient()) {
90116
runBasicTests(client);

0 commit comments

Comments
 (0)