add handler errors
This commit is contained in:
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user