good_job 4.15.0 → 4.16.0

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: d07cdaddabbb6f1966833ae6f79df8a61aef3959c3209ad64ed80e8896d7d56c
4
- data.tar.gz: 555fdb53f3ba29885d46ac62bc2ab59a0f9cd13186a729c188e13101e7567634
3
+ metadata.gz: 9b13d49cf0a8c78543c55426eaf82eafd8518bd6f9a22ab081ac1657ff129227
4
+ data.tar.gz: c8e21ac65881fa645409b00d496beff08b9f99cfbcefb832a01d29bcff2e760a
5
5
  SHA512:
6
- metadata.gz: f4a64d532756884282a5b5ff76b5fb4bc31ceb346f104cab837ef9c946f9a8b6373ae47bfff22d4cdf7785022711b4a2c9a632eb4f8a877fd19dfd674b62a495
7
- data.tar.gz: 1310d23b3386534c4c5c11f14b2bc521e877a3856371a7b7fb8700f7d03b3a23ecfcc7fa63d4e667628057e2ac6d2c9364f8688788cee8e7538a4ac7216ef1d0
6
+ metadata.gz: af467c3effc78f424155420005e79ffdbff6249ae7a02ef9084f94d81d8aa28920bcfbc71f46349f6e67597f73cf39f7f2f2cd7f4edd869084091e9ab962c951
7
+ data.tar.gz: 19732a4dbfa6c64aeddd85c721a82f94cbaf50f2e784fa04bf22d2321bb518723992535595cdcc32a172cefbb77476ea66c9e06182c640dc2dc62b0306bf2cae
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.16.0](https://github.com/bensheldon/good_job/tree/v4.16.0) (2026-04-14)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.15.0...v4.16.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Allow filtering by label on dashboard [\#1739](https://github.com/bensheldon/good_job/pull/1739) ([bensheldon](https://github.com/bensheldon))
10
+ - Allow multiple concurrency rules per job via labels [\#1700](https://github.com/bensheldon/good_job/pull/1700) ([bscofield](https://github.com/bscofield))
11
+
12
+ **Fixed bugs:**
13
+
14
+ - Fix advisory lock connection stickiness in block contexts [\#1736](https://github.com/bensheldon/good_job/pull/1736) ([bensheldon](https://github.com/bensheldon))
15
+ - Add JRuby 10 to testing matrix [\#1559](https://github.com/bensheldon/good_job/pull/1559) ([bensheldon](https://github.com/bensheldon))
16
+
17
+ **Closed issues:**
18
+
19
+ - Job duration misreported if interrupted [\#1723](https://github.com/bensheldon/good_job/issues/1723)
20
+
21
+ **Merged pull requests:**
22
+
23
+ - Use annotated git tag in release script [\#1741](https://github.com/bensheldon/good_job/pull/1741) ([bensheldon](https://github.com/bensheldon))
24
+ - Double single-thread scheduler integration test timeout on JRuby [\#1738](https://github.com/bensheldon/good_job/pull/1738) ([bensheldon](https://github.com/bensheldon))
25
+ - Fix JRuby test flakes for scheduler timeout and interrupted execution duration [\#1737](https://github.com/bensheldon/good_job/pull/1737) ([bensheldon](https://github.com/bensheldon))
26
+ - Count Advisory Locks and refactor advisory lock lifecycle [\#1735](https://github.com/bensheldon/good_job/pull/1735) ([bensheldon](https://github.com/bensheldon))
27
+ - Show interrupted execution recovery duration in dashboard [\#1733](https://github.com/bensheldon/good_job/pull/1733) ([bensheldon](https://github.com/bensheldon))
28
+ - chore: use `merge` to avoid mutate the query object [\#1717](https://github.com/bensheldon/good_job/pull/1717) ([luizkowalski](https://github.com/luizkowalski))
29
+
3
30
  ## [v4.15.0](https://github.com/bensheldon/good_job/tree/v4.15.0) (2026-04-09)
4
31
 
5
32
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v4.14.2...v4.15.0)
data/README.md CHANGED
@@ -538,6 +538,78 @@ Labels can be used to search jobs in the Dashboard. For example, to find all job
538
538
 
539
539
  GoodJob can extend Active Job to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_. Limiting concurrency can help prevent duplicate, double or unnecessary jobs from being enqueued, or race conditions when performing, for example when interacting with 3rd-party APIs.
540
540
 
541
+ ```ruby
542
+ class MyJob < ApplicationJob
543
+ include GoodJob::ActiveJobExtensions::Concurrency
544
+
545
+ # Define one or more concurrency rules. Each rule is scoped to a label,
546
+ # which is a value derived from the job's arguments at enqueue time and
547
+ # stored on the job record. Jobs must be enqueued with the matching label
548
+ # via `good_job_labels:` for the rule to apply.
549
+ #
550
+ # Multiple rules can be defined; they are evaluated in order and the first
551
+ # exceeded rule short-circuits the rest.
552
+ good_job_concurrency_rule(
553
+ # A label that scopes this rule. Can be a static String or a Lambda/Proc
554
+ # invoked in the context of the job instance. The rule only applies to jobs
555
+ # that were enqueued with this label in `good_job_labels`.
556
+ label: -> { arguments.first[:user_id] },
557
+
558
+ # Maximum number of unfinished jobs with this label to allow.
559
+ # Can be an Integer or Lambda/Proc invoked in the context of the job.
560
+ total_limit: 1,
561
+
562
+ # Or, if more control is needed:
563
+ # Maximum number of jobs with this label to be concurrently enqueued
564
+ # (excludes performing jobs). Can be an Integer or Lambda/Proc.
565
+ enqueue_limit: 2,
566
+
567
+ # Maximum number of jobs with this label to be concurrently performed
568
+ # (excludes enqueued jobs). Can be an Integer or Lambda/Proc.
569
+ perform_limit: 1,
570
+
571
+ # Maximum number of jobs with this label to be enqueued within the time
572
+ # period, looking backwards from now. Must be [count, period].
573
+ enqueue_throttle: [10, 1.minute],
574
+
575
+ # Maximum number of jobs with this label to be performed within the time
576
+ # period, looking backwards from now. Must be [count, period].
577
+ perform_throttle: [100, 1.hour],
578
+
579
+ # Note: Under heavy load, the total number of jobs may exceed the
580
+ # sum of `enqueue_limit` and `perform_limit` because of race conditions
581
+ # caused by imperfectly disjunctive states. If you need to constrain
582
+ # the total number of jobs, use `total_limit` instead. See #378.
583
+ )
584
+ # Additional rules
585
+ good_job_concurrency_rule(...)
586
+ good_job_concurrency_rule(...)
587
+
588
+ def perform(user_id:)
589
+ # do work
590
+ end
591
+ end
592
+ ```
593
+
594
+ Jobs must be enqueued with the matching label for rules to take effect:
595
+
596
+ ```ruby
597
+ MyJob.set(good_job_labels: [current_user.id]).perform_later(user_id: current_user.id)
598
+ ```
599
+
600
+ #### How concurrency controls work
601
+
602
+ GoodJob's concurrency control strategy for `perform_limit` is "optimistic retry with an incremental backoff". The [code is readable](https://github.com/bensheldon/good_job/blob/main/lib/good_job/active_job_extensions/concurrency.rb).
603
+
604
+ - "Optimistic" meaning that the implementation's performance trade-off assumes that collisions are atypical (e.g. two users enqueue the same job at the same time) rather than regular (e.g. the system enqueues thousands of colliding jobs at the same time). Depending on your concurrency requirements, you may also want to manage concurrency through the number of GoodJob threads and processes that are performing a given queue.
605
+ - "Retry with an incremental backoff" means that when `perform_limit` is exceeded, the job will raise a `GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError` which is caught by a `retry_on` handler which re-schedules the job to execute in the near future with an incremental backoff.
606
+ - First-in-first-out job execution order is not preserved when a job is retried with incremental back-off.
607
+ - For pessimistic usecases that collisions are expected, use number of threads/processes (e.g., `good_job --queues "serial:1;-serial:5"`) to control concurrency. It is also a good idea to use `perform_limit` as backstop.
608
+
609
+ #### Legacy: `good_job_control_concurrency_with`
610
+
611
+ The original concurrency interface uses a single configuration hash and scopes limits to a concurrency _key_ (a string derived from the job) stored on the job record, rather than a label. It remains fully supported.
612
+
541
613
  ```ruby
542
614
  class MyJob < ApplicationJob
543
615
  include GoodJob::ActiveJobExtensions::Concurrency
@@ -568,11 +640,6 @@ class MyJob < ApplicationJob
568
640
  # with two elements: the number of jobs and the time period.
569
641
  perform_throttle: [100, 1.hour],
570
642
 
571
- # Note: Under heavy load, the total number of jobs may exceed the
572
- # sum of `enqueue_limit` and `perform_limit` because of race conditions
573
- # caused by imperfectly disjunctive states. If you need to constrain
574
- # the total number of jobs, use `total_limit` instead. See #378.
575
-
576
643
  # A unique key to be globally locked against.
577
644
  # Can be String or Lambda/Proc that is invoked in the context of the job.
578
645
  #
@@ -606,15 +673,6 @@ job = MyJob.perform_later("Alice", version: 'v1')
606
673
  job.good_job_concurrency_key #=> "MyJob-default-Alice-v1"
607
674
  ```
608
675
 
609
- #### How concurrency controls work
610
-
611
- GoodJob's concurrency control strategy for `perform_limit` is "optimistic retry with an incremental backoff". The [code is readable](https://github.com/bensheldon/good_job/blob/main/lib/good_job/active_job_extensions/concurrency.rb).
612
-
613
- - "Optimistic" meaning that the implementation's performance trade-off assumes that collisions are atypical (e.g. two users enqueue the same job at the same time) rather than regular (e.g. the system enqueues thousands of colliding jobs at the same time). Depending on your concurrency requirements, you may also want to manage concurrency through the number of GoodJob threads and processes that are performing a given queue.
614
- - "Retry with an incremental backoff" means that when `perform_limit` is exceeded, the job will raise a `GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError` which is caught by a `retry_on` handler which re-schedules the job to execute in the near future with an incremental backoff.
615
- - First-in-first-out job execution order is not preserved when a job is retried with incremental back-off.
616
- - For pessimistic usecases that collisions are expected, use number of threads/processes (e.g., `good_job --queues "serial:1;-serial:5"`) to control concurrency. It is also a good idea to use `perform_limit` as backstop.
617
-
618
676
  ### Cron-style repeating/recurring jobs
619
677
 
620
678
  GoodJob can enqueue Active Job jobs on a recurring basis that can be used as a replacement for cron.
@@ -52,6 +52,7 @@ module GoodJob
52
52
  def to_params(override = {})
53
53
  {
54
54
  job_class: params[:job_class],
55
+ label: params[:label],
55
56
  limit: params[:limit],
56
57
  queue_name: params[:queue_name],
57
58
  query: params[:query],
@@ -23,41 +23,14 @@ module GoodJob
23
23
  end
24
24
 
25
25
  def filtered_query(filter_params = params)
26
- query = base_query
27
-
28
- query = query.job_class(filter_params[:job_class]) if filter_params[:job_class].present?
29
- query = query.where(queue_name: filter_params[:queue_name]) if filter_params[:queue_name].present?
30
-
31
- search_query = filter_params[:query]&.strip
32
- if search_query.present?
33
- query = if query_is_uuid_and_job_exists?(search_query)
34
- query.where(active_job_id: search_query)
35
- else
36
- query.search_text(search_query)
37
- end
38
- end
39
-
40
- query = query.where(cron_key: filter_params[:cron_key]) if filter_params[:cron_key].present?
41
- query = query.where(finished_at: finished_since(filter_params[:finished_since])..) if filter_params[:finished_since].present?
42
-
43
- if filter_params[:state]
44
- case filter_params[:state]
45
- when 'discarded'
46
- query = query.discarded
47
- when 'succeeded'
48
- query = query.succeeded
49
- when 'retried'
50
- query = query.retried
51
- when 'scheduled'
52
- query = query.scheduled
53
- when 'running'
54
- query = query.running
55
- when 'queued'
56
- query = query.queued
57
- end
58
- end
59
-
60
- query
26
+ base_query
27
+ .merge(filter_by_job_class(filter_params[:job_class]))
28
+ .merge(filter_by_queue_name(filter_params[:queue_name]))
29
+ .merge(filter_by_label(filter_params[:label]))
30
+ .merge(filter_by_search_query(filter_params[:query]))
31
+ .merge(filter_by_cron_key(filter_params[:cron_key]))
32
+ .merge(filter_by_finished_since(filter_params[:finished_since]))
33
+ .merge(filter_by_state(filter_params[:state]))
61
34
  end
62
35
 
63
36
  def filtered_count
@@ -79,6 +52,59 @@ module GoodJob
79
52
 
80
53
  private
81
54
 
55
+ def filter_by_job_class(job_class)
56
+ return {} if job_class.blank?
57
+
58
+ GoodJob::Job.job_class(job_class)
59
+ end
60
+
61
+ def filter_by_queue_name(queue_name)
62
+ return {} if queue_name.blank?
63
+
64
+ GoodJob::Job.where(queue_name: queue_name)
65
+ end
66
+
67
+ def filter_by_label(label)
68
+ return {} if label.blank?
69
+
70
+ GoodJob::Job.labeled(label)
71
+ end
72
+
73
+ def filter_by_search_query(query)
74
+ search_query = query&.strip
75
+ return {} if search_query.blank?
76
+
77
+ if query_is_uuid_and_job_exists?(search_query)
78
+ GoodJob::Job.where(active_job_id: search_query)
79
+ else
80
+ GoodJob::Job.search_text(search_query)
81
+ end
82
+ end
83
+
84
+ def filter_by_cron_key(cron_key)
85
+ return {} if cron_key.blank?
86
+
87
+ GoodJob::Job.where(cron_key: cron_key)
88
+ end
89
+
90
+ def filter_by_finished_since(since)
91
+ return {} if since.blank?
92
+
93
+ GoodJob::Job.where(finished_at: finished_since(since)..)
94
+ end
95
+
96
+ def filter_by_state(state)
97
+ case state
98
+ when 'discarded' then GoodJob::Job.discarded
99
+ when 'succeeded' then GoodJob::Job.succeeded
100
+ when 'retried' then GoodJob::Job.retried
101
+ when 'scheduled' then GoodJob::Job.scheduled
102
+ when 'running' then GoodJob::Job.running
103
+ when 'queued' then GoodJob::Job.queued
104
+ else {}
105
+ end
106
+ end
107
+
82
108
  def query_for_records
83
109
  filtered_query
84
110
  end