1 package io.getunleash.util;
2
3 import static java.lang.Integer.max;
4
5 import java.net.HttpURLConnection;
6 import java.net.URL;
7 import java.util.concurrent.atomic.AtomicInteger;
8 import org.slf4j.Logger;
9 import org.slf4j.LoggerFactory;
10
11 public class Throttler {
12     private static final Logger LOGGER = LoggerFactory.getLogger(Throttler.class);
13     private final int maxSkips;
14
15     private final int intervalLength;
16     private final AtomicInteger skips = new AtomicInteger(0);
17     private final AtomicInteger failures = new AtomicInteger(0);
18
19     private final URL target;
20
21     public Throttler(int intervalLengthSeconds, int longestAcceptableIntervalSeconds, URL target) {
22         this.maxSkips = max(longestAcceptableIntervalSeconds / max(intervalLengthSeconds, 1), 1);
23         this.target = target;
24         this.intervalLength = intervalLengthSeconds;
25     }
26
27     /**
28      * We've had one successful call, so if we had 10 failures in a row, this will reduce the skips
29      * down to 9, so that we gradually start polling more often, instead of doing max load
30      * immediately after a sequence of errors.
31      */

32     public void decrementFailureCountAndResetSkips() {
33         if (failures.get() > 0) {
34             skips.set(Math.max(failures.decrementAndGet(), 0));
35         }
36     }
37
38     /**
39      * We've gotten the message to back off (usually a 429 or a 50x). If we have successive
40      * failures, failure count here will be incremented higher and higher which will handle
41      * increasing our backoff, since we set the skip count to the failure count after every reset
42      */

43     public void increaseSkipCount() {
44         skips.set(Math.min(failures.incrementAndGet(), maxSkips));
45     }
46
47     /**
48      * We've received an error code that we don't expect to change, which means we've already logged
49      * an ERROR. To avoid hammering the server that just told us we did something wrong and to avoid
50      * flooding the logs, we'll increase our skip count to maximum
51      */

52     public void maximizeSkips() {
53         skips.set(maxSkips);
54         failures.incrementAndGet();
55     }
56
57     public boolean performAction() {
58         return skips.get() <= 0;
59     }
60
61     public void skipped() {
62         skips.decrementAndGet();
63     }
64
65     public void handleHttpErrorCodes(int responseCode) {
66         if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED
67                 || responseCode == HttpURLConnection.HTTP_FORBIDDEN) {
68             maximizeSkips();
69             LOGGER.error(
70                     "Client was not authorized to talk to the Unleash API at {}. Backing off to {} times our poll interval (of {} seconds) to avoid overloading server",
71                     this.target,
72                     maxSkips,
73                     this.intervalLength);
74         }
75         if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
76             maximizeSkips();
77             LOGGER.error(
78                     "Server said that the endpoint at {} does not exist. Backing off to {} times our poll interval (of {} seconds) to avoid overloading server",
79                     this.target,
80                     maxSkips,
81                     this.intervalLength);
82         } else if (responseCode == 429) {
83             increaseSkipCount();
84             LOGGER.info(
85                     "RATE LIMITED for the {}. time. Further backing off. Current backoff at {} times our interval (of {} seconds)",
86                     failures.get(),
87                     skips.get(),
88                     this.intervalLength);
89         } else if (responseCode >= HttpURLConnection.HTTP_INTERNAL_ERROR) {
90             increaseSkipCount();
91             LOGGER.info(
92                     "Server failed with a {} status code. Backing off. Current backoff at {} times our poll interval (of {} seconds)",
93                     responseCode,
94                     skips.get(),
95                     this.intervalLength);
96         }
97     }
98
99     public int getSkips() {
100         return this.skips.get();
101     }
102
103     public int getFailures() {
104         return this.failures.get();
105     }
106 }
107