gouda 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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: []