mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-27 01:24:16 +00:00
Cache library and cache invalidation
This commit is contained in:
215
application/libraries/DxclusterCache.php
Normal file
215
application/libraries/DxclusterCache.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* DXCluster Cache Library
|
||||
* Centralizes cache key generation and invalidation for DXCluster features.
|
||||
*/
|
||||
class DxclusterCache {
|
||||
|
||||
protected $CI;
|
||||
|
||||
public function __construct() {
|
||||
$this->CI =& get_instance();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CACHE KEY GENERATION
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Generate RAW spot cache key (instance-wide, shared by all users)
|
||||
*/
|
||||
public function getRawCacheKey($maxage, $band) {
|
||||
return "dxcluster_raw_{$maxage}_{$band}_Any_All";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate logbook IDs key component (user-specific)
|
||||
*/
|
||||
public function getLogbookKey($user_id, $logbook_ids, $confirmation_prefs) {
|
||||
$logbook_ids_str = implode('_', $logbook_ids);
|
||||
$confirmation_hash = md5($confirmation_prefs);
|
||||
return "{$user_id}_{$logbook_ids_str}_{$confirmation_hash}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate WORKED callsign cache key
|
||||
*/
|
||||
public function getWorkedCallKey($logbook_key, $callsign) {
|
||||
return "dxcluster_worked_call_{$logbook_key}_{$callsign}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate WORKED DXCC cache key
|
||||
*/
|
||||
public function getWorkedDxccKey($logbook_key, $dxcc) {
|
||||
return "dxcluster_worked_dxcc_{$logbook_key}_{$dxcc}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate WORKED continent cache key
|
||||
*/
|
||||
public function getWorkedContKey($logbook_key, $cont) {
|
||||
return "dxcluster_worked_cont_{$logbook_key}_{$cont}";
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CACHE INVALIDATION
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Invalidate cache after QSO add/edit/delete for current user
|
||||
* @param string $callsign - The worked callsign
|
||||
*/
|
||||
public function invalidateForCallsign($callsign) {
|
||||
if (empty($callsign)) return;
|
||||
|
||||
// Get current user's logbook key
|
||||
$logbook_key = $this->getCurrentUserLogbookKey();
|
||||
if (empty($logbook_key)) return;
|
||||
|
||||
// Delete callsign cache
|
||||
$this->deleteFile($this->getWorkedCallKey($logbook_key, $callsign));
|
||||
|
||||
// Look up DXCC and continent from callsign
|
||||
$this->CI->load->model('logbook_model');
|
||||
$dxcc_info = $this->CI->logbook_model->dxcc_lookup($callsign, date('Y-m-d'));
|
||||
|
||||
if (!empty($dxcc_info['adif'])) {
|
||||
$this->deleteFile($this->getWorkedDxccKey($logbook_key, $dxcc_info['adif']));
|
||||
}
|
||||
if (!empty($dxcc_info['cont'])) {
|
||||
$this->deleteFile($this->getWorkedContKey($logbook_key, $dxcc_info['cont']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all worked cache for current user (bulk operations)
|
||||
*/
|
||||
public function invalidateAllWorkedForCurrentUser() {
|
||||
$logbook_key = $this->getCurrentUserLogbookKey();
|
||||
if (empty($logbook_key)) return;
|
||||
|
||||
$this->invalidateByPrefix("dxcluster_worked_call_{$logbook_key}_");
|
||||
$this->invalidateByPrefix("dxcluster_worked_dxcc_{$logbook_key}_");
|
||||
$this->invalidateByPrefix("dxcluster_worked_cont_{$logbook_key}_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user's logbook key from session
|
||||
*/
|
||||
protected function getCurrentUserLogbookKey() {
|
||||
$user_id = $this->CI->session->userdata('user_id');
|
||||
$active_logbook = $this->CI->session->userdata('active_station_logbook');
|
||||
|
||||
if (empty($user_id) || empty($active_logbook)) return null;
|
||||
|
||||
$this->CI->load->model('logbooks_model');
|
||||
|
||||
$logbook_ids = $this->CI->logbooks_model->list_logbook_relationships($active_logbook);
|
||||
$confirmation_prefs = $this->CI->session->userdata('user_default_confirmation') ?? '';
|
||||
|
||||
if (empty($logbook_ids)) return null;
|
||||
|
||||
return $this->getLogbookKey($user_id, $logbook_ids, $confirmation_prefs);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// INTERNAL HELPERS
|
||||
// =========================================================================
|
||||
|
||||
protected function deleteFile($cache_key) {
|
||||
$cache_path = $this->getCachePath();
|
||||
if (!$cache_path) return;
|
||||
@unlink($cache_path . $cache_key);
|
||||
}
|
||||
|
||||
protected function invalidateByPrefix($prefix) {
|
||||
$cache_path = $this->getCachePath();
|
||||
if (!$cache_path) return;
|
||||
|
||||
$handle = @opendir($cache_path);
|
||||
if (!$handle) return;
|
||||
|
||||
while (($filename = readdir($handle)) !== false) {
|
||||
if (strpos($filename, $prefix) === 0) {
|
||||
@unlink($cache_path . $filename);
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
|
||||
protected function getCachePath() {
|
||||
$cache_path = $this->CI->config->item('cache_path');
|
||||
$cache_path = ($cache_path === '' || $cache_path === false) ? APPPATH . 'cache/' : $cache_path;
|
||||
$cache_path = rtrim($cache_path, '/\\') . DIRECTORY_SEPARATOR;
|
||||
return (is_dir($cache_path) && is_writable($cache_path)) ? $cache_path : false;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// GARBAGE COLLECTION
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Run garbage collection with probability check (1% chance)
|
||||
* Call this on each request when worked cache is enabled
|
||||
*/
|
||||
public function maybeRunGc() {
|
||||
if (mt_rand(1, 100) === 1) {
|
||||
$this->cleanExpiredCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean expired dxcluster cache files
|
||||
* Uses file mtime for fast pre-filtering before reading file contents
|
||||
*/
|
||||
public function cleanExpiredCache() {
|
||||
$cache_path = $this->getCachePath();
|
||||
if (!$cache_path || !is_readable($cache_path)) return;
|
||||
|
||||
$handle = @opendir($cache_path);
|
||||
if (!$handle) return;
|
||||
|
||||
$now = time();
|
||||
$deleted = 0;
|
||||
|
||||
// Max TTL for dxcluster files: raw=59s, worked=900s - use 900s + buffer
|
||||
$max_ttl = 1000;
|
||||
|
||||
while (($filename = readdir($handle)) !== false) {
|
||||
// Only process dxcluster cache files
|
||||
if (strpos($filename, 'dxcluster_') !== 0) continue;
|
||||
|
||||
$file = $cache_path . $filename;
|
||||
if (!is_file($file)) continue;
|
||||
|
||||
// Fast pre-filter: skip files modified recently (can't be expired yet)
|
||||
$mtime = @filemtime($file);
|
||||
if ($mtime !== false && ($now - $mtime) < $max_ttl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// File is old enough to potentially be expired - read and verify
|
||||
$data = @unserialize(@file_get_contents($file));
|
||||
if (!is_array($data) || !isset($data['time'], $data['ttl'])) {
|
||||
@unlink($file);
|
||||
$deleted++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if ($data['ttl'] > 0 && $now > $data['time'] + $data['ttl']) {
|
||||
@unlink($file);
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
closedir($handle);
|
||||
|
||||
if ($deleted > 0) {
|
||||
log_message('debug', "DXCluster cache GC: deleted {$deleted} expired files");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,9 @@ class Dxcluster_model extends CI_Model {
|
||||
|
||||
// Garbage collection: 1% chance to clean expired cache files
|
||||
// Only needed when worked cache is enabled (creates many per-callsign files)
|
||||
if ($cache_worked_enabled && mt_rand(1, 100) === 1) {
|
||||
$this->cleanExpiredDxclusterCache();
|
||||
if ($cache_worked_enabled) {
|
||||
$this->load->library('DxclusterCache');
|
||||
$this->dxclustercache->maybeRunGc();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,9 +81,9 @@ class Dxcluster_model extends CI_Model {
|
||||
$logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
|
||||
|
||||
// Cache key for RAW cluster response (instance-wide, no worked status)
|
||||
// Use fixed 'Any'/'All' for de/mode to maximize cache hits - filters applied after retrieval
|
||||
// Future: when API supports server-side filtering, use actual $de/$mode values here
|
||||
$raw_cache_key = "dxcluster_raw_{$maxage}_{$band}_Any_All";
|
||||
// Use DxclusterCache library for centralized key generation
|
||||
$this->load->library('DxclusterCache');
|
||||
$raw_cache_key = $this->dxclustercache->getRawCacheKey($maxage, $band);
|
||||
|
||||
// Check cache for raw processed spots (without worked status)
|
||||
$spotsout = null;
|
||||
@@ -733,69 +734,4 @@ class Dxcluster_model extends CI_Model {
|
||||
|
||||
return $spot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean expired DX cluster cache files
|
||||
* Called with low probability on each cache access to prevent buildup
|
||||
* Uses file mtime for fast pre-filtering before reading file contents
|
||||
*/
|
||||
protected function cleanExpiredDxclusterCache() {
|
||||
// Use configured cache path (same as CI cache driver)
|
||||
$cache_path = $this->config->item('cache_path');
|
||||
$cache_path = ($cache_path === '' || $cache_path === false) ? APPPATH . 'cache/' : $cache_path;
|
||||
|
||||
// Ensure trailing slash
|
||||
$cache_path = rtrim($cache_path, '/\\') . DIRECTORY_SEPARATOR;
|
||||
|
||||
// Check directory exists and is readable
|
||||
if (!is_dir($cache_path) || !is_readable($cache_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use opendir/readdir instead of glob (more compatible with UNC paths)
|
||||
$handle = @opendir($cache_path);
|
||||
if (!$handle) return;
|
||||
|
||||
$now = time();
|
||||
$deleted = 0;
|
||||
|
||||
// Max TTL for dxcluster files: raw=59s, worked=900s - use 900s + buffer
|
||||
$max_ttl = 1000;
|
||||
|
||||
while (($filename = readdir($handle)) !== false) {
|
||||
// Only process dxcluster cache files
|
||||
if (strpos($filename, 'dxcluster_') !== 0) continue;
|
||||
|
||||
$file = $cache_path . $filename;
|
||||
if (!is_file($file)) continue;
|
||||
|
||||
// Fast pre-filter: skip files modified recently (can't be expired yet)
|
||||
// filemtime() is much faster than reading+deserializing the file
|
||||
$mtime = @filemtime($file);
|
||||
if ($mtime !== false && ($now - $mtime) < $max_ttl) {
|
||||
continue; // File too new to be expired
|
||||
}
|
||||
|
||||
// File is old enough to potentially be expired - read and verify
|
||||
$data = @unserialize(@file_get_contents($file));
|
||||
if (!is_array($data) || !isset($data['time'], $data['ttl'])) {
|
||||
// Invalid cache file - delete it
|
||||
@unlink($file);
|
||||
$deleted++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if ($data['ttl'] > 0 && $now > $data['time'] + $data['ttl']) {
|
||||
@unlink($file);
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
closedir($handle);
|
||||
|
||||
if ($deleted > 0) {
|
||||
log_message('debug', "DXCluster cache GC: deleted {$deleted} expired files");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ class Logbook_model extends CI_Model {
|
||||
public function __construct() {
|
||||
$this->oop_populate_modes();
|
||||
$this->load->Model('Modes');
|
||||
$this->load->library('DxclusterCache');
|
||||
}
|
||||
|
||||
private $oop_modes = [];
|
||||
@@ -940,6 +941,9 @@ class Logbook_model extends CI_Model {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate DXCluster cache for this callsign
|
||||
$this->dxclustercache->invalidateForCallsign($data['COL_CALL']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1680,6 +1684,9 @@ class Logbook_model extends CI_Model {
|
||||
try {
|
||||
$this->db->update($this->config->item('table_name'), $data);
|
||||
$retvals['success']=true;
|
||||
|
||||
// Invalidate DXCluster cache for this callsign
|
||||
$this->dxclustercache->invalidateForCallsign($data['COL_CALL']);
|
||||
} catch (Exception $e) {
|
||||
$retvals['success']=false;
|
||||
$retvals['detail']=$e;
|
||||
@@ -2742,14 +2749,12 @@ class Logbook_model extends CI_Model {
|
||||
|
||||
// Build cache key with user_id, logbook_ids, and confirmation preference
|
||||
$user_id = $this->session->userdata('user_id');
|
||||
$logbook_ids_str = implode('_', $logbooks_locations_array);
|
||||
$confirmation_hash = md5($user_default_confirmation); // Hash to keep key shorter
|
||||
$logbook_ids_key = "{$user_id}_{$logbook_ids_str}_{$confirmation_hash}";
|
||||
$logbook_ids_key = $this->dxclustercache->getLogbookKey($user_id, $logbooks_locations_array, $user_default_confirmation);
|
||||
$spots_by_callsign = []; // Group spots by callsign for processing
|
||||
|
||||
foreach ($spots as $spot) {
|
||||
// Validate spot has required properties
|
||||
if (!isset($spot->spotted) || !isset($spot->dxcc_spotted->dxcc_id) || !isset($spot->dxcc_spotted->cont) || !isset($spot->band) || !isset($spot->mode)) {
|
||||
// Validate spot has required properties (must be non-empty)
|
||||
if (empty($spot->spotted) || empty($spot->dxcc_spotted->dxcc_id) || empty($spot->dxcc_spotted->cont) || empty($spot->band) || empty($spot->mode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2782,7 +2787,7 @@ class Logbook_model extends CI_Model {
|
||||
if (!isset($this->spot_status_cache[$cache_key])) {
|
||||
// Check file cache
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_call_{$logbook_ids_key}_{$callsign}";
|
||||
$file_cache_key = $this->dxclustercache->getWorkedCallKey($logbook_ids_key, $callsign);
|
||||
$cached_data = $this->cache->get($file_cache_key);
|
||||
if ($cached_data !== false) {
|
||||
// Load from file cache into in-memory cache
|
||||
@@ -2800,7 +2805,7 @@ class Logbook_model extends CI_Model {
|
||||
|
||||
if (!isset($this->spot_status_cache[$cache_key])) {
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_dxcc_{$logbook_ids_key}_{$dxcc}";
|
||||
$file_cache_key = $this->dxclustercache->getWorkedDxccKey($logbook_ids_key, $dxcc);
|
||||
$cached_data = $this->cache->get($file_cache_key);
|
||||
if ($cached_data !== false) {
|
||||
$this->spot_status_cache[$cache_key] = $cached_data;
|
||||
@@ -2816,7 +2821,7 @@ class Logbook_model extends CI_Model {
|
||||
|
||||
if (!isset($this->spot_status_cache[$cache_key])) {
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_cont_{$logbook_ids_key}_{$cont}";
|
||||
$file_cache_key = $this->dxclustercache->getWorkedContKey($logbook_ids_key, $cont);
|
||||
$cached_data = $this->cache->get($file_cache_key);
|
||||
if ($cached_data !== false) {
|
||||
$this->spot_status_cache[$cache_key] = $cached_data;
|
||||
@@ -3059,7 +3064,7 @@ class Logbook_model extends CI_Model {
|
||||
|
||||
// Save to file cache for 15 minutes
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_call_{$logbook_ids_key}_{$callsign}";
|
||||
$file_cache_key = $this->dxclustercache->getWorkedCallKey($logbook_ids_key, $callsign);
|
||||
$this->cache->save($file_cache_key, $data, $cache_ttl);
|
||||
}
|
||||
}
|
||||
@@ -3068,7 +3073,7 @@ class Logbook_model extends CI_Model {
|
||||
$this->spot_status_cache[$cache_key] = $data;
|
||||
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_dxcc_{$logbook_ids_key}_{$dxcc}";
|
||||
$file_cache_key = $this->dxclustercache->getWorkedDxccKey($logbook_ids_key, $dxcc);
|
||||
$this->cache->save($file_cache_key, $data, $cache_ttl);
|
||||
}
|
||||
}
|
||||
@@ -3077,7 +3082,7 @@ class Logbook_model extends CI_Model {
|
||||
$this->spot_status_cache[$cache_key] = $data;
|
||||
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_cont_{$logbook_ids_key}_{$cont}";
|
||||
$file_cache_key = $this->dxclustercache->getWorkedContKey($logbook_ids_key, $cont);
|
||||
$this->cache->save($file_cache_key, $data, $cache_ttl);
|
||||
}
|
||||
} // Cache NOT WORKED items (negative results) - store empty arrays
|
||||
@@ -3088,7 +3093,7 @@ class Logbook_model extends CI_Model {
|
||||
$this->spot_status_cache[$cache_key] = []; // Empty = not worked
|
||||
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_call_{$logbook_ids_key}_{$callsign}";
|
||||
$file_cache_key = $this->dxclustercache->getWorkedCallKey($logbook_ids_key, $callsign);
|
||||
$this->cache->save($file_cache_key, [], $cache_ttl);
|
||||
}
|
||||
}
|
||||
@@ -3099,7 +3104,7 @@ class Logbook_model extends CI_Model {
|
||||
$this->spot_status_cache[$cache_key] = [];
|
||||
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_dxcc_{$logbook_ids_key}_{$dxcc}";
|
||||
$file_cache_key = $this->dxclustercache->getWorkedDxccKey($logbook_ids_key, $dxcc);
|
||||
$this->cache->save($file_cache_key, [], $cache_ttl);
|
||||
}
|
||||
}
|
||||
@@ -3110,7 +3115,7 @@ class Logbook_model extends CI_Model {
|
||||
$this->spot_status_cache[$cache_key] = [];
|
||||
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_cont_{$logbook_ids_key}_{$cont}";
|
||||
$file_cache_key = $this->dxclustercache->getWorkedContKey($logbook_ids_key, $cont);
|
||||
$this->cache->save($file_cache_key, [], $cache_ttl);
|
||||
}
|
||||
}
|
||||
@@ -4294,6 +4299,10 @@ class Logbook_model extends CI_Model {
|
||||
/* Delete QSO based on the QSO ID */
|
||||
function delete($id) {
|
||||
if ($this->check_qso_is_accessible($id)) {
|
||||
// Get callsign before deleting for cache invalidation
|
||||
$qso = $this->get_qso($id);
|
||||
$callsign = ($qso->num_rows() > 0) ? $qso->row()->COL_CALL : null;
|
||||
|
||||
$this->load->model('qsl_model');
|
||||
$this->load->model('eqsl_images');
|
||||
|
||||
@@ -4305,6 +4314,9 @@ class Logbook_model extends CI_Model {
|
||||
|
||||
$this->db->where('qsoid', $id);
|
||||
$this->db->delete("oqrs");
|
||||
|
||||
// Invalidate DXCluster cache for this callsign
|
||||
$this->dxclustercache->invalidateForCallsign($callsign);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user