Open Source Developer

Yan Gusik

Building tools for

~/projects
$ composer require yangusik/balanced-queue
✓ Package installed — fair job distribution enabled
$ phppad --connect ssh://prod-server
✓ Connected · PHP 8.2 · Laravel 11 · LSP ready
$ vendor/bin/queue-inspector --strict
✗ 1 error found — exit code 1
$ watchdog --config=/etc/watchdog.yml
✓ Watching 4 PHP workers via /proc
$

Projects

Laravel Balanced Queue

A Laravel package for queue management with load balancing between partitions (user groups). Perfect for scenarios where you need fair job distribution and concurrency control per user/tenant.

User A
User B
User C
Queue
A
A
A
A
B
C
C
W1
W2
⚠ User A floods the queue. B and C are starved.

Installation

bash
composer require yangusik/balanced-queue
bash
php artisan vendor:publish --tag="balanced-queue-config"
Redis-based — high-performance, low-latency queue management
🔄
Laravel Horizon — experimental dashboard integration included

Configuration

config/balanced-queue.php
return [
    'connection' => env('BALANCED_QUEUE_CONNECTION', 'redis'),

    // Strategy: 'round-robin' | 'random' | 'smart'
    'strategy' => 'round-robin',

    // Max concurrent jobs per partition
    'limiter' => [
        'driver' => 'simple', // 'simple' | 'adaptive' | 'none'
        'max_concurrent' => 2,
    ],

    'queue' => env('BALANCED_QUEUE_NAME', 'default'),
];

Usage

app/Jobs/ProcessReport.php
use YanGusik\BalancedQueue\Traits\HasBalancedQueue;

class ProcessReport implements ShouldQueue
{
    use HasBalancedQueue;

    public function __construct(
        private User $user,
        private Report $report,
    ) {}

    // Partition key — one partition per user
    public function partitionKey(): string
    {
        return 'user:' . $this->user->id;
    }
}
Dispatching
// Dispatch normally — balancing is automatic
ProcessReport::dispatch($user, $report);
bash — Monitoring
php artisan balanced-queue:table  # Show live stats
php artisan balanced-queue:clear  # Clear all partitions

Selection Strategies

Smart

Prioritizes partitions with fewer jobs. Helps clear smaller queues faster but may not be perfectly fair.

strategy: 'smart'
Random

Randomly selects the next partition. Simple, low overhead, statistically fair over time.

strategy: 'random'

Laravel Queue Inspector

Static analyzer for Laravel queue job configurations. Catches misconfigurations before they cause duplicate execution, memory leaks, or silent failures — zero invasiveness, no changes to your jobs required.

php artisan queue:analyze -v
$ php artisan queue:analyze -v
Scanning 12 jobs...
App\Jobs\ProcessReportJob
  ✗ timeout 90s ≥ retry_after 60s — job may execute twice
    · timeout: job $timeout · retry_after: config/queue.php
  ⚠ tries=5 without backoff — all retries fire immediately
App\Jobs\SendEmailJob
  ⚠ HTTP client without timeout — job may hang indefinitely
    · best-effort: only new Client() in handle() is checked
App\Jobs\ImportDataJob
  ✓ all checks passed
Found 1 error, 2 warnings in 12 jobs

Installation

bash
composer require yangusik/laravel-queue-inspector
bash — Artisan
php artisan queue:analyze        # text output
php artisan queue:analyze -v     # verbose: show value sources
php artisan queue:analyze --format=json  # JSON for piping
bash — Standalone binary (no Laravel bootstrap)
vendor/bin/queue-inspector --path=/var/www --strict
🔍
Zero invasiveness — no extends, no traits, no changes to your jobs
🔗
Cross-source validation — queue.php → horizon.php → job class together

7 Built-in Checks

error
timeout ≥ retry_after — worker re-queues job before it finishes; duplicate execution
warn
No timeout set — no process-level kill switch; job can hang forever
warn
tries > 1, no backoff — all retries fire immediately after failure
warn
WithoutOverlapping without expireAfter() — lock never releases on crash
warn
ShouldBeUnique without uniqueFor() — permanent cache lock on crash
error
sleep total ≥ timeout — literal sleep()/usleep() sum; job cannot complete in time
warn
HTTP client without timeout — Guzzle/Http facade in handle() may hang

CI/CD Integration

bash — exit code 1 on errors
vendor/bin/queue-inspector --strict --path=/var/www/html
.github/workflows/ci.yml
- name: Analyze queue jobs
  run: vendor/bin/queue-inspector --strict --format=json | jq .
bash — exclude namespaces
php artisan queue:analyze \
  --exclude-ns="App\Notifications" \
  --exclude-ns="App\Mail"
🚀
No Laravel bootstrap — standalone binary is safe in CI without DB or Redis

Ignoring Jobs

app/Jobs/LegacyJob.php
/**
 * @deprecated
 * @queue-inspector-ignore
 */
class LegacyJob implements ShouldQueue {}

#[QueueInspectorIgnore]
class AnotherJob implements ShouldQueue {}

// --exclude-ns flag skips entire namespace:
// php artisan queue:analyze --exclude-ns="App\Notifications"
@deprecated or @queue-inspector-ignore — PHPDoc annotation
#[QueueInspectorIgnore] — PHP 8+ attribute
#[Deprecated] — JetBrains PhpStorm / native PHP 8.4
--exclude-ns=App\Notifications — skip entire namespace via CLI

PHP Watchdog

A standalone Go daemon that monitors PHP worker processes from the outside via /proc. No code changes required. Catches OOM kills and memory leaks with full context about which job and payload was running.

🔭 watchdog · reading /proc every 5s
queue:work #1234
120 MB
queue:work #1235
180 MB
queue:work #1236
512 MB
↑ threshold: 500 MB
PID 1236 — RSS 512 MB exceeds threshold · killing process

Build & Install

bash
git clone https://github.com/yangusik/php-watchdog
cd php-watchdog
go build -o watchdog ./cmd/watchdog/
go build -o rss-check ./cmd/rss-check/
cp watchdog /usr/local/bin/watchdog
systemd
systemctl enable watchdog
systemctl start watchdog
👁
Zero code changes — reads /proc/PID/status from outside the process
📊
Real RSS — same number the OOM killer sees, not memory_get_usage()

Configuration

watchdog.yml
interval: 5        # seconds between RSS snapshots
ring_buffer: 60    # snapshots kept per process

socket: /var/run/watchdog.sock  # optional framework socket

watchers:
  - name: "queue-workers"
    mask: "queue:work"
    thresholds:
      rss_absolute_mb: 500
      growth_snapshots: 10
      pool_rss_total_mb: 4096
      pool_kill_strategy: "heaviest"
    on_anomaly:
      kill: true
      dump_path: /var/log/watchdog/
      webhook: ""   # optional HTTP POST
      exec: ""      # optional shell script

Anomaly Detectors

RSS
ThresholdDetector — triggers when a single process RSS exceeds rss_absolute_mb
Trend
TrendDetector — triggers when RSS grows for growth_snapshots consecutive snapshots
Pool
PoolDetector — triggers when total RSS of all matched processes exceeds pool_rss_total_mb
bash — inspect RSS of running workers
./rss-check --filter=queue:work
./rss-check --filter=horizon

Docker Sidecar

docker-compose.yml
services:
  horizon:
    image: your-app-image
    command: php artisan horizon
    volumes:
      - watchdog-socket:/var/run/watchdog

  watchdog:
    image: yangusik/php-watchdog:latest
    volumes:
      - /proc:/proc:ro
      - ./watchdog.yml:/etc/watchdog/watchdog.yml:ro
      - watchdog-socket:/var/run/watchdog
      - watchdog-reports:/var/log/watchdog
    restart: unless-stopped

volumes:
  watchdog-socket:
  watchdog-reports:
🐳
Host /proc mount — one watchdog instance sees all containers on the host
🔌
Laravel middleware — send job context via Unix socket, appears in every report

PHPPad

Interactive PHP REPL for Laravel. Run code on remote servers via SSH or Docker — right from your desktop or inside PhpStorm.

PHPPad — prod-server (SSH)
⚡ LSP
▶ Artisan
📋 Snippets
📜 History
📄 Logs
1$users = User::limit(10)->get(); //?
2
3$active = $users->where('active', true);//?
← Collection(10)
4dump($active->count());
Output · 42ms · PHP 8.2
$users
Collection(10) [▼
User { id: 1, name: "Alice", … },
User { id: 2, name: "Bob", … },
]
SQL Queries (2)
2ms SELECT * FROM `users` LIMIT 10
1ms SELECT COUNT(*) FROM `users` WHERE `active` = 1
🔌
SSH & Docker
Connect to any remote server or Laravel Sail container. Test connection before saving.
Magic Comments
Add //? after any expression to see its value inline — no var_dump() needed.
LSP Autocomplete
Powered by phpactor. Class/method completion with auto-use imports.
🗄
SQL Query Log
Every query captured with execution time and bound parameters.
🎨
8 Themes
Catppuccin, Dracula, Nord, Tokyo Night, Rosé Pine, One Dark, GitHub Light…
📜
Snippets & History
Save reusable code, browse past 100 executions with search.

Magic Comments

PHP
// Capture line value
$users = User::find(36)->id; //?  →  ← 36

// Capture mid-chain (passes original through)
collect([1,2,3])/*?*/->count()/*?*/->map(fn($x) => $x * 2);
// ← [1,2,3]    ← 3

// Timing (ms since script start)
heavyOperation(); /*?.*/  →  ← 142ms