Skip to content

Commit e5dada4

Browse files
authored
[JENKINS-76184] Enable cache for webhook requests to avoid rate limit for large organisations (#1126) (#1132)
Invalidate webhook cache after create or update webhook
1 parent d93d29b commit e5dada4

File tree

8 files changed

+341
-56
lines changed

8 files changed

+341
-56
lines changed

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ protected String getProjectKey() {
752752

753753
private void setPrimaryCloneLinks(List<BitbucketHref> links) {
754754
links.forEach(link -> {
755-
if (Strings.CI.startsWith(link.getName(), "http")) {
755+
if (StringUtils.startsWithIgnoreCase(link.getName(), "http")) {
756756
// Remove the username from URL because it will be set into the GIT_URL variable
757757
// credentials used to git clone or push/pull operation could be different than this (for example SSH)
758758
// and will run into a failure

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudPage.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ public class BitbucketCloudPage<T> {
4040
private final List<T> values;
4141

4242
public BitbucketCloudPage(@JsonProperty("pagelen") int pageLength,
43-
@JsonProperty("page") int page,
44-
@JsonProperty("size") int size,
45-
@Nullable @JsonProperty("next") String next,
46-
@NonNull @JsonProperty("values") List<T> values) {
43+
@JsonProperty("page") int page,
44+
@JsonProperty("size") int size,
45+
@Nullable @JsonProperty("next") String next,
46+
@NonNull @JsonProperty("values") List<T> values) {
4747
this.pageLength = pageLength;
4848
this.page = page;
4949
this.size = size;

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/Cache.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
package com.cloudbees.jenkins.plugins.bitbucket.client;
2525

2626
import com.cloudbees.jenkins.plugins.bitbucket.impl.client.ICheckedCallable;
27+
import edu.umd.cs.findbugs.annotations.NonNull;
2728
import java.util.ArrayList;
2829
import java.util.Collections;
2930
import java.util.LinkedHashMap;
@@ -74,6 +75,10 @@ public void evictAll() {
7475
entries.clear();
7576
}
7677

78+
public void evict(@NonNull String key) {
79+
entries.remove(key);
80+
}
81+
7782
public int size() {
7883
return entries.size();
7984
}
@@ -179,4 +184,5 @@ public String toString() {
179184
}
180185
}
181186
}
187+
182188
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2025, Nikolas Falco
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook;
25+
26+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient;
27+
import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint;
28+
import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration;
29+
import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager;
30+
import edu.umd.cs.findbugs.annotations.NonNull;
31+
import edu.umd.cs.findbugs.annotations.Nullable;
32+
import org.apache.commons.lang3.ObjectUtils;
33+
import org.apache.commons.lang3.StringUtils;
34+
35+
import static org.apache.commons.lang3.StringUtils.upperCase;
36+
37+
public abstract class AbstractWebhookManager<T extends AbstractBitbucketWebhookConfiguration> implements BitbucketWebhookManager {
38+
39+
protected T configuration;
40+
protected String callbackURL;
41+
42+
@Override
43+
public void setCallbackURL(String callbackURL, BitbucketEndpoint endpoint) {
44+
this.callbackURL = callbackURL;
45+
}
46+
47+
@SuppressWarnings("unchecked")
48+
@Override
49+
public void apply(BitbucketWebhookConfiguration configuration) {
50+
this.configuration = (T) configuration;
51+
}
52+
53+
@Nullable
54+
protected String buildCacheKey(@NonNull BitbucketAuthenticatedClient client) {
55+
if (StringUtils.isNotBlank(client.getRepositoryName())) {
56+
return upperCase(client.getRepositoryOwner()) + "::" + client.getRepositoryName();
57+
} else {
58+
return null;
59+
}
60+
}
61+
62+
protected boolean isCacheEnabled(@NonNull BitbucketAuthenticatedClient client) {
63+
return configuration.isEnableCache() && StringUtils.isNotBlank(client.getRepositoryName());
64+
}
65+
66+
@Nullable
67+
protected String getEndpointJenkinsRootURL() {
68+
return ObjectUtils.getFirstNonNull(() -> configuration.getEndpointJenkinsRootURL(), () -> BitbucketWebhookConfiguration.getDefaultJenkinsRootURL());
69+
}
70+
}

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/cloud/CloudWebhookManager.java

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@
2626
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient;
2727
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException;
2828
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook;
29-
import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint;
3029
import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration;
31-
import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager;
3230
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudPage;
3331
import com.cloudbees.jenkins.plugins.bitbucket.client.Cache;
3432
import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketCloudWebhook;
@@ -37,6 +35,7 @@
3735
import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint;
3836
import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils;
3937
import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser;
38+
import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhookManager;
4039
import com.cloudbees.jenkins.plugins.bitbucket.util.BitbucketCredentialsUtils;
4140
import com.damnhandy.uri.template.UriTemplate;
4241
import com.fasterxml.jackson.core.type.TypeReference;
@@ -56,18 +55,15 @@
5655
import java.util.logging.Level;
5756
import java.util.logging.Logger;
5857
import jenkins.model.Jenkins;
59-
import jenkins.scm.api.trait.SCMSourceTrait;
6058
import org.apache.commons.collections.CollectionUtils;
61-
import org.apache.commons.lang3.ObjectUtils;
6259
import org.apache.commons.lang3.Strings;
6360
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
6461

6562
import static java.util.concurrent.TimeUnit.HOURS;
6663
import static java.util.concurrent.TimeUnit.MINUTES;
67-
import static org.apache.commons.lang3.StringUtils.upperCase;
6864

6965
@Extension
70-
public class CloudWebhookManager implements BitbucketWebhookManager {
66+
public class CloudWebhookManager extends AbstractWebhookManager<CloudWebhookConfiguration> {
7167
private static final String WEBHOOK_URL = "/2.0/repositories{/owner,repo}/hooks{/hook}{?page,pagelen}";
7268
private static final Logger logger = Logger.getLogger(CloudWebhookManager.class.getName());
7369
private static final Cache<String, List<BitbucketWebHook>> cachedRepositoryWebhooks = new Cache<>(3, HOURS);
@@ -91,36 +87,18 @@ public static List<String> stats() {
9187
HookEventType.PULL_REQUEST_DECLINED.getKey()
9288
));
9389

94-
private CloudWebhookConfiguration configuration;
95-
private String callbackURL;
96-
97-
@Override
98-
public Collection<Class<? extends SCMSourceTrait>> supportedTraits() {
99-
return Collections.emptyList();
100-
}
101-
102-
@Override
103-
public void apply(SCMSourceTrait configurationTrait) {
104-
// nothing to configure
105-
}
106-
10790
@Override
10891
public void apply(BitbucketWebhookConfiguration configuration) {
109-
this.configuration = (CloudWebhookConfiguration) configuration;
110-
if (this.configuration.isEnableCache()) {
92+
super.apply(configuration);
93+
if (super.configuration.isEnableCache()) {
11194
cachedRepositoryWebhooks.setExpireDuration(this.configuration.getWebhooksCacheDuration(), MINUTES);
11295
}
11396
}
11497

115-
@Override
116-
public void setCallbackURL(@NonNull String callbackURL, @NonNull BitbucketEndpoint endpoint) {
117-
this.callbackURL = callbackURL;
118-
}
119-
12098
@Override
12199
@NonNull
122100
public Collection<BitbucketWebHook> read(@NonNull BitbucketAuthenticatedClient client) throws IOException {
123-
String endpointJenkinsRootURL = ObjectUtils.firstNonNull(configuration.getEndpointJenkinsRootURL(), BitbucketWebhookConfiguration.getDefaultJenkinsRootURL());
101+
String endpointJenkinsRootURL = getEndpointJenkinsRootURL();
124102

125103
String url = UriTemplate.fromTemplate(WEBHOOK_URL)
126104
.set("owner", client.getRepositoryOwner())
@@ -145,9 +123,9 @@ public Collection<BitbucketWebHook> read(@NonNull BitbucketAuthenticatedClient c
145123
}
146124
return resources;
147125
};
148-
if (configuration.isEnableCache()) {
126+
if (isCacheEnabled(client)) {
149127
try {
150-
String cacheKey = upperCase(client.getRepositoryOwner()) + "::" + ObjectUtils.firstNonNull(client.getRepositoryName(), "<anonymous>");
128+
String cacheKey = buildCacheKey(client);
151129
return cachedRepositoryWebhooks.get(cacheKey, request);
152130
} catch (ExecutionException e) {
153131
BitbucketRequestException bre = BitbucketApiUtils.unwrap(e);
@@ -188,6 +166,10 @@ private void register(@NonNull BitbucketCloudWebhook payload, @NonNull Bitbucket
188166
.set("repo", client.getRepositoryName())
189167
.expand();
190168
client.post(url, JsonParser.toString(payload));
169+
if (isCacheEnabled(client)) {
170+
String cacheKey = buildCacheKey(client);
171+
cachedRepositoryWebhooks.evict(cacheKey);
172+
}
191173
}
192174

193175
private boolean shouldUpdate(@NonNull BitbucketCloudWebhook current, @NonNull BitbucketCloudWebhook expected) {
@@ -235,6 +217,10 @@ private void update(@NonNull BitbucketCloudWebhook payload, @NonNull BitbucketAu
235217
.set("hook", payload.getUuid())
236218
.expand();
237219
client.put(url, JsonParser.toString(payload));
220+
if (isCacheEnabled(client)) {
221+
String cacheKey = buildCacheKey(client);
222+
cachedRepositoryWebhooks.evict(cacheKey);
223+
}
238224
}
239225

240226
@Override

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/server/ServerWebhookManager.java

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@
2828
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook;
2929
import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint;
3030
import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration;
31-
import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager;
3231
import com.cloudbees.jenkins.plugins.bitbucket.client.Cache;
3332
import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType;
3433
import com.cloudbees.jenkins.plugins.bitbucket.impl.client.ICheckedCallable;
3534
import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils;
3635
import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser;
36+
import com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.AbstractWebhookManager;
3737
import com.cloudbees.jenkins.plugins.bitbucket.server.client.BitbucketServerPage;
3838
import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerWebhook;
3939
import com.cloudbees.jenkins.plugins.bitbucket.util.BitbucketCredentialsUtils;
@@ -55,17 +55,14 @@
5555
import java.util.logging.Level;
5656
import java.util.logging.Logger;
5757
import jenkins.model.Jenkins;
58-
import jenkins.scm.api.trait.SCMSourceTrait;
5958
import org.apache.commons.collections.CollectionUtils;
60-
import org.apache.commons.lang3.ObjectUtils;
6159
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
6260

6361
import static java.util.concurrent.TimeUnit.HOURS;
6462
import static java.util.concurrent.TimeUnit.MINUTES;
65-
import static org.apache.commons.lang3.StringUtils.upperCase;
6663

6764
@Extension
68-
public class ServerWebhookManager implements BitbucketWebhookManager {
65+
public class ServerWebhookManager extends AbstractWebhookManager<ServerWebhookConfiguration> {
6966
private static final String WEBHOOK_API = "/rest/api/1.0/projects/{owner}/repos/{repo}/webhooks{/id}{?start,limit}";
7067
private static final Logger logger = Logger.getLogger(ServerWebhookManager.class.getName());
7168
private static final Cache<String, List<BitbucketWebHook>> cachedRepositoryWebhooks = new Cache<>(3, HOURS);
@@ -92,19 +89,7 @@ public static List<String> stats() {
9289
HookEventType.SERVER_PULL_REQUEST_FROM_REF_UPDATED.getKey()
9390
));
9491

95-
private ServerWebhookConfiguration configuration;
9692
private String serverURL;
97-
private String callbackURL;
98-
99-
@Override
100-
public Collection<Class<? extends SCMSourceTrait>> supportedTraits() {
101-
return Collections.emptyList();
102-
}
103-
104-
@Override
105-
public void apply(SCMSourceTrait configurationTrait) {
106-
// nothing to configure
107-
}
10893

10994
@Override
11095
public void setCallbackURL(@NonNull String callbackURL, @NonNull BitbucketEndpoint endpoint) {
@@ -118,16 +103,16 @@ public void setCallbackURL(@NonNull String callbackURL, @NonNull BitbucketEndpoi
118103

119104
@Override
120105
public void apply(BitbucketWebhookConfiguration configuration) {
121-
this.configuration = (ServerWebhookConfiguration) configuration;
122-
if (this.configuration.isEnableCache()) {
106+
super.apply(configuration);
107+
if (super.configuration.isEnableCache()) {
123108
cachedRepositoryWebhooks.setExpireDuration(this.configuration.getWebhooksCacheDuration(), MINUTES);
124109
}
125110
}
126111

127112
@Override
128113
@NonNull
129114
public Collection<BitbucketWebHook> read(@NonNull BitbucketAuthenticatedClient client) throws IOException {
130-
String endpointJenkinsRootURL = ObjectUtils.firstNonNull(configuration.getEndpointJenkinsRootURL(), BitbucketWebhookConfiguration.getDefaultJenkinsRootURL());
115+
String endpointJenkinsRootURL = getEndpointJenkinsRootURL();
131116

132117
String url = UriTemplate.fromTemplate(WEBHOOK_API)
133118
.set("owner", client.getRepositoryOwner())
@@ -144,9 +129,9 @@ public Collection<BitbucketWebHook> read(@NonNull BitbucketAuthenticatedClient c
144129
.filter(hook -> hook.getUrl().startsWith(endpointJenkinsRootURL))
145130
.toList();
146131
};
147-
if (configuration.isEnableCache()) {
132+
if (isCacheEnabled(client)) {
148133
try {
149-
String cacheKey = upperCase(client.getRepositoryOwner()) + "::" + ObjectUtils.firstNonNull(client.getRepositoryName(), "<anonymous>");
134+
String cacheKey = buildCacheKey(client);
150135
return cachedRepositoryWebhooks.get(cacheKey, request);
151136
} catch (ExecutionException e) {
152137
BitbucketRequestException bre = BitbucketApiUtils.unwrap(e);
@@ -187,6 +172,10 @@ private void register(@NonNull BitbucketServerWebhook payload, @NonNull Bitbucke
187172
.set("repo", client.getRepositoryName())
188173
.expand();
189174
client.post(url, JsonParser.toString(payload));
175+
if (isCacheEnabled(client)) {
176+
String cacheKey = buildCacheKey(client);
177+
cachedRepositoryWebhooks.evict(cacheKey );
178+
}
190179
}
191180

192181
private boolean shouldUpdate(@NonNull BitbucketServerWebhook current, @NonNull BitbucketServerWebhook expected) {
@@ -235,6 +224,10 @@ private void update(@NonNull BitbucketServerWebhook payload, @NonNull BitbucketA
235224
.set("id", payload.getUuid())
236225
.expand();
237226
client.put(url, JsonParser.toString(payload));
227+
if (isCacheEnabled(client)) {
228+
String cacheKey = buildCacheKey(client);
229+
cachedRepositoryWebhooks.evict(cacheKey );
230+
}
238231
}
239232

240233
@Override

0 commit comments

Comments
 (0)