gouda 0.1.4 → 0.1.5

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: cbebec9ae881152e66d8a8e763d74e2314c6694cc46a1b547323bb4fe01fd505
4
- data.tar.gz: f9836ba9594cf2473485efbe6be8dd6b001e0e514315cb84170a62d525adbaf3
3
+ metadata.gz: 0be474cea3bc87b9c0a41c9c654a31e4dc865e46876249a1db615bf56e494a40
4
+ data.tar.gz: '048574e736404c6c76f437fdfded93d7e810a23c83bb4f28ede8dffe9761a215'
5
5
  SHA512:
6
- metadata.gz: 2bf05ef7dcfe8cc682f54da18f8ad8a415f5db3f103928188ad04ab714f4a2799b4998b25c5d53432776d4f913e22fe0a761bd1fe11ea2eb17763f89201cb809
7
- data.tar.gz: 636a2aa356b4fed48dece844dec2f26d46a430d6e4a0d956c56d5c5121d30f8d3fb8f87174b82e4d7ab6a58f3e0bcd99d3e07c18c631390cc6233b8cac2d48ba
6
+ metadata.gz: 00e687d2fc384484cf2c07bf6c661e8d5ff1287a96f4e4f0abd98df2933538d7d81946ee51fe8e7abd6f2e5b285b6762aba231d359926ba5ee09d07a8b7e526c
7
+ data.tar.gz: cbd09ad83be4b9e6b9b35e20b2c80b7c8a5b8de1e20f8adfa6d0701d1aa31be7e5ea8edd2e64fab8f67e69cdc9757d1bcf79908fac9a81e9eccf9497f0a1c073
data/CHANGELOG.md CHANGED
@@ -23,3 +23,7 @@
23
23
  - Reduce logging in local test runs.
24
24
  - Bump local ruby version to 3.3.3
25
25
 
26
+ ## [0.1.5] - 2023-06-18
27
+
28
+ - Update documentation
29
+ - Don't pass on scheduler keys to retries
data/README.md CHANGED
@@ -1,7 +1,10 @@
1
1
  Gouda is an ActiveJob adapter used at Cheddar. It requires PostgreSQL and a recent version of Rails.
2
2
 
3
- ⚠️ At the moment Gouda is only used internally at Cheddar. We do not provide support for it, nor do we accept
4
- issues or feature requests. This is likely to change in the future.
3
+ > [!CAUTION]
4
+ > At the moment Gouda is only used internally at Cheddar. Any support to external parties is on best-effort
5
+ > basis. While we are happy to see issues and pull requests, we can't guarantee that those will be addressed
6
+ > quickly. The library does receive rapid updates which may break your application if you come to depend on
7
+ > the library. That is to be expected.
5
8
 
6
9
  ## Installation
7
10
 
@@ -11,10 +14,10 @@ $ bundle install
11
14
  $ bin/rails g gouda:install
12
15
  ```
13
16
 
14
- Gouda is build as a lightweight alternative to [good_job](https://github.com/bensheldon/good_job) and has been created before [solid_queue.](https://github.com/rails/solid_queue/)
15
- It is _smaller_ than solid_queue though.
16
-
17
- It was designed to enable job processing using `SELECT ... FOR UPDATE SKIP LOCKED` on Postgres so that we could use pg_bouncer in our system setup.
17
+ Gouda is a lightweight alternative to [good_job](https://github.com/bensheldon/good_job) and [solid_queue.](https://github.com/rails/solid_queue/) - while
18
+ more similar to the latter. It has been created prior to solid_queue and is smaller. It was designed to enable job processing using `SELECT ... FOR UPDATE SKIP LOCKED`
19
+ on Postgres so that we could use pg_bouncer in our system setup. We have also observed that `SKIP LOCKED` causes less load on our database than advisory locking,
20
+ especially as queue depths would grow.
18
21
 
19
22
 
20
23
  ## Key concepts in Gouda: Workload
data/gouda.gemspec CHANGED
@@ -7,13 +7,13 @@ Gem::Specification.new do |spec|
7
7
  spec.description = "Job Scheduler for Rails and PostgreSQL"
8
8
  spec.authors = ["Sebastian van Hesteren", "Julik Tarkhanov"]
9
9
  spec.email = ["sebastian@cheddar.me", "me@julik.nl"]
10
- spec.homepage = "https://rubygems.org/gems/gouda"
10
+ spec.homepage = "https://github.com/cheddar-me/gouda"
11
11
  spec.license = "MIT"
12
12
  spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
13
13
  spec.require_paths = ["lib"]
14
14
 
15
- spec.metadata["homepage_uri"] = spec.homepage
16
- spec.metadata["source_code_uri"] = "https://github.com/cheddar-me/gouda"
15
+ spec.metadata["homepage_uri"] =
16
+ spec.metadata["source_code_uri"] = spec.homepage
17
17
  spec.metadata["changelog_uri"] = "https://github.com/cheddar-me/gouda/CHANGELOG.md"
18
18
 
19
19
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveJob # :nodoc:
4
- module QueueAdapters # :nodoc:
5
- class GoudaAdapter < Gouda::Adapter
6
- end
7
- end
3
+ # The sole purpose of this class is so that you can do
4
+ # `config.active_job.queue_adapter = :gouda` in your Rails
5
+ # config, as Rails insists on resolving the adapter module
6
+ # name from the symbol automatically. If Rails ever allows
7
+ # us to "register" an adapter to a symbol this module can
8
+ # be removed later.
9
+ class ActiveJob::QueueAdapters::GoudaAdapter < Gouda::Adapter
8
10
  end
data/lib/gouda/adapter.rb CHANGED
@@ -57,10 +57,11 @@ class Gouda::Adapter
57
57
  # We can't tell Postgres to ignore conflicts on _both_ the scheduler key and the enqueue concurrency key but not on
58
58
  # the ID - it is either "all indexes" or "just one", but never "this index and that index". MERGE https://www.postgresql.org/docs/current/sql-merge.html
59
59
  # is in theory capable of solving this but let's not complicate things all to hastily, the hour is getting late
60
+ scheduler_key = active_job.try(:executions) == 0 ? active_job.scheduler_key : nil # only enforce scheduler key on first workload
60
61
  {
61
62
  active_job_id: active_job.job_id, # Multiple jobs can have the same ID due to retries, job-iteration etc.
62
63
  scheduled_at: active_job.scheduled_at || t_now,
63
- scheduler_key: active_job.scheduler_key, # So that the scheduler_key gets retained between retries
64
+ scheduler_key: scheduler_key,
64
65
  priority: active_job.priority,
65
66
  execution_concurrency_key: extract_execution_concurrency_key(active_job),
66
67
  enqueue_concurrency_key: extract_enqueue_concurrency_key(active_job),
data/lib/gouda/bulk.rb CHANGED
@@ -1,6 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gouda
4
+ # Inside this call, all `perform_later` calls on ActiveJob subclasses
5
+ # (including mailers) will be buffered. The call is reentrant, so you
6
+ # can have multiple `in_bulk` calls with arbitrary nesting. At the end
7
+ # of the block, the buffered jobs will be enqueued using their respective
8
+ # adapters. If an adapter supports `enqueue_all` (Sidekiq does in recent
9
+ # releases of Rails, for example), this functionality will be used. This
10
+ # method is especially useful when doing things such as mass-emails, or
11
+ # maintenance tasks where a large swath of jobs gets enqueued at once.
12
+ #
13
+ # @example
14
+ # Gouda.in_bulk do
15
+ # User.recently_joined.find_each do |recently_joined_user|
16
+ # GreetingJob.perform_later(recently_joined_user)
17
+ # end
18
+ # end
19
+ # @return [Object] the return value of the block
4
20
  def self.in_bulk(&blk)
5
21
  if Thread.current[:gouda_bulk_buffer].nil?
6
22
  Thread.current[:gouda_bulk_buffer] = []
@@ -1,44 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gouda
4
+ # A queue constraint supports just one method, `to_sql`, which returns
5
+ # a condition based on the `queue_name` value of the `gouda_workloads`
6
+ # table. The minimal constraint is just a no-op - it allows execution
7
+ # of jobs from all queues in the system.
4
8
  module AnyQueue
5
9
  def self.to_sql
6
10
  "1=1"
7
11
  end
8
12
  end
9
13
 
14
+ # Allows execution of jobs only from specified queues
15
+ # For example, if you have a queue named "gpu", and you run
16
+ # jobs requiring a GPU on this queue, on your worker script
17
+ # running on GPU-equipped machines you could use
18
+ # `OnlyQueuesConstraint.new([:gpu])`
10
19
  class OnlyQueuesConstraint < Struct.new(:queue_names)
11
20
  def to_sql
12
21
  placeholders = (["?"] * queue_names.length).join(",")
13
- ActiveRecord::Base.sanitize_sql_array([<<~SQL, *queue_names])
22
+ Gouda::Workload.sanitize_sql_array([<<~SQL, *queue_names])
14
23
  queue_name IN (#{placeholders})
15
24
  SQL
16
25
  end
17
26
  end
18
27
 
28
+ # Allows execution of jobs from queues except the given ones
29
+ # For example, if you have a queue named "emails" which is time-critical,
30
+ # on all other machines your worker script can specify
31
+ # `ExceptQueueConstraint.new([:emails])`
19
32
  class ExceptQueueConstraint < Struct.new(:queue_names)
20
33
  def to_sql
21
34
  placeholders = (["?"] * queue_names.length).join(",")
22
- ActiveRecord::Base.sanitize_sql_array([<<~SQL, *queue_names])
35
+ Gouda::Workload.sanitize_sql_array([<<~SQL, *queue_names])
23
36
  queue_name NOT IN (#{placeholders})
24
37
  SQL
25
38
  end
26
39
  end
27
40
 
28
- def self.parse_queue_constraint(constraint_str_from_envvar)
29
- parsed = queue_parser(constraint_str_from_envvar)
30
- if parsed[:include]
31
- OnlyQueuesConstraint.new(parsed[:include])
32
- elsif parsed[:exclude]
33
- ExceptQueueConstraint.new(parsed[:exclude])
34
- else
35
- AnyQueue
36
- end
37
- end
38
-
39
- # Parse a string representing a group of queues into a more readable data
40
- # structure.
41
- # @param string [String] Queue string
41
+ # Parse a string representing a group of queues into a queue constraint
42
+ # Note that this works similar to good_job. For example, the
43
+ # constraints do not necessarily compose all that well.
44
+ #
45
+ # @param queue_constraint_str[String] Queue string
42
46
  # @return [Hash]
43
47
  # How to match a given queue. It can have the following keys and values:
44
48
  # - +{ all: true }+ indicates that all queues match.
@@ -49,8 +53,8 @@ module Gouda
49
53
  # @example
50
54
  # Gouda::QueueConstraints.queue_parser('-queue1,queue2')
51
55
  # => { exclude: [ 'queue1', 'queue2' ] }
52
- def self.queue_parser(string)
53
- string = string.presence || "*"
56
+ def self.parse_queue_constraint(queue_constraint_str)
57
+ string = queue_constraint_str.presence || "*"
54
58
 
55
59
  case string.first
56
60
  when "-"
@@ -63,11 +67,11 @@ module Gouda
63
67
  queues = string.split(",").map(&:strip)
64
68
 
65
69
  if queues.include?("*")
66
- {all: true}
70
+ AnyQueue
67
71
  elsif exclude_queues
68
- {exclude: queues}
72
+ ExceptQueueConstraint.new([queues])
69
73
  else
70
- {include: queues}
74
+ OnlyQueuesConstraint.new([queues])
71
75
  end
72
76
  end
73
77
  end
data/lib/gouda/railtie.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gouda
4
+ UNINITIALISED_DATABASE_EXCEPTIONS = [ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished]
5
+
4
6
  class Railtie < Rails::Railtie
5
7
  rake_tasks do
6
8
  task preload: :setup do
@@ -54,7 +56,7 @@ module Gouda
54
56
  Gouda::Scheduler.build_scheduler_entries_list!
55
57
  begin
56
58
  Gouda::Scheduler.upsert_workloads_from_entries_list!
57
- rescue ActiveRecord::NoDatabaseError
59
+ rescue *Gouda::UNINITIALISED_DATABASE_EXCEPTIONS
58
60
  # Do nothing. On a freshly checked-out Rails app, running even unrelated Rails tasks
59
61
  # (such as asset compilation) - or, more importantly, initial db:create -
60
62
  # will cause a NoDatabaseError, as this is a chicken-and-egg problem. That error
@@ -11,6 +11,9 @@ module Gouda::Scheduler
11
11
  [name, interval_seconds, cron, job_class].compact.join("_")
12
12
  end
13
13
 
14
+ # Tells when this particular task should next run
15
+ #
16
+ # @return [Time]
14
17
  def next_at
15
18
  if interval_seconds
16
19
  first_existing = Gouda::Workload.where(scheduler_key: scheduler_key).where("scheduled_at > NOW()").order("scheduled_at DESC").pluck(:scheduled_at).first
@@ -22,6 +25,9 @@ module Gouda::Scheduler
22
25
  end
23
26
  end
24
27
 
28
+ # Builds the ActiveJob which can be enqueued for this entry
29
+ #
30
+ # @return [ActiveJob::Base]
25
31
  def build_active_job
26
32
  next_at = self.next_at
27
33
  return unless next_at
data/lib/gouda/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gouda
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
@@ -24,8 +24,6 @@ class GoudaSchedulerTest < ActiveSupport::TestCase
24
24
  class MegaError < StandardError
25
25
  end
26
26
 
27
- gouda_control_concurrency_with(enqueue_limit: 1, key: -> { self.class.to_s })
28
-
29
27
  retry_on StandardError, wait: :polynomially_longer, attempts: 5
30
28
  retry_on Gouda::InterruptError, wait: 0, attempts: 5
31
29
  retry_on MegaError, attempts: 3, wait: 0
@@ -55,6 +53,29 @@ class GoudaSchedulerTest < ActiveSupport::TestCase
55
53
  assert Gouda::Workload.count > 3
56
54
  end
57
55
 
56
+ test "retries do not have a scheduler_key" do
57
+ tab = {
58
+ second_minutely: {
59
+ cron: "*/1 * * * * *", # every second
60
+ class: "GoudaSchedulerTest::FailingJob"
61
+ }
62
+ }
63
+
64
+ assert_nothing_raised do
65
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
66
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
67
+ end
68
+
69
+ assert_equal 1, Gouda::Workload.enqueued.count
70
+ assert_equal "second_minutely_*/1 * * * * *_GoudaSchedulerTest::FailingJob", Gouda::Workload.enqueued.first.scheduler_key
71
+ sleep(2)
72
+ Gouda::Workload.checkout_and_perform_one(executing_on: "Unit test")
73
+
74
+ assert_equal 1, Gouda::Workload.retried.reload.count
75
+ assert_nil Gouda::Workload.retried.first.scheduler_key
76
+ assert_equal "enqueued", Gouda::Workload.retried.first.state
77
+ end
78
+
58
79
  test "re-inserts the next subsequent job after executing the queued one" do
59
80
  tab = {
60
81
  second_minutely: {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gouda
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian van Hesteren
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-06-14 00:00:00.000000000 Z
12
+ date: 2024-06-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -166,12 +166,12 @@ files:
166
166
  - test/gouda/workload_test.rb
167
167
  - test/support/assert_helper.rb
168
168
  - tmp/.keep
169
- homepage: https://rubygems.org/gems/gouda
169
+ homepage: https://github.com/cheddar-me/gouda
170
170
  licenses:
171
171
  - MIT
172
172
  metadata:
173
- homepage_uri: https://rubygems.org/gems/gouda
174
173
  source_code_uri: https://github.com/cheddar-me/gouda
174
+ homepage_uri: https://github.com/cheddar-me/gouda
175
175
  changelog_uri: https://github.com/cheddar-me/gouda/CHANGELOG.md
176
176
  post_install_message:
177
177
  rdoc_options: []