taskinator 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/Gemfile +17 -2
  4. data/Gemfile.lock +57 -18
  5. data/README.md +20 -16
  6. data/lib/taskinator/definition.rb +2 -2
  7. data/lib/taskinator/instrumentation.rb +77 -0
  8. data/lib/taskinator/persistence.rb +72 -61
  9. data/lib/taskinator/process.rb +118 -99
  10. data/lib/taskinator/queues/delayed_job.rb +0 -14
  11. data/lib/taskinator/queues/resque.rb +0 -18
  12. data/lib/taskinator/queues/sidekiq.rb +0 -14
  13. data/lib/taskinator/queues.rb +0 -5
  14. data/lib/taskinator/task.rb +113 -70
  15. data/lib/taskinator/version.rb +1 -1
  16. data/lib/taskinator/visitor.rb +6 -0
  17. data/lib/taskinator/workflow.rb +36 -0
  18. data/lib/taskinator.rb +3 -2
  19. data/spec/examples/process_examples.rb +6 -9
  20. data/spec/examples/queue_adapter_examples.rb +2 -12
  21. data/spec/examples/task_examples.rb +5 -8
  22. data/spec/support/process_methods.rb +25 -0
  23. data/spec/support/task_methods.rb +13 -0
  24. data/spec/support/test_flows.rb +1 -3
  25. data/spec/support/test_instrumenter.rb +39 -0
  26. data/spec/support/test_queue.rb +0 -12
  27. data/spec/taskinator/definition_spec.rb +3 -5
  28. data/spec/taskinator/instrumentation_spec.rb +98 -0
  29. data/spec/taskinator/persistence_spec.rb +3 -41
  30. data/spec/taskinator/process_spec.rb +36 -34
  31. data/spec/taskinator/queues/delayed_job_spec.rb +0 -41
  32. data/spec/taskinator/queues/resque_spec.rb +0 -51
  33. data/spec/taskinator/queues/sidekiq_spec.rb +0 -50
  34. data/spec/taskinator/queues_spec.rb +1 -1
  35. data/spec/taskinator/task_spec.rb +96 -64
  36. data/spec/taskinator/test_flows_spec.rb +266 -1
  37. data/taskinator.gemspec +0 -21
  38. metadata +12 -173
  39. data/lib/taskinator/job_worker.rb +0 -17
  40. data/spec/taskinator/job_worker_spec.rb +0 -62
@@ -1,7 +1,10 @@
1
1
  module Taskinator
2
2
  class Task
3
3
  include ::Comparable
4
- include ::Workflow
4
+
5
+ include Workflow
6
+ include Persistence
7
+ include Instrumentation
5
8
 
6
9
  class << self
7
10
  def define_step_task(process, method, args, options={})
@@ -25,6 +28,8 @@ module Taskinator
25
28
  attr_reader :uuid
26
29
  attr_reader :options
27
30
  attr_reader :queue
31
+ attr_reader :created_at
32
+ attr_reader :updated_at
28
33
 
29
34
  # the next task in the sequence
30
35
  attr_accessor :next
@@ -36,6 +41,8 @@ module Taskinator
36
41
  @process = process
37
42
  @options = options
38
43
  @queue = options.delete(:queue)
44
+ @created_at = Time.now.utc
45
+ @updated_at = created_at
39
46
  end
40
47
 
41
48
  def accept(visitor)
@@ -44,6 +51,8 @@ module Taskinator
44
51
  visitor.visit_task_reference(:next)
45
52
  visitor.visit_args(:options)
46
53
  visitor.visit_attribute(:queue)
54
+ visitor.visit_attribute_time(:created_at)
55
+ visitor.visit_attribute_time(:updated_at)
47
56
  end
48
57
 
49
58
  def <=>(other)
@@ -54,73 +63,98 @@ module Taskinator
54
63
  "#<#{self.class.name}:#{uuid}>"
55
64
  end
56
65
 
57
- workflow do
58
- state :initial do
59
- event :enqueue, :transitions_to => :enqueued
60
- event :start, :transitions_to => :processing
61
- event :complete, :transitions_to => :completed # specific to a SubProcess which has no tasks
62
- event :fail, :transitions_to => :failed
63
- end
64
-
65
- state :enqueued do
66
- event :start, :transitions_to => :processing
67
- event :complete, :transitions_to => :completed
68
- event :fail, :transitions_to => :failed
69
- end
66
+ def enqueue!
67
+ return if paused? || cancelled?
70
68
 
71
- state :processing do
72
- event :complete, :transitions_to => :completed
73
- event :fail, :transitions_to => :failed
69
+ transition(:enqueued) do
70
+ instrument('taskinator.task.enqueued', enqueued_payload) do
71
+ enqueue
72
+ end
74
73
  end
74
+ end
75
75
 
76
- state :completed
77
- state :failed
76
+ def start!
77
+ return if paused? || cancelled?
78
+ self.incr_processing if incr_count?
78
79
 
79
- on_transition do |from, to, event, *args|
80
- Taskinator.logger.debug("TASK: #{self.class.name}:#{uuid} :: #{from} => #{to}")
80
+ transition(:processing) do
81
+ instrument('taskinator.task.processing', processing_payload) do
82
+ start
83
+ end
81
84
  end
82
-
83
85
  end
84
86
 
85
- # include after defining the workflow
86
- # since the load and persist state methods
87
- # need to override the ones defined by workflow
88
- include Persistence
87
+ #
88
+ # NOTE: a task can't be paused (it's too difficult to implement)
89
+ # so rather, the parent process is paused, and the task checks it
90
+ #
89
91
 
90
- def complete
91
- Taskinator.instrumenter.instrument('taskinator.task.completed', instrumentation_payload) do
92
- # notify the process that this task has completed
93
- process.task_completed(self)
94
- self.incr_completed
92
+ # helper method
93
+ def paused?
94
+ super || process.paused?
95
+ end
96
+
97
+ def complete!
98
+ transition(:completed) do
99
+ self.incr_completed if incr_count?
100
+ instrument('taskinator.task.completed', completed_payload) do
101
+ complete if respond_to?(:complete)
102
+ # notify the process that this task has completed
103
+ process.task_completed(self)
104
+ end
95
105
  end
96
106
  end
97
107
 
98
- # callback for when the task has failed
99
- def on_failed_entry(*args)
100
- Taskinator.instrumenter.instrument('taskinator.task.failed', instrumentation_payload) do
101
- self.incr_failed
102
- # notify the process that this task has failed
103
- process.task_failed(self, args.last)
108
+ def cancel!
109
+ transition(:cancelled) do
110
+ self.incr_cancelled if incr_count?
111
+ instrument('taskinator.task.cancelled', cancelled_payload) do
112
+ cancel if respond_to?(:cancel)
113
+ end
104
114
  end
105
115
  end
106
116
 
107
- # callback for when the task has cancelled
108
- def on_cancelled_entry(*args)
109
- Taskinator.instrumenter.instrument('taskinator.task.cancelled', instrumentation_payload) do
110
- self.incr_cancelled
117
+ def cancelled?
118
+ super || process.cancelled?
119
+ end
120
+
121
+ def fail!(error)
122
+ transition(:failed) do
123
+ self.incr_failed if incr_count?
124
+ instrument('taskinator.task.failed', failed_payload(error)) do
125
+ fail(error) if respond_to?(:fail)
126
+ # notify the process that this task has failed
127
+ process.task_failed(self, error)
128
+ end
111
129
  end
112
130
  end
113
131
 
114
- # helper method, delegating to process
115
- def paused?
116
- process.paused?
132
+ def incr_count?
133
+ true
117
134
  end
118
135
 
119
- # helper method, delegating to process
120
- def cancelled?
121
- process.cancelled?
136
+ #--------------------------------------------------
137
+ # subclasses must implement the following methods
138
+ #--------------------------------------------------
139
+
140
+ def enqueue
141
+ raise NotImplementedError
142
+ end
143
+
144
+ def start
145
+ raise NotImplementedError
122
146
  end
123
147
 
148
+ #--------------------------------------------------
149
+ # and optionally, provide methods:
150
+ #--------------------------------------------------
151
+ #
152
+ # * cancel
153
+ # * complete
154
+ # * fail(error)
155
+ #
156
+ #--------------------------------------------------
157
+
124
158
  # a task which invokes the specified method on the definition
125
159
  # the args must be intrinsic types, since they are serialized to YAML
126
160
  class Step < Task
@@ -140,15 +174,11 @@ module Taskinator
140
174
  end
141
175
 
142
176
  def enqueue
143
- Taskinator.instrumenter.instrument('taskinator.task.enqueued', instrumentation_payload) do
144
- Taskinator.queue.enqueue_task(self)
145
- end
177
+ Taskinator.queue.enqueue_task(self)
146
178
  end
147
179
 
148
180
  def start
149
- Taskinator.instrumenter.instrument('taskinator.task.started', instrumentation_payload) do
150
- executor.send(method, *args)
151
- end
181
+ executor.send(method, *args)
152
182
  # ASSUMPTION: when the method returns, the task is considered to be complete
153
183
  complete!
154
184
 
@@ -171,10 +201,12 @@ module Taskinator
171
201
  end
172
202
 
173
203
  def inspect
174
- %(#<#{self.class.name}:0x#{self.__id__.to_s(16)} uuid="#{uuid}", method=:#{method}, args=#{args}, state=:#{current_state.name}>)
204
+ %(#<#{self.class.name}:0x#{self.__id__.to_s(16)} uuid="#{uuid}", method=:#{method}, args=#{args}, current_state=:#{current_state}>)
175
205
  end
176
206
  end
177
207
 
208
+ #--------------------------------------------------
209
+
178
210
  # a task which invokes the specified background job
179
211
  # the args must be intrinsic types, since they are serialized to YAML
180
212
  class Job < Task
@@ -194,17 +226,22 @@ module Taskinator
194
226
  end
195
227
 
196
228
  def enqueue
197
- Taskinator.instrumenter.instrument('taskinator.task.enqueued', instrumentation_payload) do
198
- Taskinator.queue.enqueue_job(self)
199
- end
229
+ Taskinator.queue.enqueue_task(self)
200
230
  end
201
231
 
202
- # can't use the start! method, since a block is required
203
- def perform
204
- Taskinator.instrumenter.instrument('taskinator.task.started', instrumentation_payload) do
205
- yield(job, args)
232
+ def start
233
+ # NNB: if other job types are required, may need to implement how they get invoked here!
234
+ # FIXME: possible implement using ActiveJob instead, so it doesn't matter how the worker is implemented
235
+
236
+ if job.instance_of?(Module)
237
+ # resque
238
+ job.perform(args)
239
+ else
240
+ # delayedjob and sidekiq
241
+ job.new.perform(args)
206
242
  end
207
- # ASSUMPTION: when the method returns, the task is considered to be complete
243
+
244
+ # ASSUMPTION: when the job returns, the task is considered to be complete
208
245
  complete!
209
246
 
210
247
  rescue => e
@@ -222,14 +259,18 @@ module Taskinator
222
259
  end
223
260
 
224
261
  def inspect
225
- %(#<#{self.class.name}:0x#{self.__id__.to_s(16)} uuid="#{uuid}", job=#{job}, args=#{args}, state=:#{current_state.name}>)
262
+ %(#<#{self.class.name}:0x#{self.__id__.to_s(16)} uuid="#{uuid}", job=#{job}, args=#{args}, current_state=:#{current_state}>)
226
263
  end
227
264
  end
228
265
 
266
+ #--------------------------------------------------
267
+
229
268
  # a task which delegates to another process
230
269
  class SubProcess < Task
231
270
  attr_reader :sub_process
232
271
 
272
+ # NOTE: also wraps sequential and concurrent processes
273
+
233
274
  def initialize(process, sub_process, options={})
234
275
  super(process, options)
235
276
  raise ArgumentError, 'sub_process' if sub_process.nil? || !sub_process.is_a?(Process)
@@ -239,15 +280,11 @@ module Taskinator
239
280
  end
240
281
 
241
282
  def enqueue
242
- Taskinator.instrumenter.instrument('taskinator.task.enqueued', instrumentation_payload) do
243
- sub_process.enqueue!
244
- end
283
+ sub_process.enqueue!
245
284
  end
246
285
 
247
286
  def start
248
- Taskinator.instrumenter.instrument('taskinator.task.started', instrumentation_payload) do
249
- sub_process.start!
250
- end
287
+ sub_process.start!
251
288
 
252
289
  rescue => e
253
290
  Taskinator.logger.error(e)
@@ -256,13 +293,19 @@ module Taskinator
256
293
  raise e
257
294
  end
258
295
 
296
+ def incr_count?
297
+ # subprocess tasks aren't included in the total count of tasks
298
+ # since they simply delegate to the tasks of the respective subprocess definition
299
+ false
300
+ end
301
+
259
302
  def accept(visitor)
260
303
  super
261
304
  visitor.visit_process(:sub_process)
262
305
  end
263
306
 
264
307
  def inspect
265
- %(#<#{self.class.name}:0x#{self.__id__.to_s(16)} uuid="#{uuid}", sub_process=#{sub_process.inspect}, state=:#{current_state.name}>)
308
+ %(#<#{self.class.name}:0x#{self.__id__.to_s(16)} uuid="#{uuid}", sub_process=#{sub_process.inspect}, current_state=:#{current_state}>)
266
309
  end
267
310
  end
268
311
  end
@@ -1,3 +1,3 @@
1
1
  module Taskinator
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -10,6 +10,12 @@ module Taskinator
10
10
  def visit_attribute(attribute)
11
11
  end
12
12
 
13
+ def visit_attribute_time(attribute)
14
+ end
15
+
16
+ def visit_attribute_enum(attribute, type)
17
+ end
18
+
13
19
  def visit_process_reference(attribute)
14
20
  end
15
21
 
@@ -0,0 +1,36 @@
1
+ module Taskinator
2
+ module Workflow
3
+
4
+ def current_state
5
+ @current_state ||= load_workflow_state
6
+ end
7
+
8
+ def current_state=(new_state)
9
+ @current_state = persist_workflow_state(new_state)
10
+ end
11
+
12
+ def transition(new_state)
13
+ self.current_state = new_state
14
+ yield if block_given?
15
+ current_state
16
+ end
17
+
18
+ %i(
19
+ initial
20
+ enqueued
21
+ processing
22
+ paused
23
+ resumed
24
+ completed
25
+ cancelled
26
+ failed
27
+ ).each do |state|
28
+
29
+ define_method :"#{state}?" do
30
+ @current_state == state
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
data/lib/taskinator.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require 'json'
2
2
  require 'yaml'
3
3
  require 'securerandom'
4
- require 'workflow'
5
4
 
6
5
  require 'taskinator/version'
7
6
 
@@ -11,15 +10,17 @@ require 'taskinator/logger'
11
10
 
12
11
  require 'taskinator/definition'
13
12
 
13
+ require 'taskinator/workflow'
14
+
14
15
  require 'taskinator/visitor'
15
16
  require 'taskinator/persistence'
17
+ require 'taskinator/instrumentation'
16
18
 
17
19
  require 'taskinator/task'
18
20
  require 'taskinator/tasks'
19
21
  require 'taskinator/process'
20
22
 
21
23
  require 'taskinator/task_worker'
22
- require 'taskinator/job_worker'
23
24
  require 'taskinator/create_process_worker'
24
25
 
25
26
  require 'taskinator/executor'
@@ -2,15 +2,12 @@ require 'spec_helper'
2
2
 
3
3
  shared_examples_for "a process" do |process_type|
4
4
 
5
- # NOTE: definition and process must be defined by callee
5
+ # NOTE: definition and subject must be defined by callee
6
6
 
7
- # let(:definition) {}
8
- # let(:process) {}
9
-
10
- it { expect(process.definition).to eq(definition) }
11
- it { expect(process.uuid).to_not be_nil }
12
- it { expect(process.to_s).to match(/#{process.uuid}/) }
13
- it { expect(process.options).to_not be_nil }
14
- it { expect(process.tasks).to_not be_nil }
7
+ it { expect(subject.definition).to eq(definition) }
8
+ it { expect(subject.uuid).to_not be_nil }
9
+ it { expect(subject.to_s).to match(/#{subject.uuid}/) }
10
+ it { expect(subject.options).to_not be_nil }
11
+ it { expect(subject.tasks).to_not be_nil }
15
12
 
16
13
  end
@@ -16,7 +16,7 @@ shared_examples_for "a queue adapter" do |adapter_name, adapter_type|
16
16
  it "should enqueue a create process" do
17
17
  expect {
18
18
  subject.enqueue_create_process(double('definition', :name => 'definition', :queue => nil), 'xx-xx-xx-xx', :foo => :bar)
19
- }.to_not raise_error
19
+ }.to_not raise_error(StandardError)
20
20
  end
21
21
  end
22
22
 
@@ -26,17 +26,7 @@ shared_examples_for "a queue adapter" do |adapter_name, adapter_type|
26
26
  it "should enqueue a task" do
27
27
  expect {
28
28
  subject.enqueue_task(double('task', :uuid => 'xx-xx-xx-xx', :queue => nil))
29
- }.to_not raise_error
30
- end
31
- end
32
-
33
- describe "#enqueue_job" do
34
- it { expect(subject).to respond_to(:enqueue_job) }
35
-
36
- it "should enqueue a job" do
37
- expect {
38
- subject.enqueue_job(double('job', :uuid => 'xx-xx-xx-xx', :job => job, :queue => nil))
39
- }.to_not raise_error
29
+ }.to_not raise_error(StandardError)
40
30
  end
41
31
  end
42
32
 
@@ -2,14 +2,11 @@ require 'spec_helper'
2
2
 
3
3
  shared_examples_for "a task" do |task_type|
4
4
 
5
- # NOTE: process and task must be defined by callee
5
+ # NOTE: process and subject must be defined by callee
6
6
 
7
- # let(:process) {}
8
- # let(:task) {}
9
-
10
- it { expect(task.process).to eq(process) }
11
- it { expect(task.uuid).to_not be_nil }
12
- it { expect(task.to_s).to match(/#{task.uuid}/) }
13
- it { expect(task.options).to_not be_nil }
7
+ it { expect(subject.process).to eq(process) }
8
+ it { expect(subject.uuid).to_not be_nil }
9
+ it { expect(subject.to_s).to match(/#{subject.uuid}/) }
10
+ it { expect(subject.options).to_not be_nil }
14
11
 
15
12
  end
@@ -0,0 +1,25 @@
1
+ module ProcessMethods
2
+ def enqueue
3
+ end
4
+
5
+ def cancel
6
+ end
7
+
8
+ def start
9
+ end
10
+
11
+ def pause
12
+ end
13
+
14
+ def resume
15
+ end
16
+
17
+ def complete
18
+ end
19
+
20
+ def fail(error)
21
+ end
22
+
23
+ def task_completed(task)
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ module TaskMethods
2
+ def enqueue
3
+ end
4
+
5
+ def start
6
+ end
7
+
8
+ def complete
9
+ end
10
+
11
+ def fail(error)
12
+ end
13
+ end
@@ -2,9 +2,7 @@ module TestFlows
2
2
 
3
3
  module Worker
4
4
  def self.perform(*args)
5
- end
6
-
7
- def perform(*args)
5
+ # nop
8
6
  end
9
7
  end
10
8
 
@@ -0,0 +1,39 @@
1
+ class TestInstrumenter
2
+
3
+ class << self
4
+
5
+ def subscribe(callback, filter=nil, &block)
6
+
7
+ # create test instrumenter instance
8
+ instrumenter = TestInstrumenter.new do |name, payload|
9
+ if filter
10
+ callback.call(name, payload) if name =~ filter
11
+ else
12
+ callback.call(name, payload)
13
+ end
14
+ end
15
+
16
+ # hook up this instrumenter in the context of the spec
17
+ # (assuming called from RSpec binding)
18
+ spec_binding = block.binding.eval('self')
19
+ spec_binding.instance_exec do
20
+ allow(Taskinator).to receive(:instrumenter).and_return(instrumenter)
21
+ end
22
+
23
+ yield
24
+ end
25
+
26
+ end
27
+
28
+ attr_reader :callback
29
+
30
+ def initialize(&block)
31
+ @callback = block
32
+ end
33
+
34
+ def instrument(event, payload={})
35
+ @callback.call(event, payload)
36
+ yield(payload) if block_given?
37
+ end
38
+
39
+ end
@@ -13,7 +13,6 @@ module Taskinator
13
13
 
14
14
  attr_reader :creates
15
15
  attr_reader :tasks
16
- attr_reader :jobs
17
16
 
18
17
  def initialize
19
18
  clear
@@ -33,10 +32,6 @@ module Taskinator
33
32
  @tasks << task
34
33
  end
35
34
 
36
- def enqueue_job(job)
37
- @jobs << job
38
- end
39
-
40
35
  def empty?
41
36
  @creates.empty? && @tasks.empty? && @jobs.empty?
42
37
  end
@@ -62,13 +57,6 @@ module Taskinator
62
57
  end
63
58
  end
64
59
 
65
- def enqueue_job(job)
66
- super
67
- invoke do
68
- Taskinator::JobWorker.new(job.uuid).perform
69
- end
70
- end
71
-
72
60
  def invoke(&block)
73
61
  block.call
74
62
  end
@@ -136,7 +136,7 @@ describe Taskinator::Definition do
136
136
  # if an error is raised, then the context was incorrect
137
137
  expect {
138
138
  subject.create_process
139
- }.to_not raise_error
139
+ }.to_not raise_error(StandardError)
140
140
  end
141
141
 
142
142
  context "is instrumented" do
@@ -148,8 +148,7 @@ describe Taskinator::Definition do
148
148
  expect(args.first).to eq('taskinator.process.created')
149
149
  end
150
150
 
151
- # temporary subscription
152
- ActiveSupport::Notifications.subscribed(instrumentation_block, /taskinator.process.created/) do
151
+ TestInstrumenter.subscribe(instrumentation_block, /taskinator.process.created/) do
153
152
  subject.create_process :foo
154
153
  end
155
154
  end
@@ -160,8 +159,7 @@ describe Taskinator::Definition do
160
159
  expect(args.first).to eq('taskinator.process.saved')
161
160
  end
162
161
 
163
- # temporary subscription
164
- ActiveSupport::Notifications.subscribed(instrumentation_block, /taskinator.process.saved/) do
162
+ TestInstrumenter.subscribe(instrumentation_block, /taskinator.process.saved/) do
165
163
  subject.create_process :foo
166
164
  end
167
165
  end