perf: cache locking, TTL map, purge script and SW LRU limit
This commit is contained in:
+62
-16
@@ -7,8 +7,8 @@ class SimpleAPICache {
|
|||||||
private $cacheDir;
|
private $cacheDir;
|
||||||
private $enabled;
|
private $enabled;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct($cacheDir = null) {
|
||||||
$this->cacheDir = __DIR__ . '/../cache/api';
|
$this->cacheDir = $cacheDir ?? (__DIR__ . '/../cache/api');
|
||||||
$this->enabled = true;
|
$this->enabled = true;
|
||||||
|
|
||||||
// Créer le répertoire de cache s'il n'existe pas
|
// Créer le répertoire de cache s'il n'existe pas
|
||||||
@@ -66,7 +66,26 @@ class SimpleAPICache {
|
|||||||
'created' => time()
|
'created' => time()
|
||||||
];
|
];
|
||||||
|
|
||||||
file_put_contents($file, json_encode($data));
|
// Verrou exclusif : évite les écritures concurrentes tronquées
|
||||||
|
file_put_contents($file, json_encode($data), LOCK_EX);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vide entièrement le cache (entrées valides comme expirées)
|
||||||
|
*
|
||||||
|
* @return int Nombre de fichiers supprimés
|
||||||
|
*/
|
||||||
|
public function clear() {
|
||||||
|
$files = glob($this->cacheDir . '/cache_*.json');
|
||||||
|
$deleted = 0;
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (unlink($file)) {
|
||||||
|
$deleted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,29 +111,56 @@ class SimpleAPICache {
|
|||||||
$GLOBALS['simple_api_cache'] = new SimpleAPICache();
|
$GLOBALS['simple_api_cache'] = new SimpleAPICache();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version cachée de callPeerTubeApi - remplace l'originale
|
* TTL de cache selon l'endpoint PeerTube.
|
||||||
|
*
|
||||||
|
* Correspondance exacte d'abord, puis par préfixe. L'ancien matching par
|
||||||
|
* sous-chaîne (strpos) créait des collisions : « accounts/{nom}/videos »
|
||||||
|
* capturait le TTL de « videos » (10 min) au lieu de celui de « accounts »
|
||||||
|
* (5 min, pensé pour les lives).
|
||||||
|
*
|
||||||
|
* @param string $endpoint Endpoint de l'API (sans slash initial)
|
||||||
|
* @return int TTL en secondes
|
||||||
*/
|
*/
|
||||||
function callPeerTubeApiCached($endpoint, $params = []) {
|
function getPeerTubeCacheTtl($endpoint) {
|
||||||
$cache = $GLOBALS['simple_api_cache'];
|
static $exactMap = [
|
||||||
|
|
||||||
// TTL selon le type de contenu
|
|
||||||
$ttlMap = [
|
|
||||||
'videos/categories' => 3600, // 1 heure
|
'videos/categories' => 3600, // 1 heure
|
||||||
'videos' => 600, // 10 minutes (augmenté)
|
'videos' => 600, // 10 minutes
|
||||||
'search/videos' => 600, // 10 minutes
|
'search/videos' => 600, // 10 minutes
|
||||||
'wp-posts' => 900, // 15 minutes pour WordPress
|
'wp-posts' => 900, // 15 minutes pour WordPress
|
||||||
'accounts' => 300 // 5 minutes pour live streams
|
'accounts' => 300 // 5 minutes pour live streams
|
||||||
];
|
];
|
||||||
|
|
||||||
// TTL dynamique selon l'endpoint
|
static $prefixMap = [
|
||||||
$ttl = 300; // Défaut 5 minutes
|
'accounts/' => 300, // lives : accounts/{nom}/videos
|
||||||
foreach ($ttlMap as $pattern => $time) {
|
'video-channels/' => 600, // vidéos d'une chaîne
|
||||||
if (strpos($endpoint, $pattern) !== false) {
|
'videos/' => 600 // videos/{id}, commentaires
|
||||||
$ttl = $time;
|
];
|
||||||
break;
|
|
||||||
|
if (isset($exactMap[$endpoint])) {
|
||||||
|
return $exactMap[$endpoint];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($prefixMap as $prefix => $ttl) {
|
||||||
|
if (strpos($endpoint, $prefix) === 0) {
|
||||||
|
return $ttl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 300; // Défaut 5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version cachée de callPeerTubeApi - remplace l'originale
|
||||||
|
*/
|
||||||
|
function callPeerTubeApiCached($endpoint, $params = []) {
|
||||||
|
// Cache désactivé : appel direct, sans lecture ni écriture
|
||||||
|
if (!defined('CACHE_ENABLED') || !CACHE_ENABLED) {
|
||||||
|
return callPeerTubeApiOriginal($endpoint, $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache = $GLOBALS['simple_api_cache'];
|
||||||
|
$ttl = getPeerTubeCacheTtl($endpoint);
|
||||||
|
|
||||||
// Essayer le cache d'abord
|
// Essayer le cache d'abord
|
||||||
$cached = $cache->get($endpoint, $params);
|
$cached = $cache->get($endpoint, $params);
|
||||||
if ($cached !== null) {
|
if ($cached !== null) {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Vide le cache API de l'application (fichiers cache/api/cache_*.json).
|
||||||
|
*
|
||||||
|
* Usage : php scripts/purge-cache.php [répertoire-cache]
|
||||||
|
*
|
||||||
|
* Utile après un changement de configuration ou pour forcer le
|
||||||
|
* rafraîchissement des données externes (PeerTube, Castopod, Funkwhale)
|
||||||
|
* sans attendre l'expiration des entrées.
|
||||||
|
*
|
||||||
|
* Contrairement à warm-cache.php, ce script ne charge pas config.php :
|
||||||
|
* purger le cache ne nécessite aucun appel API. Le paramètre optionnel
|
||||||
|
* de répertoire sert essentiellement aux tests automatisés.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../includes/simple-cache.php';
|
||||||
|
|
||||||
|
$cache = new SimpleAPICache($argv[1] ?? null);
|
||||||
|
$deleted = $cache->clear();
|
||||||
|
|
||||||
|
echo "Cache purgé : {$deleted} entrée(s) supprimée(s).\n";
|
||||||
@@ -68,6 +68,117 @@ assertEquals(1, $cleaned, 'cleanup supprime uniquement les entrées expirées');
|
|||||||
assertEquals('valide', $cache->get('cle-valide'), 'cleanup conserve les entrées valides');
|
assertEquals('valide', $cache->get('cle-valide'), 'cleanup conserve les entrées valides');
|
||||||
assertNull($cache->get('cle-perimee'), 'cleanup a bien supprimé l\'entrée expirée');
|
assertNull($cache->get('cle-perimee'), 'cleanup a bien supprimé l\'entrée expirée');
|
||||||
|
|
||||||
|
// --- Écriture verrouillée (LOCK_EX) ------------------------------------------
|
||||||
|
|
||||||
|
// Verrou structurel : set() écrit avec un verrou exclusif
|
||||||
|
$cacheSource = file_get_contents(dirname(__DIR__, 2) . '/includes/simple-cache.php');
|
||||||
|
assertContains('LOCK_EX', $cacheSource, 'set() écrit avec file_put_contents et LOCK_EX');
|
||||||
|
|
||||||
|
// Intégrité d'écriture : un contenu volumineux est relu sans troncature
|
||||||
|
$bigContent = ['payload' => str_repeat('données-€-', 5000)];
|
||||||
|
$cache->set('lock-test', [], $bigContent);
|
||||||
|
assertEquals($bigContent, $cache->get('lock-test'), 'une écriture volumineuse est relue intégralement');
|
||||||
|
|
||||||
|
// --- getPeerTubeCacheTtl() : correspondance exacte puis préfixe --------------
|
||||||
|
|
||||||
|
assertEquals(3600, getPeerTubeCacheTtl('videos/categories'), 'catégories : TTL 1 heure');
|
||||||
|
assertEquals(600, getPeerTubeCacheTtl('videos'), 'liste des vidéos : TTL 10 minutes');
|
||||||
|
assertEquals(600, getPeerTubeCacheTtl('search/videos'), 'recherche : TTL 10 minutes (match exact)');
|
||||||
|
assertEquals(900, getPeerTubeCacheTtl('wp-posts'), 'WordPress : TTL 15 minutes');
|
||||||
|
assertEquals(300, getPeerTubeCacheTtl('accounts'), 'comptes : TTL 5 minutes');
|
||||||
|
assertEquals(
|
||||||
|
300,
|
||||||
|
getPeerTubeCacheTtl('accounts/membre/videos'),
|
||||||
|
'vidéos d\'un compte (lives) : TTL 5 minutes — l\'ancien strpos capturait « videos » à 10 minutes'
|
||||||
|
);
|
||||||
|
assertEquals(600, getPeerTubeCacheTtl('videos/9cf2e3a1-abc'), 'détail vidéo : TTL 10 minutes');
|
||||||
|
assertEquals(600, getPeerTubeCacheTtl('videos/9cf2e3a1-abc/comment-threads'), 'commentaires : TTL 10 minutes');
|
||||||
|
assertEquals(600, getPeerTubeCacheTtl('video-channels/ma-chaine/videos'), 'vidéos d\'une chaîne : TTL 10 minutes');
|
||||||
|
assertEquals(300, getPeerTubeCacheTtl('endpoint-inconnu'), 'endpoint inconnu : TTL par défaut de 5 minutes');
|
||||||
|
|
||||||
|
// --- clear() : purge complète -------------------------------------------------
|
||||||
|
|
||||||
|
$cache->clear(); // repartir d'un dossier vide
|
||||||
|
$cache->set('purge-1', [], 'un');
|
||||||
|
$cache->set('purge-2', [], 'deux', 300);
|
||||||
|
assertEquals(2, count(glob($tmpDir . '/cache_*.json')), 'deux entrées présentes avant purge');
|
||||||
|
$deleted = $cache->clear();
|
||||||
|
assertEquals(2, $deleted, 'clear() supprime toutes les entrées, même valides');
|
||||||
|
assertEquals(0, count(glob($tmpDir . '/cache_*.json')), 'le dossier est vide après clear()');
|
||||||
|
assertNull($cache->get('purge-1'), 'une entrée purgée n\'est plus lisible');
|
||||||
|
|
||||||
|
// --- CACHE_ENABLED honoré par callPeerTubeApiCached ---------------------------
|
||||||
|
// Impossible de redéfinir une constante dans ce processus (CACHE_ENABLED=true
|
||||||
|
// dans bootstrap.php) : on teste dans un sous-processus PHP avec un stub de
|
||||||
|
// callPeerTubeApiOriginal qui compte les appels réels.
|
||||||
|
|
||||||
|
$root = dirname(__DIR__, 2);
|
||||||
|
|
||||||
|
$runCacheSnippet = function (string $code): string {
|
||||||
|
$runner = tempnam(sys_get_temp_dir(), 'cache-runner-') . '.php';
|
||||||
|
file_put_contents($runner, "<?php\n" . $code . "\n");
|
||||||
|
$output = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($runner) . ' 2>&1');
|
||||||
|
unlink($runner);
|
||||||
|
return $output ?? '';
|
||||||
|
};
|
||||||
|
|
||||||
|
$flagTestDir = sys_get_temp_dir() . '/annu-kute-cache-flag-' . getmypid();
|
||||||
|
mkdir($flagTestDir, 0755, true);
|
||||||
|
|
||||||
|
$snippetTemplate = <<<'PHP'
|
||||||
|
define('CACHE_ENABLED', %s);
|
||||||
|
require %s;
|
||||||
|
$GLOBALS['simple_api_cache'] = new SimpleAPICache(%s);
|
||||||
|
$GLOBALS['api_calls'] = 0;
|
||||||
|
function callPeerTubeApiOriginal($endpoint, $params = []) {
|
||||||
|
$GLOBALS['api_calls']++;
|
||||||
|
return ['data' => ['appel-' . $GLOBALS['api_calls']]];
|
||||||
|
}
|
||||||
|
callPeerTubeApiCached('videos-test-flag');
|
||||||
|
callPeerTubeApiCached('videos-test-flag');
|
||||||
|
echo $GLOBALS['api_calls'];
|
||||||
|
PHP;
|
||||||
|
|
||||||
|
$simpleCachePath = var_export($root . '/includes/simple-cache.php', true);
|
||||||
|
$flagDirExport = var_export($flagTestDir, true);
|
||||||
|
|
||||||
|
// Cache désactivé : deux appels API réels, aucune écriture de cache
|
||||||
|
$outDisabled = trim($runCacheSnippet(sprintf($snippetTemplate, 'false', $simpleCachePath, $flagDirExport)));
|
||||||
|
assertEquals('2', $outDisabled, 'CACHE_ENABLED=false : chaque appel va à l\'API (pas de lecture cache)');
|
||||||
|
assertEquals(0, count(glob($flagTestDir . '/cache_*.json')), 'CACHE_ENABLED=false : aucune écriture de cache');
|
||||||
|
|
||||||
|
// Cache activé : le second appel est servi par le cache
|
||||||
|
$outEnabled = trim($runCacheSnippet(sprintf($snippetTemplate, 'true', $simpleCachePath, $flagDirExport)));
|
||||||
|
assertEquals('1', $outEnabled, 'CACHE_ENABLED=true : le second appel est servi par le cache');
|
||||||
|
assertEquals(1, count(glob($flagTestDir . '/cache_*.json')), 'CACHE_ENABLED=true : une entrée écrite en cache');
|
||||||
|
|
||||||
|
foreach (glob($flagTestDir . '/cache_*.json') as $file) {
|
||||||
|
unlink($file);
|
||||||
|
}
|
||||||
|
rmdir($flagTestDir);
|
||||||
|
|
||||||
|
// --- scripts/purge-cache.php ---------------------------------------------------
|
||||||
|
|
||||||
|
$purgeDir = sys_get_temp_dir() . '/annu-kute-purge-' . getmypid();
|
||||||
|
mkdir($purgeDir, 0755, true);
|
||||||
|
file_put_contents($purgeDir . '/cache_a.json', '{}');
|
||||||
|
file_put_contents($purgeDir . '/cache_b.json', '{}');
|
||||||
|
file_put_contents($purgeDir . '/cache_c.json', '{}');
|
||||||
|
file_put_contents($purgeDir . '/autre.txt', 'conservé'); // ne doit pas être supprimé
|
||||||
|
|
||||||
|
$purgeOutput = shell_exec(
|
||||||
|
escapeshellarg(PHP_BINARY) . ' ' .
|
||||||
|
escapeshellarg($root . '/scripts/purge-cache.php') . ' ' .
|
||||||
|
escapeshellarg($purgeDir) . ' 2>&1'
|
||||||
|
) ?? '';
|
||||||
|
|
||||||
|
assertContains('Cache purgé : 3', $purgeOutput, 'le script rapporte les 3 entrées supprimées');
|
||||||
|
assertEquals(0, count(glob($purgeDir . '/cache_*.json')), 'le script supprime tous les fichiers de cache');
|
||||||
|
assertTrue(file_exists($purgeDir . '/autre.txt'), 'le script conserve les fichiers hors cache');
|
||||||
|
|
||||||
|
unlink($purgeDir . '/autre.txt');
|
||||||
|
rmdir($purgeDir);
|
||||||
|
|
||||||
// --- Nettoyage du dossier temporaire ----------------------------------------
|
// --- Nettoyage du dossier temporaire ----------------------------------------
|
||||||
|
|
||||||
foreach (glob($tmpDir . '/cache_*.json') as $file) {
|
foreach (glob($tmpDir . '/cache_*.json') as $file) {
|
||||||
|
|||||||
Reference in New Issue
Block a user