add handler errors
This commit is contained in:
@@ -2,10 +2,10 @@
|
||||
<module>
|
||||
<name>dbmemorycache</name>
|
||||
<displayName><![CDATA[MySQL Memory Cache Pro]]></displayName>
|
||||
<version><![CDATA[1.2.0]]></version>
|
||||
<description><![CDATA[High-speed RAM cache. Auto-recovers after reboot.]]></description>
|
||||
<author><![CDATA[YourName]]></author>
|
||||
<version><![CDATA[1.3.0]]></version>
|
||||
<description><![CDATA[High-speed RAM cache. Auto-recovers table and handles dynamic sizes.]]></description>
|
||||
<author><![CDATA[Panariga]]></author>
|
||||
<tab><![CDATA[administration]]></tab>
|
||||
<is_configurable>0</is_configurable>
|
||||
<is_configurable>1</is_configurable>
|
||||
<need_instance>0</need_instance>
|
||||
</module>
|
||||
@@ -18,11 +18,14 @@ class DbMemoryCache extends Module
|
||||
// Statically cache the max size so we don't query the `configuration` table on every set()
|
||||
private static ?int $maxCacheSizeChars = null;
|
||||
|
||||
// Prevents recursive cleanup loops
|
||||
private static bool $cleanupInProgress = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'dbmemorycache';
|
||||
$this->tab = 'administration';
|
||||
$this->version = '1.3.0';
|
||||
$this->version = '1.4.0';
|
||||
$this->author = 'Panariga';
|
||||
$this->need_instance = 0;
|
||||
$this->ps_versions_compliancy = ['min' => '8.0.0', 'max' => _PS_VERSION_];
|
||||
@@ -159,13 +162,7 @@ class DbMemoryCache extends Module
|
||||
*/
|
||||
private function calculateAndSaveLimits(): bool
|
||||
{
|
||||
// Max row size: 65535 bytes
|
||||
// Key: 128 chars * 4 bytes (utf8mb4) = 512 bytes
|
||||
// Expiry: INT = 4 bytes
|
||||
// Available bytes for value: 65535 - 512 - 4 = 65019 bytes
|
||||
// Max characters (utf8mb4): floor(65019 / 4) = 16254
|
||||
|
||||
// We set 16000 as a safe upper boundary, but you could query DB version here if needed.
|
||||
|
||||
$safeCharLimit = 16000;
|
||||
|
||||
return Configuration::updateValue('DB_MEM_CACHE_MAX_SIZE', $safeCharLimit);
|
||||
@@ -228,29 +225,9 @@ class DbMemoryCache extends Module
|
||||
$sql = "REPLACE INTO `{$this->tableName}` (`cache_key`, `cache_value`, `expiry`)
|
||||
VALUES ('$safeKey', '$safeValue', $expiry)";
|
||||
try {
|
||||
|
||||
return $db->execute($sql);
|
||||
} catch (\PrestaShopDatabaseException $e) {
|
||||
$errorMsg = $e->getMessage();
|
||||
|
||||
// Error 1146: Table doesn't exist (Manual drop or DB crash)
|
||||
if (strpos($errorMsg, '1146') !== false) {
|
||||
if ($this->createCacheTable()) {
|
||||
return $db->execute($sql); // Retry once
|
||||
}
|
||||
}
|
||||
|
||||
// Error 1114: The table is full
|
||||
if (strpos($errorMsg, 'is full') !== false || $e->getCode() == 1114) {
|
||||
$this->prune(); // Emergency prune
|
||||
try {
|
||||
return $db->execute($sql); // Retry once
|
||||
} catch (\Exception $e2) {
|
||||
return false; // Still full, fail gracefully
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->handleWriteError($e, $sql);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,4 +310,195 @@ class DbMemoryCache extends Module
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified handler for write errors (1114 table full, 1146 table missing).
|
||||
* Attempts recovery and retries the SQL once.
|
||||
*/
|
||||
private function handleWriteError(\PrestaShopDatabaseException $e, string $retrySql): bool
|
||||
{
|
||||
$errorMsg = $e->getMessage();
|
||||
$db = Db::getInstance();
|
||||
|
||||
// Error 1146: Table doesn't exist (manual drop or DB crash/reboot)
|
||||
if (strpos($errorMsg, '1146') !== false) {
|
||||
if ($this->createCacheTable()) {
|
||||
try {
|
||||
return $db->execute($retrySql);
|
||||
} catch (\Exception $e2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Error 1114: The table is full
|
||||
if (strpos($errorMsg, '1114') !== false || strpos($errorMsg, 'is full') !== false) {
|
||||
if ($this->emergencyCleanup()) {
|
||||
try {
|
||||
return $db->execute($retrySql);
|
||||
} catch (\PrestaShopDatabaseException $e2) {
|
||||
// Still full after cleanup — escalate one more time
|
||||
$errorMsg2 = $e2->getMessage();
|
||||
if (strpos($errorMsg2, '1114') !== false || strpos($errorMsg2, 'is full') !== false) {
|
||||
$this->emergencyCleanup(true); // aggressive mode
|
||||
try {
|
||||
return $db->execute($retrySql);
|
||||
} catch (\Exception $e3) {
|
||||
PrestaShopLogger::addLog(
|
||||
'DbMemoryCache: Table still full after aggressive cleanup. Key dropped.',
|
||||
3, null, 'DbMemoryCache'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unknown write error — log and fail
|
||||
PrestaShopLogger::addLog(
|
||||
'DbMemoryCache: Unhandled write error: ' . $errorMsg,
|
||||
3, null, 'DbMemoryCache'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-level emergency cleanup for "table is full" errors.
|
||||
*
|
||||
* Strategy (escalating):
|
||||
* 1. Prune all expired rows
|
||||
* 2. Evict oldest 25% of rows by expiry (soonest to expire = least valuable)
|
||||
* 3. (aggressive) Evict oldest 50% + flush if nothing else works
|
||||
*
|
||||
* @param bool $aggressive If true, skip to the most aggressive cleanup level.
|
||||
* @return bool True if some rows were freed.
|
||||
*/
|
||||
private function emergencyCleanup(bool $aggressive = false): bool
|
||||
{
|
||||
// Prevent recursive cleanup if setValue is called during cleanup
|
||||
if (self::$cleanupInProgress) {
|
||||
return false;
|
||||
}
|
||||
self::$cleanupInProgress = true;
|
||||
|
||||
try {
|
||||
$db = Db::getInstance();
|
||||
|
||||
// Level 1: Prune expired rows
|
||||
if (!$aggressive) {
|
||||
$now = time();
|
||||
try {
|
||||
$db->execute("DELETE FROM `{$this->tableName}` WHERE `expiry` <= $now");
|
||||
} catch (\Exception $e) {
|
||||
// table might be missing, nothing to clean
|
||||
}
|
||||
|
||||
$freedRows = (int) $db->getNumberOfRows();
|
||||
if ($freedRows > 0) {
|
||||
PrestaShopLogger::addLog(
|
||||
"DbMemoryCache: Emergency prune freed $freedRows expired rows.",
|
||||
2, null, 'DbMemoryCache'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Level 2: Evict the oldest 25% by nearest expiry (least time remaining)
|
||||
if ($this->evictOldest(25)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Level 3 (aggressive): Evict 50%
|
||||
if ($this->evictOldest(50)) {
|
||||
PrestaShopLogger::addLog(
|
||||
'DbMemoryCache: Aggressive cleanup — evicted 50% of rows.',
|
||||
2, null, 'DbMemoryCache'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Level 4: Full flush as last resort
|
||||
PrestaShopLogger::addLog(
|
||||
'DbMemoryCache: Full flush triggered — all other cleanup levels failed.',
|
||||
3, null, 'DbMemoryCache'
|
||||
);
|
||||
try {
|
||||
return $db->execute("TRUNCATE TABLE `{$this->tableName}`");
|
||||
} catch (\Exception $e) {
|
||||
// TRUNCATE can't fail on MEMORY tables unless table is missing
|
||||
$this->createCacheTable();
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
self::$cleanupInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict the oldest N% of rows (by soonest expiry = least remaining TTL).
|
||||
* For a MEMORY table, this is the most efficient LRU-like eviction.
|
||||
*
|
||||
* @param int $percent Percentage of rows to evict (1-100).
|
||||
* @return bool True if any rows were deleted.
|
||||
*/
|
||||
private function evictOldest(int $percent): bool
|
||||
{
|
||||
$db = Db::getInstance();
|
||||
|
||||
try {
|
||||
$totalRows = (int) $db->getValue("SELECT COUNT(*) FROM `{$this->tableName}`");
|
||||
if ($totalRows === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$rowsToEvict = max(1, (int) ceil($totalRows * $percent / 100));
|
||||
|
||||
// Find the expiry threshold: the Nth-lowest expiry value
|
||||
$threshold = $db->getValue(
|
||||
"SELECT `expiry` FROM `{$this->tableName}` ORDER BY `expiry` ASC LIMIT $rowsToEvict, 1"
|
||||
);
|
||||
|
||||
if ($threshold !== false && $threshold !== null) {
|
||||
// Delete all rows with expiry below the threshold
|
||||
$db->execute(
|
||||
"DELETE FROM `{$this->tableName}` WHERE `expiry` < " . (int) $threshold
|
||||
);
|
||||
} else {
|
||||
// Fewer rows than the limit — delete the bottom chunk directly
|
||||
// For MEMORY engine, sub-selects in DELETE are not supported,
|
||||
// so we use a two-step approach
|
||||
$keysToDelete = $db->executeS(
|
||||
"SELECT `cache_key` FROM `{$this->tableName}` ORDER BY `expiry` ASC LIMIT $rowsToEvict"
|
||||
);
|
||||
if ($keysToDelete && count($keysToDelete) > 0) {
|
||||
$keyList = implode("','", array_map(function ($row) {
|
||||
return pSQL($row['cache_key']);
|
||||
}, $keysToDelete));
|
||||
$db->execute(
|
||||
"DELETE FROM `{$this->tableName}` WHERE `cache_key` IN ('$keyList')"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$deletedCount = max(0, $totalRows - (int) $db->getValue("SELECT COUNT(*) FROM `{$this->tableName}`"));
|
||||
if ($deletedCount > 0) {
|
||||
PrestaShopLogger::addLog(
|
||||
"DbMemoryCache: Evicted $deletedCount rows ({$percent}% target of $totalRows).",
|
||||
2, null, 'DbMemoryCache'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
PrestaShopLogger::addLog(
|
||||
'DbMemoryCache: evictOldest failed: ' . $e->getMessage(),
|
||||
3, null, 'DbMemoryCache'
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user