mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
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:
1 parent
6812a4964d
commit
8d79472ca8
2 files changed
+71
-14
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user