Skip to content

Commit 102adea

Browse files
Copilotlaeubi
andcommitted
Add retry support for HTTP 503 and 429 in Java11HttpTransport
Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com>
1 parent b0c6180 commit 102adea

1 file changed

Lines changed: 123 additions & 45 deletions

File tree

p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/transport/Java11HttpTransportFactory.java

Lines changed: 123 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ public class Java11HttpTransportFactory implements HttpTransportFactory, Initial
6767
ThreadLocal.withInitial(() -> new SimpleDateFormat("EEE MMMd HH:mm:ss yyyy", Locale.ENGLISH)));
6868

6969
static final String HINT = "Java11Client";
70+
private static final int MAX_RETRY_ATTEMPTS = 5;
71+
private static final int DEFAULT_RETRY_DELAY_SECONDS = 5;
72+
7073
@Requirement
7174
ProxyHelper proxyHelper;
7275
@Requirement
@@ -124,47 +127,106 @@ public <T> T get(ResponseConsumer<T> consumer) throws IOException {
124127
throw new InterruptedIOException();
125128
}
126129
}
130+
131+
private boolean shouldRetry(int statusCode) {
132+
return statusCode == 503 || statusCode == 429;
133+
}
134+
135+
private long getRetryDelay(HttpResponse<?> response) {
136+
String retryAfterHeader = response.headers().firstValue("Retry-After").orElse(null);
137+
if (retryAfterHeader == null || retryAfterHeader.isBlank()) {
138+
return DEFAULT_RETRY_DELAY_SECONDS;
139+
}
140+
141+
// Try to parse as seconds (integer)
142+
if (Character.isDigit(retryAfterHeader.charAt(0))) {
143+
try {
144+
return Long.parseLong(retryAfterHeader.trim());
145+
} catch (NumberFormatException e) {
146+
// Fall through to date parsing
147+
}
148+
}
149+
150+
// Try to parse as HTTP date
151+
for (ThreadLocal<DateFormat> dateFormat : DATE_PATTERNS) {
152+
try {
153+
long retryTime = dateFormat.get().parse(retryAfterHeader).getTime();
154+
long currentTime = System.currentTimeMillis();
155+
long delayMillis = retryTime - currentTime;
156+
if (delayMillis > 0) {
157+
return TimeUnit.MILLISECONDS.toSeconds(delayMillis);
158+
}
159+
} catch (ParseException e) {
160+
// Try next pattern
161+
}
162+
}
163+
164+
// Default if parsing fails
165+
return DEFAULT_RETRY_DELAY_SECONDS;
166+
}
127167

128168
private <T> T performGet(ResponseConsumer<T> consumer, HttpClient httpClient)
129169
throws IOException, InterruptedException {
130-
HttpRequest request = builder.GET().timeout(Duration.ofSeconds(TIMEOUT_SECONDS)).build();
131-
HttpResponse<InputStream> response = httpClient.send(request, BodyHandlers.ofInputStream());
132-
try (ResponseImplementation<InputStream> implementation = new ResponseImplementation<>(response) {
133-
134-
@Override
135-
public void close() {
136-
if (response.version() == Version.HTTP_1_1) {
137-
// discard any remaining data and close the stream to return the connection to
138-
// the pool..
139-
try (InputStream stream = response.body()) {
140-
int discarded = 0;
141-
while (discarded < MAX_DISCARD) {
142-
int read = stream.read(DUMMY_BUFFER);
143-
if (read < 0) {
144-
break;
170+
int retriesLeft = MAX_RETRY_ATTEMPTS;
171+
while (retriesLeft > 0) {
172+
retriesLeft--;
173+
HttpRequest request = builder.GET().timeout(Duration.ofSeconds(TIMEOUT_SECONDS)).build();
174+
HttpResponse<InputStream> response = httpClient.send(request, BodyHandlers.ofInputStream());
175+
176+
int statusCode = response.statusCode();
177+
if (shouldRetry(statusCode) && retriesLeft > 0) {
178+
long delaySeconds = getRetryDelay(response);
179+
logger.info("Server returned status " + statusCode + " for " + uri
180+
+ ", waiting " + delaySeconds + " seconds before retry. "
181+
+ retriesLeft + " retries left.");
182+
// Close the response stream before retrying
183+
try (InputStream stream = response.body()) {
184+
// Discard any content
185+
} catch (IOException e) {
186+
// Ignore
187+
}
188+
TimeUnit.SECONDS.sleep(delaySeconds);
189+
continue;
190+
}
191+
192+
try (ResponseImplementation<InputStream> implementation = new ResponseImplementation<>(response) {
193+
194+
@Override
195+
public void close() {
196+
if (response.version() == Version.HTTP_1_1) {
197+
// discard any remaining data and close the stream to return the connection to
198+
// the pool..
199+
try (InputStream stream = response.body()) {
200+
int discarded = 0;
201+
while (discarded < MAX_DISCARD) {
202+
int read = stream.read(DUMMY_BUFFER);
203+
if (read < 0) {
204+
break;
205+
}
206+
discarded += read;
145207
}
146-
discarded += read;
208+
} catch (IOException e) {
209+
// don't care...
210+
}
211+
} else {
212+
// just closing should be enough to signal to the framework...
213+
try (InputStream stream = response.body()) {
214+
} catch (IOException e) {
215+
// don't care...
147216
}
148-
} catch (IOException e) {
149-
// don't care...
150-
}
151-
} else {
152-
// just closing should be enough to signal to the framework...
153-
try (InputStream stream = response.body()) {
154-
} catch (IOException e) {
155-
// don't care...
156217
}
157218
}
158-
}
159219

160-
@Override
161-
public void transferTo(OutputStream outputStream, ContentEncoding transportEncoding)
162-
throws IOException {
163-
transportEncoding.decode(response.body()).transferTo(outputStream);
220+
@Override
221+
public void transferTo(OutputStream outputStream, ContentEncoding transportEncoding)
222+
throws IOException {
223+
transportEncoding.decode(response.body()).transferTo(outputStream);
224+
}
225+
}) {
226+
return consumer.handleResponse(implementation);
164227
}
165-
}) {
166-
return consumer.handleResponse(implementation);
167228
}
229+
throw new IOException("Maximum retry attempts exceeded for " + uri);
168230
}
169231

170232
@Override
@@ -187,22 +249,38 @@ public Response head() throws IOException {
187249
}
188250

189251
private Response doHead(HttpClient httpClient) throws IOException, InterruptedException {
190-
HttpResponse<Void> response = httpClient.send(
191-
builder.method("HEAD", BodyPublishers.noBody()).timeout(Duration.ofSeconds(TIMEOUT_SECONDS))
192-
.build(),
193-
BodyHandlers.discarding());
194-
return new ResponseImplementation<>(response) {
195-
@Override
196-
public void close() {
197-
// nothing...
252+
int retriesLeft = MAX_RETRY_ATTEMPTS;
253+
while (retriesLeft > 0) {
254+
retriesLeft--;
255+
HttpResponse<Void> response = httpClient.send(
256+
builder.method("HEAD", BodyPublishers.noBody()).timeout(Duration.ofSeconds(TIMEOUT_SECONDS))
257+
.build(),
258+
BodyHandlers.discarding());
259+
260+
int statusCode = response.statusCode();
261+
if (shouldRetry(statusCode) && retriesLeft > 0) {
262+
long delaySeconds = getRetryDelay(response);
263+
logger.info("Server returned status " + statusCode + " for HEAD " + uri
264+
+ ", waiting " + delaySeconds + " seconds before retry. "
265+
+ retriesLeft + " retries left.");
266+
TimeUnit.SECONDS.sleep(delaySeconds);
267+
continue;
198268
}
269+
270+
return new ResponseImplementation<>(response) {
271+
@Override
272+
public void close() {
273+
// nothing...
274+
}
199275

200-
@Override
201-
public void transferTo(OutputStream outputStream, ContentEncoding transportEncoding)
202-
throws IOException {
203-
throw new IOException("HEAD returns no body");
204-
}
205-
};
276+
@Override
277+
public void transferTo(OutputStream outputStream, ContentEncoding transportEncoding)
278+
throws IOException {
279+
throw new IOException("HEAD returns no body");
280+
}
281+
};
282+
}
283+
throw new IOException("Maximum retry attempts exceeded for HEAD " + uri);
206284
}
207285

208286
}

0 commit comments

Comments
 (0)