Skip to content

Commit 9de2c60

Browse files
authored
Merge pull request #192 from RADAR-base/release-0.7.4
release-0.7.4
2 parents 4f8275e + de7aeb6 commit 9de2c60

5 files changed

Lines changed: 107 additions & 17 deletions

File tree

.github/workflows/scheduled-snyk-docker.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
permissions:
4343
contents: read
4444
packages: write
45+
security-events: write
4546

4647
steps:
4748
- uses: actions/checkout@v5

.github/workflows/scheduled-snyk.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ jobs:
3636
security:
3737
needs: prepare-matrix
3838
runs-on: ubuntu-latest
39-
permissions: {}
39+
permissions:
40+
contents: read
41+
security-events: write
4042
strategy:
4143
matrix:
4244
module: ${{ fromJson(needs.prepare-matrix.outputs.modules ) }}

kafka-connect-fitbit-source/src/main/java/org/radarbase/connect/rest/fitbit/FitbitRestSourceConnectorConfig.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ public class FitbitRestSourceConnectorConfig extends RestSourceConnectorConfig {
254254
private static final String FITBIT_FORBIDDEN_BACKOFF_DISPLAY = "Forbidden backoff time (s)";
255255
private static final int FITBIT_FORBIDDEN_BACKOFF_DEFAULT = 86400; // 24 hours
256256

257+
public static final String FITBIT_TOO_MANY_REQUESTS_COOLDOWN_CONFIG = "fitbit.too.many.requests.cooldown.s";
258+
private static final String FITBIT_TOO_MANY_REQUESTS_COOLDOWN_DOC = "Cooldown time in seconds after receiving too many requests (429) response.";
259+
private static final String FITBIT_TOO_MANY_REQUESTS_COOLDOWN_DISPLAY = "Too many requests cooldown (s)";
260+
private static final int FITBIT_TOO_MANY_REQUESTS_COOLDOWN_DEFAULT = 3600; // 1 hour
261+
262+
public static final String FITBIT_COOLDOWN_STRATEGY_CONFIG = "fitbit.cooldown.strategy";
263+
private static final String FITBIT_COOLDOWN_STRATEGY_DOC = "Strategy for handling too many requests cooldown. Options: ROLLING_WINDOW, TOP_OF_HOUR";
264+
private static final String FITBIT_COOLDOWN_STRATEGY_DISPLAY = "Cooldown strategy";
265+
private static final String FITBIT_COOLDOWN_STRATEGY_DEFAULT = "ROLLING_WINDOW";
257266

258267
private UserRepository userRepository;
259268
private final Headers clientCredentials;
@@ -689,6 +698,26 @@ public String toString() {
689698
++orderInGroup,
690699
Width.SHORT,
691700
FITBIT_FORBIDDEN_BACKOFF_DISPLAY)
701+
702+
.define(FITBIT_TOO_MANY_REQUESTS_COOLDOWN_CONFIG,
703+
Type.INT,
704+
FITBIT_TOO_MANY_REQUESTS_COOLDOWN_DEFAULT,
705+
Importance.MEDIUM,
706+
FITBIT_TOO_MANY_REQUESTS_COOLDOWN_DOC,
707+
group,
708+
++orderInGroup,
709+
Width.SHORT,
710+
FITBIT_TOO_MANY_REQUESTS_COOLDOWN_DISPLAY)
711+
712+
.define(FITBIT_COOLDOWN_STRATEGY_CONFIG,
713+
Type.STRING,
714+
FITBIT_COOLDOWN_STRATEGY_DEFAULT,
715+
Importance.MEDIUM,
716+
FITBIT_COOLDOWN_STRATEGY_DOC,
717+
group,
718+
++orderInGroup,
719+
Width.SHORT,
720+
FITBIT_COOLDOWN_STRATEGY_DISPLAY)
692721
;
693722
}
694723

@@ -849,7 +878,7 @@ public Duration getPollIntervalPerUser() {
849878
}
850879

851880
public Duration getTooManyRequestsCooldownInterval() {
852-
return Duration.ofHours(1);
881+
return Duration.ofSeconds(getInt(FITBIT_TOO_MANY_REQUESTS_COOLDOWN_CONFIG));
853882
}
854883

855884
public String getFitbitIntradayCaloriesTopic() {
@@ -896,4 +925,12 @@ public int getMaxForbidden() {
896925
public int getForbiddenBackoff() {
897926
return getInt(FITBIT_FORBIDDEN_BACKOFF_CONFIG);
898927
}
928+
929+
public String getCooldownStrategy() {
930+
String value = getString(FITBIT_COOLDOWN_STRATEGY_CONFIG);
931+
if (!value.equalsIgnoreCase("ROLLING_WINDOW") && !value.equalsIgnoreCase("TOP_OF_HOUR")) {
932+
throw new ConfigException(FITBIT_COOLDOWN_STRATEGY_CONFIG, value, "Invalid cooldown strategy. Must be either ROLLING_WINDOW or TOP_OF_HOUR.");
933+
}
934+
return value;
935+
}
899936
}

kafka-connect-fitbit-source/src/main/java/org/radarbase/connect/rest/fitbit/route/FitbitPollingRoute.java

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static java.time.ZoneOffset.UTC;
2424
import java.time.ZonedDateTime;
2525
import java.time.format.DateTimeFormatter;
26+
import java.time.temporal.ChronoUnit;
2627
import static java.time.temporal.ChronoUnit.DAYS;
2728
import static java.time.temporal.ChronoUnit.MINUTES;
2829
import 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
@@ -164,6 +167,9 @@ public void requestEmpty(RestRequest request) {
164167
lastPollPerUser.put(((FitbitRestRequest) request).getUser().getId(), lastPoll);
165168
FitbitRestRequest fitbitRequest = (FitbitRestRequest) request;
166169
Instant endOffset = fitbitRequest.getDateRange().end().toInstant();
170+
// When having polled a date range for a route for HISTORICAL_TIME_DAYS days and
171+
// the response has no data, consider this data not to exist by considering
172+
// the end of the date range as the last successful data retrieval.
167173
if (DAYS.between(endOffset, lastPoll) >= HISTORICAL_TIME_DAYS) {
168174
String key = fitbitRequest.getUser().getVersionedId();
169175
offsets.put(key, endOffset);
@@ -175,19 +181,19 @@ public void requestFailed(RestRequest request, Response response) {
175181
if (response != null && response.code() == 429) {
176182
User user = ((FitbitRestRequest) request).getUser();
177183
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-
}
184+
185+
Instant backOff;
186+
if ("TOP_OF_HOUR".equalsIgnoreCase(cooldownStrategy)) {
187+
backOff = calculateTopOfHourBackoff();
188+
logger.info("Too many requests for user {}. Using TOP_OF_HOUR strategy, backing off until: {}",
189+
user, backOff);
190+
} else {
191+
backOff = calculateRollingWindowBackoff(response);
192+
logger.info("Too many requests for user {}. Using ROLLING_WINDOW strategy, backing off until: {}",
193+
user, backOff.plus(getPollIntervalPerUser()));
186194
}
187-
Instant backOff = lastPoll.plus(cooldown);
195+
188196
lastPollPerUser.put(user.getId(), backOff);
189-
logger.info("Too many requests for user {}. Backing off until {}",
190-
user, backOff.plus(getPollIntervalPerUser()));
191197
} else if (response != null && response.code() == 403) {
192198
User user = ((FitbitRestRequest) request).getUser();
193199
String userId = user.getId();
@@ -206,6 +212,46 @@ public void requestFailed(RestRequest request, Response response) {
206212
}
207213
}
208214

215+
/**
216+
* Calculate backoff using top-of-hour strategy.
217+
* Waits until the top of the next hour to align with Fitbit's rate limit reset.
218+
*/
219+
private Instant calculateTopOfHourBackoff() {
220+
Instant now = Instant.now();
221+
Instant topOfNextHour = calculateTopOfNextHour(now).plus(ONE_SECOND);
222+
return topOfNextHour;
223+
}
224+
225+
/**
226+
* Calculate backoff using rolling window strategy.
227+
* Uses configured cooldown duration from when the error occurred.
228+
*/
229+
private Instant calculateRollingWindowBackoff(Response response) {
230+
String cooldownString = response.header("Retry-After");
231+
Duration cooldown = getTooManyRequestsCooldown();
232+
233+
if (cooldownString != null) {
234+
try {
235+
cooldown = Duration.ofSeconds(Long.parseLong(cooldownString));
236+
} catch (NumberFormatException ex) {
237+
cooldown = getTooManyRequestsCooldown();
238+
}
239+
}
240+
241+
return lastPoll.plus(cooldown);
242+
}
243+
244+
/**
245+
* Calculate the top of the next hour from the given instant.
246+
*/
247+
private Instant calculateTopOfNextHour(Instant instant) {
248+
ZonedDateTime zonedDateTime = instant.atZone(UTC);
249+
ZonedDateTime topOfNextHour = zonedDateTime
250+
.plusHours(1)
251+
.truncatedTo(ChronoUnit.HOURS);
252+
return topOfNextHour.toInstant();
253+
}
254+
209255
/**
210256
* Actually construct requests, based on the current offset
211257
* @param user Fitbit user

kafka-connect-fitbit-source/src/main/java/org/radarbase/connect/rest/fitbit/route/FitbitRestingHeartRateRoute.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,12 @@
1818
package org.radarbase.connect.rest.fitbit.route;
1919

2020
import static java.time.ZoneOffset.UTC;
21-
import static java.time.temporal.ChronoUnit.SECONDS;
21+
import static java.time.temporal.ChronoUnit.DAYS;
2222

2323
import io.confluent.connect.avro.AvroData;
2424

2525
import java.time.Duration;
2626
import java.time.ZonedDateTime;
27-
import java.time.format.DateTimeFormatter;
2827
import java.util.stream.Stream;
2928

3029
import org.radarbase.connect.rest.fitbit.converter.FitbitRestingHeartRateAvroConverter;
@@ -47,9 +46,14 @@ public FitbitRestingHeartRateRoute(
4746

4847
@Override
4948
protected Stream<FitbitRestRequest> createRequests(User user) {
50-
ZonedDateTime startDate = this.getOffset(user).plus(ONE_SECOND)
49+
// Important: resting heart rate is queried at the resolution of a single
50+
// day, so the offset for the next request will be set to the next day.
51+
ZonedDateTime startDate = this.getOffset(user).plus(ONE_DAY)
5152
.atZone(UTC)
52-
.truncatedTo(SECONDS);
53+
.truncatedTo(DAYS);
54+
// Note: the date range of startDate to now() is not correct, but will ensure that in case of empty
55+
// results, the HISTORICAL_TIME_DAYS retry inactivation in requestEmpty() of FitbitPollingRoute.java
56+
// will never be used.
5357
return Stream.of(newRequest(user, new DateRange(startDate, ZonedDateTime.now(UTC)),
5458
user.getExternalUserId(), DATE_FORMAT.format(startDate)));
5559
}

0 commit comments

Comments
 (0)