Skip to content

Commit d308349

Browse files
committed
[JENKINS-76184] Enable cache for webhook requests to avoid rate limit for large organisations
Add cache of requests for native webhook implementations
1 parent faa3934 commit d308349

File tree

13 files changed

+214
-24
lines changed

13 files changed

+214
-24
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ META-INF/
2222
/.apt_generated/
2323
/.apt_generated_tests/
2424
/bin/
25+
/.asciidoctorconfig.adoc

docs/USER_GUIDE.adoc

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ You can setup a custom Jenkins URL to be used as callback URL by the webhooks.
9494
For Bitbucket Data Center only, it is possible chose which webhooks implementation server side to use:
9595

9696
- Native implementation will configure the webhooks provided by default with the Server, so it will always be available.
97-
98-
- Plugin implementation (*deprecated*) relies on the configuration available via specific APIs provided by the link:https://marketplace.atlassian.com/apps/1215474/post-webhooks-for-bitbucket?tab=overview&hosting=datacenter[Post Webhooks for Bitbucket] plugin itself. To get it worked plugin must be already pre-installed on the server instance. This provider allows custom settings managed by the _ignore committers_ trait. _Note: This specific implementation will be moved to an individual repository as soon as link:https://issues.jenkins.io/browse/JENKINS-74913[JENKINS-74913] is implemented._
97+
- Plugin implementation (*deprecated*) replaced by https://github.com/jenkinsci/bitbucket-webhooks-plugin[this plugin]
98+
- Any other extension point implementation installed in your Jenkins instance
9999

100100
image::images/screenshot-14.png[]
101101

@@ -120,6 +120,35 @@ Any incoming webhook payloads from that given endpoint will be validated against
120120

121121
image::images/screenshot-20.png[]
122122

123+
=== Enable webhooks cache
124+
125+
If your organisation has a large number of repositories (over 500), you may easily reach the API rate limit.
126+
Any requests made to Bitbucket beyond 1,000 per hour will be rejected. In this case, enable caching to allow webhooks from repositories beyond the 500th to be processed within the next hour (when the rate is unlocked).
127+
128+
See https://issues.jenkins.io/browse/JENKINS-76184[JENKINS-76184] for the use case.
129+
130+
image::images/screenshot-23.png[]
131+
132+
=== Manual registration
133+
134+
If your organisation does not allow credentials to handle repository webhooks than you can provide to register webhook manually. You can follow one of these official Atlassian guides: for https://support.atlassian.com/bitbucket-cloud/docs/manage-webhooks[Cloud] or for https://confluence.atlassian.com/bitbucketserver/manage-webhooks-938025878.html[Data Center].
135+
136+
Go to the Bitbucket _Repository_ » _Repository settings_ » _Webhooks than _Add Webhook_
137+
138+
Provide a title of your choice, if you generate a secret for payload verification than confiure signature verification in Bitbucket endpoint as in the previous chapter.
139+
140+
Select events as shown in the image; any other types selected will be ignored.
141+
142+
The callback URL must be configured as follow:
143+
* Cloud: <Jenkins root URL>/bitbucket-scmsource-hook/notify
144+
* Server: <Jenkins root URL>/bitbucket-scmsource-hook/notify?server_url=<Bitbucket Data Center URL>
145+
146+
The <Jenkins root URL> must match the Jenkins public host; if Jenkins is behind a reverse proxy, ensure the URL provided matches the one in Manage Jenkins » System » Jenkins Location.
147+
In other cases, you can provide the Jenkins root URL to use, in the webhook configuration page on the Bitbucket endpoint.
148+
The <Bitbucket Data Center URL> must match the server address configured in the Bitbucket endpoint. Otherwise, incoming webhooks will be discarded.
149+
150+
image::images/screenshot-24.png[]
151+
123152
[id=bitbucket-creds-config]
124153
== Credentials configuration
125154

docs/images/screenshot-23.png

23.1 KB
Loading

docs/images/screenshot-24.png

105 KB
Loading

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public class BitbucketCloudApiClient extends AbstractBitbucketApi implements Bit
113113
private static final Cache<String, BitbucketTeam> cachedTeam = new Cache<>(6, HOURS);
114114
private static final Cache<String, List<BitbucketCloudRepository>> cachedRepositories = new Cache<>(3, HOURS);
115115
private static final Cache<String, BitbucketCloudCommit> cachedCommits = new Cache<>(24, HOURS);
116-
private transient BitbucketRepository cachedRepository;
116+
private transient BitbucketRepository localCachedRepository;
117117
private transient String cachedDefaultBranch;
118118

119119
public static List<String> stats() {
@@ -265,14 +265,14 @@ public BitbucketRepository getRepository() throws IOException {
265265
if (repositoryName == null) {
266266
throw new UnsupportedOperationException("Cannot get a repository from an API instance that is not associated with a repository");
267267
}
268-
if (!enableCache || cachedRepository == null) {
268+
if (!enableCache || localCachedRepository == null) {
269269
String url = UriTemplate.fromTemplate(REPO_URL_TEMPLATE)
270270
.set("owner", owner)
271271
.set("repo", repositoryName)
272272
.expand();
273-
cachedRepository = getRequestAs(url, BitbucketCloudRepository.class);
273+
localCachedRepository = getRequestAs(url, BitbucketCloudRepository.class);
274274
}
275-
return cachedRepository;
275+
return localCachedRepository;
276276
}
277277

278278
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ protected BitbucketApi create(@Nullable String serverUrl, @Nullable BitbucketAut
4848
.lookupEndpoint(BitbucketCloudEndpoint.SERVER_URL, BitbucketCloudEndpoint.class)
4949
.orElse(null);
5050
boolean enableCache = false;
51-
int teamCacheDuration = 0;
52-
int repositoriesCacheDuration = 0;
51+
int teamCacheDuration = 360;
52+
int repositoriesCacheDuration = 180;
5353
if (endpoint != null) {
5454
enableCache = endpoint.isEnableCache();
5555
teamCacheDuration = endpoint.getTeamCacheDuration();

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import hudson.util.ListBoxModel;
4040
import java.net.MalformedURLException;
4141
import java.net.URL;
42+
import java.util.List;
4243
import jenkins.model.Jenkins;
4344
import org.apache.commons.lang3.StringUtils;
4445
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
@@ -84,6 +85,13 @@ public abstract class AbstractBitbucketWebhookConfiguration implements Bitbucket
8485
*/
8586
private String endpointJenkinsRootURL;
8687

88+
private boolean enableCache = false;
89+
90+
/**
91+
* How long, in minutes, to cache the webhook response.
92+
*/
93+
private Integer webhooksCacheDuration;
94+
8795
protected AbstractBitbucketWebhookConfiguration(boolean manageHooks, @CheckForNull String credentialsId,
8896
boolean enableHookSignature, @CheckForNull String hookSignatureCredentialsId) {
8997
this.manageHooks = manageHooks && StringUtils.isNotBlank(credentialsId);
@@ -144,7 +152,47 @@ public String getDisplayName() {
144152
return Messages.ServerWebhookImplementation_displayName();
145153
}
146154

155+
public boolean isEnableCache() {
156+
return enableCache;
157+
}
158+
159+
@DataBoundSetter
160+
public void setEnableCache(boolean enableCache) {
161+
this.enableCache = enableCache;
162+
}
163+
164+
public Integer getWebhooksCacheDuration() {
165+
return webhooksCacheDuration;
166+
}
167+
168+
@DataBoundSetter
169+
public void setWebhooksCacheDuration(Integer webhooksCacheDuration) {
170+
this.webhooksCacheDuration = webhooksCacheDuration == null || webhooksCacheDuration < 0 ? Integer.valueOf(180) : webhooksCacheDuration;
171+
}
172+
147173
public abstract static class AbstractBitbucketWebhookDescriptorImpl extends BitbucketWebhookDescriptor {
174+
protected abstract void clearCaches();
175+
protected abstract List<String> getStats();
176+
177+
@RequirePOST
178+
public FormValidation doShowStats() {
179+
Jenkins.get().checkPermission(Jenkins.MANAGE);
180+
181+
List<String> stats = getStats();
182+
StringBuilder builder = new StringBuilder();
183+
for (String stat : stats) {
184+
builder.append(stat).append("<br>");
185+
}
186+
return FormValidation.okWithMarkup(builder.toString());
187+
}
188+
189+
@RequirePOST
190+
public FormValidation doClear() {
191+
Jenkins.get().checkPermission(Jenkins.MANAGE);
192+
193+
clearCaches();
194+
return FormValidation.ok("Caches cleared");
195+
}
148196

149197
@RequirePOST
150198
public static FormValidation doCheckEndpointJenkinsRootURL(@QueryParameter String value) {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import edu.umd.cs.findbugs.annotations.NonNull;
3333
import hudson.Extension;
3434
import hudson.util.ListBoxModel;
35+
import java.util.List;
3536
import jenkins.model.Jenkins;
3637
import org.jenkinsci.Symbol;
3738
import org.kohsuke.stapler.DataBoundConstructor;
@@ -69,6 +70,16 @@ public Class<? extends BitbucketWebhookManager> getManager() {
6970
@Extension
7071
public static class DescriptorImpl extends AbstractBitbucketWebhookDescriptorImpl {
7172

73+
@Override
74+
protected void clearCaches() {
75+
CloudWebhookManager.clearCaches();
76+
}
77+
78+
@Override
79+
protected List<String> getStats() {
80+
return CloudWebhookManager.stats();
81+
}
82+
7283
@Override
7384
public String getDisplayName() {
7485
return "Native Cloud";

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

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@
2424
package com.cloudbees.jenkins.plugins.bitbucket.impl.webhook.cloud;
2525

2626
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticatedClient;
27+
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException;
2728
import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook;
2829
import com.cloudbees.jenkins.plugins.bitbucket.api.endpoint.BitbucketEndpoint;
2930
import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookConfiguration;
3031
import com.cloudbees.jenkins.plugins.bitbucket.api.webhook.BitbucketWebhookManager;
3132
import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudPage;
33+
import com.cloudbees.jenkins.plugins.bitbucket.client.Cache;
3234
import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketCloudWebhook;
3335
import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType;
36+
import com.cloudbees.jenkins.plugins.bitbucket.impl.client.ICheckedCallable;
3437
import com.cloudbees.jenkins.plugins.bitbucket.impl.endpoint.BitbucketCloudEndpoint;
38+
import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils;
3539
import com.cloudbees.jenkins.plugins.bitbucket.impl.util.JsonParser;
3640
import com.cloudbees.jenkins.plugins.bitbucket.util.BitbucketCredentialsUtils;
3741
import com.damnhandy.uri.template.UriTemplate;
@@ -48,6 +52,7 @@
4852
import java.util.List;
4953
import java.util.Set;
5054
import java.util.TreeSet;
55+
import java.util.concurrent.ExecutionException;
5156
import java.util.logging.Level;
5257
import java.util.logging.Logger;
5358
import jenkins.model.Jenkins;
@@ -57,10 +62,25 @@
5762
import org.apache.commons.lang3.Strings;
5863
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
5964

65+
import static java.util.concurrent.TimeUnit.HOURS;
66+
import static java.util.concurrent.TimeUnit.MINUTES;
67+
import static org.apache.commons.lang3.StringUtils.upperCase;
68+
6069
@Extension
6170
public class CloudWebhookManager implements BitbucketWebhookManager {
6271
private static final String WEBHOOK_URL = "/2.0/repositories{/owner,repo}/hooks{/hook}{?page,pagelen}";
6372
private static final Logger logger = Logger.getLogger(CloudWebhookManager.class.getName());
73+
private static final Cache<String, List<BitbucketWebHook>> cachedRepositoryWebhooks = new Cache<>(3, HOURS);
74+
75+
public static void clearCaches() {
76+
cachedRepositoryWebhooks.evictAll();
77+
}
78+
79+
public static List<String> stats() {
80+
List<String> stats = new ArrayList<>();
81+
stats.add("Repositories webhooks: " + cachedRepositoryWebhooks.stats().toString());
82+
return stats;
83+
}
6484

6585
// The list of events available in Bitbucket Cloud.
6686
private static final List<String> CLOUD_EVENTS = Collections.unmodifiableList(Arrays.asList(
@@ -87,6 +107,9 @@ public void apply(SCMSourceTrait configurationTrait) {
87107
@Override
88108
public void apply(BitbucketWebhookConfiguration configuration) {
89109
this.configuration = (CloudWebhookConfiguration) configuration;
110+
if (this.configuration.isEnableCache()) {
111+
cachedRepositoryWebhooks.setExpireDuration(this.configuration.getWebhooksCacheDuration(), MINUTES);
112+
}
90113
}
91114

92115
@Override
@@ -105,21 +128,38 @@ public Collection<BitbucketWebHook> read(@NonNull BitbucketAuthenticatedClient c
105128
.set("pagelen", 100)
106129
.expand();
107130

108-
List<BitbucketWebHook> resources = new ArrayList<>();
131+
ICheckedCallable<List<BitbucketWebHook>, IOException> request = () -> {
132+
List<BitbucketWebHook> resources = new ArrayList<>();
109133

110-
TypeReference<BitbucketCloudPage<BitbucketCloudWebhook>> type = new TypeReference<BitbucketCloudPage<BitbucketCloudWebhook>>(){};
111-
BitbucketCloudPage<BitbucketCloudWebhook> page = JsonParser.toJava(client.get(url), type);
112-
resources.addAll(page.getValues().stream()
113-
.filter(hook -> hook.getUrl().startsWith(endpointJenkinsRootURL))
114-
.toList());
115-
while (!page.isLastPage()){
116-
String response = client.get(page.getNext());
117-
page = JsonParser.toJava(response, type);
134+
TypeReference<BitbucketCloudPage<BitbucketCloudWebhook>> type = new TypeReference<BitbucketCloudPage<BitbucketCloudWebhook>>(){};
135+
BitbucketCloudPage<BitbucketCloudWebhook> page = JsonParser.toJava(client.get(url), type);
118136
resources.addAll(page.getValues().stream()
119137
.filter(hook -> hook.getUrl().startsWith(endpointJenkinsRootURL))
120138
.toList());
139+
while (!page.isLastPage()){
140+
String response = client.get(page.getNext());
141+
page = JsonParser.toJava(response, type);
142+
resources.addAll(page.getValues().stream()
143+
.filter(hook -> hook.getUrl().startsWith(endpointJenkinsRootURL))
144+
.toList());
145+
}
146+
return resources;
147+
};
148+
if (configuration.isEnableCache()) {
149+
try {
150+
String cacheKey = upperCase(client.getRepositoryOwner()) + "::" + ObjectUtils.firstNonNull(client.getRepositoryName(), "<anonymous>");
151+
return cachedRepositoryWebhooks.get(cacheKey, request);
152+
} catch (ExecutionException e) {
153+
BitbucketRequestException bre = BitbucketApiUtils.unwrap(e);
154+
if (bre != null) {
155+
throw bre;
156+
} else {
157+
throw new IOException(e);
158+
}
159+
}
160+
} else {
161+
return request.call();
121162
}
122-
return resources;
123163
}
124164

125165
@NonNull

src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/webhook/plugin/PluginPushWebhookProcessor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ public boolean canHandle(Map<String, String> headers, MultiValuedMap<String, Str
6161

6262
@Override
6363
public void process(@NonNull String eventType, @NonNull String payload, @NonNull Map<String, Object> context, @NonNull BitbucketEndpoint endpoint) {
64+
logger.warning("Plugin webhook is deprecated, it has been replaced by the bitbucket-webhooks-plugin, documentation available at https://github.com/jenkinsci/bitbucket-webhooks-plugin.");
65+
6466
BitbucketPushEvent push = BitbucketServerWebhookPayload.pushEventFromPayload(payload);
6567
if (push != null) {
6668
if (push.getChanges().isEmpty()) {

0 commit comments

Comments
 (0)