Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Release Notes
# Upstream Laravel Horizon Release Notes

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).

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

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ composer remove laravel/horizon --no-update
composer require nsumbadze/horizonflow --with-all-dependencies
```

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.
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.

### Compatibility

Expand Down Expand Up @@ -200,7 +200,7 @@ php artisan horizonxflow:demo-jobs --clear

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.

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.
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.

## Contributing

Expand Down
20 changes: 20 additions & 0 deletions UPSTREAM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Upstream Synchronization

HorizonFlow is an independently maintained fork of Laravel Horizon.

- Upstream repository: https://github.com/laravel/horizon
- Upstream branch: `5.x`
- Imported/synchronized baseline: `60e9d1369458762c55be6167bbf31930406ac3c9`
- Latest synchronized upstream commit: `cbb4d2e1e28926e8e8a2a649937eb185415f4b09`
- Synchronization method: reviewed patch range with applicable upstream commits cherry-picked
- Last synchronization date: 2026-08-06

## Checking for updates

```bash
git fetch upstream 5.x
git log --oneline cbb4d2e1e28926e8e8a2a649937eb185415f4b09..upstream/5.x
git diff --stat cbb4d2e1e28926e8e8a2a649937eb185415f4b09..upstream/5.x
```

Upstream changes must be reviewed and combined with HorizonFlow-specific modifications rather than applied blindly.
8 changes: 6 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@
"homepage": "https://github.com/nsumbadze/horizonflow",
"license": "MIT",
"authors": [
{
"name": "Nika Sumbadze",
"role": "Maintainer"
},
{
"name": "Taylor Otwell",
"role": "Original Author"
}
],
"support": {
"issues": "https://github.com/nsumbadze/horizonflow/issues",
"security": "https://github.com/nsumbadze/horizonflow/security/advisories/new",
"security": "https://github.com/nsumbadze/horizonflow/security/policy",
"source": "https://github.com/nsumbadze/horizonflow"
},
"require": {
Expand All @@ -39,7 +43,7 @@
"predis/predis": "^1.1|^2.0|^3.0"
},
"replace": {
"laravel/horizon": "self.version"
"laravel/horizon": "^5.0"
},
"suggest": {
"ext-redis": "Required to use the Redis PHP driver.",
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion src/RedisQueue.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Laravel\Horizon;

use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Foundation\Application;
use Illuminate\Queue\RedisQueue as BaseQueue;
use Illuminate\Support\Str;
use Laravel\Horizon\Contracts\JobControlRepository;
Expand Down Expand Up @@ -113,7 +114,11 @@ protected function createPayloadArray($job, $queue, $data = '')
#[\Override]
public function later($delay, $job, $data = '', $queue = null)
{
$payload = (new JobPayload($this->createPayload($job, $queue, $data, $delay)))->prepare($job)->value;
$args = version_compare(Application::VERSION, '12.11.0', '>=')
? [$job, $queue, $data, $delay]
: [$job, $queue, $data];

$payload = (new JobPayload($this->createPayload(...$args)))->prepare($job)->value;

if (method_exists($this, 'enqueueUsing')) {
return $this->enqueueUsing(
Expand Down
17 changes: 13 additions & 4 deletions src/Repositories/RedisMetricsRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Carbon\CarbonImmutable;
use Illuminate\Contracts\Redis\Factory as RedisFactory;
use Illuminate\Redis\Connections\PhpRedisConnection;
use Illuminate\Support\Str;
use Laravel\Horizon\Contracts\MetricsRepository;
use Laravel\Horizon\Lock;
Expand Down Expand Up @@ -397,12 +398,20 @@ public function clear()
$this->forget('measured_queues');
$this->forget('metrics:snapshot');

$connection = $this->connection();

// phpredis 6.1+ requires the SCAN cursor to start as null, while predis and older phpredis expect "0"...
$defaultCursorValue = match (true) {
$connection instanceof PhpRedisConnection && version_compare(phpversion('redis'), '6.1.0', '>=') => null,
default => '0',
};

foreach (['queue:*', 'job:*', 'snapshot:*'] as $pattern) {
$cursor = null;
$cursor = $defaultCursorValue;

do {
$scanResult = $this->connection()->scan(
$cursor ?? 0, ['match' => $this->snapshotPatternToMatch($pattern)]
$scanResult = $connection->scan(
$cursor, ['match' => $this->snapshotPatternToMatch($pattern)]
);

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

Expand Down
29 changes: 29 additions & 0 deletions tests/Feature/MetricsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,33 @@ public function test_only_past_24_snapshots_are_retained()

CarbonImmutable::setTestNow();
}

public function test_metrics_can_be_cleared()
{
if (getenv('REDIS_CLUSTER_HOSTS_AND_PORTS')) {
$this->markTestSkipped('Test is for standalone Redis connections.');
}

Queue::push(new Jobs\BasicJob);
$this->work();

resolve(MetricsRepository::class)->snapshot();

// Work another job so live "job:*" and "queue:*" hashes exist alongside the snapshots...
Queue::push(new Jobs\BasicJob);
$this->work();

$this->assertNotEmpty(resolve(MetricsRepository::class)->measuredJobs());
$this->assertNotEmpty(resolve(MetricsRepository::class)->snapshotsForJob(Jobs\BasicJob::class));
$this->assertSame(1, resolve(MetricsRepository::class)->throughputForJob(Jobs\BasicJob::class));

resolve(MetricsRepository::class)->clear();

$this->assertEmpty(resolve(MetricsRepository::class)->measuredJobs());
$this->assertEmpty(resolve(MetricsRepository::class)->measuredQueues());
$this->assertEmpty(resolve(MetricsRepository::class)->snapshotsForJob(Jobs\BasicJob::class));
$this->assertEmpty(resolve(MetricsRepository::class)->snapshotsForQueue('default'));
$this->assertSame(0, resolve(MetricsRepository::class)->throughputForJob(Jobs\BasicJob::class));
$this->assertSame(0, resolve(MetricsRepository::class)->throughputForQueue('default'));
}
}
13 changes: 13 additions & 0 deletions tests/Feature/QueueProcessingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Laravel\Horizon\Tests\Feature;

use Carbon\CarbonImmutable;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Redis;
Expand Down Expand Up @@ -41,6 +42,18 @@ public function test_pending_delayed_jobs_are_stored_in_pending_job_database()
$this->assertSame('pending', Redis::connection('horizon')->hget($id, 'status'));
}

public function test_pending_delayed_jobs_store_the_delay_in_their_payload()
{
if (version_compare(Application::VERSION, '12.11.0', '<')) {
$this->markTestSkipped('Delay metadata requires Laravel 12.11 or newer.');
}

$id = Queue::later(5, new Jobs\BasicJob);
$payload = json_decode(Redis::connection('horizon')->hget($id, 'payload'), true);

$this->assertSame(5, $payload['delay']);
}

public function test_pending_jobs_are_stored_with_their_tags()
{
$id = Queue::push(new Jobs\BasicJob);
Expand Down
27 changes: 27 additions & 0 deletions tests/Unit/RedisMetricsRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Container\Container;
use Illuminate\Contracts\Redis\Factory as RedisFactory;
use Illuminate\Redis\Connections\PhpRedisConnection;
use Laravel\Horizon\Repositories\RedisMetricsRepository;
use Laravel\Horizon\Tests\UnitTest;
use Mockery;
Expand Down Expand Up @@ -61,6 +62,32 @@ public function test_clear_uses_raw_metric_patterns_when_phpredis_scan_prefix_is
$this->assertTrue(true);
}

public function test_clear_starts_scan_with_null_for_phpredis_6_1_and_newer()
{
if (! extension_loaded('redis') || version_compare((string) phpversion('redis'), '6.1.0', '<')) {
$this->markTestSkipped('phpredis 6.1 or newer is required.');
}

$connection = Mockery::mock(PhpRedisConnection::class);

foreach (['last_snapshot_at', 'measured_jobs', 'measured_queues', 'metrics:snapshot'] as $key) {
$connection->shouldReceive('del')->once()->with($key);
}

foreach (['queue:*', 'job:*', 'snapshot:*'] as $pattern) {
$connection->shouldReceive('scan')
->once()
->with(null, ['match' => 'horizon:'.$pattern])
->andReturn(false);
}

$repository = $this->redisMetricsRepositoryWithConnection($connection);

$repository->clear();

$this->assertTrue(true);
}

public function test_phpredis_scan_prefix_option_is_detected()
{
if (! defined('Redis::SCAN_PREFIX')) {
Expand Down
Loading