Skip to content

Commit a035ac6

Browse files
authored
Merge pull request #407 from Vlatombe/JENKINS-55138
[JENKINS-55138] Don't close kubernetes client upon cache removal
2 parents 508b6ef + 486218a commit a035ac6

File tree

1 file changed

+131
-41
lines changed

1 file changed

+131
-41
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,88 @@
11
package org.csanchez.jenkins.plugins.kubernetes;
22

3-
import com.google.common.base.Objects;
4-
import com.google.common.cache.Cache;
5-
import com.google.common.cache.CacheBuilder;
6-
import com.google.common.cache.RemovalListener;
7-
8-
import io.fabric8.kubernetes.client.KubernetesClient;
9-
103
import java.io.IOException;
114
import java.security.KeyStoreException;
125
import java.security.NoSuchAlgorithmException;
136
import java.security.UnrecoverableKeyException;
147
import java.security.cert.CertificateEncodingException;
8+
import java.util.ArrayList;
9+
import java.util.Collections;
10+
import java.util.HashSet;
11+
import java.util.Iterator;
12+
import java.util.List;
13+
import java.util.Set;
1514
import java.util.concurrent.TimeUnit;
15+
import java.util.logging.Level;
16+
import java.util.logging.Logger;
17+
18+
import com.google.common.base.Objects;
19+
import com.google.common.cache.Cache;
20+
import com.google.common.cache.CacheBuilder;
21+
import io.fabric8.kubernetes.client.HttpClientAware;
22+
import io.fabric8.kubernetes.client.KubernetesClient;
23+
import okhttp3.Dispatcher;
24+
import okhttp3.OkHttpClient;
25+
26+
import hudson.Extension;
27+
import hudson.XmlFile;
28+
import hudson.model.AsyncPeriodicWork;
29+
import hudson.model.Saveable;
30+
import hudson.model.TaskListener;
31+
import hudson.model.listeners.SaveableListener;
32+
import jenkins.model.Jenkins;
1633

1734
/**
1835
* Manages the Kubernetes client creation per cloud
1936
*/
2037
final class KubernetesClientProvider {
2138

22-
private static final Cache<String, Client> clients;
23-
24-
private static final Integer CACHE_SIZE;
25-
private static final Integer CACHE_TTL;
26-
27-
static {
28-
CACHE_SIZE = Integer.getInteger("org.csanchez.jenkins.plugins.kubernetes.clients.cacheSize", 10);
29-
CACHE_TTL = Integer.getInteger("org.csanchez.jenkins.plugins.kubernetes.clients.cacheTtl", 60);
30-
clients = CacheBuilder.newBuilder()
31-
.maximumSize(CACHE_SIZE)
32-
.expireAfterWrite(CACHE_TTL, TimeUnit.MINUTES)
33-
.removalListener((RemovalListener<String, Client>) removalNotification -> {
34-
// https://google.github.io/guava/releases/23.0/api/docs/com/google/common/cache/RemovalNotification.html
35-
// A notification of the removal of a single entry. The key and/or value may be null if they were already garbage collected.
36-
if (removalNotification.getValue() != null) {
37-
removalNotification.getValue().getClient().close();
39+
private static final Logger LOGGER = Logger.getLogger(KubernetesClientProvider.class.getName());
40+
41+
private static final Integer CACHE_SIZE = Integer.getInteger("org.csanchez.jenkins.plugins.kubernetes.clients.cacheSize", 10);
42+
43+
private static final List<KubernetesClient> expiredClients = Collections.synchronizedList(new ArrayList());
44+
45+
private static final Cache<String, Client> clients = CacheBuilder
46+
.newBuilder()
47+
.maximumSize(CACHE_SIZE)
48+
.removalListener(rl -> {
49+
LOGGER.log(Level.FINE, "{0} cache : Removing entry for {1}", new Object[] {KubernetesClient.class.getSimpleName(), rl.getKey()});
50+
KubernetesClient client = ((Client) rl.getValue()).getClient();
51+
if (client != null) {
52+
if (client instanceof HttpClientAware) {
53+
if (!gracefulClose(client, ((HttpClientAware) client).getHttpClient())) {
54+
expiredClients.add(client);
55+
}
56+
} else {
57+
LOGGER.log(Level.WARNING, "{0} is not {1}, forcing close", new Object[] {client.toString(), HttpClientAware.class.getSimpleName()});
58+
client.close();
3859
}
39-
})
40-
.build();
41-
}
60+
}
61+
62+
})
63+
.build();
4264

4365
private KubernetesClientProvider() {
4466
}
4567

4668
static KubernetesClient createClient(KubernetesCloud cloud) throws NoSuchAlgorithmException, UnrecoverableKeyException,
4769
KeyStoreException, IOException, CertificateEncodingException {
48-
49-
final int validity = Objects.hashCode(cloud.getServerUrl(), cloud.getNamespace(), cloud.getServerCertificate(),
50-
cloud.getCredentialsId(), cloud.isSkipTlsVerify(), cloud.getConnectTimeout(), cloud.getReadTimeout(),
51-
cloud.getMaxRequestsPerHostStr());
52-
final Client c = clients.getIfPresent(cloud.getDisplayName());
53-
54-
if (c != null && validity == c.getValidity()) {
55-
return c.getClient();
56-
} else {
57-
// expire tha cache if any of these config options have changed
58-
if (c != null) {
59-
c.client.close();
60-
}
70+
String displayName = cloud.getDisplayName();
71+
final Client c = clients.getIfPresent(displayName);
72+
if (c == null) {
6173
KubernetesClient client = new KubernetesFactoryAdapter(cloud.getServerUrl(), cloud.getNamespace(),
6274
cloud.getServerCertificate(), cloud.getCredentialsId(), cloud.isSkipTlsVerify(),
6375
cloud.getConnectTimeout(), cloud.getReadTimeout(), cloud.getMaxRequestsPerHost()).createClient();
64-
clients.put(cloud.getDisplayName(), new Client(validity, client));
65-
76+
clients.put(displayName, new Client(getValidity(cloud), client));
6677
return client;
6778
}
79+
return c.getClient();
80+
}
81+
82+
private static int getValidity(KubernetesCloud cloud) {
83+
return Objects.hashCode(cloud.getServerUrl(), cloud.getNamespace(), cloud.getServerCertificate(),
84+
cloud.getCredentialsId(), cloud.isSkipTlsVerify(), cloud.getConnectTimeout(), cloud.getReadTimeout(),
85+
cloud.getMaxRequestsPerHostStr());
6886
}
6987

7088
private static class Client {
@@ -85,4 +103,76 @@ public int getValidity() {
85103
}
86104
}
87105

106+
@Extension
107+
public static class PurgeExpiredKubernetesClients extends AsyncPeriodicWork {
108+
109+
public PurgeExpiredKubernetesClients() {
110+
super("Purge expired KubernetesClients");
111+
}
112+
113+
@Override
114+
public long getRecurrencePeriod() {
115+
return TimeUnit.MINUTES.toMillis(1);
116+
}
117+
118+
@Override
119+
protected Level getNormalLoggingLevel() {
120+
return Level.FINE;
121+
}
122+
123+
@Override
124+
protected void execute(TaskListener listener) {
125+
for (Iterator<KubernetesClient> it = expiredClients.iterator(); it.hasNext();) {
126+
KubernetesClient client = it.next();
127+
if (client instanceof HttpClientAware) {
128+
if (gracefulClose(client, ((HttpClientAware) client).getHttpClient())) {
129+
it.remove();
130+
}
131+
} else {
132+
LOGGER.log(Level.WARNING, "{0} is not {1}, forcing close", new Object[] {client.toString(), HttpClientAware.class.getSimpleName()});
133+
client.close();
134+
it.remove();
135+
}
136+
}
137+
}
138+
}
139+
140+
private static boolean gracefulClose(KubernetesClient client, OkHttpClient httpClient) {
141+
Dispatcher dispatcher = httpClient.dispatcher();
142+
// Remove the client if there are no more users
143+
int runningCallsCount = dispatcher.runningCallsCount();
144+
int queuedCallsCount = dispatcher.queuedCallsCount();
145+
if (runningCallsCount == 0 && queuedCallsCount == 0) {
146+
LOGGER.log(Level.INFO, "Closing {0}", client.toString());
147+
client.close();
148+
return true;
149+
} else {
150+
LOGGER.log(Level.INFO, "Not closing {0}: there are still running ({1}) or queued ({2}) calls", new Object[] {client.toString(), runningCallsCount, queuedCallsCount});
151+
return false;
152+
}
153+
}
154+
155+
@Extension
156+
public static class SaveableListenerImpl extends SaveableListener {
157+
@Override
158+
public void onChange(Saveable o, XmlFile file) {
159+
if (o instanceof Jenkins) {
160+
Jenkins jenkins = (Jenkins) o;
161+
Set<String> cloudDisplayNames = new HashSet<>(clients.asMap().keySet());
162+
for (KubernetesCloud cloud : jenkins.clouds.getAll(KubernetesCloud.class)) {
163+
String displayName = cloud.getDisplayName();
164+
Client client = clients.getIfPresent(displayName);
165+
if (client != null && client.getValidity() == getValidity(cloud)) {
166+
cloudDisplayNames.remove(displayName);
167+
}
168+
}
169+
// Remove missing / invalid clients
170+
for (String displayName : cloudDisplayNames) {
171+
clients.invalidate(displayName);
172+
}
173+
}
174+
super.onChange(o, file);
175+
}
176+
}
177+
88178
}

0 commit comments

Comments
 (0)