Démarrage : la lecture du quota ne fait plus attendre le flux

AccountLimits interrogeait player_api.php avant d'ouvrir le flux quand
sa valeur n'était pas en cache (premier flux, puis toutes les 10 min).
Le panneau met jusqu'à 4 s à répondre (mesuré en prod) : autant de
retard au démarrage d'une chaîne ou d'un film.

La valeur connue, même périmée, ou le repli (1) est rendue tout de suite ;
la lecture chez le panneau se fait en arrière-plan, une seule à la fois.
Sans risque : le quota ne sert qu'à choisir quels flux orphelins couper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-10 22:34:26 +02:00
1 parent 6812a4964d
commit 8d79472ca8
2 files changed
+71 -14

No files matched your search

+23 -14
View File
@@ -124,13 +124,21 @@ int? parseMaxConnections(Object? json) {
}
/// Quota de connexions par compte, lu chez le fournisseur et mis en cache.
///
/// Ne fait JAMAIS attendre un flux : `player_api.php` met jusqu'à 4 s à
/// répondre (mesuré en prod), délai qui s'ajoutait au démarrage du premier
/// flux puis de chaque flux suivant l'expiration du cache. La valeur connue
/// (même périmée), sinon [fallback], est rendue immédiatement ; la lecture
/// chez le panneau se fait en arrière-plan. Sans risque : le quota ne sert
/// qu'à décider quels flux ORPHELINS couper.
class AccountLimits {
final Duration ttl;
final Future<int?> Function(PlaylistConfig) _fetch;
final Map<String, ({int max, DateTime at})> _cache = {};
final Set<String> _refreshing = {};
/// Valeur retenue quand le panneau ne répond pas : un seul flux, le cas
/// de loin le plus courant chez les fournisseurs Xtream.
/// Valeur retenue tant que le panneau n'a pas répondu : un seul flux, le
/// cas de loin le plus courant chez les fournisseurs Xtream.
static const fallback = 1;
AccountLimits({
@@ -141,19 +149,20 @@ class AccountLimits {
Future<int> maxFor(PlaylistConfig p) async {
final key = accountKeyOf(p);
final cached = _cache[key];
if (cached != null && DateTime.now().difference(cached.at) < ttl) {
return cached.max;
if (cached == null || DateTime.now().difference(cached.at) >= ttl) {
_refresh(key, p);
}
int? value;
try {
value = await _fetch(p);
} catch (_) {
value = null;
}
// Un échec n'est pas mis en cache longtemps : on réessaiera au prochain
// flux plutôt que de rester 10 min sur une valeur devinée.
if (value != null) _cache[key] = (max: value, at: DateTime.now());
return value ?? fallback;
return cached?.max ?? fallback;
}
void _refresh(String key, PlaylistConfig p) {
if (!_refreshing.add(key)) return; // une seule lecture à la fois
Future<int?>.sync(() => _fetch(p))
.then<int?>((v) => v, onError: (Object _) => null)
.then((value) {
// Un échec n'est pas mis en cache : on réessaiera au prochain flux.
if (value != null) _cache[key] = (max: value, at: DateTime.now());
}).whenComplete(() => _refreshing.remove(key));
}
static Future<int?> _fetchFromPanel(PlaylistConfig p) async {
+48
View File
@@ -1,4 +1,7 @@
import 'dart:async';
import 'package:test/test.dart';
import '../models/playlist_config.dart';
import '../services/upstream_slots.dart';
void main() {
@@ -83,6 +86,51 @@ void main() {
});
});
group('AccountLimits', () {
PlaylistConfig playlist() => PlaylistConfig(
id: 'p',
name: 'n',
dns: 'http://panel.test',
username: 'u',
password: 'x',
createdAt: DateTime(2026),
);
test('ne fait jamais attendre un flux : repli immédiat, valeur réelle ensuite', () async {
final pending = Completer<int?>();
final limits = AccountLimits(fetch: (_) => pending.future);
// Le panneau répond lentement (4 s mesurées en prod) : le premier flux
// part tout de suite sur la valeur de repli.
expect(await limits.maxFor(playlist()), AccountLimits.fallback);
pending.complete(3);
await Future<void>.delayed(Duration.zero);
expect(await limits.maxFor(playlist()), 3);
});
test('valeur périmée servie pendant le rafraîchissement', () async {
var calls = 0;
final limits = AccountLimits(
ttl: Duration.zero,
fetch: (_) async => ++calls == 1 ? 2 : 5,
);
await limits.maxFor(playlist());
await Future<void>.delayed(Duration.zero);
// Périmée (ttl nul) : la valeur connue sort sans attendre la nouvelle.
expect(await limits.maxFor(playlist()), 2);
await Future<void>.delayed(Duration.zero);
expect(await limits.maxFor(playlist()), 5);
});
test('panneau en échec : repli, sans exception', () async {
final limits = AccountLimits(fetch: (_) async => throw Exception('down'));
expect(await limits.maxFor(playlist()), AccountLimits.fallback);
await Future<void>.delayed(Duration.zero);
expect(await limits.maxFor(playlist()), AccountLimits.fallback);
});
});
group('parseMaxConnections', () {
test('lit la valeur texte renvoyée par player_api', () {
expect(