V2 : optimisation API (cache, index, vue materialisee)
- BDD (migrations 1.6.0 + 1.7.0, idempotentes) :
* 4 nouveaux index composites sur reactevents, triggerevents, sessions
* vue sessiondebriefs reecrite (sous-requetes correlees -> JOINs)
puis materialisee dans table session_debriefs_cache via procedure
stockee rebuild_debrief_cache, recalculee uniquement aux changements
* fix bug double JOIN participates : TargetRole = "IA" pour les hits
NPC/objets (au lieu d'un role aleatoire de participant)
- API PHP :
* connexion PDO en singleton + ATTR_PERSISTENT
* cache disque par session (cache/sess_<id>/) pour debrief, get, userhistory
avec invalidation ciblee sur events et lifecycle
* pagination opt-in (limit/offset) sur lists/sessions_for_user
* helpers ensure_/rebuild_/invalidate_session_debrief_cache_db
* debug/benchmark.php pour mesurer la latence des 6 endpoints cles
- Endpoints HTTP : signatures inchangees, compat Unreal preservee
- Gain mesure : -58% a -83% sur les 5 endpoints cles
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
109
ProserveAPI/htdocs/proserve/config/cache.php
Normal file
109
ProserveAPI/htdocs/proserve/config/cache.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
// Cache disque pour resultats lourds et immuables (debrief, userhistory, get session).
|
||||
// Chaque session a son propre sous-dossier cache/sess_<id>/ : invalider une session
|
||||
// se reduit a supprimer le dossier, sans risque de match partiel sur d'autres ids.
|
||||
// Toutes les fonctions sont silencieuses en cas d'echec (best-effort, jamais bloquant).
|
||||
|
||||
if (!defined('PROSERVE_CACHE_DIR'))
|
||||
define('PROSERVE_CACHE_DIR', __DIR__ . '/../cache');
|
||||
|
||||
function cache_session_dir($sessionId)
|
||||
{
|
||||
return PROSERVE_CACHE_DIR . '/sess_' . intval($sessionId);
|
||||
}
|
||||
|
||||
function cache_path($sessionId, $key)
|
||||
{
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_-]/', '_', $key);
|
||||
return cache_session_dir($sessionId) . '/' . $safe . '.json';
|
||||
}
|
||||
|
||||
function cache_get($sessionId, $key)
|
||||
{
|
||||
if (intval($sessionId) <= 0) return null;
|
||||
$path = cache_path($sessionId, $key);
|
||||
if (!file_exists($path)) return null;
|
||||
$data = @file_get_contents($path);
|
||||
if ($data === false || $data === '') return null;
|
||||
return $data;
|
||||
}
|
||||
|
||||
function cache_put($sessionId, $key, $jsonString)
|
||||
{
|
||||
if (intval($sessionId) <= 0) return;
|
||||
$dir = cache_session_dir($sessionId);
|
||||
if (!is_dir($dir)) @mkdir($dir, 0755, true);
|
||||
@file_put_contents(cache_path($sessionId, $key), $jsonString, LOCK_EX);
|
||||
}
|
||||
|
||||
function cache_invalidate_session($sessionId)
|
||||
{
|
||||
$sid = intval($sessionId);
|
||||
if ($sid <= 0) return;
|
||||
$dir = cache_session_dir($sid);
|
||||
if (!is_dir($dir)) return;
|
||||
$files = glob($dir . '/*');
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $f) @unlink($f);
|
||||
}
|
||||
@rmdir($dir);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DB-level cache for the materialized sessiondebriefs view (table session_debriefs_cache)
|
||||
// Populated by stored procedure rebuild_debrief_cache(p_session_id) created in migration 1.7.0.
|
||||
// =============================================================================
|
||||
|
||||
// Marque le cache DB d'une session comme stale (delete des lignes).
|
||||
// Appele depuis les endpoints d'evenements (storetriggerevent, storereactevent)
|
||||
// pour amortir le cout : la reconstruction se fait paresseusement au prochain
|
||||
// read via ensure_session_debriefs_cached_db, ou explicitement via rebuild...
|
||||
function invalidate_session_debrief_cache_db($db, $sessionId)
|
||||
{
|
||||
$sid = intval($sessionId);
|
||||
if ($sid <= 0 || $db === null) return;
|
||||
try {
|
||||
$stmt = $db->prepare("DELETE FROM session_debriefs_cache WHERE SessionId = ?");
|
||||
$stmt->execute([$sid]);
|
||||
} catch (Exception $e) {
|
||||
// best-effort, ne jamais bloquer la requete metier
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruit immediatement le cache DB d'une session (DELETE + INSERT via la proc).
|
||||
// Appele depuis session/stop, session/userleave, session/updateobjectives.
|
||||
function rebuild_session_debrief_cache_db($db, $sessionId)
|
||||
{
|
||||
$sid = intval($sessionId);
|
||||
if ($sid <= 0 || $db === null) return;
|
||||
try {
|
||||
$stmt = $db->prepare("CALL rebuild_debrief_cache(?)");
|
||||
$stmt->execute([$sid]);
|
||||
$stmt->closeCursor();
|
||||
} catch (Exception $e) {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
// Garantit que le cache DB contient au moins une ligne pour la session.
|
||||
// Appele avant tout SELECT sur sessiondebriefs cote stats_object.php.
|
||||
// Si le cache a ete invalide par un evenement et n'a pas encore ete reconstruit,
|
||||
// cette fonction le reconstruit a la demande.
|
||||
function ensure_session_debriefs_cached_db($db, $sessionId)
|
||||
{
|
||||
$sid = intval($sessionId);
|
||||
if ($sid <= 0 || $db === null) return;
|
||||
try {
|
||||
$stmt = $db->prepare("SELECT 1 FROM session_debriefs_cache WHERE SessionId = ? LIMIT 1");
|
||||
$stmt->execute([$sid]);
|
||||
$exists = $stmt->fetchColumn();
|
||||
$stmt->closeCursor();
|
||||
if ($exists === false) {
|
||||
rebuild_session_debrief_cache_db($db, $sid);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// si la table n'existe pas (migration 1.7.0 pas encore appliquee),
|
||||
// on ne fait rien : le code legacy continue de marcher via la vue
|
||||
}
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user