fix(plex): surface a stale-endpoint failure instead of cascading after a mid-probe promotion
This commit is contained in:
@@ -180,7 +180,9 @@ class FailoverHttpClient extends MediaServerHttpClient {
|
||||
/// With [retryInPlace] (a connection error, as opposed to a timeout or 5xx)
|
||||
/// the current endpoint gets a chance to prove it is alive before any switch:
|
||||
/// see [_retryOnCurrentEndpoint]. Otherwise, or when that retry fails, the
|
||||
/// cascade in [_cascade] runs.
|
||||
/// cascade in [_cascade] runs — unless a background promotion moved the
|
||||
/// active endpoint meanwhile, in which case the failure belongs to an
|
||||
/// endpoint that is no longer current and the caller surfaces it as-is.
|
||||
Future<MediaServerResponse?> _failoverOnce({
|
||||
required String verb,
|
||||
required Future<MediaServerResponse> Function() send,
|
||||
@@ -188,11 +190,13 @@ class FailoverHttpClient extends MediaServerHttpClient {
|
||||
bool retryInPlace = false,
|
||||
}) async {
|
||||
final manager = _endpointManager!;
|
||||
final generation = manager.generation;
|
||||
_failoverSwitching = true;
|
||||
try {
|
||||
if (retryInPlace) {
|
||||
final revived = await _retryOnCurrentEndpoint(manager, verb: verb, send: send, abort: abort);
|
||||
if (revived != null) return revived;
|
||||
if (manager.generation != generation) return null;
|
||||
}
|
||||
return await _cascade(manager, verb: verb, send: send, abort: abort);
|
||||
} finally {
|
||||
|
||||
@@ -344,6 +344,37 @@ void main() {
|
||||
expect(h.requests.map((u) => u.host), ['primary.example.com', 'fallback.example.com']);
|
||||
expect(h.client.baseUrl, fallback);
|
||||
});
|
||||
|
||||
test('a promotion landing during the probe surfaces the failure instead of cascading off it', () async {
|
||||
final probeStarted = Completer<void>();
|
||||
final releaseProbe = Completer<bool>();
|
||||
final h = build(
|
||||
endpoints: const [primary, fallback, tertiary],
|
||||
handler: (request, _) async {
|
||||
if (request.url.host == 'primary.example.com') {
|
||||
throw http.ClientException('connection reset', request.url);
|
||||
}
|
||||
return ok(request.url.host);
|
||||
},
|
||||
validateCandidate: (_, _) {
|
||||
probeStarted.complete();
|
||||
return releaseProbe.future;
|
||||
},
|
||||
);
|
||||
|
||||
final pending = h.client.get('/path');
|
||||
await probeStarted.future;
|
||||
// Background optimization promotes a different endpoint mid-probe.
|
||||
h.client.resetEndpoints(const [tertiary, primary, fallback], currentBaseUrl: tertiary);
|
||||
h.client.baseUrl = tertiary;
|
||||
releaseProbe.complete(false);
|
||||
|
||||
await expectLater(pending, throwsA(isA<MediaServerHttpException>()));
|
||||
expect(h.requests.map((u) => u.host), ['primary.example.com']);
|
||||
expect(h.switches, isEmpty);
|
||||
expect(h.exhausted, isEmpty);
|
||||
expect(h.client.baseUrl, tertiary);
|
||||
});
|
||||
});
|
||||
|
||||
test('allowEndpointFailover: false rethrows without switching', () async {
|
||||
|
||||
Reference in New Issue
Block a user