11package com .salesforce .dockerfileimageupdate .utils ;
22
33import java .time .Duration ;
4+ import java .time .format .DateTimeParseException ;
45import java .util .UnknownFormatConversionException ;
5- import java .util .regex .Matcher ;
6- import java .util .regex .Pattern ;
76import org .apache .commons .lang3 .StringUtils ;
8- import org .apache .commons .lang3 .math .NumberUtils ;
7+ import org .slf4j .Logger ;
8+ import org .slf4j .LoggerFactory ;
99
1010/**
11- * Creating this as inner class as existence of this class outside
12- * RateLimiter class will make a little sense. This class is solely
13- * meant to encapsulate the primitives needed for the RateLimiter
14- * and logic for populating those after tokenizing the input
11+ * Class meant to tokenize input and encapsulate the primitives needed for
12+ * the RateLimiter
13+ *
14+ * @see RateLimiter
1515 */
1616public class RateLimit {
1717 public static final String ERROR_MESSAGE = "Unexpected format or unit encountered, valid input is " +
18- "<integer>-per-<time-unit> where time-init is one of 's', 'm', or 'h'. " +
19- "Example: 500-per-h" ;
20- private static final Pattern eventPattern =
21- Pattern .compile ("(^[1-9]\\ d{0,18})(-per-)?([1-9]\\ d{0,18})*([smh]?)" , Pattern .CASE_INSENSITIVE );
18+ "<integer>-<ISO-8601_formatted_time> Example: 500-PT1S means(500 per second) \n " +
19+ "500-PT1M means(500 per Minute)" ;
20+ private static final Logger log = LoggerFactory .getLogger (RateLimit .class );
2221 private final long rate ; //Maximum count of PRs to be sent per rLimitDuration
2322 private final Duration duration ; //Rate Limit duration
2423 private final Duration tokenAddingRate ; //Rate at which tokens are added in the bucket
2524
2625 /**
2726 * Initialize RateLimit object with based on parameters passed
28- * @param rate Maximum count of PRs to be sent per rLimitDuration
29- * @param duration Rate Limit duration
27+ *
28+ * @param rate Maximum count of PRs to be sent per rLimitDuration
29+ * @param duration Rate Limit duration
3030 * @param tokenAddingRate Rate at which tokens are added in the bucket
3131 */
3232 public RateLimit (long rate , Duration duration , Duration tokenAddingRate ) {
@@ -43,68 +43,50 @@ public RateLimit() {
4343 Constants .DEFAULT_TOKEN_ADDING_RATE );
4444 }
4545
46- public static RateLimit tokenizeAndGetRateLimit (String input ) {
47-
48- if (input == null ) {
46+ /**
47+ * This method will accept an input string in format <integer>-<ISO-8601_formatted_time> and
48+ * will tokenize it to create a RateLimit object.
49+ * <pre>
50+ * Examples of tokenization
51+ * input : 500-PT1S -> {rate : 500, duration 1 second (PT1S), token adding rate: every .002 seconds (PT0.002S)}
52+ * 60-PT1M -> {rate : 60, duration 1 minute (PT1M), token adding rate: every 1 seconds (PT1S)}
53+ * </pre>
54+ *
55+ * @param input string in format <integer>-<ISO-8601 formatted_time]> example 500-PT1S , 500-PT60S , 500-PT1H
56+ * @return RateLimit based on the input value
57+ * @throws UnknownFormatConversionException if input is not a string with expected format <integer>-<ISO-8601_formatted_time>
58+ */
59+ public static RateLimit tokenizeAndGetRateLimit (String input ) throws UnknownFormatConversionException {
60+ if (StringUtils .isEmpty (input ) || !input .contains ("-" )) {
4961 throw new UnknownFormatConversionException (ERROR_MESSAGE );
5062 }
5163
52- long rLimit ;
53- Duration rLimitDuration ;
54- Duration tokAddingRate ;
55- String literalConstant ;
56- String rateDuration ;
57- String rateDurationUnitChar ;
64+ int delimiterIndex = input .indexOf ('-' );
5865
59- Matcher matcher = eventPattern .matcher (input );
66+ try {
67+ long inputRate = Long .parseLong (input .substring (0 , delimiterIndex ));
68+ // "PT20.345S" -- parses as "20.345 seconds"
69+ // "PT15M" -- parses as "15 minutes" (where a minute is 60 seconds)
70+ // "PT10H" -- parses as "10 hours" (where an hour is 3600 seconds)
71+ // "P2D" -- parses as "2 days" (where a day is 24 hours or 86400
72+ Duration inputDuration = Duration .parse (input .substring (delimiterIndex + 1 ));
6073
61- if (!matcher .matches ()) {
62- throw new UnknownFormatConversionException (ERROR_MESSAGE );
63- } else {
64- /**
65- * eventPattern regex is divided into 5 groups
66- * 0 group being the whole expression
67- * 1 group is numeric rate limit value and will always
68- * be present if exp matched
69- * 2 is literal constant '-per-' and is optional
70- * 3 is numeric rate duration and is optional
71- * 4 is a char(s/m/h) and is optional
72- */
73- rLimit = Long .parseLong (matcher .group (1 )); // will always have a numeric value that fits in Long
74- literalConstant = matcher .group (2 ); // can be null or empty or '-per-'
75- rateDuration = matcher .group (3 );// can be null or empty or numeric value
76- rateDurationUnitChar = matcher .group (4 ); //can be empty or char s,m,h
77- }
74+ // Keeping token adding rate based on the lowest supported rate i.e. nanoseconds.
75+ // means new tokens can be added to the rate limiter every nanosecond based on
76+ // the input passed.
77+ // Duration is auto casted to upper unit of time. so input of 600-PT60M
78+ // will be set inputTokAddingRate to PT6S means every 6 seconds one token will be
79+ // added to the pool to be consumed
80+ //
81+ Duration inputTokAddingRate = Duration .ofNanos (inputDuration .toNanos () / inputRate );
7882
79- if (StringUtils .isEmpty (literalConstant )) {
80- //return rate with default duration and token adding rate
81- return new RateLimit (rLimit , Constants .DEFAULT_RATE_LIMIT_DURATION , Constants .DEFAULT_TOKEN_ADDING_RATE );
83+ log .info ("constructing rate limit object with rate: {}, duration: {}, and token adding rate: {}" ,
84+ inputRate , inputDuration , inputTokAddingRate );
85+ return new RateLimit (inputRate , inputDuration , inputTokAddingRate );
86+ } catch (NumberFormatException | DateTimeParseException ex ) {
87+ throw new UnknownFormatConversionException (ERROR_MESSAGE );
8288 }
8389
84- // value is either going to be a valid number or null. defaulting to 1 unit(every hour/every min and so on)
85- long duration = NumberUtils .isParsable (rateDuration ) ? Long .parseLong (rateDuration ) : 1 ;
86-
87- switch (rateDurationUnitChar ) {
88- case "s" :
89- case "S" :
90- rLimitDuration = Duration .ofSeconds (duration );
91- tokAddingRate = Duration .ofSeconds (rLimit / duration );
92- break ;
93- case "m" :
94- case "M" :
95- rLimitDuration = Duration .ofMinutes (duration );
96- tokAddingRate = Duration .ofMinutes (rLimit / duration );
97- break ;
98- case "h" :
99- case "H" :
100- rLimitDuration = Duration .ofHours (duration );
101- tokAddingRate = Duration .ofHours (rLimit / duration );
102- break ;
103- default :
104- //should not reach here are regex will enforce char, keeping it for any unexpected use case.
105- throw new UnknownFormatConversionException (ERROR_MESSAGE );
106- }
107- return new RateLimit (rLimit , rLimitDuration , tokAddingRate );
10890 }
10991
11092 public long getRate () {
0 commit comments