fix(plex): surface a stale-endpoint failure instead of cascading after a mid-probe promotion

This commit is contained in:
edde746
2026-09-03 18:22:13 +02:00
parent a230d51598
commit fa054153ae
2 changed files with 36 additions and 1 deletions
+5 -1
View File
@@ -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 {
+31
View File
@@ -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 {