2024-09-23 21:48:12 +00:00
< ? php
namespace App\Console\Commands ;
use Illuminate\Console\Command ;
use Illuminate\Support\Facades\Redis ;
class CleanupRedis extends Command
{
2025-07-07 07:47:11 +00:00
protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup}' ;
2024-09-23 21:48:12 +00:00
2025-07-07 07:47:11 +00:00
protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, and related data)' ;
2024-09-23 21:48:12 +00:00
public function handle ()
{
2025-05-07 20:09:25 +00:00
$redis = Redis :: connection ( 'horizon' );
$prefix = config ( 'horizon.prefix' );
2025-07-07 07:47:11 +00:00
$dryRun = $this -> option ( 'dry-run' );
$skipOverlapping = $this -> option ( 'skip-overlapping' );
if ( $dryRun ) {
$this -> info ( 'DRY RUN MODE - No data will be deleted' );
}
$deletedCount = 0 ;
$totalKeys = 0 ;
// Get all keys with the horizon prefix
$keys = $redis -> keys ( '*' );
$totalKeys = count ( $keys );
$this -> info ( " Scanning { $totalKeys } keys for cleanup... " );
2025-05-07 20:09:25 +00:00
foreach ( $keys as $key ) {
2024-09-23 21:48:12 +00:00
$keyWithoutPrefix = str_replace ( $prefix , '' , $key );
2025-05-07 20:09:25 +00:00
$type = $redis -> command ( 'type' , [ $keyWithoutPrefix ]);
2024-09-23 21:48:12 +00:00
2025-07-07 07:47:11 +00:00
// Handle hash-type keys (individual jobs)
2025-05-07 20:09:25 +00:00
if ( $type === 5 ) {
2025-07-07 07:47:11 +00:00
if ( $this -> shouldDeleteHashKey ( $redis , $keyWithoutPrefix , $dryRun )) {
$deletedCount ++ ;
}
}
// Handle other key types (metrics, lists, etc.)
else {
if ( $this -> shouldDeleteOtherKey ( $redis , $keyWithoutPrefix , $key , $dryRun )) {
$deletedCount ++ ;
}
}
}
// Clean up overlapping queues if not skipped
if ( ! $skipOverlapping ) {
$this -> info ( 'Cleaning up overlapping queues...' );
$overlappingCleaned = $this -> cleanupOverlappingQueues ( $redis , $prefix , $dryRun );
$deletedCount += $overlappingCleaned ;
}
if ( $dryRun ) {
$this -> info ( " DRY RUN: Would delete { $deletedCount } out of { $totalKeys } keys " );
} else {
$this -> info ( " Deleted { $deletedCount } out of { $totalKeys } keys " );
}
}
private function shouldDeleteHashKey ( $redis , $keyWithoutPrefix , $dryRun )
{
$data = $redis -> command ( 'hgetall' , [ $keyWithoutPrefix ]);
$status = data_get ( $data , 'status' );
// Delete completed and failed jobs
if ( in_array ( $status , [ 'completed' , 'failed' ])) {
if ( $dryRun ) {
$this -> line ( " Would delete job: { $keyWithoutPrefix } (status: { $status } ) " );
} else {
$redis -> command ( 'del' , [ $keyWithoutPrefix ]);
$this -> line ( " Deleted job: { $keyWithoutPrefix } (status: { $status } ) " );
}
return true ;
}
return false ;
}
private function shouldDeleteOtherKey ( $redis , $keyWithoutPrefix , $fullKey , $dryRun )
{
// Clean up various Horizon data structures
$patterns = [
'recent_jobs' => 'Recent jobs list' ,
'failed_jobs' => 'Failed jobs list' ,
'completed_jobs' => 'Completed jobs list' ,
'job_classes' => 'Job classes metrics' ,
'queues' => 'Queue metrics' ,
'processes' => 'Process metrics' ,
'supervisors' => 'Supervisor data' ,
'metrics' => 'General metrics' ,
'workload' => 'Workload data' ,
];
foreach ( $patterns as $pattern => $description ) {
if ( str_contains ( $keyWithoutPrefix , $pattern )) {
if ( $dryRun ) {
$this -> line ( " Would delete { $description } : { $keyWithoutPrefix } " );
} else {
$redis -> command ( 'del' , [ $keyWithoutPrefix ]);
$this -> line ( " Deleted { $description } : { $keyWithoutPrefix } " );
}
return true ;
}
}
// Clean up old timestamped data (older than 7 days)
if ( preg_match ( '/(\d{10})/' , $keyWithoutPrefix , $matches )) {
$timestamp = ( int ) $matches [ 1 ];
$weekAgo = now () -> subDays ( 7 ) -> timestamp ;
if ( $timestamp < $weekAgo ) {
if ( $dryRun ) {
$this -> line ( " Would delete old timestamped data: { $keyWithoutPrefix } " );
} else {
2025-05-07 20:09:25 +00:00
$redis -> command ( 'del' , [ $keyWithoutPrefix ]);
2025-07-07 07:47:11 +00:00
$this -> line ( " Deleted old timestamped data: { $keyWithoutPrefix } " );
2025-05-07 20:09:25 +00:00
}
2025-07-07 07:47:11 +00:00
return true ;
}
}
return false ;
}
private function cleanupOverlappingQueues ( $redis , $prefix , $dryRun )
{
$cleanedCount = 0 ;
$queueKeys = [];
// Find all queue-related keys
$allKeys = $redis -> keys ( '*' );
foreach ( $allKeys as $key ) {
$keyWithoutPrefix = str_replace ( $prefix , '' , $key );
if ( str_contains ( $keyWithoutPrefix , 'queue:' ) || preg_match ( '/queues?[:\-]/' , $keyWithoutPrefix )) {
$queueKeys [] = $keyWithoutPrefix ;
2025-05-07 20:09:25 +00:00
}
}
2025-07-07 07:47:11 +00:00
$this -> info ( 'Found ' . count ( $queueKeys ) . ' queue-related keys' );
// Group queues by name pattern to find duplicates
$queueGroups = [];
foreach ( $queueKeys as $queueKey ) {
// Extract queue name (remove timestamps, suffixes)
$baseName = preg_replace ( '/[:\-]\d+$/' , '' , $queueKey );
$baseName = preg_replace ( '/[:\-](pending|reserved|delayed|processing)$/' , '' , $baseName );
if ( ! isset ( $queueGroups [ $baseName ])) {
$queueGroups [ $baseName ] = [];
}
$queueGroups [ $baseName ][] = $queueKey ;
}
// Process each group for overlaps
foreach ( $queueGroups as $baseName => $keys ) {
if ( count ( $keys ) > 1 ) {
$cleanedCount += $this -> deduplicateQueueGroup ( $redis , $baseName , $keys , $dryRun );
}
// Also check for duplicate jobs within individual queues
foreach ( $keys as $queueKey ) {
$cleanedCount += $this -> deduplicateQueueContents ( $redis , $queueKey , $dryRun );
}
}
return $cleanedCount ;
}
private function deduplicateQueueGroup ( $redis , $baseName , $keys , $dryRun )
{
$cleanedCount = 0 ;
$this -> line ( " Processing queue group: { $baseName } ( " . count ( $keys ) . ' keys)' );
// Sort keys to keep the most recent one
usort ( $keys , function ( $a , $b ) {
// Prefer keys without timestamps (they're usually the main queue)
$aHasTimestamp = preg_match ( '/\d{10}/' , $a );
$bHasTimestamp = preg_match ( '/\d{10}/' , $b );
if ( $aHasTimestamp && ! $bHasTimestamp ) {
return 1 ;
}
if ( ! $aHasTimestamp && $bHasTimestamp ) {
return - 1 ;
}
// If both have timestamps, prefer the newer one
if ( $aHasTimestamp && $bHasTimestamp ) {
preg_match ( '/(\d{10})/' , $a , $aMatches );
preg_match ( '/(\d{10})/' , $b , $bMatches );
return ( $bMatches [ 1 ] ? ? 0 ) <=> ( $aMatches [ 1 ] ? ? 0 );
}
return strcmp ( $a , $b );
});
// Keep the first (preferred) key, remove others that are empty or redundant
$keepKey = array_shift ( $keys );
foreach ( $keys as $redundantKey ) {
$type = $redis -> command ( 'type' , [ $redundantKey ]);
$shouldDelete = false ;
if ( $type === 1 ) { // LIST type
$length = $redis -> command ( 'llen' , [ $redundantKey ]);
if ( $length == 0 ) {
$shouldDelete = true ;
}
} elseif ( $type === 3 ) { // SET type
$count = $redis -> command ( 'scard' , [ $redundantKey ]);
if ( $count == 0 ) {
$shouldDelete = true ;
}
} elseif ( $type === 4 ) { // ZSET type
$count = $redis -> command ( 'zcard' , [ $redundantKey ]);
if ( $count == 0 ) {
$shouldDelete = true ;
}
}
if ( $shouldDelete ) {
if ( $dryRun ) {
$this -> line ( " Would delete empty queue: { $redundantKey } " );
} else {
$redis -> command ( 'del' , [ $redundantKey ]);
$this -> line ( " Deleted empty queue: { $redundantKey } " );
}
$cleanedCount ++ ;
}
}
return $cleanedCount ;
}
private function deduplicateQueueContents ( $redis , $queueKey , $dryRun )
{
$cleanedCount = 0 ;
$type = $redis -> command ( 'type' , [ $queueKey ]);
if ( $type === 1 ) { // LIST type - common for job queues
$length = $redis -> command ( 'llen' , [ $queueKey ]);
if ( $length > 1 ) {
$items = $redis -> command ( 'lrange' , [ $queueKey , 0 , - 1 ]);
$uniqueItems = array_unique ( $items );
if ( count ( $uniqueItems ) < count ( $items )) {
$duplicates = count ( $items ) - count ( $uniqueItems );
if ( $dryRun ) {
$this -> line ( " Would remove { $duplicates } duplicate jobs from queue: { $queueKey } " );
} else {
// Rebuild the list with unique items
$redis -> command ( 'del' , [ $queueKey ]);
foreach ( array_reverse ( $uniqueItems ) as $item ) {
$redis -> command ( 'lpush' , [ $queueKey , $item ]);
}
$this -> line ( " Removed { $duplicates } duplicate jobs from queue: { $queueKey } " );
}
$cleanedCount += $duplicates ;
}
}
}
return $cleanedCount ;
2024-09-23 21:48:12 +00:00
}
}