Skip to content

Commit dfdf453

Browse files
feat(quick-1): wire RetryingHttpClient into ThingsboardClient.Builder
- Add retryEnabled, maxRetries, initialRetryDelayMs, maxRetryDelayMs fields to Builder - installRetryingClient() replaces both auth.httpClient and auth.apiClient.builder - Auth refresh/re-login calls (AuthManager.httpClient) now benefit from retry - ThingsboardApi HTTP calls get a RetryingHttpClient via the replaced ApiClient builder - Make AuthManager.httpClient non-final and package-private to allow replacement - Update class Javadoc with retry examples and new Builder methods
1 parent bed160f commit dfdf453

1 file changed

Lines changed: 151 additions & 2 deletions

File tree

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

Lines changed: 151 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,21 @@
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;
2629
import java.net.URI;
2730
import java.net.http.HttpClient;
2831
import java.net.http.HttpRequest;
2932
import java.net.http.HttpResponse;
33+
import java.time.Duration;
3034
import java.util.Base64;
3135
import java.util.Map;
36+
import java.util.concurrent.Executor;
3237
import java.util.concurrent.TimeUnit;
3338
import java.util.logging.Level;
39+
import javax.net.ssl.SSLContext;
40+
import javax.net.ssl.SSLParameters;
3441

3542
/**
3643
* High-level ThingsBoard REST API client with automatic authentication management.
@@ -41,6 +48,7 @@
4148
* <li>Automatic token refresh before expiry</li>
4249
* <li>Automatic re-login when the refresh token expires</li>
4350
* <li>Client-server clock skew compensation</li>
51+
* <li>Automatic retry on HTTP 429 (Too Many Requests) with exponential backoff</li>
4452
* </ul>
4553
*
4654
* <pre>{@code
@@ -56,6 +64,22 @@
5664
* .apiKey("your-api-key")
5765
* .build();
5866
*
67+
* // Tuning rate-limit retry behaviour (retry is enabled by default)
68+
* ThingsboardClient client = ThingsboardClient.builder()
69+
* .url("http://localhost:8080")
70+
* .credentials("tenant@thingsboard.org", "password")
71+
* .maxRetries(5)
72+
* .initialRetryDelayMs(500L)
73+
* .maxRetryDelayMs(60_000L)
74+
* .build();
75+
*
76+
* // Disable retry entirely
77+
* ThingsboardClient client = ThingsboardClient.builder()
78+
* .url("http://localhost:8080")
79+
* .credentials("tenant@thingsboard.org", "password")
80+
* .retryEnabled(false)
81+
* .build();
82+
*
5983
* // All generated API methods are available directly
6084
* Device device = client.getDeviceById(deviceId);
6185
* }</pre>
@@ -96,6 +120,10 @@ public static class Builder {
96120
private String username;
97121
private String password;
98122
private String apiKey;
123+
private boolean retryEnabled = true;
124+
private int maxRetries = 3;
125+
private long initialRetryDelayMs = 1000L;
126+
private long maxRetryDelayMs = 30_000L;
99127

100128
private Builder() {}
101129

@@ -115,21 +143,142 @@ public Builder apiKey(String apiKey) {
115143
return this;
116144
}
117145

146+
/**
147+
* Enables or disables automatic retry on HTTP 429 responses.
148+
* Retry is enabled by default.
149+
*/
150+
public Builder retryEnabled(boolean retryEnabled) {
151+
this.retryEnabled = retryEnabled;
152+
return this;
153+
}
154+
155+
/**
156+
* Maximum number of retry attempts after an HTTP 429 response.
157+
* Default: 3.
158+
*/
159+
public Builder maxRetries(int maxRetries) {
160+
this.maxRetries = maxRetries;
161+
return this;
162+
}
163+
164+
/**
165+
* Initial backoff delay in milliseconds for the first retry.
166+
* Subsequent retries use exponential backoff with ±20% jitter.
167+
* Default: 1000 ms.
168+
*/
169+
public Builder initialRetryDelayMs(long initialRetryDelayMs) {
170+
this.initialRetryDelayMs = initialRetryDelayMs;
171+
return this;
172+
}
173+
174+
/**
175+
* Maximum backoff delay in milliseconds. The computed delay is capped at this value.
176+
* Default: 30 000 ms.
177+
*/
178+
public Builder maxRetryDelayMs(long maxRetryDelayMs) {
179+
this.maxRetryDelayMs = maxRetryDelayMs;
180+
return this;
181+
}
182+
118183
public ThingsboardClient build() throws ApiException {
119184
if (url == null) {
120185
throw new IllegalArgumentException("url is required");
121186
}
122187
if (apiKey != null) {
123-
return new ThingsboardClient(new AuthManager(url, AuthType.API_KEY, apiKey));
188+
AuthManager auth = new AuthManager(url, AuthType.API_KEY, apiKey);
189+
installRetryingClient(auth);
190+
return new ThingsboardClient(auth);
124191
}
125192
AuthManager auth = new AuthManager(url, AuthType.JWT, null);
193+
installRetryingClient(auth);
126194
ThingsboardClient client = new ThingsboardClient(auth);
127195
if (username != null) {
128196
client.login(username, password);
129197
}
130198
return client;
131199
}
132200

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+
}
261+
262+
@Override
263+
public HttpClient.Builder proxy(ProxySelector proxySelector) {
264+
realBuilder.proxy(proxySelector);
265+
return this;
266+
}
267+
268+
@Override
269+
public HttpClient.Builder authenticator(java.net.Authenticator authenticator) {
270+
realBuilder.authenticator(authenticator);
271+
return this;
272+
}
273+
274+
@Override
275+
public HttpClient.Builder priority(int priority) {
276+
realBuilder.priority(priority);
277+
return this;
278+
}
279+
};
280+
}
281+
133282
}
134283

135284
/**
@@ -169,7 +318,7 @@ private record TokenInfo(String token, String refreshToken, long tokenExpTs,
169318

170319
final ApiClient apiClient;
171320
private final AuthType authType;
172-
private final HttpClient httpClient;
321+
HttpClient httpClient; // package-private and non-final so Builder can replace with RetryingHttpClient
173322
private final ObjectMapper objectMapper;
174323
private final String baseUrl;
175324

0 commit comments

Comments
 (0)