From f3eaf91559e426a355023649a4d460024449be67 Mon Sep 17 00:00:00 2001 From: adonis Date: Thu, 3 Sep 2026 20:24:44 +0330 Subject: [PATCH] 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 --- app/Services/DatabaseLogPruneService.php | 19 +-- ...00002_pargar_inbound_log_prune_trigger.php | 112 ++++++++++++++++++ .../sql/prune_pargar_inbound_logs_now.sql | 46 +------ 3 files changed, 126 insertions(+), 51 deletions(-) create mode 100644 database/migrations/2026_09_03_000002_pargar_inbound_log_prune_trigger.php diff --git a/app/Services/DatabaseLogPruneService.php b/app/Services/DatabaseLogPruneService.php index 8e2ba0b..2572be0 100644 --- a/app/Services/DatabaseLogPruneService.php +++ b/app/Services/DatabaseLogPruneService.php @@ -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) { diff --git a/database/migrations/2026_09_03_000002_pargar_inbound_log_prune_trigger.php b/database/migrations/2026_09_03_000002_pargar_inbound_log_prune_trigger.php new file mode 100644 index 0000000..947a323 --- /dev/null +++ b/database/migrations/2026_09_03_000002_pargar_inbound_log_prune_trigger.php @@ -0,0 +1,112 @@ +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(<<