feat(pargar): prune inbound logs via MySQL INSERT trigger

Run one-time overflow cleanup on migrate and keep mande/document success
rows capped at 2000 with an AFTER INSERT trigger, without cron or manual SQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-03 20:24:44 +03:30
co-authored by Cursor
parent 3b7db1eb72
commit f3eaf91559
3 changed files with 126 additions and 51 deletions
+12 -7
View File
@@ -148,12 +148,17 @@ class DatabaseLogPruneService
*
* @return array{mande: int, document: int}
*/
public function prunePargarInboundOverflow(int $maxDeletePerType = 0): array
/** هر بار حداکثر — برای afterResponse؛ تریگر MySQL هم LIMIT 4000 دارد */
private const PARAGAR_PRUNE_BATCH = 4000;
public function prunePargarInboundOverflow(?int $maxDeletePerType = null): array
{
if (! Schema::hasTable('pargar_sync_logs')) {
return ['mande' => 0, 'document' => 0];
}
$batch = $maxDeletePerType ?? self::PARAGAR_PRUNE_BATCH;
ignore_user_abort(true);
if (function_exists('set_time_limit')) {
@set_time_limit(180);
@@ -168,8 +173,8 @@ class DatabaseLogPruneService
try {
return [
'mande' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_MANDE, $maxDeletePerType),
'document' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_DOCUMENT, $maxDeletePerType),
'mande' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_MANDE, $batch),
'document' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_DOCUMENT, $batch),
];
} finally {
if ($locked === true) {
@@ -197,11 +202,11 @@ class DatabaseLogPruneService
->where('id', '<=', $cutoffId)
->orderBy('id');
if ($maxDelete > 0) {
return (int) $query->limit($maxDelete)->delete();
if ($maxDelete <= 0) {
$maxDelete = self::PARAGAR_PRUNE_BATCH;
}
return (int) $query->delete();
return (int) $query->limit($maxDelete)->delete();
}
private function ensureTypeStatusIndex(): void
@@ -224,7 +229,7 @@ class DatabaseLogPruneService
private function acquirePruneLock(): ?bool
{
try {
$row = DB::selectOne('SELECT GET_LOCK(?, 0) AS l', ['khanegi_pargar_inbound_prune']);
$row = DB::selectOne('SELECT GET_LOCK(?, 3) AS l', ['khanegi_pargar_inbound_prune']);
return (int) ($row->l ?? 0) === 1;
} catch (\Throwable) {
@@ -0,0 +1,112 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* تریگر MySQL روی INSERT — بدون artisan/cron/SQL دستی.
* migrate یک‌بار: انباشت قدیمی mande/document را می‌بُرد + تریگر برای نگه‌داری ۲۰۰۰ تازه.
*/
return new class extends Migration
{
private const KEEP = 2000;
private const TRIGGER = 'pargar_sync_logs_prune_inbound_after_insert';
public function up(): void
{
if (! Schema::hasTable('pargar_sync_logs')) {
return;
}
$this->dropHourlyEvents();
$this->dropTrigger();
$this->pruneExistingOverflow('document');
$this->pruneExistingOverflow('mande');
$this->createInsertTrigger();
}
public function down(): void
{
$this->dropTrigger();
}
private function dropHourlyEvents(): void
{
try {
DB::unprepared('DROP EVENT IF EXISTS `khanegi_prune_pargar_inbound_docs`');
DB::unprepared('DROP EVENT IF EXISTS `khanegi_prune_pargar_inbound_mande`');
} catch (\Throwable) {
}
}
private function dropTrigger(): void
{
try {
DB::unprepared('DROP TRIGGER IF EXISTS `' . self::TRIGGER . '`');
} catch (\Throwable) {
}
}
private function pruneExistingOverflow(string $type): void
{
$keep = self::KEEP;
try {
DB::unprepared(<<<SQL
DELETE FROM `pargar_sync_logs`
WHERE `type` = '{$type}' AND `sync_status` = 'success'
AND `id` <= (
SELECT `cutoff` FROM (
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
WHERE `type` = '{$type}' AND `sync_status` = 'success'
ORDER BY `id` DESC
LIMIT 1 OFFSET {$keep}
) AS `keep`
)
SQL);
} catch (\Throwable) {
// اگر overflow نیست یا دسترسی محدود است، تریگر بعداً نگه می‌دارد
}
}
private function createInsertTrigger(): void
{
$keep = self::KEEP;
$trigger = self::TRIGGER;
DB::unprepared(<<<SQL
CREATE TRIGGER `{$trigger}`
AFTER INSERT ON `pargar_sync_logs`
FOR EACH ROW
BEGIN
IF NEW.`type` = 'document' AND NEW.`sync_status` = 'success' THEN
DELETE FROM `pargar_sync_logs`
WHERE `type` = 'document' AND `sync_status` = 'success'
AND `id` <= (
SELECT `cutoff` FROM (
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
WHERE `type` = 'document' AND `sync_status` = 'success'
ORDER BY `id` DESC
LIMIT 1 OFFSET {$keep}
) AS `keep`
)
LIMIT 4000;
ELSEIF NEW.`type` = 'mande' AND NEW.`sync_status` = 'success' THEN
DELETE FROM `pargar_sync_logs`
WHERE `type` = 'mande' AND `sync_status` = 'success'
AND `id` <= (
SELECT `cutoff` FROM (
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
WHERE `type` = 'mande' AND `sync_status` = 'success'
ORDER BY `id` DESC
LIMIT 1 OFFSET {$keep}
) AS `keep`
)
LIMIT 4000;
END IF;
END
SQL);
}
};
+2 -44
View File
@@ -41,47 +41,5 @@ WHERE `type` = 'mande' AND `sync_status` = 'success'
) AS `keep`
);
-- Event داخل خود MySQL — بدون کرون سیستم (Navicat → Events)
DROP EVENT IF EXISTS `khanegi_prune_pargar_inbound_docs`;
DROP EVENT IF EXISTS `khanegi_prune_pargar_inbound_mande`;
CREATE EVENT `khanegi_prune_pargar_inbound_docs`
ON SCHEDULE EVERY 1 HOUR
STARTS CURRENT_TIMESTAMP
ON COMPLETION PRESERVE
ENABLE
DO
DELETE FROM `pargar_sync_logs`
WHERE `type` = 'document' AND `sync_status` = 'success'
AND `id` <= (
SELECT `cutoff` FROM (
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
WHERE `type` = 'document' AND `sync_status` = 'success'
ORDER BY `id` DESC
LIMIT 1 OFFSET 2000
) AS `keep`
)
LIMIT 4000;
CREATE EVENT `khanegi_prune_pargar_inbound_mande`
ON SCHEDULE EVERY 1 HOUR
STARTS CURRENT_TIMESTAMP
ON COMPLETION PRESERVE
ENABLE
DO
DELETE FROM `pargar_sync_logs`
WHERE `type` = 'mande' AND `sync_status` = 'success'
AND `id` <= (
SELECT `cutoff` FROM (
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
WHERE `type` = 'mande' AND `sync_status` = 'success'
ORDER BY `id` DESC
LIMIT 1 OFFSET 2000
) AS `keep`
)
LIMIT 4000;
-- اگر دسترسی SUPER داری:
-- SET GLOBAL event_scheduler = ON;
-- SHOW VARIABLES LIKE 'event_scheduler';
-- SHOW EVENTS;
-- جایگزین: مایگریشن 2026_09_03_000002 (تریگر INSERT + prune یک‌بار روی migrate).
-- EVENT ساعتی دیگر لازم نیست.