mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-23 06:58:05 +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.
|
/// 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 {
|
class AccountLimits {
|
||||||
final Duration ttl;
|
final Duration ttl;
|
||||||
final Future<int?> Function(PlaylistConfig) _fetch;
|
final Future<int?> Function(PlaylistConfig) _fetch;
|
||||||
final Map<String, ({int max, DateTime at})> _cache = {};
|
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
|
/// Valeur retenue tant que le panneau n'a pas répondu : un seul flux, le
|
||||||
/// de loin le plus courant chez les fournisseurs Xtream.
|
/// cas de loin le plus courant chez les fournisseurs Xtream.
|
||||||
static const fallback = 1;
|
static const fallback = 1;
|
||||||
|
|
||||||
AccountLimits({
|
AccountLimits({
|
||||||
@@ -141,19 +149,20 @@ class AccountLimits {
|
|||||||
Future<int> maxFor(PlaylistConfig p) async {
|
Future<int> maxFor(PlaylistConfig p) async {
|
||||||
final key = accountKeyOf(p);
|
final key = accountKeyOf(p);
|
||||||
final cached = _cache[key];
|
final cached = _cache[key];
|
||||||
if (cached != null && DateTime.now().difference(cached.at) < ttl) {
|
if (cached == null || DateTime.now().difference(cached.at) >= ttl) {
|
||||||
return cached.max;
|
_refresh(key, p);
|
||||||
}
|
}
|
||||||
int? value;
|
return cached?.max ?? fallback;
|
||||||
try {
|
}
|
||||||
value = await _fetch(p);
|
|
||||||
} catch (_) {
|
void _refresh(String key, PlaylistConfig p) {
|
||||||
value = null;
|
if (!_refreshing.add(key)) return; // une seule lecture à la fois
|
||||||
}
|
Future<int?>.sync(() => _fetch(p))
|
||||||
// Un échec n'est pas mis en cache longtemps : on réessaiera au prochain
|
.then<int?>((v) => v, onError: (Object _) => null)
|
||||||
// flux plutôt que de rester 10 min sur une valeur devinée.
|
.then((value) {
|
||||||
if (value != null) _cache[key] = (max: value, at: DateTime.now());
|
// Un échec n'est pas mis en cache : on réessaiera au prochain flux.
|
||||||
return value ?? fallback;
|
if (value != null) _cache[key] = (max: value, at: DateTime.now());
|
||||||
|
}).whenComplete(() => _refreshing.remove(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<int?> _fetchFromPanel(PlaylistConfig p) async {
|
static Future<int?> _fetchFromPanel(PlaylistConfig p) async {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
import '../models/playlist_config.dart';
|
||||||
import '../services/upstream_slots.dart';
|
import '../services/upstream_slots.dart';
|
||||||
|
|
||||||
void main() {
|
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', () {
|
group('parseMaxConnections', () {
|
||||||
test('lit la valeur texte renvoyée par player_api', () {
|
test('lit la valeur texte renvoyée par player_api', () {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Reference in new issue
Block a user