gouda 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,187 @@
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
+ gouda_control_concurrency_with(enqueue_limit: 1, key: -> { self.class.to_s })
28
+
29
+ retry_on StandardError, wait: :polynomially_longer, attempts: 5
30
+ retry_on Gouda::InterruptError, wait: 0, attempts: 5
31
+ retry_on MegaError, attempts: 3, wait: 0
32
+
33
+ def perform
34
+ raise MegaError.new "Kaboom!"
35
+ end
36
+ end
37
+
38
+ test "keeps re-enqueueing cron jobs after failed job (also with kwargs)" do
39
+ tab = {
40
+ second_minutely: {
41
+ cron: "*/1 * * * * *", # every second
42
+ class: "GoudaSchedulerTest::FailingJob"
43
+ }
44
+ }
45
+
46
+ assert_nothing_raised do
47
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
48
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
49
+ end
50
+
51
+ assert_equal 1, Gouda::Workload.enqueued.count
52
+ Gouda.worker_loop(n_threads: 1, check_shutdown: Gouda::TimerShutdownCheck.new(2))
53
+
54
+ refute_empty Gouda::Workload.enqueued
55
+ assert Gouda::Workload.count > 3
56
+ end
57
+
58
+ test "re-inserts the next subsequent job after executing the queued one" do
59
+ tab = {
60
+ second_minutely: {
61
+ cron: "*/1 * * * * *", # every second
62
+ class: "GoudaSchedulerTest::TestJob",
63
+ args: ["omg"],
64
+ kwargs: {mandatory: "WOOHOO", optional: "yeah"},
65
+ set: {priority: 150}
66
+ }
67
+ }
68
+
69
+ assert_nothing_raised do
70
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
71
+ end
72
+
73
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
74
+ 3.times do
75
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
76
+ end
77
+ end
78
+
79
+ job = Gouda::Workload.first
80
+ assert_equal 1, Gouda::Workload.count
81
+
82
+ sleep 1
83
+
84
+ assert_equal job, Gouda::Workload.checkout_and_lock_one(executing_on: "test")
85
+
86
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
87
+ 3.times do
88
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
89
+ end
90
+ end
91
+
92
+ assert_equal 2, Gouda::Workload.count
93
+ Gouda::Workload.all.each(&:perform_and_update_state!)
94
+ assert_equal 0, Gouda::Workload.errored.count
95
+ workload = Gouda::Workload.find_by(scheduler_key: "second_minutely_*/1 * * * * *_GoudaSchedulerTest::TestJob")
96
+ assert_equal 150, workload.priority
97
+ assert_equal ["omg", {
98
+ "optional" => "yeah",
99
+ "mandatory" => "WOOHOO",
100
+ "_aj_ruby2_keywords" => ["mandatory", "optional"]
101
+ }], workload.serialized_params["arguments"]
102
+ end
103
+
104
+ test "accepts crontab with nil args" do
105
+ tab = {
106
+ first_hourly: {
107
+ cron: "@hourly",
108
+ class: "GoudaSchedulerTest::TestJob",
109
+ args: [nil, nil]
110
+ }
111
+ }
112
+
113
+ assert_nothing_raised do
114
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
115
+ end
116
+
117
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
118
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
119
+ end
120
+
121
+ assert_equal [nil, nil], Gouda::Workload.first.serialized_params["arguments"]
122
+ end
123
+
124
+ test "is able to accept a crontab" do
125
+ tab = {
126
+ first_hourly: {
127
+ cron: "@hourly",
128
+ class: "GoudaSchedulerTest::TestJob",
129
+ args: ["one"],
130
+ kwargs: {mandatory: "Yeah"}
131
+ },
132
+ second_minutely: {
133
+ cron: "*/1 * * * *",
134
+ class: "GoudaSchedulerTest::TestJob",
135
+ args: [6],
136
+ kwargs: {mandatory: "Yeah", optional: "something"}
137
+ },
138
+ third_hourly_with_args_and_kwargs: {
139
+ cron: "@hourly",
140
+ class: "GoudaSchedulerTest::TestJob",
141
+ args: [1],
142
+ kwargs: {mandatory: "alright"}
143
+ },
144
+ interval: {
145
+ interval_seconds: 250,
146
+ class: "GoudaSchedulerTest::TestJob",
147
+ args: [4],
148
+ kwargs: {mandatory: "tasty"}
149
+ }
150
+ }
151
+ assert_nothing_raised do
152
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
153
+ end
154
+
155
+ travel_to Time.utc(2023, 6, 23, 20, 0)
156
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: 4) do
157
+ 3.times do
158
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
159
+ end
160
+ end
161
+
162
+ tab[:fifth] = {
163
+ cron: "@hourly",
164
+ class: "GoudaSchedulerTest::TestJob",
165
+ kwargs: {mandatory: "good"}
166
+ }
167
+
168
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
169
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: 1) do
170
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
171
+ end
172
+
173
+ assert tab.delete(:fifth)
174
+ Gouda::Scheduler.build_scheduler_entries_list!(tab)
175
+ assert_changes_by(-> { Gouda::Workload.count }, exactly: -1) do
176
+ Gouda::Scheduler.upsert_workloads_from_entries_list!
177
+ end
178
+
179
+ Gouda::Workload.all.each(&:perform_and_update_state!)
180
+ assert_equal 0, Gouda::Workload.errored.count
181
+ assert_equal [6, {
182
+ "optional" => "something",
183
+ "mandatory" => "Yeah",
184
+ "_aj_ruby2_keywords" => ["mandatory", "optional"]
185
+ }], Gouda::Workload.find_by(scheduler_key: "second_minutely_*/1 * * * *_GoudaSchedulerTest::TestJob").serialized_params["arguments"]
186
+ end
187
+ 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
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "gouda/test_helper"
4
+
5
+ class GoudaWorkloadTest < ActiveSupport::TestCase
6
+ include AssertHelper
7
+
8
+ class TestJob < ActiveJob::Base
9
+ self.queue_adapter = Gouda::Adapter.new
10
+
11
+ def perform
12
+ end
13
+ end
14
+
15
+ test "#schedule_now!" do
16
+ freeze_time
17
+ create_enqueued_workload
18
+ create_enqueued_workload
19
+ workload = create_enqueued_workload
20
+ workload.schedule_now!
21
+ assert_equal 3, Gouda::Workload.enqueued.size
22
+ assert_equal Time.now.utc, workload.scheduled_at
23
+ end
24
+
25
+ test "#mark_finished!" do
26
+ freeze_time
27
+ create_enqueued_workload
28
+ create_enqueued_workload
29
+ workload = create_enqueued_workload
30
+ workload.mark_finished!
31
+ assert_equal 2, Gouda::Workload.enqueued.size
32
+ assert_equal 1, Gouda::Workload.finished.size
33
+ assert_equal 1, Gouda::Workload.errored.size
34
+ assert_equal Time.now.utc, workload.execution_finished_at
35
+ end
36
+
37
+ def create_enqueued_workload
38
+ now = Time.now.utc
39
+ test_job = TestJob.new
40
+
41
+ Gouda::Workload.create!(
42
+ scheduled_at: now + 1.hour,
43
+ active_job_id: test_job.job_id,
44
+ execution_started_at: nil,
45
+ last_execution_heartbeat_at: nil,
46
+ queue_name: "default",
47
+ active_job_class_name: "GoudaWorkloadTest::TestJob",
48
+ serialized_params: {
49
+ job_id: test_job.job_id,
50
+ locale: "en",
51
+ priority: nil,
52
+ timezone: "UTC",
53
+ arguments: [],
54
+ job_class: test_job.class.to_s,
55
+ executions: 0,
56
+ queue_name: "default",
57
+ enqueued_at: now - 1.hour,
58
+ exception_executions: {}
59
+ },
60
+ state: "enqueued",
61
+ execution_concurrency_key: nil,
62
+ enqueue_concurrency_key: nil,
63
+ executing_on: "unit test",
64
+ position_in_bulk: 0
65
+ )
66
+ end
67
+ end