taskinator 0.0.5 → 0.0.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 96866a7042bc2b7082aaeead94c5c90f00448f0b
4
- data.tar.gz: 42636386ce754b64ce167fe858a59e8c2a0a8286
3
+ metadata.gz: bb55f92dd0ef5d48b4fb0d1f3d82093c59fdca7c
4
+ data.tar.gz: c87595e40a584e0913571dbc50176ae8236010d2
5
5
  SHA512:
6
- metadata.gz: 061588463242d2d1d11827a339dc17a7903b07631b7194a7ce43639e1d3c4c1a96ec7a24cd62a79e7bb8fa0354a59539ae7fc026377acc22be801f816b6bd3e0
7
- data.tar.gz: 19da642b98496489815a1f2e741cda7e8f6fc9885c2becf896e09be7f48b7cdf0d85a5c0c8fb9e9bcd2aac26ecfed2fdcb7bdbb4d114d52faff356ca56eddf73
6
+ metadata.gz: 71858572f752ee6a0572607fb70607e36a49df6334e83c5c1a477675b70f41f75cbefc04e23cefb802bac39adc1207d947d69d580954d51e9992804a7ef2aa95
7
+ data.tar.gz: a405c8209923653109f732d03601f2747cf35642ae452b44598ad78487c3c639a3ae91ba065c887116e7b47c7e512374104994a8b8c9a9f7b812e47530af5b96
data/Gemfile.lock CHANGED
@@ -8,7 +8,7 @@ GIT
8
8
  PATH
9
9
  remote: .
10
10
  specs:
11
- taskinator (0.0.5)
11
+ taskinator (0.0.7)
12
12
  connection_pool (>= 2.0.0)
13
13
  json (>= 1.8.1)
14
14
  redis (>= 3.0.6)
@@ -38,18 +38,18 @@ GEM
38
38
  term-ansicolor
39
39
  thor
40
40
  debugger-linecache (1.2.0)
41
- delayed_job (4.0.3)
41
+ delayed_job (4.0.4)
42
42
  activesupport (>= 3.0, < 4.2)
43
43
  diff-lcs (1.2.5)
44
44
  docile (1.1.5)
45
45
  i18n (0.6.11)
46
46
  json (1.8.1)
47
47
  method_source (0.8.2)
48
- mime-types (2.3)
49
- minitest (5.4.1)
48
+ mime-types (2.4.2)
49
+ minitest (5.4.2)
50
50
  mono_logger (1.1.0)
51
51
  multi_json (1.10.1)
52
- netrc (0.7.7)
52
+ netrc (0.7.8)
53
53
  pry (0.9.12.6)
54
54
  coderay (~> 1.0)
55
55
  method_source (~> 0.8)
@@ -82,23 +82,23 @@ GEM
82
82
  rspec-core (~> 3.1.0)
83
83
  rspec-expectations (~> 3.1.0)
84
84
  rspec-mocks (~> 3.1.0)
85
- rspec-core (3.1.3)
85
+ rspec-core (3.1.7)
86
86
  rspec-support (~> 3.1.0)
87
- rspec-expectations (3.1.1)
87
+ rspec-expectations (3.1.2)
88
88
  diff-lcs (>= 1.2.0, < 2.0)
89
89
  rspec-support (~> 3.1.0)
90
- rspec-mocks (3.1.0)
90
+ rspec-mocks (3.1.3)
91
91
  rspec-support (~> 3.1.0)
92
- rspec-support (3.1.0)
92
+ rspec-support (3.1.2)
93
93
  sidekiq (3.2.5)
94
94
  celluloid (= 0.15.2)
95
95
  connection_pool (>= 2.0.0)
96
96
  json
97
97
  redis (>= 3.0.6)
98
98
  redis-namespace (>= 1.3.1)
99
- simplecov (0.9.0)
99
+ simplecov (0.9.1)
100
100
  docile (~> 1.1.0)
101
- multi_json
101
+ multi_json (~> 1.0)
102
102
  simplecov-html (~> 0.8.0)
103
103
  simplecov-html (0.8.0)
104
104
  sinatra (1.4.5)
@@ -112,7 +112,7 @@ GEM
112
112
  thread_safe (0.3.4)
113
113
  tilt (1.4.1)
114
114
  timers (1.1.0)
115
- tins (1.3.2)
115
+ tins (1.3.3)
116
116
  tzinfo (1.2.2)
117
117
  thread_safe (~> 0.1)
118
118
  vegas (0.1.11)
data/README.md CHANGED
@@ -69,8 +69,28 @@ module MyProcess
69
69
  end
70
70
  ```
71
71
 
72
- Specify the tasks with their corresponding implementation methods, that make up the process, using the `task` method and providing
73
- a `name` and `method` to execute for the task.
72
+ The `define_process` method optionally takes the list of expected arguments which are used to validate the
73
+ arguments supplied when creating a new process. These should be specified with symbols.
74
+
75
+ ```ruby
76
+ module MyProcess
77
+ extend Taskinator::Definition
78
+
79
+ # defines a process
80
+ define_process :date, :options do
81
+ # ...
82
+ end
83
+ end
84
+
85
+ # when creating a process, 2 arguments are expected
86
+ process = MyProcess.create_process Date.today, :option_1 => true
87
+ ```
88
+
89
+ NOTE: The current implementation performs a naive check on the count of arguments, but this will be
90
+ improved in subsequent versions.
91
+
92
+ Next, specify the tasks with their corresponding implementation methods, that make up the process,
93
+ using the `task` method and providing the `method` to execute for the task.
74
94
 
75
95
  ```ruby
76
96
  module MyProcess
@@ -124,7 +144,35 @@ module MyProcess
124
144
  end
125
145
  ```
126
146
 
127
- You can define data driven tasks using the `for_each` method, which takes an iterator method as an argument.
147
+ It is likely that you have already have worker classes for one of the queueing libraries, such as resque or delayed_job, and wish to
148
+ reuse them for executing them in the sequence defined by the process definition.
149
+
150
+ You define a `job` step, providing the class of the worker, and them taskinator will execute that worker as part of the process definition.
151
+ The `job` step will be queued and executed on the configured queue for `delayed_job`, or that of the worker for `resque` and `sidekiq`.
152
+
153
+ ```ruby
154
+ # E.g. A resque worker
155
+ class DoSomeWork
156
+ queue :high_priority
157
+
158
+ def self.perform(arg1, arg2)
159
+ # code to do the work
160
+ end
161
+ end
162
+
163
+ module MyProcess
164
+ extend Taskinator::Definition
165
+
166
+ # when creating the process, supply the same arguments
167
+ # that the DoSomeWork worker expects
168
+
169
+ define_process do
170
+ job DoSomeWork
171
+ end
172
+ end
173
+ ```
174
+
175
+ You can also define data driven tasks using the `for_each` method, which takes an iterator method name as an argument.
128
176
  The iterator method yields the items to produce a parameterized task for that item. Notice that the task method
129
177
  takes a parameter in this case, which will be the item provided by the iterator.
130
178
 
@@ -149,6 +197,67 @@ module MyProcess
149
197
  end
150
198
  ```
151
199
 
200
+ It is possible to branch the process logic based on the options hash passed in when creating a process.
201
+ The `options?` method takes the options key as an argument and calls the supplied block if the option
202
+ is present and it's value is truthy.
203
+
204
+ ```ruby
205
+ module MyProcess
206
+ extend Taskinator::Definition
207
+
208
+ define_process do
209
+
210
+ option?(:some_setting) do
211
+ task :prerequisite_step
212
+ end
213
+
214
+ task :work_step
215
+
216
+ end
217
+
218
+ def prerequisite_step
219
+ # ...
220
+ end
221
+
222
+ def work_step
223
+ # ...
224
+ end
225
+
226
+ end
227
+
228
+ # now when creating the process, the `:some_setting` option can be used to branch the logic
229
+ process1 = MyProcess.create_process :some_setting => true
230
+ process1.tasks.count #=> 2
231
+
232
+ process2 = MyProcess.create_process
233
+ process2.tasks.count #=> 1
234
+ ```
235
+
236
+ In addition, it is possible to transform the arguments used by a task or job, by including a `transform` step in the definition.
237
+ Similarly to the `for_each` method, `transform` takes a method name as an argument. The transformer method must yield the new arguments as required.
238
+
239
+ ```ruby
240
+ module MyProcess
241
+ extend Taskinator::Definition
242
+
243
+ # this process is created with a hash argument
244
+
245
+ define_process do
246
+ transform :convert_args do
247
+ task :work_step
248
+ end
249
+ end
250
+
251
+ def convert_args(options)
252
+ yield *[options[:date_from], options[:date_to]]
253
+ end
254
+
255
+ def work_step(date_from, date_to)
256
+ # TODO: supply implementation
257
+ end
258
+ end
259
+ ```
260
+
152
261
  Processes can be composed of other processes too:
153
262
 
154
263
  ```ruby
@@ -207,6 +316,33 @@ In this example, the `work_step_begin` is executed, followed by the `work_step_a
207
316
  the sub process `MySubProcess` is created and executed, followed by the `work_step_one_by_one` tasks which are executed sequentially and
208
317
  finally the `work_step_end` is executed.
209
318
 
319
+ It is also possible to embed conditional logic within the process definition stages in order to produce steps based on the required logic.
320
+ All builder methods are available within the scope of the `define_process` block. These methods include `args` and `options`
321
+ which are passed into the `create_process` method of the definition.
322
+
323
+ E.g.
324
+
325
+ ```ruby
326
+ module MyProcess
327
+ extend Taskinator::Definition
328
+
329
+ define_process do
330
+ task :task_1
331
+ task :task_2
332
+ task :task_3 if args[3] == 1
333
+ task :send_notification if options[:send_notification]
334
+ end
335
+
336
+ # "task" methods are omitted for brevity
337
+
338
+ end
339
+
340
+ # when creating this proces, you supply to option when calling `create_process`
341
+ # in this example, 'args' will be an array [1,2,3] and options will be a Hash {:send_notification => true}
342
+ MyProcess.create_process(1, 2, 3, :send_notification => true)
343
+
344
+ ```
345
+
210
346
  ### Execution
211
347
 
212
348
  A process is executed by calling the generated `create_process` method on your "process" module.
@@ -216,9 +352,201 @@ process = MyProcess.create_process
216
352
  process.enqueue!
217
353
  ```
218
354
 
355
+ Or, to start immediately, call the `start!` method.
356
+
357
+ ```ruby
358
+ process = MyProcess.create_process
359
+ process.start!
360
+ ```
361
+
219
362
  #### Arguments
220
363
 
221
- _TBD_
364
+ Argument handling for defining and executing process definitions is where things can get trickey.
365
+ _This may be something that gets refactored down the line_.
366
+
367
+ To best understand how arguments are handled, you need to break it down into 3 phases. Namely:
368
+
369
+ * Definition,
370
+ * Creation and
371
+ * Execution
372
+
373
+ Firstly, a process definition is declarative in that the `define_process` and a mix of `sequential`, `concurrent`, `for_each`,
374
+ `task` and `job` directives provide the way to specify the sequencing of the steps for the process.
375
+ Taskinator will interprete this definition and execute each step in the desired sequence or concurrency.
376
+
377
+ Consider the following process definition:
378
+
379
+ ```ruby
380
+ module MySimpleProcess
381
+ extend Taskinator::Definition
382
+
383
+ # definition
384
+
385
+ define_process do
386
+ task :work_step_1
387
+ task :work_step_2
388
+
389
+ for_each :additional_step do
390
+ task :work_step_3
391
+ end
392
+ end
393
+
394
+ # creation
395
+
396
+ def additional_step(options)
397
+ options.steps.each do |k, v|
398
+ yield k, v
399
+ end
400
+ end
401
+
402
+ # execution
403
+
404
+ def work_step_1(options)
405
+ # ...
406
+ end
407
+
408
+ def work_step_2(options)
409
+ # ...
410
+ end
411
+
412
+ def work_step_3(k, v)
413
+ # ...
414
+ end
415
+
416
+ end
417
+ ```
418
+
419
+ There are three tasks; namely `:work_step_1`, `:work_step_2` and `:work_step_3`.
420
+
421
+ The third task, `:work_step_3`, is built up using the `for_each` iterator, which means that the number of `:work_step_3` tasks
422
+ will depend on how many times the `additional_step` iterator method yields to the definition.
423
+
424
+ This brings us to the creation part. When `create_process` is called on the given module, you provide arguments to it, which will get
425
+ passed onto the respective `task` and `for_each` iterator methods.
426
+
427
+ So, considering the `MySimpleProcess` module shown above, `work_step_1`, `work_step_2` and `work_step_3` methods each expect arguments.
428
+ These will ultimately come from the arguments passed into the `create_process` method.
429
+
430
+ E.g.
431
+
432
+ ```ruby
433
+
434
+ # Given an options hash
435
+ options = {
436
+ :opt1 => true,
437
+ :opt2 => false,
438
+ :steps => {
439
+ :a => 1,
440
+ :b => 2,
441
+ :c => 3,
442
+ }
443
+ }
444
+
445
+ # You create the process, passing in the options hash
446
+ process = MySimpleProcess.create_process(options)
447
+
448
+ ```
449
+
450
+ To best understand how the process is created, consider the following "procedural" code for how it could work.
451
+
452
+ ```ruby
453
+ # A process, which maps the target and a list of steps
454
+ class Process
455
+ attr_reader :target
456
+ attr_reader :tasks
457
+
458
+ def initialize(target)
459
+ @target = target
460
+ @tasks = []
461
+ end
462
+ end
463
+
464
+ # A task, which maps the method to call and it's arguments
465
+ class Task
466
+ attr_reader :method
467
+ attr_reader :args
468
+
469
+ def initialize(method, args)
470
+ @method, @args = method, args
471
+ end
472
+ end
473
+
474
+ # Your module, with the methods which do the actual work
475
+ module MySimpleProcess
476
+
477
+ def self.work_step_1(options) ...
478
+ def self.work_step_2(options) ...
479
+ def self.work_step_3(k, v) ...
480
+
481
+ end
482
+
483
+ # Now, the creation phase of the definition
484
+ # create a process, providing the module
485
+
486
+ process = Process.new(MySimpleProcess)
487
+
488
+ # create the first and second tasks, providing the method
489
+ # for the task and it's arguments, which are the options defined above
490
+
491
+ process.tasks << Task.new(:work_step_1, options)
492
+ process.tasks << Task.new(:work_step_2, options)
493
+
494
+ # iterate over the steps hash in the options, and add the third step
495
+ # this time specify the key and value as the
496
+ # arguments for the work_step_3 method
497
+
498
+ options.steps.each do |k, v|
499
+ process.tasks << Task.new(:work_step_3, [k, v])
500
+ end
501
+
502
+ # we now have a process with the tasks defined
503
+
504
+ process.tasks #=> [<Task :method=>work_step_1, :args=>options, ...> ,
505
+ # <Task :method=>work_step_2, :args=>options, ...>,
506
+ # <Task :method=>work_step_3, :args=>[:a, 1], ...>,
507
+ # <Task :method=>work_step_3, :args=>[:b, 2], ...>,
508
+ # <Task :method=>work_step_3, :args=>[:c, 3], ...>]
509
+
510
+ ```
511
+
512
+ Finally, for the execution phase, the process and tasks will act on the supplied module.
513
+
514
+ ```ruby
515
+ # building out the "Process" class
516
+ class Process
517
+ #...
518
+
519
+ def execute
520
+ tasks.each {|task| task.execute(target) )
521
+ end
522
+ end
523
+
524
+ # and the "Task" class
525
+ class Task
526
+ #...
527
+
528
+ def execute(target)
529
+ puts "Calling '#{method}' on '#{target.name}' with #{args.inspect}..."
530
+ target.send(method, *args)
531
+ end
532
+ end
533
+
534
+ # executing the process iterates over each task and
535
+ # the target modules method is called with the arguments
536
+
537
+ process.execute
538
+
539
+ # Calling 'work_step_1' on 'MySimpleProcess' with {:opt1 => true, :opt2 => false, ...}
540
+ # Calling 'work_step_2' on 'MySimpleProcess' with {:opt1 => true, :opt2 => false, ...}
541
+ # Calling 'work_step_3' on 'MySimpleProcess' with [:a, 1]
542
+ # Calling 'work_step_3' on 'MySimpleProcess' with [:b, 2]
543
+ # Calling 'work_step_3' on 'MySimpleProcess' with [:c, 3]
544
+
545
+ ```
546
+
547
+ In reality, each task is executed by a worker process, possibly on another host, so the execution process isn't as simple,
548
+ but this example should help you to understand conceptually how the process is executed, and how the arguments are propagated
549
+ through.
222
550
 
223
551
  ### Monitoring
224
552
 
@@ -5,14 +5,20 @@ module Taskinator
5
5
  attr_reader :process
6
6
  attr_reader :definition
7
7
  attr_reader :args
8
+ attr_reader :options
8
9
 
9
- def initialize(process, definition, args)
10
+ def initialize(process, definition, *args)
10
11
  @process = process
11
12
  @definition = definition
12
13
  @args = args
14
+ @options = args.last.is_a?(Hash) ? args.last : {}
13
15
  @executor = Taskinator::Executor.new(@definition)
14
16
  end
15
17
 
18
+ def option?(key, &block)
19
+ yield if @options.key?(key) && @options[key]
20
+ end
21
+
16
22
  # defines a sub process of tasks which are executed sequentially
17
23
  def sequential(options={}, &block)
18
24
  raise ArgumentError, 'block' unless block_given?
@@ -3,10 +3,14 @@ module Taskinator
3
3
  class UndefinedProcessError < RuntimeError; end
4
4
 
5
5
  # defines a process
6
- def define_process(&block)
6
+ def define_process(*arg_list, &block)
7
7
  define_singleton_method :_create_process_ do |*args|
8
+
9
+ # TODO: better validation of arguments
10
+ raise ArgumentError, "wrong number of arguments (#{args.length} for #{arg_list.length})" if args.length < arg_list.length
11
+
8
12
  process = Process.define_sequential_process_for(self)
9
- Builder.new(process, self, args).instance_eval(&block)
13
+ Builder.new(process, self, *args).instance_eval(&block)
10
14
  process.save
11
15
  process
12
16
  end
@@ -1,3 +1,3 @@
1
1
  module Taskinator
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.7"
3
3
  end
@@ -1,7 +1,7 @@
1
1
  module TestFlow
2
2
  extend Taskinator::Definition
3
3
 
4
- define_process do
4
+ define_process :some_arg1, :some_arg2 do
5
5
  task :error_task, :continue_on_error => true
6
6
 
7
7
  task :the_task
@@ -54,7 +54,7 @@ module TestFlow
54
54
  module TestSubFlow
55
55
  extend Taskinator::Definition
56
56
 
57
- define_process do
57
+ define_process :some_arg1, :some_arg2 do
58
58
  task :the_task
59
59
  task :the_task
60
60
  task :the_task
@@ -15,7 +15,7 @@ describe Taskinator::Definition::Builder do
15
15
  Class.new(Taskinator::Process).new(definition)
16
16
  }
17
17
 
18
- let(:args) { [:arg1, :arg2] }
18
+ let(:args) { [:arg1, :arg2, {:option => 1, :another => false}] }
19
19
 
20
20
  let(:block) { SpecSupport::Block.new() }
21
21
 
@@ -24,12 +24,30 @@ describe Taskinator::Definition::Builder do
24
24
  Proc.new {|*args| the_block.call }
25
25
  }
26
26
 
27
- subject { Taskinator::Definition::Builder.new(process, definition, args) }
27
+ subject { Taskinator::Definition::Builder.new(process, definition, *args) }
28
28
 
29
29
  it "assign attributes" do
30
30
  expect(subject.process).to eq(process)
31
31
  expect(subject.definition).to eq(definition)
32
32
  expect(subject.args).to eq(args)
33
+ expect(subject.options).to eq({:option => 1, :another => false})
34
+ end
35
+
36
+ describe "#option?" do
37
+ it "invokes supplied block for 'option' option" do
38
+ expect(block).to receive(:call)
39
+ subject.option?(:option, &define_block)
40
+ end
41
+
42
+ it "does not invoke supplied block for 'another' option" do
43
+ expect(block).to_not receive(:call)
44
+ subject.option?(:another, &define_block)
45
+ end
46
+
47
+ it "does not invoke supplied block for an unspecified option" do
48
+ expect(block).to_not receive(:call)
49
+ subject.option?(:unspecified, &define_block)
50
+ end
33
51
  end
34
52
 
35
53
  describe "#sequential" do
@@ -154,7 +172,8 @@ describe Taskinator::Definition::Builder do
154
172
  Module.new() do
155
173
  extend Taskinator::Definition
156
174
 
157
- define_process {}
175
+ define_process :some_arg1, :some_arg2, :some_arg3 do
176
+ end
158
177
  end
159
178
  end
160
179
 
@@ -164,7 +183,7 @@ describe Taskinator::Definition::Builder do
164
183
  end
165
184
 
166
185
  it "creates a sub process task" do
167
- sub_process = sub_definition.create_process(:argX, :argY)
186
+ sub_process = sub_definition.create_process(:argX, :argY, :argZ)
168
187
  allow(sub_definition).to receive(:create_process) { sub_process }
169
188
  expect(Taskinator::Task).to receive(:define_sub_process_task).with(process, sub_process, {})
170
189
  subject.sub_process(sub_definition)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taskinator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Stefano
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-17 00:00:00.000000000 Z
11
+ date: 2014-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis