Logos, guide TV et fluidité : couper le flot de requêtes mortes

Logos absents : l'hébergeur des picons du fournisseur répond 503 sur
tout, y compris sa racine (serveur en maintenance). Le proxy relayait
ce 503 au navigateur, qui redemandait chaque logo à chaque affichage de
la grille — des centaines de requêtes sortantes mortes par seconde.
- Un statut >= 400 sur une image rend désormais un pixel transparent
  avec `Cache-Control: max-age=600`, au lieu de propager l'erreur
- Après trois échecs consécutifs, l'hôte est marqué hors service cinq
  minutes : plus aucun appel sortant, réponse servie localement
- Le corps d'erreur amont est drainé, sinon la connexion restait
  ouverte jusqu'au timeout

Guide TV très lent (4 à 8 s par chaîne dans les logs) : `player_api`
demande un appel par chaîne, et le panneau met ~4 s à répondre — une
grille de trente chaînes dépassait la minute.
- Le dump `xmltv.php` du panneau passe en premier : une requête couvre
  toutes les chaînes, réutilisée trois heures
- L'interrogation chaîne par chaîne reste le repli, puis la source
  XMLTV externe
- Une source XMLTV en échec n'est plus retéléchargée à chaque
  consultation : quinze minutes de recul
- Index borné à 48 h en avant : un dump national couvre sept jours pour
  un millier de chaînes, soit des centaines de Mo pour un guide qui
  n'affiche que le programme courant

Lecture saccadée : le relais FFmpeg -> navigateur de `/turbo.ts` ne
transmettait pas la pause du client à la source. Un lecteur plus lent
que le flux laissait FFmpeg produire à pleine vitesse et le serveur
empilait les paquets en mémoire, d'où la dérive derrière le direct. La
contre-pression est maintenant propagée (`pipeWithBackpressure`).

Le serveur tourne sur un seul isolate : le flot de requêtes mortes et
les attentes EPG de plusieurs secondes partageaient la boucle
d'événements avec les paquets vidéo.

Tests ajoutés : cache d'échecs par hôte, contre-pression du relais,
priorité des sources EPG, recul après échec XMLTV, horizon d'index.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-09-19 11:57:38 +02:00
1 parent 2151f2820c
commit b6ca9dab96
11 files changed
+716 -78

No files matched your search

+39 -4
View File
@@ -19,6 +19,9 @@ class XmltvEpgService {
required this.sourceUrls,
this.refreshInterval = const Duration(hours: 6),
this.retention = const Duration(hours: 6),
this.horizon = const Duration(hours: 48),
this.retryBackoff = const Duration(minutes: 15),
this.downloadTimeout = const Duration(minutes: 5),
http.Client? client,
}) : _client = client ?? http.Client();
@@ -33,11 +36,32 @@ class XmltvEpgService {
/// garder double la taille de l'index.
final Duration retention;
/// Les programmes qui commencent au-delà de cet horizon sont écartés à
/// l'indexation.
///
/// Un dump national couvre souvent sept jours pour un millier de chaînes ;
/// tout garder en mémoire dans le conteneur coûte des centaines de
/// mégaoctets pour un guide qui n'affiche que le programme courant et les
/// suivants.
final Duration horizon;
/// Délai minimal entre deux tentatives quand aucune source n'a répondu.
///
/// Sans ce recul, une source morte était retéléchargée à chaque consultation
/// du guide — une requête sortante par chaîne affichée, soit exactement le
/// flot qu'un index est censé éviter.
final Duration retryBackoff;
/// Plafond de téléchargement d'un dump. Le guide est facultatif : mieux vaut
/// abandonner et servir le panneau que faire attendre l'utilisateur.
final Duration downloadTimeout;
final http.Client _client;
/// clé de chaîne normalisée → programmes triés par heure de début.
Map<String, List<XmltvProgramme>> _index = {};
DateTime? _indexedAt;
DateTime? _failedAt;
Future<void>? _refreshInFlight;
bool get hasData => _index.isNotEmpty;
@@ -70,6 +94,14 @@ class XmltvEpgService {
? null
: DateTime.now().difference(_indexedAt!);
if (age != null && age < refreshInterval) return Future.value();
final sinceFailure = _failedAt == null
? null
: DateTime.now().difference(_failedAt!);
if (sinceFailure != null && sinceFailure < retryBackoff) {
return Future.value();
}
return _refreshInFlight ??= _refresh().whenComplete(() {
_refreshInFlight = null;
});
@@ -101,19 +133,20 @@ class XmltvEpgService {
if (ok == 0) {
// Garder l'index précédent plutôt que de servir un guide vide.
_failedAt = DateTime.now();
print('[XmltvEpg] aucune source disponible, index précédent conservé');
return;
}
_index = merged;
_indexedAt = DateTime.now();
_failedAt = null;
print('[XmltvEpg] index prêt : ${merged.length} chaînes');
}
Future<String> _download(String url) async {
final response = await _client
.get(Uri.parse(url))
.timeout(const Duration(minutes: 5));
final response =
await _client.get(Uri.parse(url)).timeout(downloadTimeout);
if (response.statusCode != 200) {
throw HttpException('HTTP ${response.statusCode}');
}
@@ -132,6 +165,7 @@ class XmltvEpgService {
Map<String, List<XmltvProgramme>> _parse(String xml) {
final cutoff = DateTime.now().toUtc().subtract(retention);
final limit = DateTime.now().toUtc().add(horizon);
final byChannel = <String, List<XmltvProgramme>>{};
// Alias : plusieurs dumps déclarent <channel id="X"> avec un
@@ -191,7 +225,8 @@ class XmltvEpgService {
if (programmeChannel != null &&
start != null &&
stop != null &&
stop.isAfter(cutoff)) {
stop.isAfter(cutoff) &&
start.isBefore(limit)) {
final key = normalizeKey(programmeChannel);
if (key.isNotEmpty) {
byChannel.putIfAbsent(key, () => []).add(