fix(git): haengengebliebene Teil-Abrufe einmal nachschlagen
CI / pruefen (pull_request) Canceled after 0s
CI / pruefen (pull_request) Canceled after 0s
Behebt florian.wilde/nebula#23. Beobachtet an der laufenden Instanz: zwei von acht Teil-Abrufen liefen in eine Zeitueberschreitung nach 15 s, waehrend dieselben Aufrufe von aussen einzeln in 80 ms antworteten. Nicht die Endpunkte sind langsam - die Achterrunde erwuergt sich gelegentlich selbst. Daraus folgt die Maßnahme, und zwar genau diese: was haengen bleibt, bekommt einen zweiten Versuch, und der laeuft NICHT wieder im Schwung mit. Die Nachschlaege haengen an einer Kette und gehen nacheinander hinaus. Die Aufrufstellen in den drei Clients bleiben unberuehrt - hol() liefert weiterhin den endgueltigen Wert. Das war der Grund, es in Teilabrufe zu loesen statt hinter dem Promise.all: dort haette jeder Client acht Variablen neu zuweisen muessen. ZWEI GRENZEN, BEIDE ABSICHTLICH Nachgeschlagen wird nur bei 408 und bei 0 - Zeitueberschreitung und eine Verbindung, die gar nicht zustande kam. Ein 403 wird durch Wiederholen nicht besser, ein 429 durch sofortiges Wiederholen schlimmer, ein 404 bleibt ein 404. Und hoechstens zweimal je Abgleich. Acht Zeitueberschreitungen zu je 15 Sekunden waeren zwei Minuten, in denen "Jetzt synchronisieren" wie ein Absturz aussieht. Wer weg ist, soll schnell als weg erkannt werden. Klappt auch der zweite Versuch nicht, steht das am Repository - mit dem Vermerk "(auch im zweiten Versuch)", damit erkennbar bleibt, dass es kein Zufall war. NICHT GEMACHT: die acht Aufrufe entzerren. Das waere eine Vermutung ueber die Ursache. Bleibt das Problem, sagt der Vermerk genau das, und dann ist die Entzerrung dran. AM RANDE BEHOBEN Den beiden afterEach-Bloecken in http.test.ts fehlte restoreAllMocks. Die Spies zaehlten ueber Tests hinweg weiter; aufgefallen, weil die neuen Pruefungen an Aufrufzahlen haengen und statt 2 gleich 10 sahen. Gegengeprobt: mit abgeschaltetem Nachschlag fallen vier der sechs neuen Tests - die beiden, die das Ausbleiben pruefen, bleiben gruen. Refs #23
This commit is contained in:
@@ -234,6 +234,16 @@ export async function getJsonSafe<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie viele zweite Versuche ein Abgleich hoechstens unternimmt.
|
||||
*
|
||||
* Zwei, nicht acht: jede Zeitueberschreitung kostet volle 15 Sekunden, und
|
||||
* acht davon hintereinander waeren zwei Minuten, in denen "Jetzt
|
||||
* synchronisieren" wie ein Absturz aussieht. Wer wirklich weg ist, soll
|
||||
* schnell als weg erkannt werden.
|
||||
*/
|
||||
const NACHSCHLAG_MAX = 2;
|
||||
|
||||
/** Warum ein einzelner Teil-Abruf nicht geklappt hat. */
|
||||
export type Teilfehler = Record<string, string>;
|
||||
|
||||
@@ -252,6 +262,18 @@ export type Teilfehler = Record<string, string>;
|
||||
export class Teilabrufe {
|
||||
readonly fehler: Teilfehler = {};
|
||||
|
||||
/**
|
||||
* Nachschlaege haengen aneinander, damit sie nacheinander laufen.
|
||||
*
|
||||
* Die Runde selbst ist gleichzeitig – acht Aufrufe auf einmal. Genau das
|
||||
* scheint sie gelegentlich zu erwuergen: beobachtet wurden zwei von acht
|
||||
* mit Zeitueberschreitung nach 15 s, waehrend dieselben Aufrufe einzeln in
|
||||
* 80 ms antworteten. Der zweite Versuch soll deshalb nicht wieder in
|
||||
* denselben Schwung geraten.
|
||||
*/
|
||||
private kette: Promise<unknown> = Promise.resolve();
|
||||
private nachgeschlagen = 0;
|
||||
|
||||
async hol<T>(
|
||||
feld: string,
|
||||
url: string,
|
||||
@@ -262,11 +284,47 @@ export class Teilabrufe {
|
||||
const result = await getJson<T>(url, options);
|
||||
return (result ?? fallback) as T;
|
||||
} catch (error) {
|
||||
if (this.lohntNachschlag(error)) {
|
||||
const zweiter = await this.nachschlag<T>(url, options);
|
||||
if (zweiter.ok) return (zweiter.wert ?? fallback) as T;
|
||||
this.fehler[feld] = `${beschreibe(zweiter.fehler)} (auch im zweiten Versuch)`;
|
||||
return fallback;
|
||||
}
|
||||
this.fehler[feld] = beschreibe(error);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein zweiter Versuch hilft nur bei Fehlern, die nichts ueber die Sache
|
||||
* aussagen: Zeitueberschreitung (408) und eine Verbindung, die gar nicht
|
||||
* zustande kam (0). Ein 403 wird durch Wiederholen nicht besser, ein 429
|
||||
* durch sofortiges Wiederholen schlimmer, und ein 404 bleibt ein 404.
|
||||
*/
|
||||
private lohntNachschlag(error: unknown): boolean {
|
||||
if (this.nachgeschlagen >= NACHSCHLAG_MAX) return false;
|
||||
if (!(error instanceof GitApiError)) return false;
|
||||
return error.status === 408 || error.status === 0;
|
||||
}
|
||||
|
||||
private async nachschlag<T>(
|
||||
url: string,
|
||||
options: FetchOptions,
|
||||
): Promise<{ ok: boolean; wert?: T; fehler?: unknown }> {
|
||||
this.nachgeschlagen += 1;
|
||||
|
||||
const versuch = this.kette.then(async () => getJson<T>(url, options));
|
||||
// Auch ein Fehlschlag darf die Kette nicht abreissen lassen, sonst
|
||||
// scheitern die folgenden Nachschlaege gleich mit.
|
||||
this.kette = versuch.catch(() => undefined);
|
||||
|
||||
try {
|
||||
return { ok: true, wert: await versuch };
|
||||
} catch (fehler) {
|
||||
return { ok: false, fehler };
|
||||
}
|
||||
}
|
||||
|
||||
/** true, sobald mindestens ein Teil-Abruf gescheitert ist. */
|
||||
get unvollstaendig(): boolean {
|
||||
return Object.keys(this.fehler).length > 0;
|
||||
|
||||
Reference in New Issue
Block a user