Skip to content

Commit ac0d446

Browse files
authored
Use retry callback block for admin API in API exporter tests. (#8485)
1 parent e684190 commit ac0d446

File tree

3 files changed

+97
-60
lines changed

3 files changed

+97
-60
lines changed

app/lib/tool/utils/pub_api_client.dart

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:async';
56
import 'dart:convert';
7+
import 'dart:io';
68
import 'dart:typed_data';
79

810
import 'package:_pub_shared/data/package_api.dart';
@@ -11,6 +13,7 @@ import 'package:gcloud/service_scope.dart';
1113
import 'package:http/http.dart' as http;
1214
import 'package:meta/meta.dart';
1315
import 'package:pub_dev/frontend/handlers/experimental.dart';
16+
import 'package:retry/retry.dart';
1417

1518
import '../../frontend/handlers/pubapi.client.dart';
1619
import '../../service/services.dart';
@@ -91,6 +94,7 @@ class _FakeTimeClient implements http.Client {
9194

9295
/// Creates a pub.dev API client and executes [fn], making sure that the HTTP
9396
/// resources are freed after the callback finishes.
97+
/// The callback [fn] is retried on the transient network errors.
9498
///
9599
/// If [bearerToken], [sessionId] or [csrfToken] is specified, the corresponding
96100
/// HTTP header will be sent alongside the request.
@@ -109,16 +113,40 @@ Future<R> withHttpPubApiClient<R>({
109113
cookieProvider: () async => {
110114
if (experimental != null) experimentalCookieName: experimental.join(':'),
111115
},
116+
client: http.Client(),
112117
);
113-
try {
114-
final apiClient = PubApiClient(
115-
pubHostedUrl ?? activeConfiguration.primaryApiUri!.toString(),
116-
client: httpClient,
117-
);
118-
return await fn(apiClient);
119-
} finally {
120-
httpClient.close();
118+
return await retry(
119+
() async {
120+
try {
121+
final apiClient = PubApiClient(
122+
pubHostedUrl ?? activeConfiguration.primaryApiUri!.toString(),
123+
client: httpClient,
124+
);
125+
return await fn(apiClient);
126+
} finally {
127+
httpClient.close();
128+
}
129+
},
130+
maxAttempts: 3,
131+
retryIf: _retryIf,
132+
);
133+
}
134+
135+
bool _retryIf(Exception e) {
136+
if (e is TimeoutException) {
137+
return true; // Timeouts we can retry
138+
}
139+
if (e is IOException) {
140+
return true; // I/O issues are worth retrying
141+
}
142+
if (e is http.ClientException) {
143+
return true; // HTTP issues are worth retrying
144+
}
145+
if (e is RequestException) {
146+
final status = e.status;
147+
return status >= 500; // 5xx errors are retried
121148
}
149+
return false;
122150
}
123151

124152
extension PubApiClientExt on PubApiClient {

app/test/admin/exported_api_sync_test.dart

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ void main() {
1919
List<String>? packages,
2020
bool forceWrite = false,
2121
}) async {
22-
final api = createPubApiClient(authToken: siteAdminToken);
23-
await api.adminInvokeAction(
24-
'exported-api-sync',
25-
AdminInvokeActionArguments(arguments: {
26-
'packages': packages?.join(' ') ?? 'ALL',
27-
if (forceWrite) 'force-write': 'true',
28-
}),
22+
await withHttpPubApiClient(
23+
bearerToken: siteAdminToken,
24+
fn: (api) async {
25+
await api.adminInvokeAction(
26+
'exported-api-sync',
27+
AdminInvokeActionArguments(arguments: {
28+
'packages': packages?.join(' ') ?? 'ALL',
29+
if (forceWrite) 'force-write': 'true',
30+
}),
31+
);
32+
},
2933
);
3034
}
3135

app/test/package/api_export/api_exporter_test.dart

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -293,17 +293,19 @@ Future<void> _testExportedApiSynchronization(
293293
// recently created files as a guard against race conditions.
294294
fakeTime.elapseSync(days: 1);
295295

296-
final adminApi = createPubApiClient(
297-
authToken: createFakeServiceAccountToken(email: 'admin@pub.dev'),
298-
);
299-
await adminApi.adminInvokeAction(
300-
'moderate-package-version',
301-
AdminInvokeActionArguments(arguments: {
302-
'case': 'none',
303-
'package': 'bar',
304-
'version': '2.0.0',
305-
'state': 'true',
306-
}),
296+
await withHttpPubApiClient(
297+
bearerToken: createFakeServiceAccountToken(email: 'admin@pub.dev'),
298+
fn: (adminApi) async {
299+
await adminApi.adminInvokeAction(
300+
'moderate-package-version',
301+
AdminInvokeActionArguments(arguments: {
302+
'case': 'none',
303+
'package': 'bar',
304+
'version': '2.0.0',
305+
'state': 'true',
306+
}),
307+
);
308+
},
307309
);
308310

309311
// Synchronize again
@@ -330,18 +332,19 @@ Future<void> _testExportedApiSynchronization(
330332

331333
_log.info('## Version reinstated');
332334
{
333-
final adminApi = createPubApiClient(
334-
authToken: createFakeServiceAccountToken(email: 'admin@pub.dev'),
335-
);
336-
await adminApi.adminInvokeAction(
337-
'moderate-package-version',
338-
AdminInvokeActionArguments(arguments: {
339-
'case': 'none',
340-
'package': 'bar',
341-
'version': '2.0.0',
342-
'state': 'false',
343-
}),
344-
);
335+
await withHttpPubApiClient(
336+
bearerToken: createFakeServiceAccountToken(email: 'admin@pub.dev'),
337+
fn: (adminApi) async {
338+
await adminApi.adminInvokeAction(
339+
'moderate-package-version',
340+
AdminInvokeActionArguments(arguments: {
341+
'case': 'none',
342+
'package': 'bar',
343+
'version': '2.0.0',
344+
'state': 'false',
345+
}),
346+
);
347+
});
345348

346349
// Synchronize again
347350
await synchronize();
@@ -371,17 +374,18 @@ Future<void> _testExportedApiSynchronization(
371374
// recently created files as a guard against race conditions.
372375
fakeTime.elapseSync(days: 1);
373376

374-
final adminApi = createPubApiClient(
375-
authToken: createFakeServiceAccountToken(email: 'admin@pub.dev'),
376-
);
377-
await adminApi.adminInvokeAction(
378-
'moderate-package',
379-
AdminInvokeActionArguments(arguments: {
380-
'case': 'none',
381-
'package': 'bar',
382-
'state': 'true',
383-
}),
384-
);
377+
await withHttpPubApiClient(
378+
bearerToken: createFakeServiceAccountToken(email: 'admin@pub.dev'),
379+
fn: (adminApi) async {
380+
await adminApi.adminInvokeAction(
381+
'moderate-package',
382+
AdminInvokeActionArguments(arguments: {
383+
'case': 'none',
384+
'package': 'bar',
385+
'state': 'true',
386+
}),
387+
);
388+
});
385389

386390
// Synchronize again
387391
await synchronize();
@@ -402,17 +406,18 @@ Future<void> _testExportedApiSynchronization(
402406

403407
_log.info('## Package reinstated');
404408
{
405-
final adminApi = createPubApiClient(
406-
authToken: createFakeServiceAccountToken(email: 'admin@pub.dev'),
407-
);
408-
await adminApi.adminInvokeAction(
409-
'moderate-package',
410-
AdminInvokeActionArguments(arguments: {
411-
'case': 'none',
412-
'package': 'bar',
413-
'state': 'false',
414-
}),
415-
);
409+
await withHttpPubApiClient(
410+
bearerToken: createFakeServiceAccountToken(email: 'admin@pub.dev'),
411+
fn: (adminApi) async {
412+
await adminApi.adminInvokeAction(
413+
'moderate-package',
414+
AdminInvokeActionArguments(arguments: {
415+
'case': 'none',
416+
'package': 'bar',
417+
'state': 'false',
418+
}),
419+
);
420+
});
416421

417422
// Synchronize again
418423
await synchronize();

0 commit comments

Comments
 (0)