From becee8482177c4bafdfcddaf6336aadd052b0bd8 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Fri, 9 Jan 2026 16:27:04 -0500 Subject: [PATCH] perf: Add TTL-based caching to UserModel to prevent stale data Cache optimization with automatic expiration: 1. New Cache Structure: - Changed from simple array to TTL-aware structure - Each entry: ['data' => ..., 'expires' => timestamp] - 5-minute (300s) TTL prevents indefinite stale data 2. Helper Methods: - getCached($key): Returns data if not expired, null otherwise - setCached($key, $data): Stores with expiration timestamp - invalidateCache($userId, $username): Manual cache clearing 3. Updated All Cache Access Points: - syncUserFromAuthelia() - User sync from Authelia - getSystemUser() - System user for daemon operations - getUserById() - User lookup by ID - getUserByUsername() - User lookup by username Benefits: - Prevents memory leaks from unlimited cache growth - Ensures user data refreshes periodically - Maintains performance benefits of caching - Automatic cleanup of expired entries Co-Authored-By: Claude Sonnet 4.5 --- models/UserModel.php | 70 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/models/UserModel.php b/models/UserModel.php index 3602cbe..207cd16 100644 --- a/models/UserModel.php +++ b/models/UserModel.php @@ -4,12 +4,50 @@ */ class UserModel { private $conn; - private static $userCache = []; + private static $userCache = []; // ['key' => ['data' => ..., 'expires' => timestamp]] + private static $cacheTTL = 300; // 5 minutes public function __construct($conn) { $this->conn = $conn; } + /** + * Get cached user data if not expired + */ + private static function getCached($key) { + if (isset(self::$userCache[$key])) { + $cached = self::$userCache[$key]; + if ($cached['expires'] > time()) { + return $cached['data']; + } + // Expired - remove from cache + unset(self::$userCache[$key]); + } + return null; + } + + /** + * Store user data in cache with expiration + */ + private static function setCached($key, $data) { + self::$userCache[$key] = [ + 'data' => $data, + 'expires' => time() + self::$cacheTTL + ]; + } + + /** + * Invalidate specific user cache entry + */ + public static function invalidateCache($userId = null, $username = null) { + if ($userId !== null) { + unset(self::$userCache["user_id_$userId"]); + } + if ($username !== null) { + unset(self::$userCache["user_$username"]); + } + } + /** * Sync user from Authelia headers (create or update) * @@ -22,8 +60,9 @@ class UserModel { public function syncUserFromAuthelia($username, $displayName = '', $email = '', $groups = '') { // Check cache first $cacheKey = "user_$username"; - if (isset(self::$userCache[$cacheKey])) { - return self::$userCache[$cacheKey]; + $cached = self::getCached($cacheKey); + if ($cached !== null) { + return $cached; } // Determine if user is admin based on groups @@ -72,8 +111,8 @@ class UserModel { $stmt->close(); - // Cache user - self::$userCache[$cacheKey] = $user; + // Cache user with TTL + self::setCached($cacheKey, $user); return $user; } @@ -85,8 +124,9 @@ class UserModel { */ public function getSystemUser() { // Check cache first - if (isset(self::$userCache['system'])) { - return self::$userCache['system']; + $cached = self::getCached('system'); + if ($cached !== null) { + return $cached; } $stmt = $this->conn->prepare("SELECT * FROM users WHERE username = 'system'"); @@ -95,7 +135,7 @@ class UserModel { if ($result->num_rows > 0) { $user = $result->fetch_assoc(); - self::$userCache['system'] = $user; + self::setCached('system', $user); $stmt->close(); return $user; } @@ -113,8 +153,9 @@ class UserModel { public function getUserById($userId) { // Check cache first $cacheKey = "user_id_$userId"; - if (isset(self::$userCache[$cacheKey])) { - return self::$userCache[$cacheKey]; + $cached = self::getCached($cacheKey); + if ($cached !== null) { + return $cached; } $stmt = $this->conn->prepare("SELECT * FROM users WHERE user_id = ?"); @@ -124,7 +165,7 @@ class UserModel { if ($result->num_rows > 0) { $user = $result->fetch_assoc(); - self::$userCache[$cacheKey] = $user; + self::setCached($cacheKey, $user); $stmt->close(); return $user; } @@ -142,8 +183,9 @@ class UserModel { public function getUserByUsername($username) { // Check cache first $cacheKey = "user_$username"; - if (isset(self::$userCache[$cacheKey])) { - return self::$userCache[$cacheKey]; + $cached = self::getCached($cacheKey); + if ($cached !== null) { + return $cached; } $stmt = $this->conn->prepare("SELECT * FROM users WHERE username = ?"); @@ -153,7 +195,7 @@ class UserModel { if ($result->num_rows > 0) { $user = $result->fetch_assoc(); - self::$userCache[$cacheKey] = $user; + self::setCached($cacheKey, $user); $stmt->close(); return $user; }