Skip to content

Commit 206da23

Browse files
authored
Merge pull request #7 from nsumbadze/chore/sync-horizon-5.x
chore(upstream): sync Laravel Horizon 5.x
2 parents b62d5a2 + 56d6df2 commit 206da23

10 files changed

Lines changed: 126 additions & 17 deletions

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# Release Notes
1+
# Upstream Laravel Horizon Release Notes
2+
3+
This file preserves Laravel Horizon release history imported into HorizonFlow. The versions below are official upstream Laravel Horizon versions, not HorizonFlow releases. HorizonFlow uses independent release numbers; synchronization state is recorded in [UPSTREAM.md](UPSTREAM.md).
24

35
## [Unreleased](https://github.com/laravel/horizon/compare/v5.48.1...5.x)
46

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ composer remove laravel/horizon --no-update
4141
composer require nsumbadze/horizonflow --with-all-dependencies
4242
```
4343

44-
The fork intentionally retains the `Laravel\Horizon` PHP namespace, service providers, Artisan commands, configuration shape, dashboard routes, and Redis data conventions. Composer declares that HorizonFlow replaces `laravel/horizon`, preventing both implementations from being installed together. Review [UPGRADE.md](UPGRADE.md) and test the change in a non-production environment before deployment; HorizonFlow uses its own versioning and does not claim the same version numbers as upstream Horizon.
44+
The fork intentionally retains the `Laravel\Horizon` PHP namespace, service providers, Artisan commands, configuration shape, dashboard routes, and Redis data conventions. Composer declares that HorizonFlow replaces the Laravel Horizon `5.x` line, preventing both implementations from being installed together. Review [UPGRADE.md](UPGRADE.md) and test the change in a non-production environment before deployment; HorizonFlow has its own releases and version numbers and does not claim the same versions as upstream Horizon.
4545

4646
### Compatibility
4747

@@ -200,7 +200,7 @@ php artisan horizonxflow:demo-jobs --clear
200200

201201
HorizonFlow is derived from Laravel Horizon and keeps its existing dashboard, queue supervision, metrics, and worker configuration. Refer to the [Laravel Horizon documentation](https://laravel.com/docs/horizon) for inherited Horizon behaviour.
202202

203-
Laravel Horizon was created by Taylor Otwell and is maintained by Laravel and its contributors. HorizonFlow retains Laravel Horizon's original copyright and license notices. Issues caused by HorizonFlow changes should be reported in this repository; upstream Laravel Horizon has its own issue tracker and release process.
203+
Laravel Horizon was created by Taylor Otwell and is maintained by Laravel and its contributors. HorizonFlow retains Laravel Horizon's original copyright and license notices. Issues caused by HorizonFlow changes should be reported in this repository; bugs that also exist in unmodified Laravel Horizon may belong in the upstream issue tracker. Upstream Laravel Horizon has its own release process.
204204

205205
## Contributing
206206

UPSTREAM.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Upstream Synchronization
2+
3+
HorizonFlow is an independently maintained fork of Laravel Horizon.
4+
5+
- Upstream repository: https://github.com/laravel/horizon
6+
- Upstream branch: `5.x`
7+
- Imported/synchronized baseline: `60e9d1369458762c55be6167bbf31930406ac3c9`
8+
- Latest synchronized upstream commit: `cbb4d2e1e28926e8e8a2a649937eb185415f4b09`
9+
- Synchronization method: reviewed patch range with applicable upstream commits cherry-picked
10+
- Last synchronization date: 2026-08-06
11+
12+
## Checking for updates
13+
14+
```bash
15+
git fetch upstream 5.x
16+
git log --oneline cbb4d2e1e28926e8e8a2a649937eb185415f4b09..upstream/5.x
17+
git diff --stat cbb4d2e1e28926e8e8a2a649937eb185415f4b09..upstream/5.x
18+
```
19+
20+
Upstream changes must be reviewed and combined with HorizonFlow-specific modifications rather than applied blindly.

composer.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
"homepage": "https://github.com/nsumbadze/horizonflow",
77
"license": "MIT",
88
"authors": [
9+
{
10+
"name": "Nika Sumbadze",
11+
"role": "Maintainer"
12+
},
913
{
1014
"name": "Taylor Otwell",
1115
"role": "Original Author"
1216
}
1317
],
1418
"support": {
1519
"issues": "https://github.com/nsumbadze/horizonflow/issues",
16-
"security": "https://github.com/nsumbadze/horizonflow/security/advisories/new",
20+
"security": "https://github.com/nsumbadze/horizonflow/security/policy",
1721
"source": "https://github.com/nsumbadze/horizonflow"
1822
},
1923
"require": {
@@ -39,7 +43,7 @@
3943
"predis/predis": "^1.1|^2.0|^3.0"
4044
},
4145
"replace": {
42-
"laravel/horizon": "self.version"
46+
"laravel/horizon": "^5.0"
4347
},
4448
"suggest": {
4549
"ext-redis": "Required to use the Redis PHP driver.",

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/RedisQueue.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Laravel\Horizon;
44

55
use Illuminate\Contracts\Events\Dispatcher;
6+
use Illuminate\Foundation\Application;
67
use Illuminate\Queue\RedisQueue as BaseQueue;
78
use Illuminate\Support\Str;
89
use Laravel\Horizon\Contracts\JobControlRepository;
@@ -113,7 +114,11 @@ protected function createPayloadArray($job, $queue, $data = '')
113114
#[\Override]
114115
public function later($delay, $job, $data = '', $queue = null)
115116
{
116-
$payload = (new JobPayload($this->createPayload($job, $queue, $data, $delay)))->prepare($job)->value;
117+
$args = version_compare(Application::VERSION, '12.11.0', '>=')
118+
? [$job, $queue, $data, $delay]
119+
: [$job, $queue, $data];
120+
121+
$payload = (new JobPayload($this->createPayload(...$args)))->prepare($job)->value;
117122

118123
if (method_exists($this, 'enqueueUsing')) {
119124
return $this->enqueueUsing(

src/Repositories/RedisMetricsRepository.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Carbon\CarbonImmutable;
66
use Illuminate\Contracts\Redis\Factory as RedisFactory;
7+
use Illuminate\Redis\Connections\PhpRedisConnection;
78
use Illuminate\Support\Str;
89
use Laravel\Horizon\Contracts\MetricsRepository;
910
use Laravel\Horizon\Lock;
@@ -397,12 +398,20 @@ public function clear()
397398
$this->forget('measured_queues');
398399
$this->forget('metrics:snapshot');
399400

401+
$connection = $this->connection();
402+
403+
// phpredis 6.1+ requires the SCAN cursor to start as null, while predis and older phpredis expect "0"...
404+
$defaultCursorValue = match (true) {
405+
$connection instanceof PhpRedisConnection && version_compare(phpversion('redis'), '6.1.0', '>=') => null,
406+
default => '0',
407+
};
408+
400409
foreach (['queue:*', 'job:*', 'snapshot:*'] as $pattern) {
401-
$cursor = null;
410+
$cursor = $defaultCursorValue;
402411

403412
do {
404-
$scanResult = $this->connection()->scan(
405-
$cursor ?? 0, ['match' => $this->snapshotPatternToMatch($pattern)]
413+
$scanResult = $connection->scan(
414+
$cursor, ['match' => $this->snapshotPatternToMatch($pattern)]
406415
);
407416

408417
if (! is_array($scanResult)) {
@@ -414,7 +423,7 @@ public function clear()
414423
foreach ($keys ?? [] as $key) {
415424
$this->forget(Str::after($key, config('horizon.prefix')));
416425
}
417-
} while ($cursor > 0);
426+
} while (((string) $cursor) !== $defaultCursorValue);
418427
}
419428
}
420429

tests/Feature/MetricsTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,33 @@ public function test_only_past_24_snapshots_are_retained()
265265

266266
CarbonImmutable::setTestNow();
267267
}
268+
269+
public function test_metrics_can_be_cleared()
270+
{
271+
if (getenv('REDIS_CLUSTER_HOSTS_AND_PORTS')) {
272+
$this->markTestSkipped('Test is for standalone Redis connections.');
273+
}
274+
275+
Queue::push(new Jobs\BasicJob);
276+
$this->work();
277+
278+
resolve(MetricsRepository::class)->snapshot();
279+
280+
// Work another job so live "job:*" and "queue:*" hashes exist alongside the snapshots...
281+
Queue::push(new Jobs\BasicJob);
282+
$this->work();
283+
284+
$this->assertNotEmpty(resolve(MetricsRepository::class)->measuredJobs());
285+
$this->assertNotEmpty(resolve(MetricsRepository::class)->snapshotsForJob(Jobs\BasicJob::class));
286+
$this->assertSame(1, resolve(MetricsRepository::class)->throughputForJob(Jobs\BasicJob::class));
287+
288+
resolve(MetricsRepository::class)->clear();
289+
290+
$this->assertEmpty(resolve(MetricsRepository::class)->measuredJobs());
291+
$this->assertEmpty(resolve(MetricsRepository::class)->measuredQueues());
292+
$this->assertEmpty(resolve(MetricsRepository::class)->snapshotsForJob(Jobs\BasicJob::class));
293+
$this->assertEmpty(resolve(MetricsRepository::class)->snapshotsForQueue('default'));
294+
$this->assertSame(0, resolve(MetricsRepository::class)->throughputForJob(Jobs\BasicJob::class));
295+
$this->assertSame(0, resolve(MetricsRepository::class)->throughputForQueue('default'));
296+
}
268297
}

tests/Feature/QueueProcessingTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Laravel\Horizon\Tests\Feature;
44

55
use Carbon\CarbonImmutable;
6+
use Illuminate\Foundation\Application;
67
use Illuminate\Support\Facades\Event;
78
use Illuminate\Support\Facades\Queue;
89
use Illuminate\Support\Facades\Redis;
@@ -41,6 +42,18 @@ public function test_pending_delayed_jobs_are_stored_in_pending_job_database()
4142
$this->assertSame('pending', Redis::connection('horizon')->hget($id, 'status'));
4243
}
4344

45+
public function test_pending_delayed_jobs_store_the_delay_in_their_payload()
46+
{
47+
if (version_compare(Application::VERSION, '12.11.0', '<')) {
48+
$this->markTestSkipped('Delay metadata requires Laravel 12.11 or newer.');
49+
}
50+
51+
$id = Queue::later(5, new Jobs\BasicJob);
52+
$payload = json_decode(Redis::connection('horizon')->hget($id, 'payload'), true);
53+
54+
$this->assertSame(5, $payload['delay']);
55+
}
56+
4457
public function test_pending_jobs_are_stored_with_their_tags()
4558
{
4659
$id = Queue::push(new Jobs\BasicJob);

tests/Unit/RedisMetricsRepositoryTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Config\Repository as ConfigRepository;
66
use Illuminate\Container\Container;
77
use Illuminate\Contracts\Redis\Factory as RedisFactory;
8+
use Illuminate\Redis\Connections\PhpRedisConnection;
89
use Laravel\Horizon\Repositories\RedisMetricsRepository;
910
use Laravel\Horizon\Tests\UnitTest;
1011
use Mockery;
@@ -61,6 +62,32 @@ public function test_clear_uses_raw_metric_patterns_when_phpredis_scan_prefix_is
6162
$this->assertTrue(true);
6263
}
6364

65+
public function test_clear_starts_scan_with_null_for_phpredis_6_1_and_newer()
66+
{
67+
if (! extension_loaded('redis') || version_compare((string) phpversion('redis'), '6.1.0', '<')) {
68+
$this->markTestSkipped('phpredis 6.1 or newer is required.');
69+
}
70+
71+
$connection = Mockery::mock(PhpRedisConnection::class);
72+
73+
foreach (['last_snapshot_at', 'measured_jobs', 'measured_queues', 'metrics:snapshot'] as $key) {
74+
$connection->shouldReceive('del')->once()->with($key);
75+
}
76+
77+
foreach (['queue:*', 'job:*', 'snapshot:*'] as $pattern) {
78+
$connection->shouldReceive('scan')
79+
->once()
80+
->with(null, ['match' => 'horizon:'.$pattern])
81+
->andReturn(false);
82+
}
83+
84+
$repository = $this->redisMetricsRepositoryWithConnection($connection);
85+
86+
$repository->clear();
87+
88+
$this->assertTrue(true);
89+
}
90+
6491
public function test_phpredis_scan_prefix_option_is_detected()
6592
{
6693
if (! defined('Redis::SCAN_PREFIX')) {

0 commit comments

Comments
 (0)