gush 2.1.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +7 -28
- data/.rubocop.yml +232 -0
- data/CHANGELOG.md +1 -77
- data/README.md +156 -10
- data/gush.gemspec +13 -8
- data/lib/gush/cli/overview.rb +2 -0
- data/lib/gush/cli.rb +27 -4
- data/lib/gush/client.rb +115 -18
- data/lib/gush/graph.rb +2 -2
- data/lib/gush/job.rb +14 -3
- data/lib/gush/migrate/1_create_gush_workflows_created.rb +21 -0
- data/lib/gush/migration.rb +37 -0
- data/lib/gush/version.rb +3 -0
- data/lib/gush/worker.rb +1 -1
- data/lib/gush/workflow.rb +29 -16
- data/lib/gush.rb +1 -0
- data/spec/features/integration_spec.rb +7 -6
- data/spec/gush/client_spec.rb +316 -7
- data/spec/gush/job_spec.rb +48 -2
- data/spec/gush/migrate/1_create_gush_workflows_created_spec.rb +42 -0
- data/spec/gush/migration_spec.rb +23 -0
- data/spec/gush/workflow_spec.rb +120 -12
- data/spec/spec_helper.rb +29 -7
- metadata +64 -12
data/spec/gush/client_spec.rb
CHANGED
@@ -19,14 +19,43 @@ describe Gush::Client do
|
|
19
19
|
it "returns Workflow object" do
|
20
20
|
expected_workflow = TestWorkflow.create
|
21
21
|
workflow = client.find_workflow(expected_workflow.id)
|
22
|
+
dependencies = workflow.dependencies
|
22
23
|
|
23
24
|
expect(workflow.id).to eq(expected_workflow.id)
|
25
|
+
expect(workflow.persisted).to eq(true)
|
24
26
|
expect(workflow.jobs.map(&:name)).to match_array(expected_workflow.jobs.map(&:name))
|
27
|
+
expect(workflow.dependencies).to eq(dependencies)
|
25
28
|
end
|
26
29
|
|
27
30
|
context "when workflow has parameters" do
|
28
31
|
it "returns Workflow object" do
|
29
|
-
expected_workflow = ParameterTestWorkflow.create(true)
|
32
|
+
expected_workflow = ParameterTestWorkflow.create(true, kwarg: 123)
|
33
|
+
workflow = client.find_workflow(expected_workflow.id)
|
34
|
+
|
35
|
+
expect(workflow.id).to eq(expected_workflow.id)
|
36
|
+
expect(workflow.arguments).to eq([true])
|
37
|
+
expect(workflow.kwargs).to eq({ kwarg: 123 })
|
38
|
+
expect(workflow.jobs.map(&:name)).to match_array(expected_workflow.jobs.map(&:name))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when workflow has globals" do
|
43
|
+
it "returns Workflow object" do
|
44
|
+
expected_workflow = TestWorkflow.create(globals: { global1: 'foo' })
|
45
|
+
workflow = client.find_workflow(expected_workflow.id)
|
46
|
+
|
47
|
+
expect(workflow.id).to eq(expected_workflow.id)
|
48
|
+
expect(workflow.globals[:global1]).to eq('foo')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "when workflow was persisted without job_klasses" do
|
53
|
+
it "returns Workflow object" do
|
54
|
+
expected_workflow = TestWorkflow.create
|
55
|
+
|
56
|
+
json = Gush::JSON.encode(expected_workflow.to_hash.except(:job_klasses))
|
57
|
+
redis.set("gush.workflows.#{expected_workflow.id}", json)
|
58
|
+
|
30
59
|
workflow = client.find_workflow(expected_workflow.id)
|
31
60
|
|
32
61
|
expect(workflow.id).to eq(expected_workflow.id)
|
@@ -37,6 +66,18 @@ describe Gush::Client do
|
|
37
66
|
end
|
38
67
|
|
39
68
|
describe "#start_workflow" do
|
69
|
+
context "when there is wait parameter configured" do
|
70
|
+
let(:freeze_time) { Time.utc(2023, 01, 21, 14, 36, 0) }
|
71
|
+
|
72
|
+
it "schedules job execution" do
|
73
|
+
travel_to freeze_time do
|
74
|
+
workflow = WaitableTestWorkflow.create
|
75
|
+
client.start_workflow(workflow)
|
76
|
+
expect(Gush::Worker).to have_a_job_enqueued_at(workflow.id, job_with_id("Prepare"), 5.minutes)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
40
81
|
it "enqueues next jobs from the workflow" do
|
41
82
|
workflow = TestWorkflow.create
|
42
83
|
expect {
|
@@ -70,42 +111,222 @@ describe Gush::Client do
|
|
70
111
|
end
|
71
112
|
end
|
72
113
|
|
114
|
+
describe "#next_free_job_id" do
|
115
|
+
it "returns an id" do
|
116
|
+
expect(client.next_free_job_id('123', Prepare.to_s)).to match(/^\h{8}-\h{4}-(\h{4})-\h{4}-\h{12}$/)
|
117
|
+
end
|
118
|
+
|
119
|
+
it "returns an id that doesn't match an existing job id" do
|
120
|
+
workflow = TestWorkflow.create
|
121
|
+
job = workflow.jobs.first
|
122
|
+
|
123
|
+
second_try_id = '1234'
|
124
|
+
allow(SecureRandom).to receive(:uuid).and_return(job.id, second_try_id)
|
125
|
+
|
126
|
+
expect(client.next_free_job_id(workflow.id, job.class.to_s)).to eq(second_try_id)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "#next_free_workflow_id" do
|
131
|
+
it "returns an id" do
|
132
|
+
expect(client.next_free_workflow_id).to match(/^\h{8}-\h{4}-(\h{4})-\h{4}-\h{12}$/)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "returns an id that doesn't match an existing workflow id" do
|
136
|
+
workflow = TestWorkflow.create
|
137
|
+
|
138
|
+
second_try_id = '1234'
|
139
|
+
allow(SecureRandom).to receive(:uuid).and_return(workflow.id, second_try_id)
|
140
|
+
|
141
|
+
expect(client.next_free_workflow_id).to eq(second_try_id)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
73
145
|
describe "#persist_workflow" do
|
74
146
|
it "persists JSON dump of the Workflow and its jobs" do
|
75
147
|
job = double("job", to_json: 'json')
|
76
148
|
workflow = double("workflow", id: 'abcd', jobs: [job, job, job], to_json: '"json"')
|
77
|
-
expect(client).to receive(:persist_job).exactly(3).times.with(workflow.id, job)
|
149
|
+
expect(client).to receive(:persist_job).exactly(3).times.with(workflow.id, job, expires_at: nil)
|
78
150
|
expect(workflow).to receive(:mark_as_persisted)
|
79
151
|
client.persist_workflow(workflow)
|
80
152
|
expect(redis.keys("gush.workflows.abcd").length).to eq(1)
|
81
153
|
end
|
154
|
+
|
155
|
+
it "sets created_at index" do
|
156
|
+
workflow = double("workflow", id: 'abcd', jobs: [], to_json: '"json"')
|
157
|
+
expect(workflow).to receive(:mark_as_persisted).twice
|
158
|
+
|
159
|
+
freeze_time = Time.now.round # travel_to doesn't support fractions of a second
|
160
|
+
travel_to(freeze_time) do
|
161
|
+
client.persist_workflow(workflow)
|
162
|
+
end
|
163
|
+
|
164
|
+
expect(redis.zrange("gush.idx.workflows.created_at", 0, -1, with_scores: true))
|
165
|
+
.to eq([[workflow.id, freeze_time.to_f]])
|
166
|
+
|
167
|
+
# Persisting the workflow again should not affect its created_at index score
|
168
|
+
client.persist_workflow(workflow)
|
169
|
+
expect(redis.zrange("gush.idx.workflows.created_at", 0, -1, with_scores: true))
|
170
|
+
.to eq([[workflow.id, freeze_time.to_f]])
|
171
|
+
end
|
172
|
+
|
173
|
+
it "sets expires_at index when there is a ttl configured" do
|
174
|
+
allow_any_instance_of(Gush::Configuration).to receive(:ttl).and_return(1000)
|
175
|
+
|
176
|
+
workflow = double("workflow", id: 'abcd', jobs: [], to_json: '"json"')
|
177
|
+
expect(workflow).to receive(:mark_as_persisted).twice
|
178
|
+
|
179
|
+
freeze_time = Time.now.round # travel_to doesn't support fractions of a second
|
180
|
+
travel_to(freeze_time) do
|
181
|
+
client.persist_workflow(workflow)
|
182
|
+
end
|
183
|
+
|
184
|
+
expires_at = freeze_time + 1000
|
185
|
+
expect(redis.zrange("gush.idx.workflows.expires_at", 0, -1, with_scores: true))
|
186
|
+
.to eq([[workflow.id, expires_at.to_f]])
|
187
|
+
|
188
|
+
# Persisting the workflow again should not affect its expires_at index score
|
189
|
+
client.persist_workflow(workflow)
|
190
|
+
expect(redis.zrange("gush.idx.workflows.expires_at", 0, -1, with_scores: true))
|
191
|
+
.to eq([[workflow.id, expires_at.to_f]])
|
192
|
+
end
|
193
|
+
|
194
|
+
it "does not set expires_at index when there is no ttl configured" do
|
195
|
+
workflow = double("workflow", id: 'abcd', jobs: [], to_json: '"json"')
|
196
|
+
expect(workflow).to receive(:mark_as_persisted)
|
197
|
+
client.persist_workflow(workflow)
|
198
|
+
|
199
|
+
expect(redis.zcard("gush.idx.workflows.expires_at")).to eq(0)
|
200
|
+
end
|
201
|
+
|
202
|
+
it "does not set expires_at index when updating a pre-existing workflow without a ttl" do
|
203
|
+
allow_any_instance_of(Gush::Configuration).to receive(:ttl).and_return(1000)
|
204
|
+
|
205
|
+
workflow = double("workflow", id: 'abcd', jobs: [], to_json: '"json"')
|
206
|
+
expect(workflow).to receive(:mark_as_persisted).twice
|
207
|
+
|
208
|
+
client.persist_workflow(workflow)
|
209
|
+
|
210
|
+
client.expire_workflow(workflow, -1)
|
211
|
+
expect(redis.zcard("gush.idx.workflows.expires_at")).to eq(0)
|
212
|
+
|
213
|
+
client.persist_workflow(workflow)
|
214
|
+
expect(redis.zcard("gush.idx.workflows.expires_at")).to eq(0)
|
215
|
+
end
|
216
|
+
|
217
|
+
it "does not change expires_at index when updating a pre-existing workflow with a non-standard ttl" do
|
218
|
+
allow_any_instance_of(Gush::Configuration).to receive(:ttl).and_return(1000)
|
219
|
+
|
220
|
+
workflow = double("workflow", id: 'abcd', jobs: [], to_json: '"json"')
|
221
|
+
expect(workflow).to receive(:mark_as_persisted).twice
|
222
|
+
|
223
|
+
freeze_time = Time.now.round # travel_to doesn't support fractions of a second
|
224
|
+
travel_to(freeze_time) do
|
225
|
+
client.persist_workflow(workflow)
|
226
|
+
|
227
|
+
expires_at = freeze_time.to_i + 1234
|
228
|
+
client.expire_workflow(workflow, 1234)
|
229
|
+
expect(redis.zscore("gush.idx.workflows.expires_at", workflow.id)).to eq(expires_at)
|
230
|
+
|
231
|
+
client.persist_workflow(workflow)
|
232
|
+
expect(redis.zscore("gush.idx.workflows.expires_at", workflow.id)).to eq(expires_at)
|
233
|
+
end
|
234
|
+
end
|
82
235
|
end
|
83
236
|
|
84
237
|
describe "#destroy_workflow" do
|
85
238
|
it "removes all Redis keys related to the workflow" do
|
239
|
+
allow_any_instance_of(Gush::Configuration).to receive(:ttl).and_return(1000)
|
240
|
+
|
86
241
|
workflow = TestWorkflow.create
|
87
242
|
expect(redis.keys("gush.workflows.#{workflow.id}").length).to eq(1)
|
88
243
|
expect(redis.keys("gush.jobs.#{workflow.id}.*").length).to eq(5)
|
244
|
+
expect(redis.zcard("gush.idx.workflows.created_at")).to eq(1)
|
245
|
+
expect(redis.zcard("gush.idx.workflows.expires_at")).to eq(1)
|
246
|
+
expect(redis.zcard("gush.idx.jobs.expires_at")).to eq(5)
|
89
247
|
|
90
248
|
client.destroy_workflow(workflow)
|
91
249
|
|
92
250
|
expect(redis.keys("gush.workflows.#{workflow.id}").length).to eq(0)
|
93
251
|
expect(redis.keys("gush.jobs.#{workflow.id}.*").length).to eq(0)
|
252
|
+
expect(redis.zcard("gush.idx.workflows.created_at")).to eq(0)
|
253
|
+
expect(redis.zcard("gush.idx.workflows.expires_at")).to eq(0)
|
254
|
+
expect(redis.zcard("gush.idx.jobs.expires_at")).to eq(0)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
describe "#expire_workflows" do
|
259
|
+
it "removes auto-expired workflows" do
|
260
|
+
allow_any_instance_of(Gush::Configuration).to receive(:ttl).and_return(1000)
|
261
|
+
|
262
|
+
workflow = TestWorkflow.create
|
263
|
+
|
264
|
+
# before workflow's expiration time
|
265
|
+
client.expire_workflows
|
266
|
+
|
267
|
+
expect(redis.keys("gush.workflows.*").length).to eq(1)
|
268
|
+
|
269
|
+
# after workflow's expiration time
|
270
|
+
client.expire_workflows(Time.now.to_f + 1001)
|
271
|
+
|
272
|
+
expect(redis.keys("gush.workflows.#{workflow.id}").length).to eq(0)
|
273
|
+
expect(redis.keys("gush.jobs.#{workflow.id}.*").length).to eq(0)
|
274
|
+
expect(redis.zcard("gush.idx.workflows.created_at")).to eq(0)
|
275
|
+
expect(redis.zcard("gush.idx.workflows.expires_at")).to eq(0)
|
276
|
+
expect(redis.zcard("gush.idx.jobs.expires_at")).to eq(0)
|
277
|
+
end
|
278
|
+
|
279
|
+
it "removes manually-expired workflows" do
|
280
|
+
workflow = TestWorkflow.create
|
281
|
+
|
282
|
+
# workflow hasn't been expired
|
283
|
+
client.expire_workflows(Time.now.to_f + 100_000)
|
284
|
+
|
285
|
+
expect(redis.keys("gush.workflows.*").length).to eq(1)
|
286
|
+
|
287
|
+
client.expire_workflow(workflow, 10)
|
288
|
+
|
289
|
+
# after workflow's expiration time
|
290
|
+
client.expire_workflows(Time.now.to_f + 20)
|
291
|
+
|
292
|
+
expect(redis.keys("gush.workflows.#{workflow.id}").length).to eq(0)
|
293
|
+
expect(redis.keys("gush.jobs.#{workflow.id}.*").length).to eq(0)
|
294
|
+
expect(redis.zcard("gush.idx.workflows.created_at")).to eq(0)
|
295
|
+
expect(redis.zcard("gush.idx.workflows.expires_at")).to eq(0)
|
296
|
+
expect(redis.zcard("gush.idx.jobs.expires_at")).to eq(0)
|
94
297
|
end
|
95
298
|
end
|
96
299
|
|
97
300
|
describe "#expire_workflow" do
|
98
301
|
let(:ttl) { 2000 }
|
99
302
|
|
100
|
-
it "sets
|
303
|
+
it "sets an expiration time for the workflow" do
|
304
|
+
workflow = TestWorkflow.create
|
305
|
+
|
306
|
+
freeze_time = Time.now.round # travel_to doesn't support fractions of a second
|
307
|
+
expires_at = freeze_time.to_f + ttl
|
308
|
+
travel_to(freeze_time) do
|
309
|
+
client.expire_workflow(workflow, ttl)
|
310
|
+
end
|
311
|
+
|
312
|
+
expect(redis.zscore("gush.idx.workflows.expires_at", workflow.id)).to eq(expires_at)
|
313
|
+
|
314
|
+
workflow.jobs.each do |job|
|
315
|
+
expect(redis.zscore("gush.idx.jobs.expires_at", "#{workflow.id}.#{job.klass}")).to eq(expires_at)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
it "clears an expiration time for the workflow when given -1" do
|
101
320
|
workflow = TestWorkflow.create
|
102
321
|
|
103
|
-
client.expire_workflow(workflow,
|
322
|
+
client.expire_workflow(workflow, 100)
|
323
|
+
expect(redis.zscore("gush.idx.workflows.expires_at", workflow.id)).to be > 0
|
104
324
|
|
105
|
-
|
325
|
+
client.expire_workflow(workflow, -1)
|
326
|
+
expect(redis.zscore("gush.idx.workflows.expires_at", workflow.id)).to eq(nil)
|
106
327
|
|
107
328
|
workflow.jobs.each do |job|
|
108
|
-
expect(redis.
|
329
|
+
expect(redis.zscore("gush.idx.jobs.expires_at", "#{workflow.id}.#{job.klass}")).to eq(nil)
|
109
330
|
end
|
110
331
|
end
|
111
332
|
end
|
@@ -113,11 +334,99 @@ describe Gush::Client do
|
|
113
334
|
describe "#persist_job" do
|
114
335
|
it "persists JSON dump of the job in Redis" do
|
115
336
|
|
116
|
-
job = BobJob.new(name: 'bob')
|
337
|
+
job = BobJob.new(name: 'bob', id: 'abcd123')
|
117
338
|
|
118
339
|
client.persist_job('deadbeef', job)
|
119
340
|
expect(redis.keys("gush.jobs.deadbeef.*").length).to eq(1)
|
120
341
|
end
|
342
|
+
|
343
|
+
it "sets expires_at index when expires_at is provided" do
|
344
|
+
job = BobJob.new(name: 'bob', id: 'abcd123')
|
345
|
+
|
346
|
+
freeze_time = Time.now.round # travel_to doesn't support fractions of a second
|
347
|
+
expires_at = freeze_time.to_f + 1000
|
348
|
+
|
349
|
+
travel_to(freeze_time) do
|
350
|
+
client.persist_job('deadbeef', job, expires_at: expires_at)
|
351
|
+
end
|
352
|
+
|
353
|
+
expect(redis.zrange("gush.idx.jobs.expires_at", 0, -1, with_scores: true))
|
354
|
+
.to eq([["deadbeef.#{job.klass}", expires_at]])
|
355
|
+
|
356
|
+
# Persisting the workflow again should not affect its expires_at index score
|
357
|
+
client.persist_job('deadbeef', job)
|
358
|
+
expect(redis.zrange("gush.idx.jobs.expires_at", 0, -1, with_scores: true))
|
359
|
+
.to eq([["deadbeef.#{job.klass}", expires_at]])
|
360
|
+
end
|
361
|
+
|
362
|
+
it "does not set expires_at index when there is no ttl configured" do
|
363
|
+
job = BobJob.new(name: 'bob', id: 'abcd123')
|
364
|
+
client.persist_job('deadbeef', job)
|
365
|
+
|
366
|
+
expect(redis.zcard("gush.idx.jobs.expires_at")).to eq(0)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
describe "#workflow_ids" do
|
371
|
+
it "returns a page of registered workflow ids" do
|
372
|
+
workflow = TestWorkflow.create
|
373
|
+
ids = client.workflow_ids
|
374
|
+
expect(ids).to eq([workflow.id])
|
375
|
+
end
|
376
|
+
|
377
|
+
it "sorts workflow ids by created time or reverse created time" do
|
378
|
+
ids = 3.times.map { TestWorkflow.create }.map(&:id)
|
379
|
+
|
380
|
+
expect(client.workflow_ids).to eq(ids)
|
381
|
+
expect(client.workflow_ids(order: :asc)).to eq(ids)
|
382
|
+
expect(client.workflow_ids(order: :desc)).to eq(ids.reverse)
|
383
|
+
end
|
384
|
+
|
385
|
+
it "supports start and stop params" do
|
386
|
+
ids = 3.times.map { TestWorkflow.create }.map(&:id)
|
387
|
+
|
388
|
+
expect(client.workflow_ids(0, 1)).to eq(ids.slice(0..1))
|
389
|
+
expect(client.workflow_ids(1, 1)).to eq(ids.slice(1..1))
|
390
|
+
expect(client.workflow_ids(1, 10)).to eq(ids.slice(1..2))
|
391
|
+
expect(client.workflow_ids(0, -1)).to eq(ids)
|
392
|
+
end
|
393
|
+
|
394
|
+
it "supports start and stop params using created timestamps" do
|
395
|
+
times = [100, 200, 300]
|
396
|
+
ids = []
|
397
|
+
|
398
|
+
times.each do |t|
|
399
|
+
travel_to Time.at(t) do
|
400
|
+
ids << TestWorkflow.create.id
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
expect(client.workflow_ids(0, 1, by_ts: true)).to be_empty
|
405
|
+
expect(client.workflow_ids(50, 150, by_ts: true)).to eq(ids.slice(0..0))
|
406
|
+
expect(client.workflow_ids(150, 50, by_ts: true, order: :desc)).to eq(ids.slice(0..0))
|
407
|
+
expect(client.workflow_ids("-inf", "inf", by_ts: true)).to eq(ids)
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
describe "#workflows" do
|
412
|
+
it "returns a page of registered workflows" do
|
413
|
+
workflow = TestWorkflow.create
|
414
|
+
expect(client.workflows.map(&:id)).to eq([workflow.id])
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
describe "#workflows_count" do
|
419
|
+
it "returns a count of registered workflows" do
|
420
|
+
allow_any_instance_of(Gush::Configuration).to receive(:ttl).and_return(1000)
|
421
|
+
|
422
|
+
expect(client.workflows_count).to eq(0)
|
423
|
+
|
424
|
+
workflow = TestWorkflow.create
|
425
|
+
expect(client.workflows_count).to eq(1)
|
426
|
+
|
427
|
+
client.expire_workflows(Time.now.to_f + 1001)
|
428
|
+
expect(client.workflows_count).to eq(0)
|
429
|
+
end
|
121
430
|
end
|
122
431
|
|
123
432
|
describe "#all_workflows" do
|
data/spec/gush/job_spec.rb
CHANGED
@@ -49,6 +49,45 @@ describe Gush::Job do
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
+
describe "#enqueue_worker!" do
|
53
|
+
it "enqueues the job using Gush::Worker" do
|
54
|
+
job = described_class.new(name: "a-job", workflow_id: 123)
|
55
|
+
|
56
|
+
expect {
|
57
|
+
job.enqueue_worker!
|
58
|
+
}.to change{ActiveJob::Base.queue_adapter.enqueued_jobs.size}.from(0).to(1)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "handles ActiveJob.set options" do
|
62
|
+
freeze_time = Time.utc(2023, 01, 21, 14, 36, 0)
|
63
|
+
|
64
|
+
travel_to freeze_time do
|
65
|
+
job = described_class.new(name: "a-job", workflow_id: 123)
|
66
|
+
job.enqueue_worker!(wait_until: freeze_time + 5.minutes)
|
67
|
+
expect(Gush::Worker).to have_a_job_enqueued_at(123, job_with_id(job.class.name), 5.minutes)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#worker_options" do
|
73
|
+
it "returns a blank options hash by default" do
|
74
|
+
job = described_class.new
|
75
|
+
expect(job.worker_options).to eq({})
|
76
|
+
end
|
77
|
+
|
78
|
+
it "returns a hash with the queue setting" do
|
79
|
+
job = described_class.new
|
80
|
+
job.queue = 'my-queue'
|
81
|
+
expect(job.worker_options).to eq({ queue: 'my-queue' })
|
82
|
+
end
|
83
|
+
|
84
|
+
it "returns a hash with the wait setting" do
|
85
|
+
job = described_class.new
|
86
|
+
job.wait = 123
|
87
|
+
expect(job.worker_options).to eq({ wait: 123 })
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
52
91
|
describe "#start!" do
|
53
92
|
it "resets flags and marks as running" do
|
54
93
|
job = described_class.new(name: "a-job")
|
@@ -70,7 +109,13 @@ describe Gush::Job do
|
|
70
109
|
describe "#as_json" do
|
71
110
|
context "finished and enqueued set to true" do
|
72
111
|
it "returns correct hash" do
|
73
|
-
job = described_class.new(
|
112
|
+
job = described_class.new(
|
113
|
+
workflow_id: 123,
|
114
|
+
id: '702bced5-bb72-4bba-8f6f-15a3afa358bd',
|
115
|
+
finished_at: 123,
|
116
|
+
enqueued_at: 120,
|
117
|
+
wait: 300
|
118
|
+
)
|
74
119
|
expected = {
|
75
120
|
id: '702bced5-bb72-4bba-8f6f-15a3afa358bd',
|
76
121
|
klass: "Gush::Job",
|
@@ -83,7 +128,8 @@ describe Gush::Job do
|
|
83
128
|
params: {},
|
84
129
|
queue: nil,
|
85
130
|
output_payload: nil,
|
86
|
-
workflow_id: 123
|
131
|
+
workflow_id: 123,
|
132
|
+
wait: 300
|
87
133
|
}
|
88
134
|
expect(job.as_json).to eq(expected)
|
89
135
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'gush/migrate/1_create_gush_workflows_created'
|
3
|
+
|
4
|
+
describe Gush::IndexWorkflowsByCreatedAtAndExpiresAt do
|
5
|
+
|
6
|
+
describe "#up" do
|
7
|
+
it "adds existing workflows to created_at index, but not expires_at index" do
|
8
|
+
TestWorkflow.create
|
9
|
+
redis.del("gush.idx.workflows.created_at")
|
10
|
+
|
11
|
+
allow_any_instance_of(Gush::Configuration).to receive(:ttl).and_return(1000)
|
12
|
+
|
13
|
+
subject.migrate
|
14
|
+
|
15
|
+
expect(redis.zcard("gush.idx.workflows.created_at")).to eq(1)
|
16
|
+
expect(redis.zcard("gush.idx.workflows.expires_at")).to eq(0)
|
17
|
+
expect(redis.zcard("gush.idx.jobs.expires_at")).to eq(0)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "adds expiring workflows to expires_at index" do
|
21
|
+
workflow = TestWorkflow.create
|
22
|
+
redis.del("gush.idx.workflows.created_at")
|
23
|
+
|
24
|
+
freeze_time = Time.now.round # travel_to doesn't support fractions of a second
|
25
|
+
travel_to(freeze_time) do
|
26
|
+
redis.expire("gush.workflows.#{workflow.id}", 1234)
|
27
|
+
expires_at = freeze_time.to_f + 1234
|
28
|
+
|
29
|
+
allow_any_instance_of(Gush::Configuration).to receive(:ttl).and_return(1000)
|
30
|
+
|
31
|
+
subject.migrate
|
32
|
+
|
33
|
+
expect(redis.ttl("gush.workflows.#{workflow.id}")).to eq(-1)
|
34
|
+
expect(redis.ttl("gush.jobs.#{workflow.id}.#{workflow.jobs.first.class.name}")).to eq(-1)
|
35
|
+
|
36
|
+
expect(redis.zrange("gush.idx.workflows.expires_at", 0, -1, with_scores: true))
|
37
|
+
.to eq([[workflow.id, expires_at]])
|
38
|
+
expect(redis.zcard("gush.idx.jobs.expires_at")).to eq(5)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Gush::Migration do
|
4
|
+
|
5
|
+
describe "#migrate" do
|
6
|
+
it "applies a migration once" do
|
7
|
+
class TestMigration < Gush::Migration
|
8
|
+
def self.version
|
9
|
+
123
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
migration = TestMigration.new
|
14
|
+
expect(migration).to receive(:up).once
|
15
|
+
|
16
|
+
expect(migration.migrated?).to be(false)
|
17
|
+
migration.migrate
|
18
|
+
|
19
|
+
expect(migration.migrated?).to be(true)
|
20
|
+
migration.migrate
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/spec/gush/workflow_spec.rb
CHANGED
@@ -15,16 +15,68 @@ describe Gush::Workflow do
|
|
15
15
|
expect_any_instance_of(klass).to receive(:configure).with("arg1", "arg2")
|
16
16
|
klass.new("arg1", "arg2")
|
17
17
|
end
|
18
|
-
end
|
19
18
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
19
|
+
it "passes constructor keyword arguments to the method" do
|
20
|
+
klass = Class.new(Gush::Workflow) do
|
21
|
+
def configure(*args, **kwargs)
|
22
|
+
run FetchFirstJob
|
23
|
+
run PersistFirstJob, after: FetchFirstJob
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
expect_any_instance_of(klass).to receive(:configure).with("arg1", "arg2", arg3: 123)
|
28
|
+
klass.new("arg1", "arg2", arg3: 123)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "accepts globals" do
|
32
|
+
flow = TestWorkflow.new(globals: { global1: 'foo' })
|
33
|
+
expect(flow.globals[:global1]).to eq('foo')
|
34
|
+
end
|
35
|
+
|
36
|
+
it "accepts internal_state" do
|
37
|
+
flow = TestWorkflow.new
|
38
|
+
|
39
|
+
internal_state = {
|
40
|
+
id: flow.id,
|
41
|
+
jobs: flow.jobs,
|
42
|
+
dependencies: flow.dependencies,
|
43
|
+
persisted: true,
|
44
|
+
stopped: true
|
45
|
+
}
|
46
|
+
|
47
|
+
flow_copy = TestWorkflow.new(internal_state: internal_state)
|
48
|
+
|
49
|
+
expect(flow_copy.id).to eq(flow.id)
|
50
|
+
expect(flow_copy.jobs).to eq(flow.jobs)
|
51
|
+
expect(flow_copy.dependencies).to eq(flow.dependencies)
|
52
|
+
expect(flow_copy.persisted).to eq(true)
|
53
|
+
expect(flow_copy.stopped).to eq(true)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "does not call #configure if needs_setup is false" do
|
57
|
+
INTERNAL_SETUP_SPY = double('configure spy')
|
58
|
+
klass = Class.new(Gush::Workflow) do
|
59
|
+
def configure(*args)
|
60
|
+
INTERNAL_SETUP_SPY.some_method
|
61
|
+
end
|
27
62
|
end
|
63
|
+
|
64
|
+
expect(INTERNAL_SETUP_SPY).not_to receive(:some_method)
|
65
|
+
|
66
|
+
flow = TestWorkflow.new(internal_state: { needs_setup: false })
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#find" do
|
71
|
+
it "fiends a workflow by id" do
|
72
|
+
expect(Gush::Workflow.find(subject.id).id).to eq(subject.id)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#page" do
|
77
|
+
it "returns a page of registered workflows" do
|
78
|
+
flow = TestWorkflow.create
|
79
|
+
expect(Gush::Workflow.page.map(&:id)).to eq([flow.id])
|
28
80
|
end
|
29
81
|
end
|
30
82
|
|
@@ -81,6 +133,41 @@ describe Gush::Workflow do
|
|
81
133
|
end
|
82
134
|
end
|
83
135
|
|
136
|
+
describe "#status" do
|
137
|
+
context "when failed" do
|
138
|
+
it "returns :failed" do
|
139
|
+
flow = TestWorkflow.create
|
140
|
+
flow.find_job("Prepare").fail!
|
141
|
+
flow.persist!
|
142
|
+
expect(flow.reload.status).to eq(:failed)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
it "returns failed" do
|
147
|
+
subject.find_job('Prepare').fail!
|
148
|
+
expect(subject.status).to eq(:failed)
|
149
|
+
end
|
150
|
+
|
151
|
+
it "returns running" do
|
152
|
+
subject.find_job('Prepare').start!
|
153
|
+
expect(subject.status).to eq(:running)
|
154
|
+
end
|
155
|
+
|
156
|
+
it "returns finished" do
|
157
|
+
subject.jobs.each {|n| n.finish! }
|
158
|
+
expect(subject.status).to eq(:finished)
|
159
|
+
end
|
160
|
+
|
161
|
+
it "returns stopped" do
|
162
|
+
subject.stopped = true
|
163
|
+
expect(subject.status).to eq(:stopped)
|
164
|
+
end
|
165
|
+
|
166
|
+
it "returns pending" do
|
167
|
+
expect(subject.status).to eq(:pending)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
84
171
|
describe "#to_json" do
|
85
172
|
it "returns correct hash" do
|
86
173
|
klass = Class.new(Gush::Workflow) do
|
@@ -90,18 +177,25 @@ describe Gush::Workflow do
|
|
90
177
|
end
|
91
178
|
end
|
92
179
|
|
93
|
-
result = JSON.parse(klass.create("arg1", "arg2").to_json)
|
180
|
+
result = JSON.parse(klass.create("arg1", "arg2", arg3: 123).to_json)
|
94
181
|
expected = {
|
95
182
|
"id" => an_instance_of(String),
|
96
183
|
"name" => klass.to_s,
|
97
184
|
"klass" => klass.to_s,
|
98
|
-
"
|
185
|
+
"job_klasses" => ["FetchFirstJob", "PersistFirstJob"],
|
186
|
+
"status" => "pending",
|
99
187
|
"total" => 2,
|
100
188
|
"finished" => 0,
|
101
189
|
"started_at" => nil,
|
102
190
|
"finished_at" => nil,
|
103
191
|
"stopped" => false,
|
104
|
-
"
|
192
|
+
"dependencies" => [{
|
193
|
+
"from" => "FetchFirstJob",
|
194
|
+
"to" => job_with_id("PersistFirstJob")
|
195
|
+
}],
|
196
|
+
"arguments" => ["arg1", "arg2"],
|
197
|
+
"kwargs" => {"arg3" => 123},
|
198
|
+
"globals" => {}
|
105
199
|
}
|
106
200
|
expect(result).to match(expected)
|
107
201
|
end
|
@@ -118,7 +212,21 @@ describe Gush::Workflow do
|
|
118
212
|
flow = Gush::Workflow.new
|
119
213
|
flow.run(Gush::Job, params: { something: 1 })
|
120
214
|
flow.save
|
121
|
-
expect(flow.jobs.first.params).to eq
|
215
|
+
expect(flow.jobs.first.params).to eq({ something: 1 })
|
216
|
+
end
|
217
|
+
|
218
|
+
it "merges globals with params and passes them to the job, with job param taking precedence" do
|
219
|
+
flow = Gush::Workflow.new(globals: { something: 2, global1: 123 })
|
220
|
+
flow.run(Gush::Job, params: { something: 1 })
|
221
|
+
flow.save
|
222
|
+
expect(flow.jobs.first.params).to eq({ something: 1, global1: 123 })
|
223
|
+
end
|
224
|
+
|
225
|
+
it "allows passing wait param to the job" do
|
226
|
+
flow = Gush::Workflow.new
|
227
|
+
flow.run(Gush::Job, wait: 5.seconds)
|
228
|
+
flow.save
|
229
|
+
expect(flow.jobs.first.wait).to eq(5.seconds)
|
122
230
|
end
|
123
231
|
|
124
232
|
context "when graph is empty" do
|