Skip to content

Commit ddb9183

Browse files
Refactor rate-limit retry: fix overflow, rename API, simplify wiring
- Fix long overflow in exponential backoff by clamping shift to 30 - Rename retryEnabled -> retryOnRateLimit for clarity - Replace anonymous HttpClient.Builder hack with RetryableApiClient subclass that overrides getHttpClient(), eliminating 60 lines - Restore AuthManager.httpClient to private final - Unify Builder.build() into a single linear flow
1 parent 17013af commit ddb9183

8 files changed

Lines changed: 686 additions & 218 deletions

File tree

ce/src/main/java/org/thingsboard/client/RetryingHttpClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ private <T> long computeDelay(HttpResponse<T> response, int attempt) {
163163
}
164164
}
165165
// Exponential backoff: initialDelayMs * 2^(attempt-1), capped at maxDelayMs
166-
long base = Math.min(initialDelayMs * (1L << (attempt - 1)), maxDelayMs);
166+
int shift = Math.min(attempt - 1, 30); // prevent long overflow on large attempt values
167+
long base = Math.min(initialDelayMs * (1L << shift), maxDelayMs);
167168
// ±20% jitter
168169
double jitter = (random.nextDouble() * 0.4) - 0.2; // range [-0.2, 0.2]
169170
return Math.max(0, Math.round(base * (1.0 + jitter)));

ce/src/main/java/org/thingsboard/client/ThingsboardClient.java

Lines changed: 32 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,14 @@
2323
import org.thingsboard.client.model.LoginResponse;
2424

2525
import java.io.InputStream;
26-
import java.net.Authenticator;
27-
import java.net.CookieHandler;
28-
import java.net.ProxySelector;
2926
import java.net.URI;
3027
import java.net.http.HttpClient;
3128
import java.net.http.HttpRequest;
3229
import java.net.http.HttpResponse;
33-
import java.time.Duration;
3430
import java.util.Base64;
3531
import java.util.Map;
36-
import java.util.concurrent.Executor;
3732
import java.util.concurrent.TimeUnit;
3833
import java.util.logging.Level;
39-
import javax.net.ssl.SSLContext;
40-
import javax.net.ssl.SSLParameters;
4134

4235
/**
4336
* High-level ThingsBoard REST API client with automatic authentication management.
@@ -73,11 +66,11 @@
7366
* .maxRetryDelayMs(60_000L)
7467
* .build();
7568
*
76-
* // Disable retry entirely
69+
* // Disable rate-limit retry
7770
* ThingsboardClient client = ThingsboardClient.builder()
7871
* .url("http://localhost:8080")
7972
* .credentials("tenant@thingsboard.org", "password")
80-
* .retryEnabled(false)
73+
* .retryOnRateLimit(false)
8174
* .build();
8275
*
8376
* // All generated API methods are available directly
@@ -120,7 +113,7 @@ public static class Builder {
120113
private String username;
121114
private String password;
122115
private String apiKey;
123-
private boolean retryEnabled = true;
116+
private boolean retryOnRateLimit = true;
124117
private int maxRetries = 3;
125118
private long initialRetryDelayMs = 1000L;
126119
private long maxRetryDelayMs = 30_000L;
@@ -144,11 +137,11 @@ public Builder apiKey(String apiKey) {
144137
}
145138

146139
/**
147-
* Enables or disables automatic retry on HTTP 429 responses.
148-
* Retry is enabled by default.
140+
* Enables or disables automatic retry on rate-limit (HTTP 429) responses.
141+
* Enabled by default.
149142
*/
150-
public Builder retryEnabled(boolean retryEnabled) {
151-
this.retryEnabled = retryEnabled;
143+
public Builder retryOnRateLimit(boolean retryOnRateLimit) {
144+
this.retryOnRateLimit = retryOnRateLimit;
152145
return this;
153146
}
154147

@@ -184,101 +177,39 @@ public ThingsboardClient build() throws ApiException {
184177
if (url == null) {
185178
throw new IllegalArgumentException("url is required");
186179
}
187-
if (apiKey != null) {
188-
AuthManager auth = new AuthManager(url, AuthType.API_KEY, apiKey);
189-
installRetryingClient(auth);
190-
return new ThingsboardClient(auth);
191-
}
192-
AuthManager auth = new AuthManager(url, AuthType.JWT, null);
193-
installRetryingClient(auth);
180+
ApiClient apiClient = retryOnRateLimit
181+
? new RetryableApiClient(maxRetries, initialRetryDelayMs, maxRetryDelayMs)
182+
: new ApiClient();
183+
AuthType authType = apiKey != null ? AuthType.API_KEY : AuthType.JWT;
184+
AuthManager auth = new AuthManager(url, authType, apiKey, apiClient);
194185
ThingsboardClient client = new ThingsboardClient(auth);
195-
if (username != null) {
186+
if (authType == AuthType.JWT && username != null) {
196187
client.login(username, password);
197188
}
198189
return client;
199190
}
200191

201-
private void installRetryingClient(AuthManager auth) {
202-
if (!retryEnabled) {
203-
return;
204-
}
205-
// Replace the HttpClient already captured by AuthManager for raw auth calls
206-
RetryingHttpClient retrying = RetryingHttpClient.wrap(
207-
auth.httpClient, maxRetries, initialRetryDelayMs, maxRetryDelayMs);
208-
auth.httpClient = retrying;
209-
210-
// Replace the ApiClient's builder so that ThingsboardApi (constructed next)
211-
// also gets a RetryingHttpClient when it calls apiClient.getHttpClient()
212-
HttpClient.Builder realBuilder = auth.apiClient.builder;
213-
auth.apiClient.builder = new HttpClient.Builder() {
214-
@Override
215-
public HttpClient build() {
216-
return RetryingHttpClient.wrap(
217-
realBuilder.build(), maxRetries, initialRetryDelayMs, maxRetryDelayMs);
218-
}
219-
220-
@Override
221-
public HttpClient.Builder cookieHandler(CookieHandler cookieHandler) {
222-
realBuilder.cookieHandler(cookieHandler);
223-
return this;
224-
}
225-
226-
@Override
227-
public HttpClient.Builder connectTimeout(Duration duration) {
228-
realBuilder.connectTimeout(duration);
229-
return this;
230-
}
231-
232-
@Override
233-
public HttpClient.Builder sslContext(SSLContext sslContext) {
234-
realBuilder.sslContext(sslContext);
235-
return this;
236-
}
237-
238-
@Override
239-
public HttpClient.Builder sslParameters(SSLParameters sslParameters) {
240-
realBuilder.sslParameters(sslParameters);
241-
return this;
242-
}
243-
244-
@Override
245-
public HttpClient.Builder executor(Executor executor) {
246-
realBuilder.executor(executor);
247-
return this;
248-
}
249-
250-
@Override
251-
public HttpClient.Builder followRedirects(HttpClient.Redirect policy) {
252-
realBuilder.followRedirects(policy);
253-
return this;
254-
}
255-
256-
@Override
257-
public HttpClient.Builder version(HttpClient.Version version) {
258-
realBuilder.version(version);
259-
return this;
260-
}
192+
}
261193

262-
@Override
263-
public HttpClient.Builder proxy(ProxySelector proxySelector) {
264-
realBuilder.proxy(proxySelector);
265-
return this;
266-
}
194+
/**
195+
* ApiClient subclass that wraps every built HttpClient with retry-on-429 logic.
196+
*/
197+
private static class RetryableApiClient extends ApiClient {
267198

268-
@Override
269-
public HttpClient.Builder authenticator(java.net.Authenticator authenticator) {
270-
realBuilder.authenticator(authenticator);
271-
return this;
272-
}
199+
private final int maxRetries;
200+
private final long initialDelayMs;
201+
private final long maxDelayMs;
273202

274-
@Override
275-
public HttpClient.Builder priority(int priority) {
276-
realBuilder.priority(priority);
277-
return this;
278-
}
279-
};
203+
RetryableApiClient(int maxRetries, long initialDelayMs, long maxDelayMs) {
204+
this.maxRetries = maxRetries;
205+
this.initialDelayMs = initialDelayMs;
206+
this.maxDelayMs = maxDelayMs;
280207
}
281208

209+
@Override
210+
public HttpClient getHttpClient() {
211+
return RetryingHttpClient.wrap(super.getHttpClient(), maxRetries, initialDelayMs, maxDelayMs);
212+
}
282213
}
283214

284215
/**
@@ -318,7 +249,7 @@ private record TokenInfo(String token, String refreshToken, long tokenExpTs,
318249

319250
final ApiClient apiClient;
320251
private final AuthType authType;
321-
HttpClient httpClient; // package-private and non-final so Builder can replace with RetryingHttpClient
252+
private final HttpClient httpClient;
322253
private final ObjectMapper objectMapper;
323254
private final String baseUrl;
324255

@@ -327,9 +258,9 @@ private record TokenInfo(String token, String refreshToken, long tokenExpTs,
327258
private volatile String password;
328259
private volatile boolean refreshing;
329260

330-
AuthManager(String url, AuthType authType, String apiKey) {
261+
AuthManager(String url, AuthType authType, String apiKey, ApiClient apiClient) {
331262
this.authType = authType;
332-
this.apiClient = new ApiClient();
263+
this.apiClient = apiClient;
333264
apiClient.updateBaseUri(url);
334265
this.baseUrl = apiClient.getBaseUri();
335266
this.httpClient = apiClient.getHttpClient();

common/src/main/java/org/thingsboard/client/RetryingHttpClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ private <T> long computeDelay(HttpResponse<T> response, int attempt) {
163163
}
164164
}
165165
// Exponential backoff: initialDelayMs * 2^(attempt-1), capped at maxDelayMs
166-
long base = Math.min(initialDelayMs * (1L << (attempt - 1)), maxDelayMs);
166+
int shift = Math.min(attempt - 1, 30); // prevent long overflow on large attempt values
167+
long base = Math.min(initialDelayMs * (1L << shift), maxDelayMs);
167168
// ±20% jitter
168169
double jitter = (random.nextDouble() * 0.4) - 0.2; // range [-0.2, 0.2]
169170
return Math.max(0, Math.round(base * (1.0 + jitter)));

0 commit comments

Comments
 (0)