001package org.nasdanika.models.gitlab.util; 002 003import java.time.ZonedDateTime; 004import java.time.format.DateTimeFormatter; 005import java.util.concurrent.atomic.AtomicInteger; 006import java.util.concurrent.atomic.AtomicLong; 007import java.util.logging.Handler; 008import java.util.logging.LogRecord; 009import java.util.regex.Matcher; 010import java.util.regex.Pattern; 011 012/** 013 * A {@link Handler} for throttling API calls based on rate limit HTTP response headers 014 */ 015public class ThrottlingHandler extends Handler { 016 017 private static final Pattern REQUEST_PATTERN = Pattern.compile("\\d+ - Sending client request on thread .+"); 018 private static final Pattern RESPONSE_PATTERN = Pattern.compile("\\d+ - Received server response on thread .+"); 019 020 private static final Pattern RATE_LIMIT_PREFIX_PATTERN = Pattern.compile("^\\d+ < RateLimit-Limit: "); 021// private static final Pattern RATE_LIMIT_OBSERVED_PREFIX_PATTERN = Pattern.compile("^\\d+ < RateLimit-Observed: "); 022 private static final Pattern RATE_LIMIT_REMAINING_PREFIX_PATTERN = Pattern.compile("^\\d+ < RateLimit-Remaining: "); 023 private static final Pattern RATE_LIMIT_RESET_PREFIX_PATTERN = Pattern.compile("^\\d+ < RateLimit-Reset: "); 024 private static final Pattern DATE_PREFIX_PATTERN = Pattern.compile("^\\d+ < Date: "); 025 026 private int conservatism = 3; 027 private int factor = 10; 028 029 // Client-provided rate window 030 private long clientRateLimitWindow = -1; 031 private int clientRateLimit = -1; 032 033 public ThrottlingHandler() { 034 } 035 036 /** 037 * Client rate limit is enforced before sending a request. 038 * @param clientRateLimitWindow Client rate window in milliseconds. Client rate limit is enforced if this value and clientRateLimit are positive. 039 * @param clientRateLimit Client rate limit per rate window. Client rate limit is enforced if this value and clientRateLimitWindow are positive. 040 */ 041 public ThrottlingHandler(long clientRateLimitWindow, int clientRateLimit) { 042 043 this.clientRateLimitWindow = clientRateLimitWindow; 044 this.clientRateLimit = clientRateLimit; 045 } 046 047 public int getConservatism() { 048 return conservatism; 049 } 050 051 /** 052 * Number in seconds to add to the server rate window when computing throttling sleep interval. 053 * Non-zero conservatism ensures that not all rate is used before the window ends. The default value is 3. 054 * @param conservatism 055 */ 056 public void setConservatism(int conservatism) { 057 this.conservatism = conservatism; 058 } 059 060 public int getFactor() { 061 return factor; 062 } 063 064 /** 065 * Determines when to start throttling. 066 * Throttling starts when the remaining server rate limit is less than rate limit divided by factor. 067 * E.g. if factor is 20 then throttling will start when 95% of the rate has been used. 068 * The default is 10 - throttling starts after 90% of the rate has been used. 069 * @param factor 070 */ 071 public void setFactor(int factor) { 072 this.factor = factor; 073 } 074 075 private AtomicLong clientRateLimitWindowEnd = new AtomicLong(); 076 private AtomicInteger clientRateLimitWindowRequestsRemaining = new AtomicInteger(); 077 078 private AtomicInteger serverRateLimit = new AtomicInteger(); 079 private AtomicInteger serverRateLimitRemaining = new AtomicInteger(); 080 private AtomicLong serverRateLimitReset = new AtomicLong(); 081 private AtomicLong serverResponseTime = new AtomicLong(); 082 083 @Override 084 public void publish(LogRecord logRecord) { 085 String message = logRecord.getMessage(); 086 String lines[] = message.split("\\r?\\n"); 087 if (REQUEST_PATTERN.matcher(lines[0]).matches()) { 088 long now = System.currentTimeMillis(); 089 if (clientRateLimit > 0 && clientRateLimitWindow > 0) { 090 long clientRateWindowDelta = now - clientRateLimitWindowEnd.get(); 091 if (clientRateWindowDelta >= 0) { 092 // Updating window end and replenishing requests budget 093 clientRateLimitWindowEnd.set(now + clientRateLimitWindow); 094 clientRateLimitWindowRequestsRemaining.set(clientRateLimit); 095 } 096 } 097 098 int rateLimit = serverRateLimit.get(); 099 int rateLimitRemaining = serverRateLimitRemaining.get(); 100 long rateLimitReset = serverRateLimitReset.get(); 101 102 long toSleep = -1; 103 104 if (rateLimit > 0 105 && rateLimitRemaining > 0 106 && rateLimitRemaining * factor < rateLimit) { // Start throttling when less than rateLimit / factor remains 107 108 long delta = rateLimitReset - serverResponseTime.get(); // Seconds remaining until reset 109 delta += conservatism; 110 toSleep = delta * 1000 / rateLimitRemaining; 111 } 112 113 if (clientRateLimit > 0 && clientRateLimitWindow > 0) { 114 long clientRateWindowDelta = clientRateLimitWindowEnd.get() - now; // Milliseconds until window end (client rate reset) 115 long clientToSleep = clientRateWindowDelta / Math.max(clientRateLimitWindowRequestsRemaining.decrementAndGet(), 1); // If ran out of rate limit - wait until the end of the window 116 if (clientToSleep > toSleep) { 117 toSleep = clientToSleep; 118 } 119 } 120 121 if (toSleep > 0) { 122 try { 123 Thread.sleep(toSleep); 124 } catch (InterruptedException e) { 125 e.printStackTrace(); 126 }; 127 } 128 } 129 130 if (RESPONSE_PATTERN.matcher(lines[0]).matches()) { 131 int rateLimit = -1; 132// int rateLimitObserved = -1; 133 int rateLimitRemaining = -1; 134 long rateLimitReset = -1; 135 ZonedDateTime date = null; 136 137 for (String line: lines) { 138 Matcher rateLimitMatcher = RATE_LIMIT_PREFIX_PATTERN.matcher(line); 139 if (rateLimitMatcher.find()) { 140 rateLimit = Integer.parseInt(line.substring(rateLimitMatcher.end())); 141 } else { 142 Matcher rateLimitRemainingMatcher = RATE_LIMIT_REMAINING_PREFIX_PATTERN.matcher(line); 143 if (rateLimitRemainingMatcher.find()) { 144 rateLimitRemaining = Integer.parseInt(line.substring(rateLimitRemainingMatcher.end())); 145 } else { 146 Matcher rateLimitResetMatcher = RATE_LIMIT_RESET_PREFIX_PATTERN.matcher(line); 147 if (rateLimitResetMatcher.find()) { 148 rateLimitReset = Long.parseLong(line.substring(rateLimitResetMatcher.end())); 149 } else { 150 Matcher dateMatcher = DATE_PREFIX_PATTERN.matcher(line); 151 if (dateMatcher.find()) { 152 String dateString = line.substring(dateMatcher.end()); 153 date = ZonedDateTime.parse(dateString, DateTimeFormatter.RFC_1123_DATE_TIME); 154 } else { 155 // Rate limit observed if needed 156 } 157 158 } 159 } 160 } 161 } 162 163 serverRateLimit.set(rateLimit); 164 serverRateLimitRemaining.set(rateLimitRemaining); 165 serverRateLimitReset.set(rateLimitReset); 166 serverResponseTime.set(date == null ? System.currentTimeMillis() / 1000 : date.toEpochSecond()); 167 } 168 } 169 170 @Override 171 public void flush() { 172 // NOP 173 } 174 175 @Override 176 public void close() throws SecurityException { 177 // NOP 178 } 179 180}