gush 3.0.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 +4 -0
- data/.rubocop.yml +232 -0
- data/CHANGELOG.md +1 -77
- data/README.md +107 -6
- data/gush.gemspec +4 -2
- data/lib/gush/cli/overview.rb +2 -0
- data/lib/gush/cli.rb +27 -4
- data/lib/gush/client.rb +116 -24
- data/lib/gush/graph.rb +2 -2
- data/lib/gush/job.rb +13 -4
- data/lib/gush/migrate/1_create_gush_workflows_created.rb +21 -0
- data/lib/gush/migration.rb +37 -0
- data/lib/gush/version.rb +1 -1
- data/lib/gush/worker.rb +1 -1
- data/lib/gush/workflow.rb +27 -15
- data/lib/gush.rb +1 -0
- data/spec/features/integration_spec.rb +3 -3
- data/spec/gush/client_spec.rb +303 -6
- 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 +114 -13
- data/spec/spec_helper.rb +7 -7
- metadata +51 -2
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)
|
@@ -82,42 +111,222 @@ describe Gush::Client do
|
|
82
111
|
end
|
83
112
|
end
|
84
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
|
+
|
85
145
|
describe "#persist_workflow" do
|
86
146
|
it "persists JSON dump of the Workflow and its jobs" do
|
87
147
|
job = double("job", to_json: 'json')
|
88
148
|
workflow = double("workflow", id: 'abcd', jobs: [job, job, job], to_json: '"json"')
|
89
|
-
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)
|
90
150
|
expect(workflow).to receive(:mark_as_persisted)
|
91
151
|
client.persist_workflow(workflow)
|
92
152
|
expect(redis.keys("gush.workflows.abcd").length).to eq(1)
|
93
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
|
94
235
|
end
|
95
236
|
|
96
237
|
describe "#destroy_workflow" do
|
97
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
|
+
|
98
241
|
workflow = TestWorkflow.create
|
99
242
|
expect(redis.keys("gush.workflows.#{workflow.id}").length).to eq(1)
|
100
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)
|
101
247
|
|
102
248
|
client.destroy_workflow(workflow)
|
103
249
|
|
104
250
|
expect(redis.keys("gush.workflows.#{workflow.id}").length).to eq(0)
|
105
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)
|
106
297
|
end
|
107
298
|
end
|
108
299
|
|
109
300
|
describe "#expire_workflow" do
|
110
301
|
let(:ttl) { 2000 }
|
111
302
|
|
112
|
-
it "sets
|
303
|
+
it "sets an expiration time for the workflow" do
|
113
304
|
workflow = TestWorkflow.create
|
114
305
|
|
115
|
-
|
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
|
116
311
|
|
117
|
-
expect(redis.
|
312
|
+
expect(redis.zscore("gush.idx.workflows.expires_at", workflow.id)).to eq(expires_at)
|
118
313
|
|
119
314
|
workflow.jobs.each do |job|
|
120
|
-
expect(redis.
|
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
|
320
|
+
workflow = TestWorkflow.create
|
321
|
+
|
322
|
+
client.expire_workflow(workflow, 100)
|
323
|
+
expect(redis.zscore("gush.idx.workflows.expires_at", workflow.id)).to be > 0
|
324
|
+
|
325
|
+
client.expire_workflow(workflow, -1)
|
326
|
+
expect(redis.zscore("gush.idx.workflows.expires_at", workflow.id)).to eq(nil)
|
327
|
+
|
328
|
+
workflow.jobs.each do |job|
|
329
|
+
expect(redis.zscore("gush.idx.jobs.expires_at", "#{workflow.id}.#{job.klass}")).to eq(nil)
|
121
330
|
end
|
122
331
|
end
|
123
332
|
end
|
@@ -130,6 +339,94 @@ describe Gush::Client do
|
|
130
339
|
client.persist_job('deadbeef', job)
|
131
340
|
expect(redis.keys("gush.jobs.deadbeef.*").length).to eq(1)
|
132
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
|
133
430
|
end
|
134
431
|
|
135
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,14 +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 })
|
122
223
|
end
|
123
224
|
|
124
225
|
it "allows passing wait param to the job" do
|
125
226
|
flow = Gush::Workflow.new
|
126
227
|
flow.run(Gush::Job, wait: 5.seconds)
|
127
228
|
flow.save
|
128
|
-
expect(flow.jobs.first.wait).to eq
|
229
|
+
expect(flow.jobs.first.wait).to eq(5.seconds)
|
129
230
|
end
|
130
231
|
|
131
232
|
context "when graph is empty" do
|