diff --git a/.env.example b/.env.example index 93f186c2..1cbfb4b2 100644 --- a/.env.example +++ b/.env.example @@ -77,3 +77,6 @@ STRIPE_WEBHOOK_SECRET= SUBSCRIPTIONS_ENABLED=false STRIPE_PRO_MONTHLY_PRICE_ID= STRIPE_PRO_YEARLY_PRICE_ID= + +# Horizon Allowed Emails (comma-separated list of emails allowed to access Horizon in production) +HORIZON_ALLOWED_EMAILS= diff --git a/app/Providers/HorizonServiceProvider.php b/app/Providers/HorizonServiceProvider.php new file mode 100644 index 00000000..1e777fdc --- /dev/null +++ b/app/Providers/HorizonServiceProvider.php @@ -0,0 +1,41 @@ +email, $allowedEmails); + }); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 0ad9c573..63da17d7 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -3,4 +3,5 @@ return [ App\Providers\AppServiceProvider::class, App\Providers\FortifyServiceProvider::class, + App\Providers\HorizonServiceProvider::class, ]; diff --git a/composer.json b/composer.json index d9871fc5..75ab9a00 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "laravel/cashier": "^16.1", "laravel/fortify": "^1.30", "laravel/framework": "^12.0", + "laravel/horizon": "^5.40", "laravel/tinker": "^2.10.1", "laravel/wayfinder": "^0.1.9", "resend/resend-php": "^1.1" diff --git a/composer.lock b/composer.lock index 5d456f88..17851d4b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "40779ce6890f5022e5e7c7c569bd328c", + "content-hash": "c0e2b34e60716f64e9ffb87ec573f103", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1598,6 +1598,85 @@ }, "time": "2025-11-04T15:39:33+00:00" }, + { + "name": "laravel/horizon", + "version": "v5.40.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/horizon.git", + "reference": "005e5638478db9e25f7ae5cfb30c56846fbad793" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/horizon/zipball/005e5638478db9e25f7ae5cfb30c56846fbad793", + "reference": "005e5638478db9e25f7ae5cfb30c56846fbad793", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcntl": "*", + "ext-posix": "*", + "illuminate/contracts": "^9.21|^10.0|^11.0|^12.0", + "illuminate/queue": "^9.21|^10.0|^11.0|^12.0", + "illuminate/support": "^9.21|^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.17|^3.0", + "php": "^8.0", + "ramsey/uuid": "^4.0", + "symfony/console": "^6.0|^7.0", + "symfony/error-handler": "^6.0|^7.0", + "symfony/polyfill-php83": "^1.28", + "symfony/process": "^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.55|^8.36|^9.15|^10.8", + "phpstan/phpstan": "^1.10|^2.0", + "predis/predis": "^1.1|^2.0|^3.0" + }, + "suggest": { + "ext-redis": "Required to use the Redis PHP driver.", + "predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0|^3.0)." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Horizon": "Laravel\\Horizon\\Horizon" + }, + "providers": [ + "Laravel\\Horizon\\HorizonServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Horizon\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Dashboard and code-driven configuration for Laravel queues.", + "keywords": [ + "laravel", + "queue" + ], + "support": { + "issues": "https://github.com/laravel/horizon/issues", + "source": "https://github.com/laravel/horizon/tree/v5.40.2" + }, + "time": "2025-11-28T20:12:12+00:00" + }, { "name": "laravel/prompts", "version": "v0.3.7", diff --git a/config/horizon.php b/config/horizon.php new file mode 100644 index 00000000..0f272708 --- /dev/null +++ b/config/horizon.php @@ -0,0 +1,262 @@ + env('HORIZON_NAME'), + + /* + |-------------------------------------------------------------------------- + | Horizon Domain + |-------------------------------------------------------------------------- + | + | This is the subdomain where Horizon will be accessible from. If this + | setting is null, Horizon will reside under the same domain as the + | application. Otherwise, this value will serve as the subdomain. + | + */ + + 'domain' => env('HORIZON_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Horizon Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Horizon will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('HORIZON_PATH', 'horizon'), + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Connection + |-------------------------------------------------------------------------- + | + | This is the name of the Redis connection where Horizon will store the + | meta information required for it to function. It includes the list + | of supervisors, failed jobs, job metrics, and other information. + | + */ + + 'use' => 'default', + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Prefix + |-------------------------------------------------------------------------- + | + | This prefix will be used when storing all Horizon data in Redis. You + | may modify the prefix when you are running multiple installations + | of Horizon on the same server so that they don't have problems. + | + */ + + 'prefix' => env( + 'HORIZON_PREFIX', + Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:' + ), + + /* + |-------------------------------------------------------------------------- + | Horizon Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will get attached onto each Horizon route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Horizon Basic Auth + |-------------------------------------------------------------------------- + | + | These credentials are used for HTTP Basic Authentication on the Horizon + | dashboard. If these are not set, empty, or null, the page will be + | accessible without authentication. + | + */ + + 'basic_auth_username' => env('HORIZON_BASIC_AUTH_USERNAME'), + 'basic_auth_password' => env('HORIZON_BASIC_AUTH_PASSWORD'), + + /* + |-------------------------------------------------------------------------- + | Queue Wait Time Thresholds + |-------------------------------------------------------------------------- + | + | This option allows you to configure when the LongWaitDetected event + | will be fired. Every connection / queue combination may have its + | own, unique threshold (in seconds) before this event is fired. + | + */ + + 'waits' => [ + 'redis:default' => 60, + ], + + /* + |-------------------------------------------------------------------------- + | Job Trimming Times + |-------------------------------------------------------------------------- + | + | Here you can configure for how long (in minutes) you desire Horizon to + | persist the recent and failed jobs. Typically, recent jobs are kept + | for one hour while all failed jobs are stored for an entire week. + | + */ + + 'trim' => [ + 'recent' => 60, + 'pending' => 60, + 'completed' => 60, + 'recent_failed' => 10080, + 'failed' => 10080, + 'monitored' => 10080, + ], + + /* + |-------------------------------------------------------------------------- + | Silenced Jobs + |-------------------------------------------------------------------------- + | + | Silencing a job will instruct Horizon to not place the job in the list + | of completed jobs within the Horizon dashboard. This setting may be + | used to fully remove any noisy jobs from the completed jobs list. + | + */ + + 'silenced' => [ + // App\Jobs\ExampleJob::class, + ], + + 'silenced_tags' => [ + // 'notifications', + ], + + /* + |-------------------------------------------------------------------------- + | Metrics + |-------------------------------------------------------------------------- + | + | Here you can configure how many snapshots should be kept to display in + | the metrics graph. This will get used in combination with Horizon's + | `horizon:snapshot` schedule to define how long to retain metrics. + | + */ + + 'metrics' => [ + 'trim_snapshots' => [ + 'job' => 24, + 'queue' => 24, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Fast Termination + |-------------------------------------------------------------------------- + | + | When this option is enabled, Horizon's "terminate" command will not + | wait on all of the workers to terminate unless the --wait option + | is provided. Fast termination can shorten deployment delay by + | allowing a new instance of Horizon to start while the last + | instance will continue to terminate each of its workers. + | + */ + + 'fast_termination' => false, + + /* + |-------------------------------------------------------------------------- + | Memory Limit (MB) + |-------------------------------------------------------------------------- + | + | This value describes the maximum amount of memory the Horizon master + | supervisor may consume before it is terminated and restarted. For + | configuring these limits on your workers, see the next section. + | + */ + + 'memory_limit' => 64, + + /* + |-------------------------------------------------------------------------- + | Queue Worker Configuration + |-------------------------------------------------------------------------- + | + | Here you may define the queue worker settings used by your application + | in all environments. These supervisors and settings handle all your + | queued jobs and will be provisioned by Horizon during deployment. + | + */ + + 'defaults' => [ + 'supervisor-default' => [ + 'connection' => 'redis', + 'queue' => ['default'], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'maxProcesses' => 1, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 128, + 'tries' => 1, + 'timeout' => 60, + 'nice' => 0, + ], + 'supervisor-emails' => [ + 'connection' => 'redis', + 'queue' => ['emails'], + 'balance' => 'simple', + 'maxProcesses' => 2, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 128, + 'tries' => 3, + 'timeout' => 60, + 'nice' => 0, + ], + ], + + 'environments' => [ + 'production' => [ + 'supervisor-default' => [ + 'maxProcesses' => 10, + 'balanceMaxShift' => 1, + 'balanceCooldown' => 3, + ], + 'supervisor-emails' => [ + 'maxProcesses' => 3, + ], + ], + + 'local' => [ + 'supervisor-default' => [ + 'maxProcesses' => 3, + ], + 'supervisor-emails' => [ + 'maxProcesses' => 2, + ], + ], + ], +]; diff --git a/config/queue.php b/config/queue.php index 4fe599aa..79c2c0a2 100644 --- a/config/queue.php +++ b/config/queue.php @@ -41,7 +41,7 @@ return [ 'table' => env('DB_QUEUE_TABLE', 'jobs'), 'queue' => env('DB_QUEUE', 'default'), 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), - 'after_commit' => true, + 'after_commit' => false, ], 'beanstalkd' => [ diff --git a/docker/supervisor/supervisord.conf b/docker/supervisor/supervisord.conf index 0c4874be..c15e17d5 100644 --- a/docker/supervisor/supervisord.conf +++ b/docker/supervisor/supervisord.conf @@ -39,13 +39,6 @@ autorestart=true stdout_logfile=/var/log/memcached.log stderr_logfile=/var/log/memcached.log -[program:redis] -command=redis-server --bind 127.0.0.1 --port 6379 --daemonize no -autostart=true -autorestart=true -stdout_logfile=/var/log/redis.log -stderr_logfile=/var/log/redis.log - [program:inertia-ssr] process_name=%(program_name)s_%(process_num)02d command=bash -c 'exec php /app/artisan inertia:start-ssr --runtime=bun' @@ -54,12 +47,11 @@ autorestart=true stderr_logfile=/var/log/worker-inertia-ssr.log stdout_logfile=/var/log/worker-inertia-ssr.log -[program:queue-worker] -process_name=%(program_name)s_%(process_num)02d -command=php /app/artisan queue:work database --queue=emails,default --sleep=3 --tries=3 --max-time=3600 +[program:horizon] +process_name=%(program_name)s +command=php /app/artisan horizon autostart=true autorestart=true redirect_stderr=true -stdout_logfile=/var/log/queue-worker.log +stdout_logfile=/var/log/horizon.log stopwaitsecs=3600 -numprocs=2