gush 2.1.0 → 4.0.0
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/.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
|