Skip to content

Commit ede1eac

Browse files
authored
Use new HTTP GET retry logic in SearchClient. (#8732)
1 parent aa63b6f commit ede1eac

File tree

2 files changed

+56
-17
lines changed

2 files changed

+56
-17
lines changed

app/lib/search/search_client.dart

+16-11
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import 'dart:convert';
88
import 'package:_pub_shared/utils/http.dart';
99
import 'package:clock/clock.dart';
1010
import 'package:gcloud/service_scope.dart' as ss;
11-
import 'package:http/http.dart' as http;
1211

1312
import '../../../service/rate_limit/rate_limit.dart';
1413
import '../shared/configuration.dart';
@@ -33,16 +32,13 @@ SearchClient get searchClient => ss.lookup(#_searchClient) as SearchClient;
3332
/// indexed data.
3433
class SearchClient {
3534
/// The HTTP client used for making calls to our search service.
36-
final http.Client _httpClient;
35+
final _httpClient = httpRetryClient();
3736

3837
/// Before this timestamp we may use the fallback search service URL, which
3938
/// is the unversioned service URL, potentially getting responses from an
4039
/// older instance.
4140
final _fallbackSearchThreshold = clock.now().add(Duration(minutes: 30));
4241

43-
SearchClient([http.Client? client])
44-
: _httpClient = client ?? httpRetryClient(retries: 3);
45-
4642
/// Calls the search service (or uses cache) to serve the [query].
4743
Future<PackageSearchResult> search(
4844
ServiceSearchQuery query, {
@@ -69,16 +65,25 @@ class SearchClient {
6965
skipCache = true;
7066
}
7167

72-
// returns null on timeout (after 5 seconds)
73-
Future<http.Response?> doCallHttpServiceEndpoint({String? prefix}) async {
68+
// Returns the status code and the body of the last response, or null on timeout.
69+
Future<({int statusCode, String? body})?> doCallHttpServiceEndpoint(
70+
{String? prefix}) async {
7471
final httpHostPort = prefix ?? activeConfiguration.searchServicePrefix;
7572
final serviceUrl = '$httpHostPort/search$serviceUrlParams';
7673
try {
77-
return await _httpClient
78-
.get(Uri.parse(serviceUrl), headers: cloudTraceHeaders())
79-
.timeout(Duration(seconds: 5));
74+
return await httpGetWithRetry(
75+
Uri.parse(serviceUrl),
76+
client: _httpClient,
77+
headers: cloudTraceHeaders(),
78+
perRequestTimeout: Duration(seconds: 5),
79+
retryIf: (e) => (e is UnexpectedStatusException &&
80+
e.statusCode == searchIndexNotReadyCode),
81+
responseFn: (rs) => (statusCode: rs.statusCode, body: rs.body),
82+
);
8083
} on TimeoutException {
8184
return null;
85+
} on UnexpectedStatusException catch (e) {
86+
return (statusCode: e.statusCode, body: null);
8287
}
8388
}
8489

@@ -103,7 +108,7 @@ class SearchClient {
103108
}
104109
if (response.statusCode == 200) {
105110
return PackageSearchResult.fromJson(
106-
json.decode(response.body) as Map<String, dynamic>,
111+
json.decode(response.body!) as Map<String, dynamic>,
107112
);
108113
}
109114
// Search request before the service initialization completed.

pkg/_pub_shared/lib/utils/http.dart

+40-6
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,42 @@ Future<K> httpGetWithRetry<K>(
4747
Uri uri, {
4848
required FutureOr<K> Function(http.Response response) responseFn,
4949
int maxAttempts = 3,
50+
51+
/// The HTTP client to use.
52+
///
53+
/// Note: when the client is not specified, the inner loop will create a new [http.Client] object on each retry attempt.
54+
http.Client? client,
55+
Map<String, String>? headers,
56+
57+
/// Per-request time amount that will be applied on the overall HTTP request.
58+
Duration? perRequestTimeout,
59+
60+
/// Additional retry conditions (on top of the default ones).
61+
/// Note: check for [UnexpectedStatusException] to allow non-200 response status codes.
62+
bool Function(Exception e)? retryIf,
5063
}) async {
5164
return await retry(
5265
() async {
53-
final client = http.Client();
66+
final closeClient = client == null;
67+
final effectiveClient = client ?? http.Client();
5468
try {
55-
final rs = await client.get(uri);
69+
var f = effectiveClient.get(uri, headers: headers);
70+
if (perRequestTimeout != null) {
71+
f = f.timeout(perRequestTimeout);
72+
}
73+
final rs = await f;
5674
if (rs.statusCode == 200) {
5775
return responseFn(rs);
5876
}
59-
throw http.ClientException(
60-
'Unexpected status code for $uri: ${rs.statusCode}.');
77+
throw UnexpectedStatusException(rs.statusCode, uri);
6178
} finally {
62-
client.close();
79+
if (closeClient) {
80+
effectiveClient.close();
81+
}
6382
}
6483
},
6584
maxAttempts: maxAttempts,
66-
retryIf: _retryIf,
85+
retryIf: (e) => _retryIf(e) || (retryIf != null && retryIf(e)),
6786
);
6887
}
6988

@@ -77,5 +96,20 @@ bool _retryIf(Exception e) {
7796
if (e is http.ClientException) {
7897
return true; // HTTP issues are worth retrying
7998
}
99+
if (e is UnexpectedStatusException) {
100+
return _transientStatusCodes.contains(e.statusCode);
101+
}
80102
return false;
81103
}
104+
105+
/// Thrown when status code is not 200.
106+
class UnexpectedStatusException implements Exception {
107+
final int statusCode;
108+
final String message;
109+
110+
UnexpectedStatusException(this.statusCode, Uri uri)
111+
: message = 'Unexpected status code for $uri: $statusCode.';
112+
113+
@override
114+
String toString() => 'UnexpectedStatusException: $message';
115+
}

0 commit comments

Comments
 (0)