Skip to content

Commit 28f53f7

Browse files
committed
Improve serialization performance 10x for data stored in redis. Makes the jobs run even faster. And huge difference in listing batches in Horizon performance.
1 parent 7c0837c commit 28f53f7

File tree

1 file changed

+61
-62
lines changed

1 file changed

+61
-62
lines changed

src/Repositories/RedisBatchRepository.php

+61-62
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
<?php
22

3-
namespace Cyppe\LaravelBatchJobsRedisDriver\Repositories;
3+
namespace App\Repositories;
44

55
use Carbon\CarbonImmutable;
66
use Closure;
77
use DateTimeInterface;
88
use Illuminate\Bus\Batch;
99
use Illuminate\Bus\BatchFactory;
1010
use Illuminate\Bus\BatchRepository;
11-
use Illuminate\Bus\DatabaseBatchRepository;
1211
use Illuminate\Bus\PendingBatch;
1312
use Illuminate\Bus\PrunableBatchRepository;
1413
use Illuminate\Bus\UpdatedBatchJobCounts;
@@ -24,47 +23,55 @@ class RedisBatchRepository extends DatabaseBatchRepository implements BatchRepos
2423
public function __construct( BatchFactory $factory )
2524
{
2625
$this->lockTimeout = 120;
27-
$this->factory = $factory;
26+
$this->factory = $factory;
2827
}
2928

30-
public function get( $limit = 50, $before = null )
29+
public function get( $limit = 50, $before = null ): array
3130
{
3231
if ( !Redis::exists( 'batches_list' ) ) {
33-
// Handle the case where the batches_list does not exist
3432
return [];
3533
}
3634

37-
$allBatchIds = Redis::lrange( 'batches_list', 0, -1 );
38-
$batches = [];
35+
$totalBatches = Redis::llen( 'batches_list' );
3936

40-
foreach ( $allBatchIds as $batchId ) {
41-
$data = Redis::get( "batch:$batchId" );
42-
if ( $data !== false ) {
43-
$batchData = unserialize( $data );
44-
$batches[$batchId] = $batchData;
37+
// Determine the starting index
38+
$startIndex = 0;
39+
if ( $before !== null ) {
40+
// Fetch a range of batch IDs surrounding the 'before' value
41+
// Adjust the range size as needed for efficiency
42+
$rangeSize = 100;
43+
$allBatchIds = Redis::lrange( 'batches_list', 0, $rangeSize - 1 );
44+
$beforeIndex = array_search( $before, $allBatchIds );
45+
46+
if ( $beforeIndex !== false ) {
47+
$startIndex = $beforeIndex + 1;
48+
} else {
49+
// TODO:
50+
// Handle case where 'before' is not found in the initial range
51+
// Consider fetching additional ranges or handling this scenario differently
4552
}
4653
}
4754

48-
// Sort batches by 'created_at' in descending order
49-
uasort( $batches, function ( $a, $b ) {
50-
return $b['created_at'] <=> $a['created_at'];
51-
} );
55+
// Calculate the range to fetch
56+
$rangeEnd = min( $startIndex + $limit - 1, $totalBatches - 1 );
5257

53-
// If 'before' is specified, find the start position
54-
$startIndex = 0;
55-
if ( $before !== null ) {
56-
$startIndex = array_search( $before, array_keys( $batches ) );
57-
$startIndex = $startIndex === false ? 0 : $startIndex + 1;
58-
}
58+
// Fetch only the required batch IDs
59+
$batchIds = Redis::lrange( 'batches_list', $startIndex, $rangeEnd );
5960

60-
// Slice the array to apply limit and offset
61-
$batches = array_slice( $batches, $startIndex, $limit, true );
61+
// Use Redis pipeline to bulk fetch batch data
62+
$responses = Redis::pipeline( function ( $pipe ) use ( $batchIds ) {
63+
foreach ( $batchIds as $batchId ) {
64+
$pipe->get( "batch:$batchId" );
65+
}
66+
} );
6267

63-
return array_map( function ( $batchId ) {
64-
return $this->find( $batchId );
65-
}, array_keys( $batches ) );
66-
}
68+
// Filter, unserialize, and map to Batch objects
69+
$batches = array_map( function ( $response ) {
70+
return $response ? $this->toBatch( json_decode( $response, true ) ) : null;
71+
}, $responses );
6772

73+
return array_filter( $batches );
74+
}
6875

6976
public function find( string $batchId )
7077
{
@@ -75,7 +82,7 @@ public function find( string $batchId )
7582
return null;
7683
}
7784

78-
$batchData = unserialize( $data );
85+
$batchData = json_decode( $data, true );
7986
return $this->toBatch( $batchData );
8087
}
8188

@@ -96,7 +103,7 @@ public function store( PendingBatch $batch )
96103
'finished_at' => null,
97104
];
98105

99-
Redis::set( "batch:$id", serialize( $batchData ) );
106+
Redis::set( "batch:$id", json_encode( $batchData ) );
100107
Redis::rpush( 'batches_list', $id ); // Add the batch ID to the list
101108

102109
return $this->find( $id );
@@ -110,15 +117,14 @@ public function incrementTotalJobs( string $batchId, int $amount )
110117
Log::error( "Batch not found for incrementTotalJobs: " . $batchId );
111118
return new UpdatedBatchJobCounts( 0, 0 );
112119
}
113-
$batchData = unserialize( $data );
114-
$batchData['total_jobs'] += $amount;
120+
$batchData = json_decode( $data, true );
121+
$batchData['total_jobs'] += $amount;
115122
$batchData['pending_jobs'] += $amount;
116-
Redis::set( "batch:$batchId", serialize( $batchData ) );
123+
Redis::set( "batch:$batchId", json_encode( $batchData ) );
117124
return new UpdatedBatchJobCounts( $batchData['pending_jobs'], $batchData['failed_jobs'] );
118-
}, 100, 200 );
125+
}, 100, 200 );
119126
}
120127

121-
122128
protected function acquireLock( string $key ): bool
123129
{
124130
$isAcquired = Redis::set( $key, true, 'EX', $this->lockTimeout, 'NX' );
@@ -132,10 +138,10 @@ protected function executeWithLock( string $lockKey, Closure $callback, $retryCo
132138
if ( $this->acquireLock( $lockKey ) ) {
133139
try {
134140
if ( $attempts > 2 ) {
135-
// Log::info( "Finally got lock. Attempt: " . $attempts );
141+
//Log::info( "Finally got lock. Attempt: " . $attempts );
136142
}
137143
return $callback();
138-
} catch ( \Exception $e ) {
144+
} catch ( \Throwable $e ) {
139145
Log::error( "Error in executeWithLock: " . $e->getMessage() );
140146
throw $e;
141147
} finally {
@@ -167,15 +173,14 @@ public function incrementFailedJobs( string $batchId, string $jobId )
167173
Log::error( "Batch not found for incrementFailedJobs: " . $batchId );
168174
return new UpdatedBatchJobCounts( 0, 0 );
169175
}
170-
$batchData = unserialize( $data );
176+
$batchData = json_decode( $data, true );
171177
$batchData['failed_jobs']++;
172178
$batchData['failed_job_ids'][] = $jobId;
173-
Redis::set( "batch:$batchId", serialize( $batchData ) );
179+
Redis::set( "batch:$batchId", json_encode( $batchData ) );
174180
return new UpdatedBatchJobCounts( $batchData['pending_jobs'], $batchData['failed_jobs'] );
175-
}, 100, 200 );
181+
}, 100, 200 );
176182
}
177183

178-
179184
public function decrementPendingJobs( string $batchId, string $jobId )
180185
{
181186
return $this->executeWithLock( "lock:batch:$batchId", function () use ( $batchId, $jobId ) {
@@ -184,11 +189,11 @@ public function decrementPendingJobs( string $batchId, string $jobId )
184189
Log::error( "Batch not found for decrementPendingJobs: " . $batchId );
185190
return new UpdatedBatchJobCounts( 0, 0 );
186191
}
187-
$batchData = unserialize( $data );
192+
$batchData = json_decode( $data, true );
188193
$batchData['pending_jobs']--;
189-
Redis::set( "batch:$batchId", serialize( $batchData ) );
194+
Redis::set( "batch:$batchId", json_encode( $batchData ) );
190195
return new UpdatedBatchJobCounts( $batchData['pending_jobs'], $batchData['failed_jobs'] );
191-
}, 100, 200 );
196+
}, 100, 200 );
192197
}
193198

194199

@@ -202,13 +207,13 @@ public function markAsFinished( string $batchId )
202207
return;
203208
}
204209

205-
$batchData = unserialize( $data );
210+
$batchData = json_decode( $data, true );
206211
// Convert finished_at to a Unix timestamp before storing
207212
$batchData['finished_at'] = CarbonImmutable::now()->getTimestamp();
208-
Redis::set( "batch:$batchId", serialize( $batchData ) );
213+
Redis::set( "batch:$batchId", json_encode( $batchData ) );
209214

210-
//Log::debug( "Batch marked as finished: " . $batchId . " with finished_at: " . $batchData['finished_at'] );
211-
}, 100, 200 );
215+
Log::debug( "Batch marked as finished: " . $batchId . " with finished_at: " . $batchData['finished_at'] );
216+
}, 100, 200 );
212217
}
213218

214219

@@ -257,21 +262,18 @@ public function cancel( string $batchId )
257262
if ( $data === false ) {
258263
return;
259264
}
260-
$batchData = unserialize( $data );
265+
$batchData = json_decode( $data, true );
261266
// Convert cancelled_at to a Unix timestamp before storing
262267
$batchData['cancelled_at'] = CarbonImmutable::now()->getTimestamp();
263-
Redis::set( "batch:$batchId", serialize( $batchData ) );
264-
}, 100, 200 ); // Retry 100 times with 200 milliseconds between retries
268+
Redis::set( "batch:$batchId", json_encode( $batchData ) );
269+
}, 100, 200 ); // Retry 100 times with 200 milliseconds between retries
265270
}
266271

267-
268272
public function transaction( Closure $callback )
269273
{
270274
return $callback();
271275
}
272276

273-
274-
// Work in progress
275277
public function prune( DateTimeInterface $before )
276278
{
277279
return $this->pruneBatches( $before, true );
@@ -289,7 +291,7 @@ public function pruneCancelled( DateTimeInterface $before )
289291

290292
protected function pruneBatches( DateTimeInterface $before, $isFinished = null, $isCancelled = false )
291293
{
292-
$batchIds = Redis::lrange( 'batches_list', 0, -1 );
294+
$batchIds = Redis::lrange( 'batches_list', 0, -1 );
293295
$totalDeleted = 0;
294296

295297
foreach ( $batchIds as $batchId ) {
@@ -300,12 +302,12 @@ protected function pruneBatches( DateTimeInterface $before, $isFinished = null,
300302
continue;
301303
}
302304

303-
$batchData = unserialize( $data );
305+
$batchData = json_decode( $data, true );
304306

305307
$shouldBeDeleted = false;
306308

307-
$createdAt = CarbonImmutable::createFromTimestamp( $batchData['created_at'] );
308-
$finishedAt = isset( $batchData['finished_at'] ) ? CarbonImmutable::createFromTimestamp( $batchData['finished_at'] ) : null;
309+
$createdAt = CarbonImmutable::createFromTimestamp( $batchData['created_at'] );
310+
$finishedAt = isset( $batchData['finished_at'] ) ? CarbonImmutable::createFromTimestamp( $batchData['finished_at'] ) : null;
309311
$cancelledAt = isset( $batchData['cancelled_at'] ) ? CarbonImmutable::createFromTimestamp( $batchData['cancelled_at'] ) : null;
310312

311313
if ( $isFinished === true && $finishedAt && $finishedAt < $before ) {
@@ -316,7 +318,6 @@ protected function pruneBatches( DateTimeInterface $before, $isFinished = null,
316318
$shouldBeDeleted = true;
317319
}
318320

319-
320321
if ( $shouldBeDeleted ) {
321322
Redis::del( "batch:$batchId" );
322323
Redis::lrem( 'batches_list', 0, $batchId );
@@ -326,6 +327,4 @@ protected function pruneBatches( DateTimeInterface $before, $isFinished = null,
326327

327328
return $totalDeleted;
328329
}
329-
330-
331-
}
330+
}

0 commit comments

Comments
 (0)