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.
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "gouda/test_helper"
4
+
5
+ class GoudaSchedulerTest < ActiveSupport::TestCase
6
+ include AssertHelper
7
+
8
+ setup do
9
+ Gouda::Workload.delete_all
10
+ Gouda::JobFuse.delete_all
11
+ end
12
+
13
+ class TestJob < ActiveJob::Base
14
+ self.queue_adapter = Gouda::Adapter.new
15
+
16
+ def perform(regular = "ok", mandatory:, optional: "hidden")
17
+ end
18
+ end
19
+
20
+ class FailingJob < ActiveJob::Base
21
+ include Gouda::ActiveJobExtensions::Concurrency
22
+ self.queue_adapter = Gouda::Adapter.new
23
+
24
+ class MegaError < StandardError
25
+ end
26
+
27
+ retry_on StandardError, wait: :polynomially_longer, attempts: 5
28
+ retry_on Gouda::InterruptError, wait: 0, attempts: 5
29
+ retry_on MegaError, attempts: 3, wait: 0
30
+
31
+ def perform
32
+ raise MegaError.new "Kaboom!"
33
+ end
34
+ end
35
+
36
+ test "keeps re-enqueueing cron jobs after failed job (also with kwargs)" do
37
+ tab = {
38
+ second_minutely: {
39
+ cron: "*/1 * * * * *", # every second
40
+ class: "GoudaSchedulerTest::FailingJob"
41
+ }
42
+ }
43
+
44
+ assert_nothing_raised do
45
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
46
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
47
+ end
48
+
49
+ assert_equal 1, Gouda::Workload.enqueued.count
50
+ Gouda.worker_loop(n_threads: 1, check_shutdown: Gouda::TimerShutdownCheck.new(2))
51
+
52
+ refute_empty Gouda::Workload.enqueued
53
+ assert Gouda::Workload.count > 3
54
+ end
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
+
79
+ test "re-inserts the next subsequent job after executing the queued one" do
80
+ tab = {
81
+ second_minutely: {
82
+ cron: "*/1 * * * * *", # every second
83
+ class: "GoudaSchedulerTest::TestJob",
84
+ args: ["omg"],
85
+ kwargs: {mandatory: "WOOHOO", optional: "yeah"},
86
+ set: {priority: 150}
87
+ }
88
+ }
89
+
90
+ assert_nothing_raised do
91
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
92
+ end
93
+
94
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
95
+ 3.times do
96
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
97
+ end
98
+ end
99
+
100
+ job = Gouda::Workload.first
101
+ assert_equal 1, Gouda::Workload.count
102
+
103
+ sleep 1
104
+
105
+ assert_equal job, Gouda::Workload.checkout_and_lock_one(executing_on: "test")
106
+
107
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
108
+ 3.times do
109
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
110
+ end
111
+ end
112
+
113
+ assert_equal 2, Gouda::Workload.count
114
+ Gouda::Workload.all.each(&:perform_and_update_state!)
115
+ assert_equal 0, Gouda::Workload.errored.count
116
+ workload = Gouda::Workload.find_by(scheduler_key: "second_minutely_*/1 * * * * *_GoudaSchedulerTest::TestJob")
117
+ assert_equal 150, workload.priority
118
+ assert_equal ["omg", {
119
+ "optional" => "yeah",
120
+ "mandatory" => "WOOHOO",
121
+ "_aj_ruby2_keywords" => ["mandatory", "optional"]
122
+ }], workload.serialized_params["arguments"]
123
+ end
124
+
125
+ test "accepts crontab with nil args" do
126
+ tab = {
127
+ first_hourly: {
128
+ cron: "@hourly",
129
+ class: "GoudaSchedulerTest::TestJob",
130
+ args: [nil, nil]
131
+ }
132
+ }
133
+
134
+ assert_nothing_raised do
135
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
136
+ end
137
+
138
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
139
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
140
+ end
141
+
142
+ assert_equal [nil, nil], Gouda::Workload.first.serialized_params["arguments"]
143
+ end
144
+
145
+ test "is able to accept a crontab" do
146
+ tab = {
147
+ first_hourly: {
148
+ cron: "@hourly",
149
+ class: "GoudaSchedulerTest::TestJob",
150
+ args: ["one"],
151
+ kwargs: {mandatory: "Yeah"}
152
+ },
153
+ second_minutely: {
154
+ cron: "*/1 * * * *",
155
+ class: "GoudaSchedulerTest::TestJob",
156
+ args: [6],
157
+ kwargs: {mandatory: "Yeah", optional: "something"}
158
+ },
159
+ third_hourly_with_args_and_kwargs: {
160
+ cron: "@hourly",
161
+ class: "GoudaSchedulerTest::TestJob",
162
+ args: [1],
163
+ kwargs: {mandatory: "alright"}
164
+ },
165
+ interval: {
166
+ interval_seconds: 250,
167
+ class: "GoudaSchedulerTest::TestJob",
168
+ args: [4],
169
+ kwargs: {mandatory: "tasty"}
170
+ }
171
+ }
172
+ assert_nothing_raised do
173
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
174
+ end
175
+
176
+ travel_to Time.utc(2023, 6, 23, 20, 0)
177
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: 4) do
178
+ 3.times do
179
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
180
+ end
181
+ end
182
+
183
+ tab[:fifth] = {
184
+ cron: "@hourly",
185
+ class: "GoudaSchedulerTest::TestJob",
186
+ kwargs: {mandatory: "good"}
187
+ }
188
+
189
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
190
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
191
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
192
+ end
193
+
194
+ assert tab.delete(:fifth)
195
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
196
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: -1) do
197
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
198
+ end
199
+
200
+ Gouda::Workload.all.each(&:perform_and_update_state!)
201
+ assert_equal 0, Gouda::Workload.errored.count
202
+ assert_equal [6, {
203
+ "optional" => "something",
204
+ "mandatory" => "Yeah",
205
+ "_aj_ruby2_keywords" => ["mandatory", "optional"]
206
+ }], Gouda::Workload.find_by(scheduler_key: "second_minutely_*/1 * * * *_GoudaSchedulerTest::TestJob").serialized_params["arguments"]
207
+ end
208
+ end
@@ -0,0 +1,280 @@
1
+ "count","seconds_to_execution"
2
+ 1,29146
3
+ 1,28471
4
+ 1,27796
5
+ 1,27121
6
+ 1,26446
7
+ 1,25771
8
+ 1,25096
9
+ 1,24421
10
+ 1,23746
11
+ 1,23071
12
+ 1,22396
13
+ 1,21720
14
+ 1,21045
15
+ 1,20370
16
+ 1,19695
17
+ 1,19020
18
+ 1,18345
19
+ 1,17670
20
+ 1,16995
21
+ 1,16320
22
+ 1,15645
23
+ 1,14970
24
+ 1,14295
25
+ 1,13620
26
+ 1,12945
27
+ 1,12270
28
+ 1,11595
29
+ 1,10920
30
+ 1,10245
31
+ 1,9570
32
+ 1,8895
33
+ 1,8220
34
+ 1,7545
35
+ 1,6870
36
+ 1,6195
37
+ 1,5915
38
+ 1,5752
39
+ 1,5520
40
+ 1,5438
41
+ 1,5190
42
+ 1,5114
43
+ 2,4929
44
+ 1,4845
45
+ 1,4609
46
+ 1,4489
47
+ 1,4373
48
+ 1,4170
49
+ 1,4133
50
+ 1,4106
51
+ 1,4090
52
+ 1,3956
53
+ 1,3945
54
+ 2,3834
55
+ 1,3598
56
+ 1,3495
57
+ 1,3383
58
+ 1,3377
59
+ 1,3341
60
+ 1,3302
61
+ 1,3174
62
+ 1,3139
63
+ 1,2987
64
+ 1,2869
65
+ 1,2861
66
+ 1,2820
67
+ 2,2805
68
+ 1,2328
69
+ 1,2179
70
+ 1,2145
71
+ 1,1982
72
+ 1,1947
73
+ 1,1678
74
+ 1,1673
75
+ 1,1470
76
+ 1,1459
77
+ 1,1441
78
+ 1,1318
79
+ 1,1270
80
+ 1,1170
81
+ 1,1123
82
+ 1,1079
83
+ 1,974
84
+ 1,970
85
+ 1,957
86
+ 1,893
87
+ 1,795
88
+ 1,778
89
+ 1,740
90
+ 1,478
91
+ 1,442
92
+ 1,262
93
+ 1,207
94
+ 4,197
95
+ 1,166
96
+ 1,131
97
+ 2,123
98
+ 1,122
99
+ 3,120
100
+ 2,119
101
+ 1,118
102
+ 1,117
103
+ 1,116
104
+ 1,114
105
+ 3,111
106
+ 1,110
107
+ 2,109
108
+ 1,108
109
+ 2,107
110
+ 5,106
111
+ 3,105
112
+ 3,104
113
+ 4,103
114
+ 1,102
115
+ 1,101
116
+ 1,100
117
+ 3,99
118
+ 1,98
119
+ 2,97
120
+ 2,96
121
+ 2,95
122
+ 2,94
123
+ 1,93
124
+ 1,92
125
+ 4,91
126
+ 1,90
127
+ 2,89
128
+ 2,88
129
+ 1,87
130
+ 1,86
131
+ 2,85
132
+ 3,84
133
+ 2,83
134
+ 1,82
135
+ 2,81
136
+ 1,80
137
+ 3,79
138
+ 1,78
139
+ 5,77
140
+ 1,76
141
+ 3,75
142
+ 2,74
143
+ 1,73
144
+ 3,72
145
+ 5,71
146
+ 1,70
147
+ 3,69
148
+ 4,68
149
+ 3,67
150
+ 2,66
151
+ 1,65
152
+ 3,64
153
+ 2,63
154
+ 2,62
155
+ 2,61
156
+ 1,60
157
+ 2,59
158
+ 1,58
159
+ 1,57
160
+ 3,56
161
+ 1,55
162
+ 8,54
163
+ 2,53
164
+ 2,52
165
+ 2,51
166
+ 1,50
167
+ 2,49
168
+ 2,48
169
+ 2,47
170
+ 3,46
171
+ 3,45
172
+ 4,43
173
+ 3,42
174
+ 4,41
175
+ 2,40
176
+ 4,39
177
+ 2,37
178
+ 2,35
179
+ 4,34
180
+ 2,33
181
+ 4,32
182
+ 4,31
183
+ 1,30
184
+ 2,29
185
+ 2,28
186
+ 5,27
187
+ 1,26
188
+ 1,25
189
+ 4,24
190
+ 4,23
191
+ 1,22
192
+ 3,21
193
+ 2,20
194
+ 2,19
195
+ 3,17
196
+ 2,16
197
+ 3,15
198
+ 3,13
199
+ 1,12
200
+ 2,11
201
+ 5,10
202
+ 3,9
203
+ 4,8
204
+ 4,7
205
+ 4,6
206
+ 3,5
207
+ 2,4
208
+ 3,3
209
+ 3,2
210
+ 2,1
211
+ 3,0
212
+ 2,-2
213
+ 1,-5
214
+ 2,-7
215
+ 1,-8
216
+ 3,-10
217
+ 1,-12
218
+ 3,-13
219
+ 2,-16
220
+ 1,-18
221
+ 1,-21
222
+ 2,-28
223
+ 2,-30
224
+ 2,-35
225
+ 1,-36
226
+ 3,-40
227
+ 1,-41
228
+ 1,-42
229
+ 3,-45
230
+ 1,-46
231
+ 2,-47
232
+ 1,-49
233
+ 3,-50
234
+ 2,-51
235
+ 1,-52
236
+ 2,-54
237
+ 2,-55
238
+ 1,-56
239
+ 2,-57
240
+ 2,-58
241
+ 2,-59
242
+ 2,-60
243
+ 2,-62
244
+ 5,-63
245
+ 2,-64
246
+ 3,-65
247
+ 1,-66
248
+ 2,-68
249
+ 4,-69
250
+ 2,-70
251
+ 1,-71
252
+ 3,-75
253
+ 1,-76
254
+ 1,-77
255
+ 1,-80
256
+ 1,-82
257
+ 2,-83
258
+ 2,-84
259
+ 1,-85
260
+ 1,-86
261
+ 2,-87
262
+ 1,-88
263
+ 1,-89
264
+ 2,-90
265
+ 1,-91
266
+ 2,-92
267
+ 3,-93
268
+ 1,-94
269
+ 4,-95
270
+ 1,-100
271
+ 2,-101
272
+ 2,-102
273
+ 1,-107
274
+ 1,-180
275
+ 1,-270
276
+ 2,-275
277
+ 1,-302
278
+ 2,-308
279
+ 1,-455
280
+ 1,-1129
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+ require "debug"
5
+ require "active_record"
6
+ require "active_job"
7
+ require "active_support/test_case"
8
+ require "minitest/autorun"
9
+ require "minitest"
10
+ require "support/assert_helper"
11
+ require_relative "../../lib/gouda"
12
+
13
+ class ActiveSupport::TestCase
14
+ SEED_DB_NAME = -> { "gouda_tests_%s" % Random.new(Minitest.seed).hex(4) }
15
+
16
+ def self.adapter
17
+ end
18
+
19
+ attr_reader :case_random
20
+
21
+ setup do
22
+ create_postgres_database_if_none
23
+ @adapter || Gouda::Adapter.new
24
+ @case_random = Random.new(Minitest.seed)
25
+ Gouda::Railtie.initializers.each(&:run)
26
+ ActiveJob::Base.logger = nil
27
+ Gouda.config.logger.level = 4
28
+ end
29
+
30
+ teardown do
31
+ truncate_test_tables
32
+ end
33
+
34
+ def create_postgres_database_if_none
35
+ ActiveRecord::Base.establish_connection(adapter: "postgresql", encoding: "unicode", database: SEED_DB_NAME.call)
36
+ ActiveRecord::Base.connection.execute("SELECT 1 FROM gouda_workloads")
37
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
38
+ create_postgres_database
39
+ retry
40
+ rescue ActiveRecord::StatementInvalid
41
+ ActiveRecord::Schema.define(version: 1) do |via_definer|
42
+ Gouda.create_tables(via_definer)
43
+ end
44
+ retry
45
+ end
46
+
47
+ def create_postgres_database
48
+ ActiveRecord::Migration.verbose = false
49
+ ActiveRecord::Base.establish_connection(adapter: "postgresql", database: "postgres")
50
+ ActiveRecord::Base.connection.create_database(SEED_DB_NAME.call, charset: :unicode)
51
+ ActiveRecord::Base.connection.close
52
+ ActiveRecord::Base.establish_connection(adapter: "postgresql", encoding: "unicode", database: SEED_DB_NAME.call)
53
+ end
54
+
55
+ def truncate_test_tables
56
+ ActiveRecord::Base.connection.execute("TRUNCATE TABLE gouda_workloads")
57
+ ActiveRecord::Base.connection.execute("TRUNCATE TABLE gouda_job_fuses")
58
+ end
59
+
60
+ def test_create_tables
61
+ ActiveRecord::Base.transaction do
62
+ ActiveRecord::Base.connection.execute("DROP TABLE gouda_workloads")
63
+ ActiveRecord::Base.connection.execute("DROP TABLE gouda_job_fuses")
64
+ # The adapter has to be in a variable as the schema definition is scoped to the migrator, not self
65
+ ActiveRecord::Schema.define(version: 1) do |via_definer|
66
+ Gouda.create_tables(via_definer)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "gouda/test_helper"
4
+
5
+ class GoudaWorkerTest < ActiveSupport::TestCase
6
+ include AssertHelper
7
+
8
+ # self.use_transactional_tests = false
9
+
10
+ # This is a bit obtuse but we need to be able to compute this value from inside the ActiveJob
11
+ # and the job won't have access to the test case object. To avoid mistakes, we can put it
12
+ # in a constant - which is lexically-scoped. We also evaluate it lazily since the Rails
13
+ # constant is possibly not initialized yet when this code loads.
14
+ # We need to include the PID in the path, because we might be running
15
+ # multiple test processes on the same box - and they might start touching
16
+ # files from each other.
17
+ PATH_TO_TEST_FILE = -> { File.expand_path(File.join("tmp", "#{Process.pid}-gouda-worker-test-output.bin")) }
18
+
19
+ class JobWithEnqueueKey < ActiveJob::Base
20
+ self.queue_adapter = :gouda
21
+
22
+ def enqueue_concurrency_key
23
+ "zombie"
24
+ end
25
+
26
+ def perform
27
+ end
28
+ end
29
+
30
+ class SimpleJob < ActiveJob::Base
31
+ self.queue_adapter = :gouda
32
+
33
+ def perform
34
+ File.open(PATH_TO_TEST_FILE.call, "a") do |f|
35
+ f.write("A")
36
+ end
37
+ end
38
+ end
39
+
40
+ test "runs workloads from all queues without a queue constraint" do
41
+ Gouda.in_bulk do
42
+ 6.times { SimpleJob.perform_later }
43
+ 6.times { SimpleJob.set(queue: "urgent").perform_later }
44
+ end
45
+ assert_equal 12, Gouda::Workload.where(state: "enqueued").count
46
+
47
+ Gouda.worker_loop(n_threads: 1, check_shutdown: Gouda::EmptyQueueShutdownCheck.new)
48
+
49
+ # Check that the side effects of the job have been performed - in this case, that
50
+ # 12 chars have been written into the file. Every job writes a char.
51
+ assert_equal 12, File.size(PATH_TO_TEST_FILE.call)
52
+
53
+ assert_equal 0, Gouda::Workload.where(state: "enqueued").count
54
+ assert_equal 12, Gouda::Workload.where(state: "finished").count
55
+ end
56
+
57
+ test "does not run workloads destined for a different queue" do
58
+ only_from_bravo = Gouda.parse_queue_constraint("queue-bravo")
59
+ bravo_queue_has_no_jobs = Gouda::EmptyQueueShutdownCheck.new(only_from_bravo)
60
+
61
+ Gouda.in_bulk do
62
+ 12.times { SimpleJob.set(queue: "queue-alpha").perform_later }
63
+ end
64
+ assert_equal 12, Gouda::Workload.where(state: "enqueued").count
65
+
66
+ Gouda.worker_loop(n_threads: 1, queue_constraint: only_from_bravo, check_shutdown: bravo_queue_has_no_jobs)
67
+
68
+ assert_equal 12, Gouda::Workload.where(state: "enqueued").count
69
+ assert_equal 0, Gouda::Workload.where(state: "finished").count
70
+ end
71
+
72
+ test "reaps zombie workloads and then executes replacements" do
73
+ past = 10.minutes.ago
74
+ 2.times do
75
+ zombie_job = JobWithEnqueueKey.new
76
+ Gouda::Workload.create!(
77
+ scheduled_at: past,
78
+ active_job_id: zombie_job.job_id,
79
+ execution_started_at: past,
80
+ last_execution_heartbeat_at: past,
81
+ queue_name: "default",
82
+ active_job_class_name: "GoudaWorkerTest::JobWithEnqueueKey",
83
+ serialized_params: {
84
+ job_id: zombie_job.job_id,
85
+ locale: "en",
86
+ priority: nil,
87
+ timezone: "UTC",
88
+ arguments: [],
89
+ job_class: zombie_job.class.to_s,
90
+ executions: 0,
91
+ queue_name: "default",
92
+ enqueued_at: past,
93
+ exception_executions: {}
94
+ },
95
+ state: "executing",
96
+ execution_concurrency_key: nil,
97
+ enqueue_concurrency_key: nil,
98
+ executing_on: "unit test",
99
+ position_in_bulk: 0
100
+ )
101
+ end
102
+
103
+ assert_equal 2, Gouda::Workload.where(state: "executing").count
104
+
105
+ Gouda.worker_loop(n_threads: 2, check_shutdown: Gouda::TimerShutdownCheck.new(2.0))
106
+
107
+ assert_equal 0, Gouda::Workload.where(state: "enqueued").count
108
+ assert_equal 0, Gouda::Workload.where(state: "executing").count
109
+
110
+ # The original 2 workloads got zombie-reaped and marked "finished".
111
+ # There should have been only 1 workload enqueued as a retry, because due to
112
+ # the enqueue concurrency key there would only be 1 allowed into the queue. It
113
+ # then should have executed normally and marked "finished".
114
+ assert_equal 3, Gouda::Workload.where(state: "finished").count
115
+ end
116
+ end