solid_queue_autoscaler 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c25a5d2a7570ea01da30c54a42624674b23d7112bef33da9d82c5ec1257933a5
4
- data.tar.gz: '081a9f3000bcb460bc7254e7597c2f3e0ddd7afd9517f1f213bf1bd3cc2020c7'
3
+ metadata.gz: 2caadbfc7fd3df7b06be9dbea2e3e667b5e8b49e8c302720fb07d56cfb1a7d1c
4
+ data.tar.gz: 0b4c14b56f29e1ed6537ecac97d279661d08d943320df901bf7c73f3a6bd8694
5
5
  SHA512:
6
- metadata.gz: 580ac675beb8175d1c4897bbb251d20ab7475779a8d49d2fabfd2ea800f8f27b079df5c7f627a27e22ee2220a6e8d5fa06411a00c846888c5c456a32cc3bbdc6
7
- data.tar.gz: b1a8f294a803035338f49436f98d479ee8ed39a2f5bdbb7b64e0701a5e00365a05d32e7e7ba8385a89d9dd72ba2c1f09019e9d7ce79e5de3f1e0a2a20ff50180
6
+ metadata.gz: f2b316153846191155d72961c4be0e95b6d6c2dd71aedc32a268dff92bd570736e703cc2a8e1e7be564b9899d407c80959350014a3ea167f0f3472d0224d2109
7
+ data.tar.gz: 4048d221c232b9b079d08f24e571dcc9b56c7c4b3c69d7ce14262e5986568c87c93d07152664667021e981018da79ac581335db82420070c5fcc243bfe626efd
data/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.8] - 2025-01-17
11
+
12
+ ### Added
13
+ - **`job_queue` configuration option** - Configure which queue the AutoscaleJob runs on (default: `:autoscaler`)
14
+ - **`job_priority` configuration option** - Set job priority for AutoscaleJob (lower = higher priority)
15
+ - **Multi-database migration support** - Migration templates now automatically create tables in the same database as Solid Queue
16
+ - **Common Configuration Examples** - New README section with 8 copy-paste ready configurations for different use cases:
17
+ - Simple/Starter setup
18
+ - Cost-Optimized (scale to zero)
19
+ - E-Commerce/SaaS (multiple worker types)
20
+ - High-Volume API (webhook processing with proportional scaling)
21
+ - Data Processing/ETL
22
+ - High-Availability
23
+ - Kubernetes
24
+ - Development/Testing
25
+ - **Expanded Troubleshooting** - 15+ new troubleshooting topics with code examples
26
+ - **Configuration Comparison Table** - Quick reference for common configurations
27
+
28
+ ### Changed
29
+ - AutoscaleJob now uses `queue_as` and `queue_with_priority` blocks for dynamic queue/priority selection
30
+ - Each worker configuration can have its own `job_queue` and `job_priority` settings
31
+
10
32
  ## [1.0.7] - 2025-01-16
11
33
 
12
34
  ### Fixed
data/README.md CHANGED
@@ -240,6 +240,13 @@ Scaling down triggers when **ALL** thresholds are met:
240
240
  | `scale_down_cooldown_seconds` | Integer | `nil` | Override for scale-down cooldown |
241
241
  | `persist_cooldowns` | Boolean | `true` | Save cooldowns to database |
242
242
 
243
+ ### AutoscaleJob Settings
244
+
245
+ | Option | Type | Default | Description |
246
+ |--------|------|---------|-------------|
247
+ | `job_queue` | Symbol/String | `:autoscaler` | Queue for the AutoscaleJob |
248
+ | `job_priority` | Integer | `nil` | Priority for the AutoscaleJob (lower = higher priority) |
249
+
243
250
  ### Heroku-Specific
244
251
 
245
252
  | Option | Type | Default | Description |
@@ -256,6 +263,482 @@ Scaling down triggers when **ALL** thresholds are met:
256
263
  | `kubernetes_deployment` | String | `nil` | Deployment name to scale |
257
264
  | `kubernetes_config_path` | String | `nil` | Path to kubeconfig (optional) |
258
265
 
266
+ ## Common Configuration Examples
267
+
268
+ These examples show typical setups for different use cases. Copy and adapt them to your needs.
269
+
270
+ ### Simple Setup (Single Worker, Heroku)
271
+
272
+ Ideal for small apps, side projects, or getting started:
273
+
274
+ ```ruby
275
+ # config/initializers/solid_queue_autoscaler.rb
276
+ SolidQueueAutoscaler.configure do |config|
277
+ config.adapter = :heroku
278
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
279
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
280
+ config.process_type = 'worker'
281
+
282
+ config.min_workers = 1
283
+ config.max_workers = 5
284
+
285
+ # Scale up when queue backs up
286
+ config.scale_up_queue_depth = 50
287
+ config.scale_up_latency_seconds = 180 # 3 minutes
288
+
289
+ # Scale down when queue is nearly empty
290
+ config.scale_down_queue_depth = 5
291
+ config.scale_down_latency_seconds = 30
292
+
293
+ # Safety: only run in production
294
+ config.dry_run = !Rails.env.production?
295
+ config.enabled = Rails.env.production?
296
+ end
297
+ ```
298
+
299
+ ```yaml
300
+ # config/recurring.yml
301
+ autoscaler:
302
+ class: SolidQueueAutoscaler::AutoscaleJob
303
+ queue: autoscaler
304
+ schedule: every 30 seconds
305
+ ```
306
+
307
+ ---
308
+
309
+ ### Cost-Optimized Setup (Scale to Zero)
310
+
311
+ For apps with sporadic workloads where you want to minimize costs during idle periods:
312
+
313
+ ```ruby
314
+ SolidQueueAutoscaler.configure do |config|
315
+ config.adapter = :heroku
316
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
317
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
318
+ config.process_type = 'worker'
319
+
320
+ # Allow scaling to zero - no workers when idle
321
+ config.min_workers = 0
322
+ config.max_workers = 5
323
+
324
+ # Scale up immediately when any job is queued
325
+ config.scale_up_queue_depth = 1
326
+ config.scale_up_latency_seconds = 60 # 1 minute
327
+
328
+ # Scale down aggressively when empty
329
+ config.scale_down_queue_depth = 0
330
+ config.scale_down_latency_seconds = 10
331
+
332
+ # Shorter cooldowns for faster response
333
+ config.scale_up_cooldown_seconds = 30
334
+ config.scale_down_cooldown_seconds = 300 # 5 min before scaling to zero
335
+
336
+ config.enabled = Rails.env.production?
337
+ end
338
+ ```
339
+
340
+ **⚠️ Note:** With `min_workers = 0`, there's cold-start latency when the first job arrives. The autoscaler must run on a web dyno or separate process, not on the workers themselves.
341
+
342
+ ---
343
+
344
+ ### E-Commerce / SaaS (Multiple Worker Types)
345
+
346
+ For apps with different job priorities (payments, notifications, reports):
347
+
348
+ ```ruby
349
+ # Critical jobs - payments, webhooks, user-facing notifications
350
+ SolidQueueAutoscaler.configure(:critical_worker) do |config|
351
+ config.adapter = :heroku
352
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
353
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
354
+ config.process_type = 'critical_worker'
355
+
356
+ config.queues = ['critical', 'payments', 'webhooks']
357
+
358
+ # Always have capacity, scale aggressively
359
+ config.min_workers = 2
360
+ config.max_workers = 10
361
+ config.scale_up_queue_depth = 5
362
+ config.scale_up_latency_seconds = 30
363
+
364
+ # Short cooldowns for responsiveness
365
+ config.cooldown_seconds = 60
366
+
367
+ # High-priority autoscaler job
368
+ config.job_queue = :autoscaler
369
+ config.job_priority = 0
370
+ end
371
+
372
+ # Default jobs - emails, notifications, analytics
373
+ SolidQueueAutoscaler.configure(:default_worker) do |config|
374
+ config.adapter = :heroku
375
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
376
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
377
+ config.process_type = 'worker'
378
+
379
+ config.queues = ['default', 'mailers', 'analytics']
380
+
381
+ # Standard capacity, moderate scaling
382
+ config.min_workers = 1
383
+ config.max_workers = 8
384
+ config.scale_up_queue_depth = 100
385
+ config.scale_up_latency_seconds = 300
386
+
387
+ config.cooldown_seconds = 120
388
+ end
389
+
390
+ # Batch jobs - reports, exports, data processing
391
+ SolidQueueAutoscaler.configure(:batch_worker) do |config|
392
+ config.adapter = :heroku
393
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
394
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
395
+ config.process_type = 'batch_worker'
396
+
397
+ config.queues = ['batch', 'reports', 'exports']
398
+
399
+ # Scale to zero when no batch jobs, scale up for any batch work
400
+ config.min_workers = 0
401
+ config.max_workers = 3
402
+ config.scale_up_queue_depth = 1
403
+ config.scale_down_queue_depth = 0
404
+
405
+ # Long cooldowns - batch jobs take time
406
+ config.cooldown_seconds = 300
407
+ end
408
+ ```
409
+
410
+ ```yaml
411
+ # config/recurring.yml
412
+ # Scale critical workers frequently
413
+ autoscaler_critical:
414
+ class: SolidQueueAutoscaler::AutoscaleJob
415
+ queue: autoscaler
416
+ schedule: every 15 seconds
417
+ args: [:critical_worker]
418
+
419
+ # Scale default workers normally
420
+ autoscaler_default:
421
+ class: SolidQueueAutoscaler::AutoscaleJob
422
+ queue: autoscaler
423
+ schedule: every 30 seconds
424
+ args: [:default_worker]
425
+
426
+ # Scale batch workers less frequently
427
+ autoscaler_batch:
428
+ class: SolidQueueAutoscaler::AutoscaleJob
429
+ queue: autoscaler
430
+ schedule: every 60 seconds
431
+ args: [:batch_worker]
432
+ ```
433
+
434
+ ---
435
+
436
+ ### High-Volume API (Webhook Processing)
437
+
438
+ For apps processing many incoming webhooks or API callbacks:
439
+
440
+ ```ruby
441
+ SolidQueueAutoscaler.configure do |config|
442
+ config.adapter = :heroku
443
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
444
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
445
+ config.process_type = 'worker'
446
+
447
+ config.queues = ['webhooks', 'callbacks', 'api_jobs']
448
+
449
+ # Maintain baseline capacity
450
+ config.min_workers = 2
451
+ config.max_workers = 20
452
+
453
+ # Proportional scaling - scale based on actual load
454
+ config.scaling_strategy = :proportional
455
+ config.scale_up_queue_depth = 50
456
+ config.scale_up_latency_seconds = 60
457
+
458
+ # Add 1 worker per 25 jobs over threshold
459
+ config.scale_up_jobs_per_worker = 25
460
+ # Add 1 worker per 30 seconds over latency threshold
461
+ config.scale_up_latency_per_worker = 30
462
+
463
+ # Scale down when under capacity
464
+ config.scale_down_queue_depth = 10
465
+ config.scale_down_jobs_per_worker = 50
466
+
467
+ # Fast cooldowns for responsive scaling
468
+ config.scale_up_cooldown_seconds = 30
469
+ config.scale_down_cooldown_seconds = 120
470
+
471
+ config.job_priority = 0 # Process autoscaler jobs first
472
+ end
473
+ ```
474
+
475
+ ---
476
+
477
+ ### Data Processing / ETL Pipeline
478
+
479
+ For apps with heavy data processing, imports, or batch ETL jobs:
480
+
481
+ ```ruby
482
+ SolidQueueAutoscaler.configure(:etl_worker) do |config|
483
+ config.adapter = :heroku
484
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
485
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
486
+ config.process_type = 'etl_worker'
487
+
488
+ config.queues = ['imports', 'exports', 'etl', 'data_sync']
489
+
490
+ # Scale to zero when no work, burst when needed
491
+ config.min_workers = 0
492
+ config.max_workers = 10
493
+
494
+ # Scale up as soon as work is queued
495
+ config.scale_up_queue_depth = 1
496
+ config.scale_up_latency_seconds = 120
497
+
498
+ # Use fixed scaling for predictable behavior
499
+ config.scaling_strategy = :fixed
500
+ config.scale_up_increment = 2 # Add 2 workers at a time
501
+ config.scale_down_decrement = 1
502
+
503
+ # Long cooldowns - ETL jobs are long-running
504
+ config.scale_up_cooldown_seconds = 120
505
+ config.scale_down_cooldown_seconds = 600 # 10 minutes
506
+
507
+ # Scale down only when truly idle
508
+ config.scale_down_queue_depth = 0
509
+ config.scale_down_latency_seconds = 0
510
+ end
511
+ ```
512
+
513
+ ---
514
+
515
+ ### High-Availability Setup
516
+
517
+ For mission-critical apps requiring guaranteed capacity:
518
+
519
+ ```ruby
520
+ SolidQueueAutoscaler.configure do |config|
521
+ config.adapter = :heroku
522
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
523
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
524
+ config.process_type = 'worker'
525
+
526
+ # Always maintain minimum capacity
527
+ config.min_workers = 3
528
+ config.max_workers = 15
529
+
530
+ # Scale up proactively before queue backs up
531
+ config.scale_up_queue_depth = 25
532
+ config.scale_up_latency_seconds = 60
533
+
534
+ # Conservative scale-down
535
+ config.scale_down_queue_depth = 5
536
+ config.scale_down_latency_seconds = 15
537
+
538
+ # Longer cooldowns to prevent flapping
539
+ config.cooldown_seconds = 180
540
+ config.scale_down_cooldown_seconds = 300 # Extra cautious on scale-down
541
+
542
+ # Record all events for monitoring
543
+ config.record_events = true
544
+ config.record_all_events = true # Even no-change events
545
+ end
546
+ ```
547
+
548
+ ---
549
+
550
+ ### Kubernetes Setup
551
+
552
+ For apps deployed on Kubernetes:
553
+
554
+ ```ruby
555
+ SolidQueueAutoscaler.configure do |config|
556
+ config.adapter = :kubernetes
557
+ config.kubernetes_namespace = ENV.fetch('K8S_NAMESPACE', 'production')
558
+ config.kubernetes_deployment = 'solid-queue-worker'
559
+
560
+ # Optional: specify kubeconfig for local development
561
+ # config.kubernetes_kubeconfig = '~/.kube/config'
562
+ # config.kubernetes_context = 'my-cluster'
563
+
564
+ config.min_workers = 2 # Minimum replicas
565
+ config.max_workers = 20
566
+
567
+ config.scale_up_queue_depth = 100
568
+ config.scale_up_latency_seconds = 180
569
+
570
+ config.scale_down_queue_depth = 10
571
+ config.scale_down_latency_seconds = 30
572
+
573
+ # K8s scaling can be faster than Heroku
574
+ config.cooldown_seconds = 60
575
+
576
+ config.enabled = Rails.env.production?
577
+ end
578
+ ```
579
+
580
+ **Required RBAC configuration:**
581
+
582
+ ```yaml
583
+ apiVersion: rbac.authorization.k8s.io/v1
584
+ kind: Role
585
+ metadata:
586
+ name: solid-queue-autoscaler
587
+ namespace: production
588
+ rules:
589
+ - apiGroups: ["apps"]
590
+ resources: ["deployments", "deployments/scale"]
591
+ verbs: ["get", "patch", "update"]
592
+ ---
593
+ apiVersion: rbac.authorization.k8s.io/v1
594
+ kind: RoleBinding
595
+ metadata:
596
+ name: solid-queue-autoscaler
597
+ namespace: production
598
+ subjects:
599
+ - kind: ServiceAccount
600
+ name: solid-queue-autoscaler
601
+ namespace: production
602
+ roleRef:
603
+ kind: Role
604
+ name: solid-queue-autoscaler
605
+ apiGroup: rbac.authorization.k8s.io
606
+ ```
607
+
608
+ ---
609
+
610
+ ### Development / Testing Setup
611
+
612
+ For local development and CI environments:
613
+
614
+ ```ruby
615
+ SolidQueueAutoscaler.configure do |config|
616
+ config.adapter = :heroku
617
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
618
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
619
+ config.process_type = 'worker'
620
+
621
+ config.min_workers = 1
622
+ config.max_workers = 3
623
+
624
+ config.scale_up_queue_depth = 10
625
+ config.scale_up_latency_seconds = 60
626
+
627
+ # IMPORTANT: Disable in development, use dry_run in staging
628
+ case Rails.env
629
+ when 'production'
630
+ config.enabled = true
631
+ config.dry_run = false
632
+ when 'staging'
633
+ config.enabled = true
634
+ config.dry_run = true # Log decisions but don't scale
635
+ else
636
+ config.enabled = false
637
+ end
638
+
639
+ # Verbose logging for debugging
640
+ config.logger = Logger.new(STDOUT)
641
+ config.logger.level = Rails.env.production? ? Logger::INFO : Logger::DEBUG
642
+ end
643
+ ```
644
+
645
+ ---
646
+
647
+ ### Configuration Comparison
648
+
649
+ | Use Case | min | max | scale_up_depth | scale_up_latency | cooldown | strategy |
650
+ |----------|-----|-----|----------------|------------------|----------|----------|
651
+ | Simple/Starter | 1 | 5 | 50 | 180s | 120s | fixed |
652
+ | Cost-Optimized | 0 | 5 | 1 | 60s | 30s/300s | fixed |
653
+ | E-Commerce Critical | 2 | 10 | 5 | 30s | 60s | fixed |
654
+ | E-Commerce Default | 1 | 8 | 100 | 300s | 120s | fixed |
655
+ | Webhook Processing | 2 | 20 | 50 | 60s | 30s/120s | proportional |
656
+ | ETL/Batch | 0 | 10 | 1 | 120s | 120s/600s | fixed |
657
+ | High-Availability | 3 | 15 | 25 | 60s | 180s/300s | fixed |
658
+
659
+ ## Configuring a High-Priority Queue for the Autoscaler
660
+
661
+ The autoscaler job should run reliably and quickly, even when your queues are backed up. By default, the autoscaler job runs on the `:autoscaler` queue. You can configure this and set up Solid Queue to prioritize it.
662
+
663
+ ### Configure the Job Queue and Priority
664
+
665
+ In your initializer, set the queue and priority for the autoscaler job:
666
+
667
+ ```ruby
668
+ SolidQueueAutoscaler.configure do |config|
669
+ # Use a dedicated high-priority queue for the autoscaler
670
+ config.job_queue = :autoscaler # Default value
671
+
672
+ # Or use an existing high-priority queue
673
+ config.job_queue = :critical
674
+
675
+ # Set job priority (lower = higher priority, processed first)
676
+ # This works with queue backends that support job-level priority like Solid Queue
677
+ config.job_priority = 0 # Highest priority
678
+
679
+ # ... other config
680
+ end
681
+ ```
682
+
683
+ For multi-worker configurations, each worker type can have its own queue and priority:
684
+
685
+ ```ruby
686
+ SolidQueueAutoscaler.configure(:critical_worker) do |config|
687
+ config.job_queue = :autoscaler_critical
688
+ config.job_priority = 0 # Highest priority for critical worker scaling
689
+ # ... other config
690
+ end
691
+
692
+ SolidQueueAutoscaler.configure(:default_worker) do |config|
693
+ config.job_queue = :autoscaler_default
694
+ config.job_priority = 10 # Lower priority for default worker scaling
695
+ # ... other config
696
+ end
697
+ ```
698
+
699
+ ### Configure Solid Queue to Prioritize the Autoscaler
700
+
701
+ In your `config/solid_queue.yml`, ensure the autoscaler queue is processed by a dedicated worker or listed first in the queue order:
702
+
703
+ ```yaml
704
+ # Option 1: Dedicated dispatcher/worker for autoscaler (recommended)
705
+ production:
706
+ dispatchers:
707
+ - polling_interval: 1
708
+ batch_size: 500
709
+ concurrency_maintenance_interval: 30
710
+
711
+ workers:
712
+ # Dedicated worker for autoscaler - always responsive
713
+ - queues: [autoscaler]
714
+ threads: 1
715
+ processes: 1
716
+ polling_interval: 0.5 # Check frequently
717
+
718
+ # Main workers for business logic
719
+ - queues: [critical, default, mailers]
720
+ threads: 5
721
+ processes: 2
722
+ polling_interval: 1
723
+ ```
724
+
725
+ ```yaml
726
+ # Option 2: Include autoscaler first in queue list (simpler)
727
+ production:
728
+ workers:
729
+ - queues: [autoscaler, critical, default, mailers]
730
+ threads: 5
731
+ processes: 2
732
+ ```
733
+
734
+ Solid Queue processes queues in order, so listing `autoscaler` first ensures those jobs are picked up before others.
735
+
736
+ ### Why This Matters
737
+
738
+ - **Responsiveness**: When your queues are backed up, you want the autoscaler to scale up workers quickly
739
+ - **Reliability**: A dedicated queue prevents autoscaler jobs from waiting behind thousands of business jobs
740
+ - **Isolation**: Separating autoscaler jobs makes monitoring and debugging easier
741
+
259
742
  ## Usage
260
743
 
261
744
  ### Running as a Solid Queue Recurring Job (Recommended)
@@ -506,10 +989,32 @@ KEEP_DAYS=7 bundle exec rake solid_queue_autoscaler:cleanup_events
506
989
 
507
990
  Another autoscaler instance is currently running. This is expected behavior — only one instance should run at a time per worker type.
508
991
 
992
+ **If you believe no other instance is running:**
993
+
994
+ ```ruby
995
+ # Check for stale advisory locks
996
+ ActiveRecord::Base.connection.execute(<<~SQL)
997
+ SELECT * FROM pg_locks WHERE locktype = 'advisory'
998
+ SQL
999
+
1000
+ # Force release a stuck lock (use with caution!)
1001
+ lock_key = SolidQueueAutoscaler.config.lock_key
1002
+ lock_id = Zlib.crc32(lock_key) & 0x7FFFFFFF
1003
+ ActiveRecord::Base.connection.execute("SELECT pg_advisory_unlock(#{lock_id})")
1004
+ ```
1005
+
509
1006
  ### "Cooldown active"
510
1007
 
511
1008
  A recent scaling event triggered the cooldown. Wait for the cooldown to expire or adjust `cooldown_seconds`.
512
1009
 
1010
+ ```ruby
1011
+ # Check cooldown status
1012
+ bundle exec rake solid_queue_autoscaler:cooldown
1013
+
1014
+ # Reset cooldowns (for testing only)
1015
+ SolidQueueAutoscaler::CooldownTracker.reset!
1016
+ ```
1017
+
513
1018
  ### Workers not scaling
514
1019
 
515
1020
  1. Check that `enabled` is `true`
@@ -518,12 +1023,263 @@ A recent scaling event triggered the cooldown. Wait for the cooldown to expire o
518
1023
  4. Enable dry-run to see what decisions would be made
519
1024
  5. Check the logs for error messages
520
1025
 
1026
+ **Debug with a manual scale attempt:**
1027
+
1028
+ ```ruby
1029
+ # Check configuration
1030
+ config = SolidQueueAutoscaler.config
1031
+ puts "Enabled: #{config.enabled?}"
1032
+ puts "Dry Run: #{config.dry_run?}"
1033
+ puts "API Key Set: #{config.heroku_api_key.present?}"
1034
+
1035
+ # Check current metrics
1036
+ metrics = SolidQueueAutoscaler.metrics
1037
+ puts "Queue depth: #{metrics.queue_depth}"
1038
+ puts "Latency: #{metrics.oldest_job_age_seconds}s"
1039
+
1040
+ # Try a manual scale
1041
+ result = SolidQueueAutoscaler.scale!
1042
+ puts result.decision.inspect if result.decision
1043
+ puts result.skipped_reason if result.skipped?
1044
+ puts result.error if result.error
1045
+ ```
1046
+
1047
+ ### Workers not scaling down
1048
+
1049
+ Scale-down requires **ALL** conditions to be met:
1050
+
1051
+ ```ruby
1052
+ metrics = SolidQueueAutoscaler.metrics
1053
+ config = SolidQueueAutoscaler.config
1054
+
1055
+ puts "Queue depth: #{metrics.queue_depth} (threshold: <= #{config.scale_down_queue_depth})"
1056
+ puts "Latency: #{metrics.oldest_job_age_seconds}s (threshold: <= #{config.scale_down_latency_seconds}s)"
1057
+ puts "Claimed jobs: #{metrics.claimed_jobs}" # Must be 0 for idle scale-down
1058
+ puts "Current workers: #{SolidQueueAutoscaler.current_workers}"
1059
+ puts "Min workers: #{config.min_workers}" # Can't scale below this
1060
+ ```
1061
+
1062
+ ### Heroku API errors
1063
+
1064
+ **401 Unauthorized:**
1065
+
1066
+ ```bash
1067
+ # Check if API key is valid
1068
+ heroku authorizations
1069
+
1070
+ # Create a new authorization
1071
+ heroku authorizations:create -d "Solid Queue Autoscaler"
1072
+ ```
1073
+
1074
+ **404 Not Found:**
1075
+
1076
+ ```bash
1077
+ # Verify app name
1078
+ heroku apps
1079
+ heroku apps:info -a $HEROKU_APP_NAME
1080
+ ```
1081
+
1082
+ **429 Rate Limited:**
1083
+
1084
+ Increase cooldown to reduce API calls:
1085
+
1086
+ ```ruby
1087
+ config.cooldown_seconds = 180 # 3 minutes instead of default 2
1088
+ ```
1089
+
521
1090
  ### Kubernetes authentication issues
522
1091
 
523
1092
  1. Ensure the service account has permissions to patch deployments
524
1093
  2. Check namespace is correct
525
1094
  3. Verify deployment name matches exactly
526
1095
 
1096
+ **Check RBAC permissions:**
1097
+
1098
+ ```yaml
1099
+ # Required RBAC rules for the autoscaler service account
1100
+ apiVersion: rbac.authorization.k8s.io/v1
1101
+ kind: Role
1102
+ metadata:
1103
+ name: solid-queue-autoscaler
1104
+ rules:
1105
+ - apiGroups: ["apps"]
1106
+ resources: ["deployments", "deployments/scale"]
1107
+ verbs: ["get", "patch", "update"]
1108
+ ```
1109
+
1110
+ ### AutoscaleJob not running
1111
+
1112
+ **Check recurring.yml configuration:**
1113
+
1114
+ ```yaml
1115
+ # config/recurring.yml
1116
+ autoscaler:
1117
+ class: SolidQueueAutoscaler::AutoscaleJob
1118
+ queue: autoscaler
1119
+ schedule: every 30 seconds
1120
+ ```
1121
+
1122
+ **Ensure a worker processes the autoscaler queue:**
1123
+
1124
+ ```yaml
1125
+ # config/solid_queue.yml
1126
+ workers:
1127
+ - queues: [autoscaler] # Must include autoscaler queue
1128
+ threads: 1
1129
+ ```
1130
+
1131
+ **Test manual enqueue:**
1132
+
1133
+ ```ruby
1134
+ SolidQueueAutoscaler::AutoscaleJob.perform_later
1135
+ ```
1136
+
1137
+ ### Multi-worker configuration issues
1138
+
1139
+ **"Unknown worker: :my_worker":**
1140
+
1141
+ Ensure you've configured the worker before referencing it:
1142
+
1143
+ ```ruby
1144
+ # Configure the worker first
1145
+ SolidQueueAutoscaler.configure(:my_worker) do |config|
1146
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
1147
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
1148
+ config.process_type = 'my_worker'
1149
+ end
1150
+
1151
+ # Then reference it
1152
+ SolidQueueAutoscaler.scale!(:my_worker)
1153
+ ```
1154
+
1155
+ **List all registered workers:**
1156
+
1157
+ ```ruby
1158
+ SolidQueueAutoscaler.registered_workers
1159
+ # => [:default, :critical_worker, :batch_worker]
1160
+ ```
1161
+
1162
+ ### Database/Migration issues
1163
+
1164
+ **"relation 'solid_queue_autoscaler_state' does not exist":**
1165
+
1166
+ ```bash
1167
+ rails generate solid_queue_autoscaler:migration
1168
+ rails db:migrate
1169
+ ```
1170
+
1171
+ **"relation 'solid_queue_ready_executions' does not exist":**
1172
+
1173
+ Solid Queue tables are missing. Run Solid Queue migrations:
1174
+
1175
+ ```bash
1176
+ rails solid_queue:install:migrations
1177
+ rails db:migrate
1178
+ ```
1179
+
1180
+ **Multi-database setup (Solid Queue on separate database):**
1181
+
1182
+ The autoscaler automatically detects `SolidQueue::Record.connection`. If auto-detection fails:
1183
+
1184
+ ```ruby
1185
+ SolidQueueAutoscaler.configure do |config|
1186
+ config.database_connection = SolidQueue::Record.connection
1187
+ end
1188
+ ```
1189
+
1190
+ ### Dashboard not loading
1191
+
1192
+ **404 when visiting /autoscaler:**
1193
+
1194
+ Ensure the engine is mounted in `config/routes.rb`:
1195
+
1196
+ ```ruby
1197
+ mount SolidQueueAutoscaler::Dashboard::Engine => "/autoscaler"
1198
+ ```
1199
+
1200
+ **"ActionView::MissingTemplate" errors:**
1201
+
1202
+ Run the dashboard generator:
1203
+
1204
+ ```bash
1205
+ rails generate solid_queue_autoscaler:dashboard
1206
+ rails db:migrate
1207
+ ```
1208
+
1209
+ ### Wrong process type being scaled
1210
+
1211
+ ```ruby
1212
+ # Check what process type is configured
1213
+ puts SolidQueueAutoscaler.config.process_type
1214
+
1215
+ # Verify it matches your Procfile
1216
+ # Procfile:
1217
+ # web: bundle exec puma -C config/puma.rb
1218
+ # worker: bundle exec rake solid_queue:start # <- This is "worker"
1219
+ ```
1220
+
1221
+ ### Scaling too aggressively or too slowly
1222
+
1223
+ **Scaling up too often (flapping):**
1224
+
1225
+ ```ruby
1226
+ config.cooldown_seconds = 180 # Increase cooldown
1227
+ config.scale_up_cooldown_seconds = 120 # Or set scale-up specific cooldown
1228
+ config.scale_up_queue_depth = 200 # Increase threshold
1229
+ ```
1230
+
1231
+ **Not scaling up fast enough:**
1232
+
1233
+ ```ruby
1234
+ config.scale_up_queue_depth = 50 # Lower threshold
1235
+ config.scale_up_latency_seconds = 120 # Trigger on 2 min latency
1236
+ config.cooldown_seconds = 60 # Reduce cooldown
1237
+ config.scaling_strategy = :proportional # Scale based on load, not fixed increment
1238
+ config.scale_up_jobs_per_worker = 25 # More workers per jobs over threshold
1239
+ ```
1240
+
1241
+ **Not scaling down:**
1242
+
1243
+ ```ruby
1244
+ config.scale_down_queue_depth = 5 # More aggressive scale-down threshold
1245
+ config.scale_down_latency_seconds = 10 # Tighter latency requirement
1246
+ config.min_workers = 0 # Allow scaling to zero (if appropriate)
1247
+ ```
1248
+
1249
+ ### Debugging tips
1250
+
1251
+ **Enable debug logging:**
1252
+
1253
+ ```ruby
1254
+ SolidQueueAutoscaler.configure do |config|
1255
+ config.logger = Logger.new(STDOUT)
1256
+ config.logger.level = Logger::DEBUG
1257
+ end
1258
+ ```
1259
+
1260
+ **Simulate a scaling decision without making changes:**
1261
+
1262
+ ```ruby
1263
+ metrics = SolidQueueAutoscaler.metrics
1264
+ workers = SolidQueueAutoscaler.current_workers
1265
+ engine = SolidQueueAutoscaler::DecisionEngine.new(config: SolidQueueAutoscaler.config)
1266
+ decision = engine.decide(metrics: metrics, current_workers: workers)
1267
+
1268
+ puts "Action: #{decision.action}" # :scale_up, :scale_down, or :no_change
1269
+ puts "From: #{decision.from} -> To: #{decision.to}"
1270
+ puts "Reason: #{decision.reason}"
1271
+ ```
1272
+
1273
+ **Run diagnostics:**
1274
+
1275
+ ```bash
1276
+ bundle exec rake solid_queue_autoscaler:metrics
1277
+ bundle exec rake solid_queue_autoscaler:formation
1278
+ bundle exec rake solid_queue_autoscaler:cooldown
1279
+ ```
1280
+
1281
+ For more detailed troubleshooting, see [docs/troubleshooting.md](docs/troubleshooting.md).
1282
+
527
1283
  ## Architecture Notes
528
1284
 
529
1285
  This gem acts as a **control plane** for Solid Queue:
@@ -1,6 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class CreateSolidQueueAutoscalerEvents < ActiveRecord::Migration<%= migration_version %>
4
+ # Use the same database connection as SolidQueue for multi-database setups
5
+ def self.connection
6
+ if defined?(SolidQueue::Record) && SolidQueue::Record.respond_to?(:connection)
7
+ SolidQueue::Record.connection
8
+ else
9
+ super
10
+ end
11
+ end
12
+
4
13
  def change
5
14
  create_table :solid_queue_autoscaler_events do |t|
6
15
  t.string :worker_name, null: false
@@ -1,6 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class CreateSolidQueueAutoscalerState < ActiveRecord::Migration<%= migration_version %>
4
+ # Use the same database connection as SolidQueue for multi-database setups
5
+ def self.connection
6
+ if defined?(SolidQueue::Record) && SolidQueue::Record.respond_to?(:connection)
7
+ SolidQueue::Record.connection
8
+ else
9
+ super
10
+ end
11
+ end
12
+
4
13
  def change
5
14
  create_table :solid_queue_autoscaler_state do |t|
6
15
  t.string :key, null: false
@@ -55,4 +55,10 @@ SolidQueueAutoscaler.configure do |config|
55
55
  config.record_events = true
56
56
  # Also record no_change events (verbose, generates many records)
57
57
  # config.record_all_events = false
58
+
59
+ # AutoscaleJob Settings
60
+ # Queue for the autoscaler job (use a fast/high-priority Solid Queue queue)
61
+ config.job_queue = :autoscaler
62
+ # Priority for the autoscaler job (lower = higher priority, nil = default)
63
+ # config.job_priority = 0 # Uncomment to set highest priority
58
64
  end
@@ -2,7 +2,40 @@
2
2
 
3
3
  module SolidQueueAutoscaler
4
4
  class AutoscaleJob < ActiveJob::Base
5
- queue_as :autoscaler
5
+ # Use configured queue for the target worker (defaults to :autoscaler)
6
+ queue_as do
7
+ # perform(worker_name = :default)
8
+ worker_name = arguments.first
9
+
10
+ # When scaling all workers, or when worker_name is nil, use the default configuration
11
+ config_name =
12
+ if worker_name.nil? || worker_name == :all || worker_name == "all"
13
+ :default
14
+ else
15
+ # Handle both Symbol and String values safely
16
+ worker_name.to_sym rescue :default
17
+ end
18
+
19
+ SolidQueueAutoscaler.config(config_name).job_queue || :autoscaler
20
+ end
21
+
22
+ # Use configured priority for the target worker (defaults to nil/no priority)
23
+ queue_with_priority do
24
+ # perform(worker_name = :default)
25
+ worker_name = arguments.first
26
+
27
+ # When scaling all workers, or when worker_name is nil, use the default configuration
28
+ config_name =
29
+ if worker_name.nil? || worker_name == :all || worker_name == "all"
30
+ :default
31
+ else
32
+ # Handle both Symbol and String values safely
33
+ worker_name.to_sym rescue :default
34
+ end
35
+
36
+ SolidQueueAutoscaler.config(config_name).job_priority
37
+ end
38
+
6
39
  discard_on ConfigurationError
7
40
 
8
41
  # Scale a specific worker type, or all workers if :all is passed
@@ -63,6 +63,9 @@ module SolidQueueAutoscaler
63
63
  # Dashboard/event recording settings
64
64
  attr_accessor :record_events, :record_all_events
65
65
 
66
+ # AutoscaleJob settings
67
+ attr_accessor :job_queue, :job_priority
68
+
66
69
  def initialize
67
70
  # Configuration name (auto-set when using named configurations)
68
71
  @name = :default
@@ -128,6 +131,10 @@ module SolidQueueAutoscaler
128
131
  # Dashboard/event recording settings
129
132
  @record_events = true # Record scale events to database
130
133
  @record_all_events = false # Also record no_change events (verbose)
134
+
135
+ # AutoscaleJob settings
136
+ @job_queue = :autoscaler # Queue name for the autoscaler job
137
+ @job_priority = nil # Job priority (lower = higher priority, nil = default)
131
138
  end
132
139
 
133
140
  # Returns the lock key, auto-generating based on name if not explicitly set
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueAutoscaler
4
- VERSION = '1.0.7'
4
+ VERSION = '1.0.8'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_autoscaler
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - reillyse
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activejob
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '7.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '7.0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement