add handler errors

This commit is contained in:
O K
2026-04-25 08:58:47 +03:00
parent 1cd920a91f
commit c405c2a4b3
2 changed files with 201 additions and 33 deletions

View File

@@ -2,10 +2,10 @@
<module> <module>
<name>dbmemorycache</name> <name>dbmemorycache</name>
<displayName><![CDATA[MySQL Memory Cache Pro]]></displayName> <displayName><![CDATA[MySQL Memory Cache Pro]]></displayName>
<version><![CDATA[1.2.0]]></version> <version><![CDATA[1.3.0]]></version>
<description><![CDATA[High-speed RAM cache. Auto-recovers after reboot.]]></description> <description><![CDATA[High-speed RAM cache. Auto-recovers table and handles dynamic sizes.]]></description>
<author><![CDATA[YourName]]></author> <author><![CDATA[Panariga]]></author>
<tab><![CDATA[administration]]></tab> <tab><![CDATA[administration]]></tab>
<is_configurable>0</is_configurable> <is_configurable>1</is_configurable>
<need_instance>0</need_instance> <need_instance>0</need_instance>
</module> </module>

View File

@@ -18,11 +18,14 @@ class DbMemoryCache extends Module
// Statically cache the max size so we don't query the `configuration` table on every set() // Statically cache the max size so we don't query the `configuration` table on every set()
private static ?int $maxCacheSizeChars = null; private static ?int $maxCacheSizeChars = null;
// Prevents recursive cleanup loops
private static bool $cleanupInProgress = false;
public function __construct() public function __construct()
{ {
$this->name = 'dbmemorycache'; $this->name = 'dbmemorycache';
$this->tab = 'administration'; $this->tab = 'administration';
$this->version = '1.3.0'; $this->version = '1.4.0';
$this->author = 'Panariga'; $this->author = 'Panariga';
$this->need_instance = 0; $this->need_instance = 0;
$this->ps_versions_compliancy = ['min' => '8.0.0', 'max' => _PS_VERSION_]; $this->ps_versions_compliancy = ['min' => '8.0.0', 'max' => _PS_VERSION_];
@@ -159,13 +162,7 @@ class DbMemoryCache extends Module
*/ */
private function calculateAndSaveLimits(): bool 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; $safeCharLimit = 16000;
return Configuration::updateValue('DB_MEM_CACHE_MAX_SIZE', $safeCharLimit); 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`) $sql = "REPLACE INTO `{$this->tableName}` (`cache_key`, `cache_value`, `expiry`)
VALUES ('$safeKey', '$safeValue', $expiry)"; VALUES ('$safeKey', '$safeValue', $expiry)";
try { try {
return $db->execute($sql); return $db->execute($sql);
} catch (\PrestaShopDatabaseException $e) { } catch (\PrestaShopDatabaseException $e) {
$errorMsg = $e->getMessage(); return $this->handleWriteError($e, $sql);
// 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;
} }
} }
@@ -333,4 +310,195 @@ class DbMemoryCache extends Module
return false; 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;
}
} }