Skip to content

Commit b83f53a

Browse files
authored
Merge pull request #717 from salesforce/rate-limit
DFIU to support rate limiting the PR creation in a given amount of time.
2 parents 4d77f25 + a66bf44 commit b83f53a

File tree

18 files changed

+635
-86
lines changed

18 files changed

+635
-86
lines changed

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,11 @@ named arguments:
141141
-s {true,false}, --skipprcreation {true,false}
142142
Only update image tag store. Skip creating PRs
143143
-x X comment snippet mentioned in line just before FROM instruction for ignoring a child image. Defaults to 'no-dfiu'
144-
144+
-r, --rate-limit-pr-creations
145+
Enable rateLimiting for throttling the number of PRs DFIU will cut over a period of time.
146+
The argument value should be in format "<positive_integer>-<ISO-8601_formatted_time>". For example "--rate-limit-pr-creations 60-PT1H" to create 60 PRs per hour.
147+
Default is not set, this means no ratelimiting is imposed.
148+
145149
subcommands:
146150
Specify which feature to perform
147151
@@ -213,6 +217,41 @@ Example:
213217
FROM imagename:imagetag # no-dfiu
214218
```
215219

220+
### PR throttling
221+
222+
In case you want to throttle the number of PRs cut by DFIU over a period of time,
223+
set --rate-limit-pr-creations with appropriate value.
224+
225+
##### Default case:
226+
227+
By default, this feature is disabled. This will be enabled when argument ``--rate-limit-pr-creations`` will be passed
228+
with appropriate value.
229+
230+
```
231+
example: dockerfile-image-update all image-tag-store-repo-falcon //throttling will be disabled by default
232+
```
233+
234+
##### Configuring the rate limit:
235+
236+
Below are some examples that will throttle the number of PRs cut based on values passed to the
237+
argument ``--rate-limit-pr-creations``
238+
The argument value should be in format ``<positive_integer>-<ISO-8601_formatted_time>``.
239+
For example ``--rate-limit-pr-creations 60-PT1H`` would mean the tool will cut 60 PRs every hour and the rate of adding
240+
a new PR will be (PT1H/60) i.e. one minute.
241+
This will distribute the load uniformly and avoid sudden spikes, The process will go in waiting state until next PR
242+
could be sent.
243+
244+
Below are some more examples:
245+
246+
```
247+
Usage:
248+
dockerfile-image-update --rate-limit-pr-creations 60-PT1H all image-tag-store-repo-falcon //DFIU can send up to 60 PRs per hour.
249+
dockerfile-image-update --rate-limit-pr-creations 500-PT1H all image-tag-store-repo-falcon //DFIU can send up to 500 PRs per hour.
250+
dockerfile-image-update --rate-limit-pr-creations 86400-PT24H all image-tag-store-repo-falcon //DFIU can send up to 1 PRs per second.
251+
dockerfile-image-update --rate-limit-pr-creations 1-PT1S all image-tag-store-repo-falcon //Same as above. DFIU can send up to 1 PRs per second.
252+
dockerfile-image-update --rate-limit-pr-creations 5000 all image-tag-store-repo-falcon //rate limiting will be disabled because argument is not in correct format.
253+
```
254+
216255
## Developer Guide
217256

218257
### Building

dockerfile-image-update/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@
139139
<version>3.24.0</version>
140140
<scope>compile</scope>
141141
</dependency>
142+
<dependency>
143+
<groupId>com.bucket4j</groupId>
144+
<artifactId>bucket4j-core</artifactId>
145+
<version>8.1.1</version>
146+
</dependency>
142147
</dependencies>
143148

144149
<build>

dockerfile-image-update/src/main/java/com/salesforce/dockerfileimageupdate/CommandLine.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Set;
2727
import java.util.TreeSet;
2828
import java.util.stream.Collectors;
29+
import static com.salesforce.dockerfileimageupdate.utils.Constants.*;
2930

3031
/**
3132
* Created by minho-park on 6/29/2016.
@@ -84,6 +85,12 @@ static ArgumentParser getArgumentParser() {
8485
parser.addArgument("-x")
8586
.help("comment snippet mentioned in line just before FROM instruction for ignoring a child image. " +
8687
"Defaults to 'no-dfiu'");
88+
parser.addArgument("-r", "--" + RATE_LIMIT_PR_CREATION)
89+
.type(String.class)
90+
.setDefault("")
91+
.required(false)
92+
.help("Use RateLimiting when sending PRs. RateLimiting is enabled only if this value is set it's disabled by default.");
93+
8794
return parser;
8895
}
8996

dockerfile-image-update/src/main/java/com/salesforce/dockerfileimageupdate/subcommands/impl/All.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.salesforce.dockerfileimageupdate.utils.ImageStoreUtil;
2020
import com.salesforce.dockerfileimageupdate.utils.ProcessingErrors;
2121
import com.salesforce.dockerfileimageupdate.utils.PullRequests;
22+
import com.salesforce.dockerfileimageupdate.utils.RateLimiter;
2223
import net.sourceforge.argparse4j.inf.Namespace;
2324
import org.kohsuke.github.*;
2425
import org.slf4j.Logger;
@@ -37,20 +38,22 @@ public class All implements ExecutableWithNamespace {
3738
@Override
3839
public void execute(final Namespace ns, final DockerfileGitHubUtil dockerfileGitHubUtil) throws Exception {
3940
loadDockerfileGithubUtil(dockerfileGitHubUtil);
41+
RateLimiter rateLimiter = RateLimiter.getInstance(ns);
4042
String store = ns.get(Constants.STORE);
4143
try {
4244
ImageTagStore imageTagStore = ImageStoreUtil.initializeImageTagStore(this.dockerfileGitHubUtil, store);
4345
List<ImageTagStoreContent> imageNamesWithTag = imageTagStore.getStoreContent(dockerfileGitHubUtil, store);
4446
Integer numberOfImagesToProcess = imageNamesWithTag.size();
45-
List<ProcessingErrors> imagesThatCouldNotBeProcessed = processImagesWithTag(ns, imageNamesWithTag);
47+
List<ProcessingErrors> imagesThatCouldNotBeProcessed = processImagesWithTag(ns, imageNamesWithTag, rateLimiter);
4648
printSummary(imagesThatCouldNotBeProcessed, numberOfImagesToProcess);
4749
} catch (Exception e) {
4850
log.error("Encountered issues while initializing the image tag store or getting its contents. Cannot continue. Exception: ", e);
4951
System.exit(2);
5052
}
5153
}
5254

53-
protected List<ProcessingErrors> processImagesWithTag(Namespace ns, List<ImageTagStoreContent> imageNamesWithTag) {
55+
protected List<ProcessingErrors> processImagesWithTag(Namespace ns, List<ImageTagStoreContent> imageNamesWithTag,
56+
RateLimiter rateLimiter) {
5457
Integer gitApiSearchLimit = ns.get(Constants.GIT_API_SEARCH_LIMIT);
5558
Map<String, Boolean> orgsToIncludeInSearch = new HashMap<>();
5659
if (ns.get(Constants.GIT_ORG) != null) {
@@ -64,13 +67,15 @@ protected List<ProcessingErrors> processImagesWithTag(Namespace ns, List<ImageTa
6467
for (ImageTagStoreContent content : imageNamesWithTag) {
6568
String image = content.getImageName();
6669
String tag = content.getTag();
67-
failureMessage = processImageWithTag(image, tag, ns, orgsToIncludeInSearch, gitApiSearchLimit);
70+
failureMessage = processImageWithTag(image, tag, ns, orgsToIncludeInSearch, gitApiSearchLimit,
71+
rateLimiter);
6872
failureMessage.ifPresent(message -> imagesThatCouldNotBeProcessed.add(processErrorMessages(image, tag, Optional.of(message))));
6973
}
7074
return imagesThatCouldNotBeProcessed;
7175
}
7276

73-
protected Optional<Exception> processImageWithTag(String image, String tag, Namespace ns, Map<String, Boolean> orgsToIncludeInSearch, Integer gitApiSearchLimit) {
77+
protected Optional<Exception> processImageWithTag(String image, String tag, Namespace ns, Map<String, Boolean> orgsToIncludeInSearch,
78+
Integer gitApiSearchLimit, RateLimiter rateLimiter) {
7479
Optional<Exception> failureMessage = Optional.empty();
7580
try {
7681
PullRequests pullRequests = getPullRequests();
@@ -85,7 +90,7 @@ protected Optional<Exception> processImageWithTag(String image, String tag, Name
8590
while (it.hasNext()){
8691
try {
8792
pullRequests.prepareToCreate(ns, pullRequestSender,
88-
it.next(), gitForkBranch, dockerfileGitHubUtil);
93+
it.next(), gitForkBranch, dockerfileGitHubUtil, rateLimiter);
8994
} catch (IOException e) {
9095
log.error("Could not send pull request for image {}.", image);
9196
failureMessage = Optional.of(e);

dockerfile-image-update/src/main/java/com/salesforce/dockerfileimageupdate/subcommands/impl/Child.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.salesforce.dockerfileimageupdate.utils.Constants;
1717
import com.salesforce.dockerfileimageupdate.utils.DockerfileGitHubUtil;
1818
import com.salesforce.dockerfileimageupdate.utils.ImageStoreUtil;
19+
import com.salesforce.dockerfileimageupdate.utils.RateLimiter;
1920
import net.sourceforge.argparse4j.inf.Namespace;
2021
import org.kohsuke.github.GHRepository;
2122
import org.slf4j.Logger;
@@ -36,6 +37,7 @@ public void execute(final Namespace ns, final DockerfileGitHubUtil dockerfileGit
3637
String forceTag = ns.get(Constants.FORCE_TAG);
3738
String store = ns.get(Constants.STORE);
3839
ImageStoreUtil imageStoreUtil = getImageStoreUtil();
40+
RateLimiter rateLimiter = RateLimiter.getInstance(ns);
3941
try {
4042
ImageTagStore imageTagStore = imageStoreUtil.initializeImageTagStore(dockerfileGitHubUtil, store);
4143
/* Updates store if a store is specified. */
@@ -68,7 +70,8 @@ public void execute(final Namespace ns, final DockerfileGitHubUtil dockerfileGit
6870
dockerfileGitHubUtil.createPullReq(repo,
6971
gitForkBranch.getBranchName(),
7072
fork,
71-
pullRequestInfo);
73+
pullRequestInfo,
74+
rateLimiter);
7275
}
7376

7477
protected ImageStoreUtil getImageStoreUtil(){

dockerfile-image-update/src/main/java/com/salesforce/dockerfileimageupdate/subcommands/impl/Parent.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.salesforce.dockerfileimageupdate.utils.DockerfileGitHubUtil;
1919
import com.salesforce.dockerfileimageupdate.utils.ImageStoreUtil;
2020
import com.salesforce.dockerfileimageupdate.utils.PullRequests;
21+
import com.salesforce.dockerfileimageupdate.utils.RateLimiter;
2122
import net.sourceforge.argparse4j.inf.Namespace;
2223
import org.kohsuke.github.GHContent;
2324
import org.kohsuke.github.PagedSearchIterable;
@@ -59,6 +60,7 @@ public void execute(final Namespace ns, DockerfileGitHubUtil dockerfileGitHubUti
5960
PullRequests pullRequests = getPullRequests();
6061
GitHubPullRequestSender pullRequestSender = getPullRequestSender(dockerfileGitHubUtil, ns);
6162
GitForkBranch gitForkBranch = getGitForkBranch(ns);
63+
RateLimiter rateLimiter = RateLimiter.getInstance(ns);
6264
log.info("Finding Dockerfiles with the given image...");
6365

6466
Integer gitApiSearchLimit = ns.get(Constants.GIT_API_SEARCH_LIMIT);
@@ -69,7 +71,8 @@ public void execute(final Namespace ns, DockerfileGitHubUtil dockerfileGitHubUti
6971
for (int i = 0; i < contentsFoundWithImage.size(); i++ ) {
7072
try {
7173
pullRequests.prepareToCreate(ns, pullRequestSender,
72-
contentsFoundWithImage.get(i), gitForkBranch, dockerfileGitHubUtil);
74+
contentsFoundWithImage.get(i), gitForkBranch,
75+
dockerfileGitHubUtil, rateLimiter);
7376
} catch (IOException e) {
7477
log.error("Could not send pull request.", e);
7578
}

dockerfile-image-update/src/main/java/com/salesforce/dockerfileimageupdate/utils/Constants.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
package com.salesforce.dockerfileimageupdate.utils;
1010

1111

12+
import java.time.Duration;
13+
1214
/**
1315
* @author minho-park
1416
*/
@@ -37,5 +39,13 @@ private Constants() {
3739
public static final String GIT_API_SEARCH_LIMIT = "ghapisearchlimit";
3840
public static final String SKIP_PR_CREATION = "skipprcreation";
3941
public static final String IGNORE_IMAGE_STRING = "x";
42+
public static final String RATE_LIMIT_PR_CREATION = "rate-limit-pr-creations";
43+
//max number of PRs to be sent (or tokens to be added) per DEFAULT_RATE_LIMIT_DURATION(per hour in this case)
44+
public static final long DEFAULT_RATE_LIMIT = 60;
45+
46+
public static final long DEFAULT_CONSUMING_TOKEN_RATE = 1;
47+
public static final Duration DEFAULT_RATE_LIMIT_DURATION = Duration.ofMinutes(DEFAULT_RATE_LIMIT);
48+
//token adding rate(here:a token added every 2 minutes in the bucket)
49+
public static final Duration DEFAULT_TOKEN_ADDING_RATE = Duration.ofMinutes(DEFAULT_CONSUMING_TOKEN_RATE);
4050

4151
}

dockerfile-image-update/src/main/java/com/salesforce/dockerfileimageupdate/utils/DockerfileGitHubUtil.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,10 +357,22 @@ public GitHubJsonStore getGitHubJsonStore(String store) {
357357
}
358358

359359
public void createPullReq(GHRepository origRepo,
360-
String branch, GHRepository forkRepo,
361-
PullRequestInfo pullRequestInfo) throws InterruptedException, IOException {
360+
String branch,
361+
GHRepository forkRepo,
362+
PullRequestInfo pullRequestInfo,
363+
RateLimiter rateLimiter) throws InterruptedException, IOException {
362364
// TODO: This may loop forever in the event of constant -1 pullRequestExitCodes...
363365
while (true) {
366+
// TODO: accept rateLimiter Optional with option to get no-op rateLimiter
367+
// where it's not required.
368+
if(rateLimiter != null) {
369+
log.info("Trying to consume a token before creating pull request..");
370+
// Consume a token from the token bucket.
371+
// If a token is not available this method will block until
372+
// the refill adds one to the bucket.
373+
rateLimiter.consume();
374+
log.info("Token consumed, proceeding with PR creation..");
375+
}
364376
int pullRequestExitCode = gitHubUtil.createPullReq(origRepo,
365377
branch, forkRepo, pullRequestInfo.getTitle(), pullRequestInfo.getBody());
366378
if (pullRequestExitCode == 0) {
@@ -484,7 +496,8 @@ public void changeDockerfiles(Namespace ns,
484496
Multimap<String, GitHubContentToProcess> pathToDockerfilesInParentRepo,
485497
GitHubContentToProcess gitHubContentToProcess,
486498
List<String> skippedRepos,
487-
GitForkBranch gitForkBranch) throws IOException,
499+
GitForkBranch gitForkBranch,
500+
RateLimiter rateLimiter) throws IOException,
488501
InterruptedException {
489502
// Should we skip doing a getRepository just to fill in the parent value? We already know this to be the parent...
490503
GHRepository parent = gitHubContentToProcess.getParent();
@@ -527,7 +540,8 @@ public void changeDockerfiles(Namespace ns,
527540
createPullReq(parent,
528541
gitForkBranch.getBranchName(),
529542
forkedRepo,
530-
pullRequestInfo);
543+
pullRequestInfo,
544+
rateLimiter);
531545
}
532546
}
533547
}

dockerfile-image-update/src/main/java/com/salesforce/dockerfileimageupdate/utils/PullRequests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public void prepareToCreate(final Namespace ns,
1616
GitHubPullRequestSender pullRequestSender,
1717
PagedSearchIterable<GHContent> contentsFoundWithImage,
1818
GitForkBranch gitForkBranch,
19-
DockerfileGitHubUtil dockerfileGitHubUtil) throws IOException {
19+
DockerfileGitHubUtil dockerfileGitHubUtil,
20+
RateLimiter rateLimiter) throws IOException {
2021
Multimap<String, GitHubContentToProcess> pathToDockerfilesInParentRepo =
2122
pullRequestSender.forkRepositoriesFoundAndGetPathToDockerfiles(contentsFoundWithImage, gitForkBranch);
2223
List<IOException> exceptions = new ArrayList<>();
@@ -28,7 +29,8 @@ public void prepareToCreate(final Namespace ns,
2829
try {
2930
dockerfileGitHubUtil.changeDockerfiles(ns,
3031
pathToDockerfilesInParentRepo,
31-
forkWithContentPaths.get(), skippedRepos, gitForkBranch);
32+
forkWithContentPaths.get(), skippedRepos,
33+
gitForkBranch, rateLimiter);
3234
} catch (IOException | InterruptedException e) {
3335
log.error(String.format("Error changing Dockerfile for %s", forkWithContentPaths.get().getParent().getFullName()), e);
3436
exceptions.add((IOException) e);

0 commit comments

Comments
 (0)