2323import static java .time .ZoneOffset .UTC ;
2424import java .time .ZonedDateTime ;
2525import java .time .format .DateTimeFormatter ;
26+ import java .time .temporal .ChronoUnit ;
2627import static java .time .temporal .ChronoUnit .DAYS ;
2728import static java .time .temporal .ChronoUnit .MINUTES ;
2829import static java .time .temporal .ChronoUnit .NANOS ;
@@ -120,6 +121,7 @@ public abstract class FitbitPollingRoute implements PollingRequestRoute {
120121 private final Map <String , Integer > forbidden403Counter ;
121122 private int maxForbiddenResponses ;
122123 private Duration forbidden403Cooldown ;
124+ private String cooldownStrategy ;
123125
124126 public FitbitPollingRoute (
125127 FitbitRequestGenerator generator ,
@@ -147,6 +149,7 @@ public void initialize(RestSourceConnectorConfig config) {
147149 this .maxForbiddenResponses = fitbitConfig .getMaxForbidden ();
148150 this .converter ().initialize (fitbitConfig );
149151 this .forbidden403Cooldown = Duration .ofSeconds (fitbitConfig .getForbiddenBackoff ());
152+ this .cooldownStrategy = fitbitConfig .getCooldownStrategy ();
150153 }
151154
152155 @ Override
@@ -175,19 +178,19 @@ public void requestFailed(RestRequest request, Response response) {
175178 if (response != null && response .code () == 429 ) {
176179 User user = ((FitbitRestRequest ) request ).getUser ();
177180 tooManyRequestsForUser .add (user );
178- String cooldownString = response .header ("Retry-After" );
179- Duration cooldown = getTooManyRequestsCooldown ();
180- if (cooldownString != null ) {
181- try {
182- cooldown = Duration .ofSeconds (Long .parseLong (cooldownString ));
183- } catch (NumberFormatException ex ) {
184- cooldown = getTooManyRequestsCooldown ();
185- }
181+
182+ Instant backOff ;
183+ if ("TOP_OF_HOUR" .equalsIgnoreCase (cooldownStrategy )) {
184+ backOff = calculateTopOfHourBackoff ();
185+ logger .info ("Too many requests for user {}. Using TOP_OF_HOUR strategy, backing off until: {}" ,
186+ user , backOff );
187+ } else {
188+ backOff = calculateRollingWindowBackoff (response );
189+ logger .info ("Too many requests for user {}. Using ROLLING_WINDOW strategy, backing off until: {}" ,
190+ user , backOff .plus (getPollIntervalPerUser ()));
186191 }
187- Instant backOff = lastPoll . plus ( cooldown );
192+
188193 lastPollPerUser .put (user .getId (), backOff );
189- logger .info ("Too many requests for user {}. Backing off until {}" ,
190- user , backOff .plus (getPollIntervalPerUser ()));
191194 } else if (response != null && response .code () == 403 ) {
192195 User user = ((FitbitRestRequest ) request ).getUser ();
193196 String userId = user .getId ();
@@ -206,6 +209,46 @@ public void requestFailed(RestRequest request, Response response) {
206209 }
207210 }
208211
212+ /**
213+ * Calculate backoff using top-of-hour strategy.
214+ * Waits until the top of the next hour to align with Fitbit's rate limit reset.
215+ */
216+ private Instant calculateTopOfHourBackoff () {
217+ Instant now = Instant .now ();
218+ Instant topOfNextHour = calculateTopOfNextHour (now ).plus (ONE_SECOND );
219+ return topOfNextHour ;
220+ }
221+
222+ /**
223+ * Calculate backoff using rolling window strategy.
224+ * Uses configured cooldown duration from when the error occurred.
225+ */
226+ private Instant calculateRollingWindowBackoff (Response response ) {
227+ String cooldownString = response .header ("Retry-After" );
228+ Duration cooldown = getTooManyRequestsCooldown ();
229+
230+ if (cooldownString != null ) {
231+ try {
232+ cooldown = Duration .ofSeconds (Long .parseLong (cooldownString ));
233+ } catch (NumberFormatException ex ) {
234+ cooldown = getTooManyRequestsCooldown ();
235+ }
236+ }
237+
238+ return lastPoll .plus (cooldown );
239+ }
240+
241+ /**
242+ * Calculate the top of the next hour from the given instant.
243+ */
244+ private Instant calculateTopOfNextHour (Instant instant ) {
245+ ZonedDateTime zonedDateTime = instant .atZone (UTC );
246+ ZonedDateTime topOfNextHour = zonedDateTime
247+ .plusHours (1 )
248+ .truncatedTo (ChronoUnit .HOURS );
249+ return topOfNextHour .toInstant ();
250+ }
251+
209252 /**
210253 * Actually construct requests, based on the current offset
211254 * @param user Fitbit user
0 commit comments