gouda 0.1.3 → 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 +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +12 -0
- data/README.md +9 -6
- data/gouda.gemspec +4 -4
- data/lib/active_job/queue_adapters/gouda_adapter.rb +7 -5
- data/lib/gouda/adapter.rb +2 -1
- data/lib/gouda/bulk.rb +16 -0
- data/lib/gouda/queue_constraints.rb +25 -21
- data/lib/gouda/railtie.rb +3 -1
- data/lib/gouda/scheduler.rb +6 -0
- data/lib/gouda/version.rb +1 -1
- data/test/gouda/concurrency_extension_test.rb +160 -0
- data/test/gouda/gouda_test.rb +686 -0
- data/test/gouda/scheduler_test.rb +208 -0
- data/test/gouda/seconds_to_start_distribution.csv +280 -0
- data/test/gouda/test_helper.rb +70 -0
- data/test/gouda/worker_test.rb +116 -0
- data/test/gouda/workload_test.rb +67 -0
- data/test/support/assert_helper.rb +51 -0
- metadata +13 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0be474cea3bc87b9c0a41c9c654a31e4dc865e46876249a1db615bf56e494a40
|
4
|
+
data.tar.gz: '048574e736404c6c76f437fdfded93d7e810a23c83bb4f28ede8dffe9761a215'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 00e687d2fc384484cf2c07bf6c661e8d5ff1287a96f4e4f0abd98df2933538d7d81946ee51fe8e7abd6f2e5b285b6762aba231d359926ba5ee09d07a8b7e526c
|
7
|
+
data.tar.gz: cbd09ad83be4b9e6b9b35e20b2c80b7c8a5b8de1e20f8adfa6d0701d1aa31be7e5ea8edd2e64fab8f67e69cdc9757d1bcf79908fac9a81e9eccf9497f0a1c073
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.3.
|
1
|
+
3.3.3
|
data/CHANGELOG.md
CHANGED
@@ -15,3 +15,15 @@
|
|
15
15
|
## [0.1.3] - 2023-06-11
|
16
16
|
|
17
17
|
- Allow the Rails app to boot even if there is no database yet
|
18
|
+
|
19
|
+
## [0.1.4] - 2023-06-14
|
20
|
+
|
21
|
+
- Rescue NoDatabaseError at scheduler update.
|
22
|
+
- Include tests in gem, for sake of easier debugging.
|
23
|
+
- Reduce logging in local test runs.
|
24
|
+
- Bump local ruby version to 3.3.3
|
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
|
-
|
4
|
-
|
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
|
15
|
-
It
|
16
|
-
|
17
|
-
|
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,17 +7,17 @@ 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://
|
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"] =
|
16
|
-
spec.metadata["source_code_uri"] =
|
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
|
20
|
-
`git ls-files -z`.split("\x0")
|
20
|
+
`git ls-files -z`.split("\x0")
|
21
21
|
end
|
22
22
|
|
23
23
|
spec.add_dependency "activerecord", "~> 7"
|
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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:
|
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
|
-
|
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
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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.
|
53
|
-
string =
|
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
|
-
|
70
|
+
AnyQueue
|
67
71
|
elsif exclude_queues
|
68
|
-
|
72
|
+
ExceptQueueConstraint.new([queues])
|
69
73
|
else
|
70
|
-
|
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
|
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
|
data/lib/gouda/scheduler.rb
CHANGED
@@ -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
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "gouda/test_helper"
|
4
|
+
|
5
|
+
class GoudaConcurrencyExtensionTest < ActiveSupport::TestCase
|
6
|
+
include AssertHelper
|
7
|
+
class TestJobWithoutConcurrency < ActiveJob::Base
|
8
|
+
self.queue_adapter = Gouda::Adapter.new
|
9
|
+
end
|
10
|
+
|
11
|
+
class TestJobWithPerformConcurrency < ActiveJob::Base
|
12
|
+
self.queue_adapter = Gouda::Adapter.new
|
13
|
+
include Gouda::ActiveJobExtensions::Concurrency
|
14
|
+
gouda_control_concurrency_with(perform_limit: 1)
|
15
|
+
|
16
|
+
def perform(*args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
setup do
|
21
|
+
@adapter ||= Gouda::Adapter.new
|
22
|
+
Gouda::Railtie.initializers.each(&:run)
|
23
|
+
end
|
24
|
+
|
25
|
+
test "gouda_control_concurrency_with with just perform_limit sets a perform concurrency key and no enqueue concurrency key" do
|
26
|
+
job = TestJobWithPerformConcurrency.new
|
27
|
+
assert_nil job.enqueue_concurrency_key
|
28
|
+
assert job.execution_concurrency_key
|
29
|
+
end
|
30
|
+
|
31
|
+
test "gouda_control_concurrency_with with just perform_limit makes the perform concurrency key dependent on job params" do
|
32
|
+
job1 = TestJobWithPerformConcurrency.new(1, 2, :something)
|
33
|
+
assert job1.execution_concurrency_key
|
34
|
+
|
35
|
+
job2 = TestJobWithPerformConcurrency.new(1, 2, :something)
|
36
|
+
assert_equal job2.execution_concurrency_key, job1.execution_concurrency_key
|
37
|
+
|
38
|
+
job3 = TestJobWithPerformConcurrency.new(1, 2, :something_else)
|
39
|
+
refute_equal job3.execution_concurrency_key, job1.execution_concurrency_key
|
40
|
+
end
|
41
|
+
|
42
|
+
class TestJobWithCommonConcurrency < ActiveJob::Base
|
43
|
+
self.queue_adapter = Gouda::Adapter.new
|
44
|
+
include Gouda::ActiveJobExtensions::Concurrency
|
45
|
+
gouda_control_concurrency_with(total_limit: 1)
|
46
|
+
|
47
|
+
def perform(*args)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
test "gouda_control_concurrency_with with total_limit sets a perform concurrency key and an enqueue concurrency key" do
|
52
|
+
job = TestJobWithCommonConcurrency.new
|
53
|
+
assert job.enqueue_concurrency_key
|
54
|
+
assert job.execution_concurrency_key
|
55
|
+
end
|
56
|
+
|
57
|
+
test "gouda_control_concurrency_with with total_limit makes the perform concurrency key dependent on job params" do
|
58
|
+
job1 = TestJobWithCommonConcurrency.new(1, 2, :something)
|
59
|
+
assert job1.execution_concurrency_key
|
60
|
+
|
61
|
+
job2 = TestJobWithCommonConcurrency.new(1, 2, :something)
|
62
|
+
assert_equal job2.execution_concurrency_key, job1.execution_concurrency_key
|
63
|
+
|
64
|
+
job3 = TestJobWithCommonConcurrency.new(1, 2, :something_else)
|
65
|
+
refute_equal job3.execution_concurrency_key, job1.execution_concurrency_key
|
66
|
+
end
|
67
|
+
|
68
|
+
test "gouda_control_concurrency_with with total_limit makes the enqueue concurrency key dependent on job params" do
|
69
|
+
job1 = TestJobWithCommonConcurrency.new(1, 2, :something)
|
70
|
+
assert job1.enqueue_concurrency_key
|
71
|
+
|
72
|
+
job2 = TestJobWithCommonConcurrency.new(1, 2, :something)
|
73
|
+
assert_equal job2.enqueue_concurrency_key, job1.enqueue_concurrency_key
|
74
|
+
|
75
|
+
job3 = TestJobWithCommonConcurrency.new(1, 2, :something_else)
|
76
|
+
refute_equal job3.enqueue_concurrency_key, job1.enqueue_concurrency_key
|
77
|
+
end
|
78
|
+
|
79
|
+
class TestJobWithEnqueueConcurrency < ActiveJob::Base
|
80
|
+
self.queue_adapter = Gouda::Adapter.new
|
81
|
+
include Gouda::ActiveJobExtensions::Concurrency
|
82
|
+
gouda_control_concurrency_with(enqueue_limit: 1)
|
83
|
+
|
84
|
+
def perform(*args)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
test "gouda_control_concurrency_with with enqueue_limit sets a perform concurrency key and an enqueue concurrency key" do
|
89
|
+
job = TestJobWithEnqueueConcurrency.new
|
90
|
+
assert job.enqueue_concurrency_key
|
91
|
+
assert_nil job.execution_concurrency_key
|
92
|
+
end
|
93
|
+
|
94
|
+
test "gouda_control_concurrency_with with enqueue_limit makes the enqueue concurrency key dependent on job params" do
|
95
|
+
job1 = TestJobWithEnqueueConcurrency.new(1, 2, :something)
|
96
|
+
assert job1.enqueue_concurrency_key
|
97
|
+
|
98
|
+
job2 = TestJobWithEnqueueConcurrency.new(1, 2, :something)
|
99
|
+
assert_equal job2.enqueue_concurrency_key, job1.enqueue_concurrency_key
|
100
|
+
|
101
|
+
job3 = TestJobWithEnqueueConcurrency.new(1, 2, :something_else)
|
102
|
+
refute_equal job3.enqueue_concurrency_key, job1.enqueue_concurrency_key
|
103
|
+
end
|
104
|
+
|
105
|
+
class TestJobWithCustomKey < ActiveJob::Base
|
106
|
+
self.queue_adapter = Gouda::Adapter.new
|
107
|
+
include Gouda::ActiveJobExtensions::Concurrency
|
108
|
+
gouda_control_concurrency_with total_limit: 1, key: "42"
|
109
|
+
end
|
110
|
+
|
111
|
+
test "can use an arbitrary string as the custom key" do
|
112
|
+
job = TestJobWithCustomKey.new
|
113
|
+
assert_equal "42", job.enqueue_concurrency_key
|
114
|
+
assert_equal "42", job.execution_concurrency_key
|
115
|
+
end
|
116
|
+
|
117
|
+
class TestJobWithCustomKeyProc < ActiveJob::Base
|
118
|
+
self.queue_adapter = Gouda::Adapter.new
|
119
|
+
include Gouda::ActiveJobExtensions::Concurrency
|
120
|
+
gouda_control_concurrency_with total_limit: 1, key: -> { @ivar }
|
121
|
+
|
122
|
+
def initialize(...)
|
123
|
+
super
|
124
|
+
@ivar = "123"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
test "can use a proc that gets instance_exec'd as the custom key" do
|
129
|
+
job = TestJobWithCustomKeyProc.new
|
130
|
+
assert_equal "123", job.enqueue_concurrency_key
|
131
|
+
assert_equal "123", job.execution_concurrency_key
|
132
|
+
end
|
133
|
+
|
134
|
+
class TestJobWithWithUnconfiguredConcurrency < ActiveJob::Base
|
135
|
+
self.queue_adapter = Gouda::Adapter.new
|
136
|
+
include Gouda::ActiveJobExtensions::Concurrency
|
137
|
+
end
|
138
|
+
|
139
|
+
test "validates arguments" do
|
140
|
+
assert_raises ArgumentError do
|
141
|
+
TestJobWithWithUnconfiguredConcurrency.gouda_control_concurrency_with
|
142
|
+
end
|
143
|
+
|
144
|
+
assert_raises ArgumentError do
|
145
|
+
TestJobWithWithUnconfiguredConcurrency.gouda_control_concurrency_with total_limit: 2
|
146
|
+
end
|
147
|
+
|
148
|
+
assert_raises ArgumentError do
|
149
|
+
TestJobWithWithUnconfiguredConcurrency.gouda_control_concurrency_with perform_limit: 2
|
150
|
+
end
|
151
|
+
|
152
|
+
assert_raises ArgumentError do
|
153
|
+
TestJobWithWithUnconfiguredConcurrency.gouda_control_concurrency_with enqueue_limit: 2
|
154
|
+
end
|
155
|
+
|
156
|
+
assert_raises ArgumentError do
|
157
|
+
TestJobWithWithUnconfiguredConcurrency.gouda_control_concurrency_with total_limit: 2, bollocks: 4
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|