From 39459baf10ee91abbbbc6ee59342cec950ed140b Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Mon, 21 Feb 2022 15:21:31 +0000 Subject: [PATCH 01/15] remove old documentation --- processes_workflow.png | Bin 78920 -> 0 bytes sequence.txt | 70 ----------------------------------------- tasks_workflow.png | Bin 36518 -> 0 bytes 3 files changed, 70 deletions(-) delete mode 100644 processes_workflow.png delete mode 100644 sequence.txt delete mode 100644 tasks_workflow.png diff --git a/processes_workflow.png b/processes_workflow.png deleted file mode 100644 index 346ad79aa61287a6e551689c2c61b03ad4b7dabb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78920 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

LDp5E!*zhM;m3Kx%|QwO{!OIv=jX8k)1eRP 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 ZiG?oaGb;){X&%iyEcIRPQ}B_X{x{+Web: Request process -Web-->+Process(seq): Create sequential process -Process(seq)-->-Web: -Web-->+Queue: Enqueue process -Queue-->-Web: -Web-->-User: - -opt Sequential process - Queue->+Worker: Dequeue process - note right of Worker: Start sequential process - Worker->+Queue: Enqueue task - Queue-->-Worker: - Worker-->-Queue: - - loop Sequential Tasks - Queue->+Worker: Dequeue task - note right of Worker: Start task - Worker->+Queue: Enqueue task - Queue-->-Worker: - Worker->Process(seq): Task completed - note left of Process(seq): All tasks complete? - Worker-->-Queue: - end - - opt Sub Process Task - Queue->+Worker: Dequeue task - note right of Worker: Start task - Worker-->+Process(con): Create concurrent process - Process(con)-->-Worker: - Worker-->+Queue: Enqueue process - Queue-->-Worker: - Worker->Process(seq): Task completed - note left of Process(seq): All tasks complete? - Worker-->-Queue: - end - - opt Concurrent process - Queue->+Worker: Dequeue process - note right of Worker: Start concurrent process - Worker->+Queue: Enqueue task - Queue-->-Worker: - Worker->+Queue: Enqueue task - Queue-->-Worker: - Worker->Process(seq): Task completed - note left of Process(seq): All tasks complete? - Worker-->-Queue: - - opt Concurrent Tasks - Queue->+Worker: Dequeue task - Queue->+Worker: Dequeue task - Queue->+Worker: Dequeue task - Worker->Process(con): Task completed - note right of Process(con): All tasks complete? - Worker->Process(con): Task completed - note right of Process(con): All tasks complete? - Worker-->-Queue: - Worker-->-Queue: - Worker->Process(con): Task completed - Worker-->-Queue: - end - - note right of Process(con): All tasks complete? - Process(con)->Process(seq): Process completed - note left of Process(seq): All tasks complete? - - end -end diff --git a/tasks_workflow.png b/tasks_workflow.png deleted file mode 100644 index d92d1c54b8a9a4aa7cacb95a3cda6daf8d1e84cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36518 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 EAC0E^P5=M^ From 2cc1240a8e272fc92cb32546d4707a8d2266c76c Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Mon, 21 Feb 2022 15:21:17 +0000 Subject: [PATCH 02/15] correction to license filename --- LICENSE.txt => LICENSE | 0 lib/taskinator.rb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename LICENSE.txt => LICENSE (100%) diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE diff --git a/lib/taskinator.rb b/lib/taskinator.rb index 3f8f171..8d4e1e8 100644 --- a/lib/taskinator.rb +++ b/lib/taskinator.rb @@ -33,7 +33,7 @@ module Taskinator NAME = "Taskinator" - LICENSE = 'See LICENSE.txt for licensing details.' + LICENSE = 'See LICENSE for licensing details.' DEFAULTS = { # none for now... From a76d48d783426c90d6f09facf253f2f77a4133d4 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Tue, 22 Feb 2022 17:18:48 +0000 Subject: [PATCH 03/15] readme updates --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e212fb4..9359c0c 100644 --- a/README.md +++ b/README.md @@ -363,12 +363,12 @@ MyProcess.create_process(1, 2, 3, :send_notification => true) ``` -#### Reusing ActiveJob jobs +#### Reusing `ActiveJob` jobs It is likely that you already have one or more [jobs](https://guides.rubyonrails.org/active_job_basics.html) and want to reuse them within the process definition. -Define a `job` step, providing the class of the Active Job to run and then taskinator will +Define a `job` step, providing the class of the `ActiveJob` to run and then taskinator will invoke that job as part of the process. The `job` step will be queued and executed on same queue as @@ -425,7 +425,7 @@ _This may be something that gets refactored down the line_. To best understand how arguments are handled, you need to break it down into 3 phases. Namely: * Definition, - * Creation and + * Creation, and * Execution Firstly, a process definition is declarative in that the `define_process` and a mix of From f08cda74a74021063c01fe212e61aaa8d352e2cb Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Mon, 21 Feb 2022 12:02:05 +0000 Subject: [PATCH 04/15] bug fix options for sequential, concurrent, for_each and sub_process --- lib/taskinator/builder.rb | 9 ++- spec/taskinator/builder_spec.rb | 109 +++++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/lib/taskinator/builder.rb b/lib/taskinator/builder.rb index 619dd16..3f55baa 100644 --- a/lib/taskinator/builder.rb +++ b/lib/taskinator/builder.rb @@ -15,6 +15,9 @@ def initialize(process, definition, *args) end def option?(key, &block) + # instead of LocalJumpError + raise ArgumentError, 'block' unless block_given? + yield if builder_options[key] end @@ -24,7 +27,7 @@ def sequential(options={}, &block) sub_process = Process.define_sequential_process_for(@definition, options) task = define_sub_process_task(@process, sub_process, options) - Builder.new(sub_process, @definition, *@args).instance_eval(&block) + Builder.new(sub_process, @definition, *@args, @builder_options).instance_eval(&block) @process.tasks << task if sub_process.tasks.any? nil end @@ -35,7 +38,7 @@ def concurrent(complete_on=CompleteOn::Default, options={}, &block) sub_process = Process.define_concurrent_process_for(@definition, complete_on, options) task = define_sub_process_task(@process, sub_process, options) - Builder.new(sub_process, @definition, *@args).instance_eval(&block) + Builder.new(sub_process, @definition, *@args, @builder_options).instance_eval(&block) @process.tasks << task if sub_process.tasks.any? nil end @@ -54,7 +57,7 @@ def for_each(method, options={}, &block) # method_args = options.any? ? [*@args, options] : @args @executor.send(method, *method_args) do |*args| - Builder.new(@process, @definition, *args).instance_eval(&block) + Builder.new(@process, @definition, *args, @builder_options).instance_eval(&block) end nil end diff --git a/spec/taskinator/builder_spec.rb b/spec/taskinator/builder_spec.rb index 6bece90..09b17a2 100644 --- a/spec/taskinator/builder_spec.rb +++ b/spec/taskinator/builder_spec.rb @@ -6,7 +6,10 @@ Module.new do extend Taskinator::Definition - def iterator_method(*); end + def iterator_method(*args) + yield *args + end + def task_method(*); end end end @@ -50,6 +53,110 @@ def task_method(*); end expect(block).to_not receive(:call) subject.option?(:unspecified, &define_block) end + + describe "scopes" do + it "base" do + expect(block).to receive(:call) + blk = define_block + + definition.define_process :a, :b do + option?(:option1, &blk) + end + + definition.create_process(*args, builder_options) + end + + it "sequential" do + expect(block).to receive(:call) + blk = define_block + + definition.define_process :a, :b do + sequential do + option?(:option1, &blk) + end + end + + definition.create_process(*args, builder_options) + end + + it "concurrent" do + expect(block).to receive(:call) + blk = define_block + + definition.define_process :a, :b do + concurrent do + option?(:option1, &blk) + end + end + + definition.create_process(*args, builder_options) + end + + it "for_each" do + expect(block).to receive(:call) + blk = define_block + + definition.define_process :a, :b do + for_each :iterator_method do + option?(:option1, &blk) + end + end + + definition.create_process(*args, builder_options) + end + + it "nested" do + expect(block).to receive(:call) + blk = define_block + + definition.define_process :a, :b do + concurrent do + sequential do + for_each :iterator_method do + option?(:option1, &blk) + end + end + end + end + + definition.create_process(*args, builder_options) + end + + it "sub-process" do + expect(block).to receive(:call).exactly(4).times + blk = define_block + + sub_definition = Module.new do + extend Taskinator::Definition + + define_process do + option?(:option1, &blk) #1 + + sequential do + option?(:option1, &blk) #2 + end + + concurrent do + option?(:option1, &blk) #3 + end + + for_each :iterator_method do + option?(:option1, &blk) #4 + end + end + + def iterator_method(*args) + yield *args + end + end + + definition.define_process :a, :b do + sub_process sub_definition + end + + definition.create_process(*args, builder_options) + end + end end describe "#sequential" do From bb48f13ffb3bdf1c43804d7045417fbc3db4d2e1 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Mon, 21 Feb 2022 14:59:14 +0000 Subject: [PATCH 05/15] clean up requires --- lib/taskinator.rb | 2 ++ lib/taskinator/definition.rb | 2 -- lib/taskinator/persistence.rb | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/taskinator.rb b/lib/taskinator.rb index 8d4e1e8..b699b89 100644 --- a/lib/taskinator.rb +++ b/lib/taskinator.rb @@ -3,6 +3,7 @@ require 'securerandom' require 'benchmark' require 'delegate' +require 'builder' require 'taskinator/version' @@ -10,6 +11,7 @@ require 'taskinator/redis_connection' require 'taskinator/logger' +require 'taskinator/builder' require 'taskinator/definition' require 'taskinator/workflow' diff --git a/lib/taskinator/definition.rb b/lib/taskinator/definition.rb index 3db418b..a16e069 100644 --- a/lib/taskinator/definition.rb +++ b/lib/taskinator/definition.rb @@ -1,5 +1,3 @@ -require 'taskinator/builder' - module Taskinator module Definition diff --git a/lib/taskinator/persistence.rb b/lib/taskinator/persistence.rb index 0eb16e0..436fb44 100644 --- a/lib/taskinator/persistence.rb +++ b/lib/taskinator/persistence.rb @@ -1,5 +1,3 @@ -require 'builder' - module Taskinator module Persistence From fd14bcfc42b47777a91197552a70cf807fda73bf Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Mon, 21 Feb 2022 12:52:12 +0000 Subject: [PATCH 06/15] spec clean up, improvements and increased coverage --- lib/taskinator/instrumentation.rb | 12 +- lib/taskinator/persistence.rb | 2 +- spec/support/test_definition.rb | 7 - .../{test_flows.rb => test_definitions.rb} | 88 ++++++---- spec/support/test_flow.rb | 84 ---------- spec/taskinator/builder_spec.rb | 24 ++- spec/taskinator/complex_process_spec.rb | 23 --- spec/taskinator/instrumentation_spec.rb | 155 ++++++++++++++---- spec/taskinator/persistence_spec.rb | 22 +-- spec/taskinator/process_spec.rb | 15 +- spec/taskinator/task_spec.rb | 14 +- ...flows_spec.rb => test_definitions_spec.rb} | 38 ++--- 12 files changed, 240 insertions(+), 244 deletions(-) delete mode 100644 spec/support/test_definition.rb rename spec/support/{test_flows.rb => test_definitions.rb} (62%) delete mode 100644 spec/support/test_flow.rb delete mode 100644 spec/taskinator/complex_process_spec.rb rename spec/taskinator/{test_flows_spec.rb => test_definitions_spec.rb} (92%) diff --git a/lib/taskinator/instrumentation.rb b/lib/taskinator/instrumentation.rb index 6e78a59..6756041 100644 --- a/lib/taskinator/instrumentation.rb +++ b/lib/taskinator/instrumentation.rb @@ -44,7 +44,7 @@ def payload_for(state, additional={}) # need to cache here, since this method hits redis, so can't be part of multi statement following process_key = self.process_key - tasks_count, processing_count, completed_count, cancelled_count, failed_count = Taskinator.redis do |conn| + count, processing, completed, cancelled, failed = Taskinator.redis do |conn| conn.hmget process_key, :tasks_count, :tasks_processing, @@ -53,7 +53,7 @@ def payload_for(state, additional={}) :tasks_failed end - tasks_count = tasks_count.to_f + count = count.to_f return OpenStruct.new( { @@ -64,10 +64,10 @@ def payload_for(state, additional={}) :uuid => uuid, :options => options.dup, :state => state, - :percentage_failed => (tasks_count > 0) ? (failed_count.to_i / tasks_count) * 100.0 : 0.0, - :percentage_cancelled => (tasks_count > 0) ? (cancelled_count.to_i / tasks_count) * 100.0 : 0.0, - :percentage_processing => (tasks_count > 0) ? (processing_count.to_i / tasks_count) * 100.0 : 0.0, - :percentage_completed => (tasks_count > 0) ? (completed_count.to_i / tasks_count) * 100.0 : 0.0, + :percentage_failed => (count > 0) ? (failed.to_i / count) * 100.0 : 0.0, + :percentage_cancelled => (count > 0) ? (cancelled.to_i / count) * 100.0 : 0.0, + :percentage_processing => (count > 0) ? (processing.to_i / count) * 100.0 : 0.0, + :percentage_completed => (count > 0) ? (completed.to_i / count) * 100.0 : 0.0, }.merge(additional) ).freeze diff --git a/lib/taskinator/persistence.rb b/lib/taskinator/persistence.rb index 436fb44..2628cfc 100644 --- a/lib/taskinator/persistence.rb +++ b/lib/taskinator/persistence.rb @@ -365,7 +365,7 @@ def visit @instance.accept(self) - @attributes << [:task_count, @task_count] + @attributes << [:task_count, task_count] @attributes.each do |(name, value)| builder.tag!('attribute', name => value) diff --git a/spec/support/test_definition.rb b/spec/support/test_definition.rb deleted file mode 100644 index 81ab422..0000000 --- a/spec/support/test_definition.rb +++ /dev/null @@ -1,7 +0,0 @@ -module TestDefinition - extend Taskinator::Definition - - def do_task(*args) - end - -end diff --git a/spec/support/test_flows.rb b/spec/support/test_definitions.rb similarity index 62% rename from spec/support/test_flows.rb rename to spec/support/test_definitions.rb index 739c00a..d7dbbd5 100644 --- a/spec/support/test_flows.rb +++ b/spec/support/test_definitions.rb @@ -1,4 +1,4 @@ -module TestFlows +module TestDefinitions module Worker def self.perform(*args) @@ -14,32 +14,62 @@ def iterator(task_count, *args) end end - def do_task(*args) - Taskinator.logger.info(">>> Executing task do_task [#{uuid}]...") - end - - # just create lots of these, so it's easy to see which task + # generate task methods so it's easy to see which task # corresponds with each method when debugging specs 20.times do |i| - define_method "task_#{i}" do |*args| + define_method "task#{i}" do |*args| Taskinator.logger.info(">>> Executing task #{__method__} [#{uuid}]...") end end end + module Definition + extend Taskinator::Definition + include Support + + end + module Task extend Taskinator::Definition include Support define_process :task_count do for_each :iterator do - task :do_task, :queue => :foo + task :task1, :queue => :foo end end end + module TaskAfterCompleted + extend Taskinator::Definition + include Support + + define_process :task_count do + for_each :iterator do + task :task1, :queue => :foo + end + + after_completed :task2, :queue => :foo + end + + end + + module TaskAfterFailed + extend Taskinator::Definition + include Support + + define_process :task_count do + for_each :iterator do + task :task1, :queue => :foo + end + + after_failed :task2, :queue => :foo + end + + end + module Job extend Taskinator::Definition include Support @@ -69,7 +99,7 @@ module Sequential define_process :task_count do sequential do for_each :iterator do - task :do_task + task :task1 end end end @@ -83,7 +113,7 @@ module Concurrent define_process :task_count do concurrent do for_each :iterator do - task :do_task + task :task1 end end end @@ -96,17 +126,17 @@ module EmptySequentialProcessTest define_process do - task :task_0 + task :task0 sequential do # NB: empty! end sequential do - task :task_1 + task :task1 end - task :task_2 + task :task2 end end @@ -117,17 +147,17 @@ module EmptyConcurrentProcessTest define_process do - task :task_0 + task :task0 concurrent do # NB: empty! end concurrent do - task :task_1 + task :task1 end - task :task_2 + task :task2 end end @@ -137,36 +167,36 @@ module NestedTask include Support define_process :task_count do - task :task_1 + task :task1 concurrent do - task :task_2 - task :task_3 + task :task2 + task :task3 sequential do - task :task_4 - task :task_5 + task :task4 + task :task5 concurrent do - task :task_6 - task :task_7 + task :task6 + task :task7 sequential do - task :task_8 - task :task_9 + task :task8 + task :task9 end - task :task_10 + task :task10 end - task :task_11 + task :task11 end - task :task_12 + task :task12 end - task :task_13 + task :task13 end end diff --git a/spec/support/test_flow.rb b/spec/support/test_flow.rb deleted file mode 100644 index c2fd114..0000000 --- a/spec/support/test_flow.rb +++ /dev/null @@ -1,84 +0,0 @@ -module TestFlow - extend Taskinator::Definition - - define_process :some_arg1, :some_arg2 do - - # TODO: add support for "continue_on_error" - task :error_task, :continue_on_error => true - - task :the_task - - for_each :iterator do - task :the_task - end - - for_each :iterator, :sub_option => 1 do - task :the_task - end - - sequential do - task :the_task - task :the_task - task :the_task - end - - task :the_task - - concurrent do - 20.times do |i| - task :the_task - end - task :the_task - end - - task :the_task - - # invoke the specified sub process - sub_process TestSubFlow - - job TestWorkerJob - end - - def error_task(*args) - raise "It's a huge problem!" - end - - # note: arg1 and arg2 are passed in all the way from the - # definition#create_process method - def iterator(arg1, arg2, options={}) - 3.times do |i| - yield [arg1, arg2, i] - end - end - - def the_task(*args) - t = rand(1..11) - Taskinator.logger.info "Executing task '#{task}' with [#{args}] for #{t} secs..." - sleep 1 # 1 - end - - module TestSubFlow - extend Taskinator::Definition - - define_process :some_arg1, :some_arg2 do - task :the_task - task :the_task - task :the_task - end - - def the_task(*args) - t = rand(1..11) - Taskinator.logger.info "Executing sub task '#{task}' with [#{args}] for #{t} secs..." - sleep 1 # t - end - end - - module TestWorkerJob - def self.perform(*args) - end - - def perform(*args) - end - end - -end diff --git a/spec/taskinator/builder_spec.rb b/spec/taskinator/builder_spec.rb index 09b17a2..029778a 100644 --- a/spec/taskinator/builder_spec.rb +++ b/spec/taskinator/builder_spec.rb @@ -319,6 +319,12 @@ def iterator_method(*args) expect(Taskinator::Task).to receive(:define_step_task).with(process, :task_method, args, builder_options.merge(options)) subject.task(:task_method, options) end + + it "adds task to process" do + expect { + subject.task(:task_method) + }.to change { process.tasks.count }.by(1) + end end describe "#job" do @@ -346,6 +352,12 @@ def iterator_method(*args) expect(Taskinator::Task).to receive(:define_job_task).with(process, job, args, builder_options.merge(options)) subject.job(job, options) end + + it "adds job to process" do + expect { + subject.task(:task_method) + }.to change { process.tasks.count }.by(1) + end end describe "#sub_process" do @@ -379,16 +391,16 @@ def iterator_method(*args) block = Proc.new {|p| p.task :task_method } - expect(process.tasks).to be_empty - subject.sequential(options, &block) - expect(process.tasks).to_not be_empty + expect { + subject.sequential(options, &block) + }.to change { process.tasks.count }.by(1) end it "ignores sub-processes without tasks" do allow(block).to receive(:call) - expect(process.tasks).to be_empty - subject.sequential(options, &define_block) - expect(process.tasks).to be_empty + expect { + subject.sequential(options, &define_block) + }.to_not change { process.tasks.count } end end diff --git a/spec/taskinator/complex_process_spec.rb b/spec/taskinator/complex_process_spec.rb deleted file mode 100644 index 4843275..0000000 --- a/spec/taskinator/complex_process_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe TestFlow, :redis => true do - it "should persist and retrieve" do - processA = TestFlow.create_process(:arg1, :arg2) - - processB = Taskinator::Process.fetch(processA.uuid) - - expect(processB.uuid).to eq(processA.uuid) - expect(processB.definition).to eq(processA.definition) - expect(processB.options).to eq(processA.options) - - expect(processB.tasks.count).to eq(processA.tasks.count) - - tasks = processA.tasks.zip(processB.tasks) - - tasks.each do |(taskB, taskA)| - expect(taskA.process).to eq(taskB.process) - expect(taskA.uuid).to eq(taskB.uuid) - expect(taskA.options).to eq(taskB.options) - end - end -end diff --git a/spec/taskinator/instrumentation_spec.rb b/spec/taskinator/instrumentation_spec.rb index 67cac51..0e9fe02 100644 --- a/spec/taskinator/instrumentation_spec.rb +++ b/spec/taskinator/instrumentation_spec.rb @@ -18,7 +18,7 @@ def self.base_key def initialize @uuid = Taskinator.generate_uuid @options = { :bar => :baz } - @definition = TestDefinition + @definition = TestDefinitions::Definition end end @@ -49,54 +49,137 @@ def initialize } end - describe "#enqueued_payload" do - pending - end + describe "#payload_for" do + [ + [ 1, 100, 0, 0, 0, 0, 0 ], + [ 2, 100, 10, 10, 0, 0, 20 ], + [ 3, 100, 20, 30, 0, 0, 50 ], + [ 4, 100, 25, 40, 0, 0, 65 ], + [ 5, 100, 0, 100, 0, 0, 100 ], + [ 6, 100, 0, 90, 1, 0, 91 ], + ].each do |(s, count, processing, completed, cancelled, failed, check)| + + it "scenario ##{s}" do + Taskinator.redis do |conn| + conn.hset(subject.key, :process_uuid, subject.uuid) + conn.hmset( + subject.process_key, + [:options, YAML.dump({:foo => :bar})], + [:tasks_count, count], + [:tasks_processing, processing], + [:tasks_completed, completed], + [:tasks_cancelled, cancelled], + [:tasks_failed, failed] + ) + end + + # private method, so use "send" + payload = subject.send(:payload_for, "baz", {:qux => :quuz}) + + expect(payload.instance_eval { + percentage_failed + + percentage_cancelled + + percentage_processing + + percentage_completed + }).to eq(check) + + expect(payload).to eq( + OpenStruct.new({ + :type => subject.class.name, + :definition => subject.definition.name, + :process_uuid => subject.uuid, + :process_options => {:foo => :bar}, + :uuid => subject.uuid, + :options => subject.options, + :state => "baz", + :percentage_failed => (failed / count.to_f) * 100.0, + :percentage_cancelled => (cancelled / count.to_f) * 100.0, + :percentage_processing => (processing / count.to_f) * 100.0, + :percentage_completed => (completed / count.to_f) * 100.0, + :qux => :quuz + }) + ) - describe "#processing_payload" do - pending + end + end end - describe "#completed_payload" do - it { + describe "payloads per state" do + let(:additional) { {:qux => :quuz} } + + def payload_for(state, moar={}) + OpenStruct.new({ + :type => subject.class.name, + :definition => subject.definition.name, + :process_uuid => subject.uuid, + :process_options => {:foo => :bar}, + :uuid => subject.uuid, + :options => subject.options, + :state => state, + :percentage_failed => 40.0, + :percentage_cancelled => 30.0, + :percentage_processing => 10.0, + :percentage_completed => 20.0, + }.merge(additional).merge(moar)) + end + + before do Taskinator.redis do |conn| conn.hset(subject.key, :process_uuid, subject.uuid) conn.hmset( subject.process_key, [:options, YAML.dump({:foo => :bar})], [:tasks_count, 100], - [:tasks_processing, 1], - [:tasks_completed, 2], - [:tasks_cancelled, 3], - [:tasks_failed, 4] + [:tasks_processing, 10], + [:tasks_completed, 20], + [:tasks_cancelled, 30], + [:tasks_failed, 40] ) end + end - expect(subject.completed_payload(:baz => :qux)).to eq( - OpenStruct.new({ - :type => subject.class.name, - :definition => subject.definition.name, - :process_uuid => subject.uuid, - :process_options => {:foo => :bar}, - :uuid => subject.uuid, - :state => :completed, - :options => subject.options, - :percentage_processing => 1.0, - :percentage_completed => 2.0, - :percentage_cancelled => 3.0, - :percentage_failed => 4.0, - :baz => :qux - }) - ) - } - end + describe "#enqueued_payload" do + it { + expect(subject.enqueued_payload(additional)).to eq(payload_for(:enqueued)) + } + end - describe "#cancelled_payload" do - pending - end + describe "#processing_payload" do + it { + expect(subject.processing_payload(additional)).to eq(payload_for(:processing)) + } + end - describe "#failed_payload" do - pending - end + describe "#paused_payload" do + it { + expect(subject.paused_payload(additional)).to eq(payload_for(:paused)) + } + end + describe "#resumed_payload" do + it { + expect(subject.resumed_payload(additional)).to eq(payload_for(:resumed)) + } + end + + describe "#completed_payload" do + it { + expect(subject.completed_payload(additional)).to eq(payload_for(:completed)) + } + end + + describe "#cancelled_payload" do + it { + expect(subject.cancelled_payload(additional)).to eq(payload_for(:cancelled)) + } + end + + describe "#failed_payload" do + it { + err = StandardError.new + expect(subject.failed_payload(err, additional)).to eq( + payload_for(:failed, :exception => err.to_s, :backtrace => err.backtrace)) + } + end + end end diff --git a/spec/taskinator/persistence_spec.rb b/spec/taskinator/persistence_spec.rb index 2b2ac08..385e232 100644 --- a/spec/taskinator/persistence_spec.rb +++ b/spec/taskinator/persistence_spec.rb @@ -2,7 +2,7 @@ describe Taskinator::Persistence, :redis => true do - let(:definition) { TestDefinition } + let(:definition) { TestDefinitions::Definition } describe "class methods" do subject { @@ -336,7 +336,7 @@ def initialize describe "#to_xml" do it { - process = TestFlows::Task.create_process(1) + process = TestDefinitions::NestedTask.create_process(1) expect(process.to_xml).to match(/xml/) } end @@ -493,14 +493,14 @@ def initialize describe "#cleanup" do [ - TestFlows::Task, - TestFlows::Job, - TestFlows::SubProcess, - TestFlows::Sequential, - TestFlows::Concurrent, - TestFlows::EmptySequentialProcessTest, - TestFlows::EmptyConcurrentProcessTest, - TestFlows::NestedTask, + TestDefinitions::Task, + TestDefinitions::Job, + TestDefinitions::SubProcess, + TestDefinitions::Sequential, + TestDefinitions::Concurrent, + TestDefinitions::EmptySequentialProcessTest, + TestDefinitions::EmptyConcurrentProcessTest, + TestDefinitions::NestedTask, ].each do |definition| describe "#{definition.name} expire immediately" do @@ -531,7 +531,7 @@ def initialize # sanity check expect(conn.keys).to be_empty - process = TestFlows::Task.create_process(1) + process = TestDefinitions::Task.create_process(1) # sanity check expect(conn.hget(process.key, :uuid)).to eq(process.uuid) diff --git a/spec/taskinator/process_spec.rb b/spec/taskinator/process_spec.rb index 84ffec9..2fa158a 100644 --- a/spec/taskinator/process_spec.rb +++ b/spec/taskinator/process_spec.rb @@ -2,7 +2,7 @@ describe Taskinator::Process do - let(:definition) { TestDefinition } + let(:definition) { TestDefinitions::Definition } describe "Base" do @@ -199,7 +199,7 @@ expect(visitor).to receive(:visit_attribute).with(:uuid) expect(visitor).to receive(:visit_args).with(:options) expect(visitor).to receive(:visit_task_reference).with(:parent) - expect(visitor).to receive(:visit_tasks) + expect(visitor).to receive(:visit_tasks).with(subject.tasks) expect(visitor).to receive(:visit_attribute).with(:scope) expect(visitor).to receive(:visit_attribute).with(:queue) expect(visitor).to receive(:visit_attribute_time).with(:created_at) @@ -208,13 +208,6 @@ subject.accept(visitor) } end - - describe "#tasks_count" do - it { - expect(subject.tasks_count).to eq(0) - } - end - end describe Taskinator::Process::Sequential do @@ -400,7 +393,7 @@ expect(visitor).to receive(:visit_attribute).with(:uuid) expect(visitor).to receive(:visit_args).with(:options) expect(visitor).to receive(:visit_task_reference).with(:parent) - expect(visitor).to receive(:visit_tasks) + expect(visitor).to receive(:visit_tasks).with(subject.tasks) expect(visitor).to receive(:visit_attribute).with(:scope) expect(visitor).to receive(:visit_attribute).with(:queue) expect(visitor).to receive(:visit_attribute_time).with(:created_at) @@ -685,7 +678,7 @@ expect(visitor).to receive(:visit_attribute_enum).with(:complete_on, Taskinator::CompleteOn) expect(visitor).to receive(:visit_args).with(:options) expect(visitor).to receive(:visit_task_reference).with(:parent) - expect(visitor).to receive(:visit_tasks) + expect(visitor).to receive(:visit_tasks).with(subject.tasks) expect(visitor).to receive(:visit_attribute).with(:scope) expect(visitor).to receive(:visit_attribute).with(:queue) expect(visitor).to receive(:visit_attribute_time).with(:created_at) diff --git a/spec/taskinator/task_spec.rb b/spec/taskinator/task_spec.rb index c9f8cd2..032849e 100644 --- a/spec/taskinator/task_spec.rb +++ b/spec/taskinator/task_spec.rb @@ -2,7 +2,7 @@ describe Taskinator::Task do - let(:definition) { TestDefinition } + let(:definition) { TestDefinitions::Definition } let(:process) do Class.new(Taskinator::Process) do @@ -164,25 +164,17 @@ subject.accept(visitor) } end - - describe "#tasks_count" do - it { - process_uuid = SecureRandom.hex - allow(subject).to receive(:process_uuid) { process_uuid } - expect(subject.tasks_count).to eq(0) - } - end end describe Taskinator::Task::Step do - subject { Taskinator::Task.define_step_task(process, :do_task, {:a => 1, :b => 2}) } + subject { Taskinator::Task.define_step_task(process, :task1, {:a => 1, :b => 2}) } it_should_behave_like "a task", Taskinator::Task::Step describe ".define_step_task" do it "sets the queue to use" do - task = Taskinator::Task.define_step_task(process, :do_task, {:a => 1, :b => 2}, :queue => :foo) + task = Taskinator::Task.define_step_task(process, :task1, {:a => 1, :b => 2}, :queue => :foo) expect(task.queue).to eq(:foo) end end diff --git a/spec/taskinator/test_flows_spec.rb b/spec/taskinator/test_definitions_spec.rb similarity index 92% rename from spec/taskinator/test_flows_spec.rb rename to spec/taskinator/test_definitions_spec.rb index 5366ec4..37bf2e9 100644 --- a/spec/taskinator/test_flows_spec.rb +++ b/spec/taskinator/test_definitions_spec.rb @@ -1,12 +1,12 @@ require 'spec_helper' -describe TestFlows do +describe TestDefinitions do [ - TestFlows::Task, - TestFlows::Job, - TestFlows::SubProcess, - TestFlows::Sequential + TestDefinitions::Task, + TestDefinitions::Job, + TestDefinitions::SubProcess, + TestDefinitions::Sequential ].each do |definition| describe definition.name do @@ -95,7 +95,7 @@ context "empty subprocesses" do context "sequential" do - let(:definition) { TestFlows::EmptySequentialProcessTest } + let(:definition) { TestDefinitions::EmptySequentialProcessTest } subject { definition.create_process } it "contains 3 tasks" do @@ -103,9 +103,9 @@ end it "invokes each task" do - expect_any_instance_of(definition).to receive(:task_0) - expect_any_instance_of(definition).to receive(:task_1) - expect_any_instance_of(definition).to receive(:task_2) + expect_any_instance_of(definition).to receive(:task0) + expect_any_instance_of(definition).to receive(:task1) + expect_any_instance_of(definition).to receive(:task2) expect { subject.enqueue! @@ -114,7 +114,7 @@ end context "concurrent" do - let(:definition) { TestFlows::EmptyConcurrentProcessTest } + let(:definition) { TestDefinitions::EmptyConcurrentProcessTest } subject { definition.create_process } it "contains 3 tasks" do @@ -122,9 +122,9 @@ end it "invokes each task" do - expect_any_instance_of(definition).to receive(:task_0) - expect_any_instance_of(definition).to receive(:task_1) - expect_any_instance_of(definition).to receive(:task_2) + expect_any_instance_of(definition).to receive(:task0) + expect_any_instance_of(definition).to receive(:task1) + expect_any_instance_of(definition).to receive(:task2) expect { subject.enqueue! @@ -146,7 +146,7 @@ end let(:task_count) { 2 } - let(:definition) { TestFlows::Task } + let(:definition) { TestDefinitions::Task } subject { definition.create_process(task_count) } it "reports process and task state" do @@ -184,7 +184,7 @@ pending end - describe "subprocess" do + describe "sub_process" do pending end end @@ -200,7 +200,7 @@ end let(:task_count) { 10 } - let(:definition) { TestFlows::Task } + let(:definition) { TestDefinitions::Task } subject { definition.create_process(task_count) } it "reports task completed" do @@ -266,7 +266,7 @@ end let(:task_count) { 10 } - let(:definition) { TestFlows::Job } + let(:definition) { TestDefinitions::Job } subject { definition.create_process(task_count) } it "reports task completed" do @@ -322,7 +322,7 @@ end - describe "sub process" do + describe "sub_process" do before do # override enqueue allow_any_instance_of(Taskinator::Task::Step).to receive(:enqueue!) { |task| @@ -338,7 +338,7 @@ end let(:task_count) { 10 } - let(:definition) { TestFlows::SubProcess } + let(:definition) { TestDefinitions::SubProcess } subject { definition.create_process(task_count) } it "reports task completed" do From e9589bb7473da593c7d70c7ced2984cbf69bf14c Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 23 Feb 2022 20:12:07 +0000 Subject: [PATCH 07/15] bug fix ~ don't freeze instrumentation payload --- lib/taskinator/instrumentation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/taskinator/instrumentation.rb b/lib/taskinator/instrumentation.rb index 6756041..1085a84 100644 --- a/lib/taskinator/instrumentation.rb +++ b/lib/taskinator/instrumentation.rb @@ -69,7 +69,7 @@ def payload_for(state, additional={}) :percentage_processing => (count > 0) ? (processing.to_i / count) * 100.0 : 0.0, :percentage_completed => (count > 0) ? (completed.to_i / count) * 100.0 : 0.0, }.merge(additional) - ).freeze + ) end From 554cf03de3b87e45b4971be2c0ece33110e83bba Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 23 Feb 2022 23:40:00 +0000 Subject: [PATCH 08/15] use SecureRandom.hex instead of uuid for shorter IDs --- lib/taskinator.rb | 2 +- spec/spec_helper.rb | 2 +- spec/support/mock_definition.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/taskinator.rb b/lib/taskinator.rb index b699b89..30d30c9 100644 --- a/lib/taskinator.rb +++ b/lib/taskinator.rb @@ -50,7 +50,7 @@ def options=(opts) end def generate_uuid - SecureRandom.uuid + SecureRandom.hex(10) end ## diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 135c5d1..404af58 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -74,7 +74,7 @@ class ApplicationJob < ActiveJob::Base end config.before(:each, :redis => true) do - Taskinator.redis = { :namespace => "taskinator:test:#{SecureRandom.uuid}" } + Taskinator.redis = { :namespace => "taskinator:test:#{SecureRandom.hex(4)}" } end config.before(:each, :sidekiq => true) do diff --git a/spec/support/mock_definition.rb b/spec/support/mock_definition.rb index 2b95e14..29050dd 100644 --- a/spec/support/mock_definition.rb +++ b/spec/support/mock_definition.rb @@ -14,7 +14,7 @@ def create(queue=nil) definition.queue = queue # create a constant, so that the mock definition isn't anonymous - Object.const_set("Mock#{SecureRandom.hex}Definition", definition) + Object.const_set("Mock#{SecureRandom.hex(4)}Definition", definition) end end From 2529920a662f2aa84558158c2566f258eaaf366a Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 23 Feb 2022 23:40:24 +0000 Subject: [PATCH 09/15] add logger helper method; will be available within task methods --- lib/taskinator/executor.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/taskinator/executor.rb b/lib/taskinator/executor.rb index aece2b1..da359d1 100644 --- a/lib/taskinator/executor.rb +++ b/lib/taskinator/executor.rb @@ -25,5 +25,10 @@ def options task.options if task end + # helpers + def logger + Taskinator.logger + end + end end From d674ba7e36d32a796cba9c9e6ef28eca519a63c7 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Mon, 21 Feb 2022 12:52:30 +0000 Subject: [PATCH 10/15] add before_started, after_completed and after_failed functionality --- README.md | 40 +++++- lib/taskinator/builder.rb | 55 ++++++-- lib/taskinator/persistence.rb | 129 ++++++++++++++----- lib/taskinator/process.rb | 24 ++++ lib/taskinator/task.rb | 71 ++++++++++- lib/taskinator/visitor.rb | 9 ++ spec/examples/process_examples.rb | 3 + spec/support/test_definitions.rb | 14 +++ spec/taskinator/builder_spec.rb | 90 +++++++++++++ spec/taskinator/process_spec.rb | 45 +++++++ spec/taskinator/task_spec.rb | 202 +++++++++++++++++++++++++++++- 11 files changed, 641 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 9359c0c..916c19d 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ end process = MyProcess.create_process Date.today, :option_1 => true ``` -_NOTE:_ The current implementation performs a naive check on the count of arguments. +_NOTE:_ The current implementation performs a naïve check on the count of arguments. Next, specify the tasks with their corresponding implementation methods, that make up the process, using the `task` method and providing the `method` to execute for the task. @@ -291,6 +291,44 @@ module MyProcess end ``` +#### Before Process Started and After Process Completion or Failure + +You may want to run further tasks asynchrously before or after a process has completed +or failed. These tasks provide a way to execute logic independently of the process. + +Specify these tasks using the `before_started`, `after_completed` or `after_failed` methods. + +For example, using `after_completed` to set off another business process or `after_failed` to +send an email to an operator. + +```ruby +module MyProcess + extend Taskinator::Definition + + # defines a process + define_process do + + # tasks, sub-process, etc. + + # define task to execute on completion + after_completed :further_process + + # define task to execute on failure + after_failed :email_operations + + end + + def further_process + # ... + end + + def email_operations + # ... + end + +end +``` + #### Complex Process Definitions Any combination or nesting of `task`, `sequential`, `concurrent` and `for_each` steps are diff --git a/lib/taskinator/builder.rb b/lib/taskinator/builder.rb index 3f55baa..d27e106 100644 --- a/lib/taskinator/builder.rb +++ b/lib/taskinator/builder.rb @@ -83,9 +83,32 @@ def job(job, options={}) nil end - # TODO: add mailer - # TODO: add complete! - # TODO: add fail! + # defines a task which executes the given @method before the process has started + def before_started(method, options={}) + raise ArgumentError, 'method' if method.nil? + raise NoMethodError, method unless @executor.respond_to?(method) + + define_before_started_task(@process, method, @args, options) + nil + end + + # defines a task which executes the given @method after the process has completed + def after_completed(method, options={}) + raise ArgumentError, 'method' if method.nil? + raise NoMethodError, method unless @executor.respond_to?(method) + + define_after_completed_task(@process, method, @args, options) + nil + end + + # defines a task which executes the given @method after the process has failed + def after_failed(method, options={}) + raise ArgumentError, 'method' if method.nil? + raise NoMethodError, method unless @executor.respond_to?(method) + + define_after_failed_task(@process, method, @args, options) + nil + end # defines a sub process task, for the given @definition # the definition specified must have input compatible arguments @@ -104,13 +127,31 @@ def sub_process(definition, options={}) private def define_step_task(process, method, args, options={}) - define_task(process) { + add_task(process.tasks) { Task.define_step_task(process, method, args, combine_options(options)) } end + def define_before_started_task(process, method, args, options={}) + add_task(process.before_started_tasks) { + Task.define_hook_task(process, method, args, combine_options(options)) + } + end + + def define_after_completed_task(process, method, args, options={}) + add_task(process.after_completed_tasks) { + Task.define_hook_task(process, method, args, combine_options(options)) + } + end + + def define_after_failed_task(process, method, args, options={}) + add_task(process.after_failed_tasks) { + Task.define_hook_task(process, method, args, combine_options(options)) + } + end + def define_job_task(process, job, args, options={}) - define_task(process) { + add_task(process.tasks) { Task.define_job_task(process, job, args, combine_options(options)) } end @@ -119,8 +160,8 @@ def define_sub_process_task(process, sub_process, options={}) Task.define_sub_process_task(process, sub_process, combine_options(options)) end - def define_task(process) - process.tasks << task = yield + def add_task(list) + list << task = yield task end diff --git a/lib/taskinator/persistence.rb b/lib/taskinator/persistence.rb index 2628cfc..d526bb8 100644 --- a/lib/taskinator/persistence.rb +++ b/lib/taskinator/persistence.rb @@ -274,16 +274,19 @@ def visit_process(attribute) end def visit_tasks(tasks) - tasks.each do |task| - RedisSerializationVisitor.new(@conn, task, @base_visitor).visit - @conn.rpush "#{@key}:tasks", task.uuid - unless task.is_a?(Task::SubProcess) - incr_task_count unless self == @base_visitor - @base_visitor.incr_task_count - end - end - @conn.set("#{@key}.count", tasks.count) - @conn.set("#{@key}.pending", tasks.count) + _visit_tasks(tasks) + end + + def visit_before_started_tasks(tasks) + _visit_tasks(tasks, ':before_started') + end + + def visit_after_completed_tasks(tasks) + _visit_tasks(tasks, ':after_completed') + end + + def visit_after_failed_tasks(tasks) + _visit_tasks(tasks, ':after_failed') end def visit_attribute(attribute) @@ -334,6 +337,21 @@ def task_count def incr_task_count @task_count += 1 end + + private + + def _visit_tasks(tasks, list='') + tasks.each do |task| + RedisSerializationVisitor.new(@conn, task, @base_visitor).visit + @conn.rpush "#{@key}#{list}:tasks", task.uuid + unless task.is_a?(Task::SubProcess) + incr_task_count unless self == @base_visitor + @base_visitor.incr_task_count + end + end + @conn.set("#{@key}#{list}.count", tasks.count) + @conn.set("#{@key}#{list}.pending", tasks.count) + end end class XmlSerializationVisitor < Taskinator::Visitor::Base @@ -386,17 +404,19 @@ def visit_process(attribute) end def visit_tasks(tasks) - builder.tag!('tasks', :count => tasks.count) do |xml| - tasks.each do |task| - xml.tag!('task', :key => task.key) do |xml2| - XmlSerializationVisitor.new(xml2, task, @base_visitor).visit - unless task.is_a?(Task::SubProcess) - incr_task_count unless self == @base_visitor - @base_visitor.incr_task_count - end - end - end - end + _visit_tasks(tasks) + end + + def visit_before_started_tasks(tasks) + _visit_tasks(tasks, 'before_started') + end + + def visit_after_completed_tasks(tasks) + _visit_tasks(tasks, 'after_completed') + end + + def visit_after_failed_tasks(tasks) + _visit_tasks(tasks, 'after_failed') end def visit_attribute(attribute) @@ -446,6 +466,22 @@ def task_count def incr_task_count @task_count += 1 end + + private + + def _visit_tasks(tasks, list='tasks') + builder.tag!(list, :count => tasks.count) do |xml| + tasks.each do |task| + xml.tag!('task', :key => task.key) do |xml2| + XmlSerializationVisitor.new(xml2, task, @base_visitor).visit + unless task.is_a?(Task::SubProcess) + incr_task_count unless self == @base_visitor + @base_visitor.incr_task_count + end + end + end + end + end end class UnknownTypeError < StandardError @@ -541,11 +577,19 @@ def visit_process(attribute) end def visit_tasks(tasks) - # tasks are a linked list, so just get the first one - Taskinator.redis do |conn| - uuid = conn.lindex("#{@key}:tasks", 0) - tasks.attach(lazy_instance_for(Task, uuid), conn.get("#{@key}.count").to_i) if uuid - end + _visit_tasks(tasks) + end + + def visit_before_started_tasks(tasks) + _visit_tasks(tasks, ':before_started') + end + + def visit_after_completed_tasks(tasks) + _visit_tasks(tasks, ':after_completed') + end + + def visit_after_failed_tasks(tasks) + _visit_tasks(tasks, ':after_failed') end def visit_process_reference(attribute) @@ -607,6 +651,14 @@ def visit_args(attribute) private + def _visit_tasks(tasks, list='') + # tasks are a linked list, so just get the first one + Taskinator.redis do |conn| + uuid = conn.lindex("#{@key}#{list}:tasks", 0) + tasks.attach(lazy_instance_for(Task, uuid), conn.get("#{@key}#{list}.count").to_i) if uuid + end + end + # # creates a proxy for the instance which # will only fetch the instance when used @@ -649,14 +701,31 @@ def visit_process(attribute) end def visit_tasks(tasks) - @conn.expire "#{@key}:tasks", expire_in - @conn.expire "#{@key}.count", expire_in - @conn.expire "#{@key}.pending", expire_in + _visit_tasks(tasks) + end + + def visit_before_started_tasks(tasks) + _visit_tasks(tasks, ':before_started') + end + + def visit_after_completed_tasks(tasks) + _visit_tasks(tasks, ':after_completed') + end + + def visit_after_failed_tasks(tasks) + _visit_tasks(tasks, ':after_failed') + end + + private + + def _visit_tasks(tasks, list='') + @conn.expire "#{@key}#{list}:tasks", expire_in + @conn.expire "#{@key}#{list}.count", expire_in + @conn.expire "#{@key}#{list}.pending", expire_in tasks.each do |task| RedisCleanupVisitor.new(@conn, task, expire_in).visit end end - end # lazily loads the object specified by the type and uuid diff --git a/lib/taskinator/process.rb b/lib/taskinator/process.rb index 421a617..dadb3e5 100644 --- a/lib/taskinator/process.rb +++ b/lib/taskinator/process.rb @@ -55,6 +55,18 @@ def tasks @tasks ||= Tasks.new end + def before_started_tasks + @before_started_tasks ||= Tasks.new + end + + def after_completed_tasks + @after_completed_tasks ||= Tasks.new + end + + def after_failed_tasks + @after_failed_tasks ||= Tasks.new + end + def no_tasks_defined? tasks.empty? end @@ -64,6 +76,9 @@ def accept(visitor) visitor.visit_task_reference(:parent) visitor.visit_type(:definition) visitor.visit_tasks(tasks) + visitor.visit_before_started_tasks(before_started_tasks) + visitor.visit_after_completed_tasks(after_completed_tasks) + visitor.visit_after_failed_tasks(after_failed_tasks) visitor.visit_args(:options) visitor.visit_attribute(:scope) visitor.visit_attribute(:queue) @@ -92,6 +107,9 @@ def enqueue! def start! return if paused? || cancelled? + # enqueue before started tasks independently + before_started_tasks.each(&:enqueue!) + transition(:processing) do instrument('taskinator.process.processing', processing_payload) do start @@ -132,6 +150,9 @@ def complete! end end end + + # enqueue completion tasks independently + after_completed_tasks.each(&:enqueue!) end # TODO: add retry method - to pick up from a failed task @@ -159,6 +180,9 @@ def fail!(error) parent.fail!(error) unless parent.nil? end end + + # enqueue completion tasks independently + after_failed_tasks.each(&:enqueue!) end def task_failed(task, error) diff --git a/lib/taskinator/task.rb b/lib/taskinator/task.rb index 109f913..d019b1f 100644 --- a/lib/taskinator/task.rb +++ b/lib/taskinator/task.rb @@ -15,6 +15,10 @@ def define_job_task(process, job, args, options={}) Job.new(process, job, args, options) end + def define_hook_task(process, method, args, options={}) + Hook.new(process, method, args, options) + end + def define_sub_process_task(process, sub_process, options={}) SubProcess.new(process, sub_process, options) end @@ -100,7 +104,7 @@ def complete! instrument('taskinator.task.completed', completed_payload) do complete if respond_to?(:complete) # notify the process that this task has completed - process.task_completed(self) + process.task_completed(self) if notify_process? end end end @@ -124,7 +128,7 @@ def fail!(error) instrument('taskinator.task.failed', failed_payload(error)) do fail(error) if respond_to?(:fail) # notify the process that this task has failed - process.task_failed(self, error) + process.task_failed(self, error) if notify_process? end end end @@ -133,6 +137,10 @@ def incr_count? true end + def notify_process? + true + end + #-------------------------------------------------- # subclasses must implement the following methods #-------------------------------------------------- @@ -206,6 +214,65 @@ def inspect #-------------------------------------------------- + # a task which invokes the specified method on the definition + # the task is executed independently of the process, so there isn't any further + # processing once it completes (or fails) + # the args must be intrinsic types, since they are serialized to YAML + class Hook < Task + attr_reader :method + attr_reader :args + + def initialize(process, method, args, options={}) + super(process, options) + + raise ArgumentError, 'method' if method.nil? + raise NoMethodError, method unless executor.respond_to?(method) + + @method = method + @args = args + end + + def enqueue + Taskinator.queue.enqueue_task(self) + end + + def start + executor.send(method, *args) + # ASSUMPTION: when the method returns, the task is considered to be complete + complete! + + rescue => e + Taskinator.logger.error(e) + Taskinator.logger.debug(e.backtrace) + fail!(e) + raise e + end + + def accept(visitor) + super + visitor.visit_attribute(:method) + visitor.visit_args(:args) + end + + def executor + @executor ||= Taskinator::Executor.new(definition, self) + end + + def incr_count? + false + end + + def notify_process? + false + end + + def inspect + %(#<#{self.class.name}:0x#{self.__id__.to_s(16)} uuid="#{uuid}", definition=:#{definition}, method=:#{method}, args=#{args}, current_state=:#{current_state}>) + end + end + + #-------------------------------------------------- + # a task which invokes the specified background job # the args must be intrinsic types, since they are serialized to YAML class Job < Task diff --git a/lib/taskinator/visitor.rb b/lib/taskinator/visitor.rb index 85dfa59..619b667 100644 --- a/lib/taskinator/visitor.rb +++ b/lib/taskinator/visitor.rb @@ -7,6 +7,15 @@ def visit_process(attribute) def visit_tasks(tasks) end + def visit_before_started_tasks(tasks) + end + + def visit_after_completed_tasks(tasks) + end + + def visit_after_failed_tasks(tasks) + end + def visit_attribute(attribute) end diff --git a/spec/examples/process_examples.rb b/spec/examples/process_examples.rb index c39f26a..29edc84 100644 --- a/spec/examples/process_examples.rb +++ b/spec/examples/process_examples.rb @@ -9,5 +9,8 @@ it { expect(subject.to_s).to match(/#{subject.uuid}/) } it { expect(subject.options).to_not be_nil } it { expect(subject.tasks).to_not be_nil } + it { expect(subject.before_started_tasks).to_not be_nil } + it { expect(subject.after_completed_tasks).to_not be_nil } + it { expect(subject.after_failed_tasks).to_not be_nil } end diff --git a/spec/support/test_definitions.rb b/spec/support/test_definitions.rb index d7dbbd5..290949a 100644 --- a/spec/support/test_definitions.rb +++ b/spec/support/test_definitions.rb @@ -42,6 +42,20 @@ module Task end + module TaskBeforeStarted + extend Taskinator::Definition + include Support + + define_process :task_count do + before_started :task1, :queue => :foo + + for_each :iterator do + task :task2, :queue => :foo + end + end + + end + module TaskAfterCompleted extend Taskinator::Definition include Support diff --git a/spec/taskinator/builder_spec.rb b/spec/taskinator/builder_spec.rb index 029778a..fc84742 100644 --- a/spec/taskinator/builder_spec.rb +++ b/spec/taskinator/builder_spec.rb @@ -360,6 +360,96 @@ def iterator_method(*args) end end + describe "#before_started" do + it "creates a task" do + expect(Taskinator::Task).to receive(:define_hook_task).with(process, :task_method, args, builder_options) + subject.before_started(:task_method) + end + + it "fails if task method is nil" do + expect { + subject.before_started(nil) + }.to raise_error(ArgumentError) + end + + it "fails if task method is not defined" do + expect { + subject.before_started(:undefined) + }.to raise_error(NoMethodError) + end + + it "includes options" do + expect(Taskinator::Task).to receive(:define_hook_task).with(process, :task_method, args, builder_options.merge(options)) + subject.before_started(:task_method, options) + end + + it "adds task to process" do + expect { + subject.before_started(:task_method) + }.to change { process.before_started_tasks.count }.by(1) + end + end + + describe "#after_completed" do + it "creates a task" do + expect(Taskinator::Task).to receive(:define_hook_task).with(process, :task_method, args, builder_options) + subject.after_completed(:task_method) + end + + it "fails if task method is nil" do + expect { + subject.after_completed(nil) + }.to raise_error(ArgumentError) + end + + it "fails if task method is not defined" do + expect { + subject.after_completed(:undefined) + }.to raise_error(NoMethodError) + end + + it "includes options" do + expect(Taskinator::Task).to receive(:define_hook_task).with(process, :task_method, args, builder_options.merge(options)) + subject.after_completed(:task_method, options) + end + + it "adds task to process" do + expect { + subject.after_completed(:task_method) + }.to change { process.after_completed_tasks.count }.by(1) + end + end + + describe "#after_failed" do + it "creates a task" do + expect(Taskinator::Task).to receive(:define_hook_task).with(process, :task_method, args, builder_options) + subject.after_failed(:task_method) + end + + it "fails if task method is nil" do + expect { + subject.after_failed(nil) + }.to raise_error(ArgumentError) + end + + it "fails if method is not defined" do + expect { + subject.after_failed(:undefined) + }.to raise_error(NoMethodError) + end + + it "includes options" do + expect(Taskinator::Task).to receive(:define_hook_task).with(process, :task_method, args, builder_options.merge(options)) + subject.after_failed(:task_method, options) + end + + it "adds task to process" do + expect { + subject.after_failed(:task_method) + }.to change { process.after_failed_tasks.count }.by(1) + end + end + describe "#sub_process" do let(:sub_definition) do Module.new do diff --git a/spec/taskinator/process_spec.rb b/spec/taskinator/process_spec.rb index 2fa158a..ee7953a 100644 --- a/spec/taskinator/process_spec.rb +++ b/spec/taskinator/process_spec.rb @@ -35,6 +35,18 @@ it { expect(subject.tasks).to be_a(Taskinator::Tasks) } end + describe "#before_started_tasks" do + it { expect(subject.before_started_tasks).to be_a(Taskinator::Tasks) } + end + + describe "#after_completed_tasks" do + it { expect(subject.after_completed_tasks).to be_a(Taskinator::Tasks) } + end + + describe "#after_failed_tasks" do + it { expect(subject.after_failed_tasks).to be_a(Taskinator::Tasks) } + end + describe "#no_tasks_defined?" do it { expect(subject.no_tasks_defined?).to be } it { @@ -91,6 +103,14 @@ subject.start! expect(subject.current_state).to eq(:processing) } + + it "enqueues before_started_tasks" do + task = Class.new(Taskinator::Task).new(subject) + expect(task).to receive(:enqueue!) + subject.before_started_tasks << task + + subject.start! + end end describe "#cancel!" do @@ -149,6 +169,14 @@ subject.complete! expect(subject.current_state).to eq(:completed) } + + it "enqueues after_completed_tasks" do + task = Class.new(Taskinator::Task).new(subject) + expect(task).to receive(:enqueue!) + subject.after_completed_tasks << task + + subject.complete! + end end describe "#fail!" do @@ -164,6 +192,14 @@ subject.fail!(StandardError.new) expect(subject.current_state).to eq(:failed) } + + it "enqueues after_failed_tasks" do + task = Class.new(Taskinator::Task).new(subject) + expect(task).to receive(:enqueue!) + subject.after_failed_tasks << task + + subject.fail!(StandardError.new) + end end end @@ -200,6 +236,9 @@ expect(visitor).to receive(:visit_args).with(:options) expect(visitor).to receive(:visit_task_reference).with(:parent) expect(visitor).to receive(:visit_tasks).with(subject.tasks) + expect(visitor).to receive(:visit_before_started_tasks).with(subject.before_started_tasks) + expect(visitor).to receive(:visit_after_completed_tasks).with(subject.after_completed_tasks) + expect(visitor).to receive(:visit_after_failed_tasks).with(subject.after_failed_tasks) expect(visitor).to receive(:visit_attribute).with(:scope) expect(visitor).to receive(:visit_attribute).with(:queue) expect(visitor).to receive(:visit_attribute_time).with(:created_at) @@ -394,6 +433,9 @@ expect(visitor).to receive(:visit_args).with(:options) expect(visitor).to receive(:visit_task_reference).with(:parent) expect(visitor).to receive(:visit_tasks).with(subject.tasks) + expect(visitor).to receive(:visit_before_started_tasks).with(subject.before_started_tasks) + expect(visitor).to receive(:visit_after_completed_tasks).with(subject.after_completed_tasks) + expect(visitor).to receive(:visit_after_failed_tasks).with(subject.after_failed_tasks) expect(visitor).to receive(:visit_attribute).with(:scope) expect(visitor).to receive(:visit_attribute).with(:queue) expect(visitor).to receive(:visit_attribute_time).with(:created_at) @@ -679,6 +721,9 @@ expect(visitor).to receive(:visit_args).with(:options) expect(visitor).to receive(:visit_task_reference).with(:parent) expect(visitor).to receive(:visit_tasks).with(subject.tasks) + expect(visitor).to receive(:visit_before_started_tasks).with(subject.before_started_tasks) + expect(visitor).to receive(:visit_after_completed_tasks).with(subject.after_completed_tasks) + expect(visitor).to receive(:visit_after_failed_tasks).with(subject.after_failed_tasks) expect(visitor).to receive(:visit_attribute).with(:scope) expect(visitor).to receive(:visit_attribute).with(:queue) expect(visitor).to receive(:visit_attribute_time).with(:created_at) diff --git a/spec/taskinator/task_spec.rb b/spec/taskinator/task_spec.rb index 032849e..f89af36 100644 --- a/spec/taskinator/task_spec.rb +++ b/spec/taskinator/task_spec.rb @@ -282,7 +282,7 @@ end end - describe "#complete" do + describe "#complete!" do it "notifies parent process" do expect(process).to receive(:task_completed).with(subject) @@ -304,6 +304,206 @@ end end + describe "#fail!" do + it "notifies parent process" do + err = StandardError.new + expect(process).to receive(:task_failed).with(subject, err) + + subject.fail!(err) + end + + it "is instrumented" do + err = StandardError.new + allow(process).to receive(:task_failed).with(subject, err) + + instrumentation_block = SpecSupport::Block.new + + expect(instrumentation_block).to receive(:call) do |*args| + expect(args.first).to eq('taskinator.task.failed') + end + + TestInstrumenter.subscribe(instrumentation_block, /taskinator.task/) do + subject.fail!(err) + end + end + end + + describe "#accept" do + it { + expect(subject).to receive(:accept) + subject.save + } + + it { + visitor = double('visitor') + expect(visitor).to receive(:visit_type).with(:definition) + expect(visitor).to receive(:visit_attribute).with(:uuid) + expect(visitor).to receive(:visit_process_reference).with(:process) + expect(visitor).to receive(:visit_task_reference).with(:next) + expect(visitor).to receive(:visit_args).with(:options) + expect(visitor).to receive(:visit_attribute).with(:method) + expect(visitor).to receive(:visit_args).with(:args) + expect(visitor).to receive(:visit_attribute).with(:queue) + expect(visitor).to receive(:visit_attribute_time).with(:created_at) + expect(visitor).to receive(:visit_attribute_time).with(:updated_at) + + subject.accept(visitor) + } + end + + describe "#inspect" do + it { expect(subject.inspect).to_not be_nil } + it { expect(subject.inspect).to include(definition.name) } + end + end + + describe Taskinator::Task::Hook do + + subject { Taskinator::Task.define_hook_task(process, :task1, {:a => 1, :b => 2}) } + + it_should_behave_like "a task", Taskinator::Task::Hook + + describe ".define_hook_task" do + it "sets the queue to use" do + task = Taskinator::Task.define_hook_task(process, :task1, {:a => 1, :b => 2}, :queue => :foo) + expect(task.queue).to eq(:foo) + end + end + + describe "#executor" do + it { expect(subject.executor).to_not be_nil } + it { expect(subject.executor).to be_a(definition) } + + it "handles failure" do + error = StandardError.new + allow(subject.executor).to receive(subject.method).with(*subject.args).and_raise(error) + expect(subject).to receive(:fail!).with(error) + expect { + subject.start! + }.to raise_error(error) + end + end + + describe "#enqueue!" do + it { + expect { + subject.enqueue! + }.to change { Taskinator.queue.tasks.length }.by(1) + } + + it "is instrumented" do + allow(subject.executor).to receive(subject.method).with(*subject.args) + + instrumentation_block = SpecSupport::Block.new + + expect(instrumentation_block).to receive(:call) do |*args| + expect(args.first).to eq('taskinator.task.enqueued') + end + + TestInstrumenter.subscribe(instrumentation_block, /taskinator.task/) do + subject.enqueue! + end + end + end + + describe "#start!" do + before do + allow(process).to receive(:task_completed).with(subject) + end + + it "invokes executor" do + expect(subject.executor).to receive(subject.method).with(*subject.args) + subject.start! + end + + it "provides execution context" do + executor = Taskinator::Executor.new(definition, subject) + + method = subject.method + + executor.singleton_class.class_eval do + define_method method do |*args| + # this method executes in the scope of the executor + # store the context in an instance variable + @exec_context = self + end + end + + # replace the internal executor instance for the task + # with this one, so we can hook into the methods + subject.instance_eval { @executor = executor } + + # task start will invoke the method on the executor + subject.start! + + # extract the instance variable + exec_context = executor.instance_eval { @exec_context } + + expect(exec_context).to eq(executor) + expect(exec_context.uuid).to eq(subject.uuid) + expect(exec_context.options).to eq(subject.options) + end + + it "is instrumented" do + instrumentation_block = SpecSupport::Block.new + + expect(instrumentation_block).to receive(:call) do |*args| + expect(args.first).to eq('taskinator.task.processing') + end + + expect(instrumentation_block).to receive(:call) do |*args| + expect(args.first).to eq('taskinator.task.completed') + end + + TestInstrumenter.subscribe(instrumentation_block) do + subject.start! + end + end + end + + describe "#complete!" do + it "does not notify parent process" do + expect(process).to_not receive(:task_completed).with(subject) + + subject.complete! + end + + it "is instrumented" do + allow(process).to receive(:task_completed).with(subject) + + instrumentation_block = SpecSupport::Block.new + + expect(instrumentation_block).to receive(:call) do |*args| + expect(args.first).to eq('taskinator.task.completed') + end + + TestInstrumenter.subscribe(instrumentation_block, /taskinator.task/) do + subject.complete! + end + end + end + + describe "#fail!" do + it "does not notify parent process" do + err = StandardError.new + expect(process).to_not receive(:task_failed).with(subject, err) + + subject.fail!(err) + end + + it "is instrumented" do + instrumentation_block = SpecSupport::Block.new + + expect(instrumentation_block).to receive(:call) do |*args| + expect(args.first).to eq('taskinator.task.failed') + end + + TestInstrumenter.subscribe(instrumentation_block, /taskinator.task/) do + subject.fail!(StandardError.new) + end + end + end + describe "#accept" do it { expect(subject).to receive(:accept) From 9003c7ce0fc6bcbd53186c876246fc62f4271dcf Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Sat, 7 Jan 2023 18:00:05 +0000 Subject: [PATCH 11/15] readme update for `before_started` --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 916c19d..7bcdc97 100644 --- a/README.md +++ b/README.md @@ -308,7 +308,10 @@ module MyProcess # defines a process define_process do - # tasks, sub-process, etc. + # define task to execute on before + before_started :slack_notification + + # usual tasks, sub-process, etc. # define task to execute on completion after_completed :further_process @@ -318,6 +321,10 @@ module MyProcess end + def slack_notification + # ... + end + def further_process # ... end From 2274ca4c51ac2f7e50993b61db9c43148ffdf83a Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Sun, 8 Jan 2023 15:25:20 +0000 Subject: [PATCH 12/15] add before_started, after_completed and after_failed functionality --- lib/taskinator/persistence.rb | 2 - lib/taskinator/task.rb | 126 ++++++++++++++-------------- spec/examples/visitor_examples.rb | 46 ++++++++++ spec/taskinator/persistence_spec.rb | 1 + spec/taskinator/visitor_spec.rb | 15 ++-- 5 files changed, 116 insertions(+), 74 deletions(-) create mode 100644 spec/examples/visitor_examples.rb diff --git a/lib/taskinator/persistence.rb b/lib/taskinator/persistence.rb index d526bb8..3ed923b 100644 --- a/lib/taskinator/persistence.rb +++ b/lib/taskinator/persistence.rb @@ -222,7 +222,6 @@ def cleanup(expire_in=EXPIRE_IN) end end - end class RedisSerializationVisitor < Taskinator::Visitor::Base @@ -750,7 +749,6 @@ def __getobj__ # and memoize for subsequent calls @instance ||= @type.fetch(@uuid, @instance_cache) end - end class << self diff --git a/lib/taskinator/task.rb b/lib/taskinator/task.rb index d019b1f..030baec 100644 --- a/lib/taskinator/task.rb +++ b/lib/taskinator/task.rb @@ -15,13 +15,13 @@ def define_job_task(process, job, args, options={}) Job.new(process, job, args, options) end - def define_hook_task(process, method, args, options={}) - Hook.new(process, method, args, options) - end - def define_sub_process_task(process, sub_process, options={}) SubProcess.new(process, sub_process, options) end + + def define_hook_task(process, method, args, options={}) + Hook.new(process, method, args, options) + end end attr_reader :process @@ -214,65 +214,6 @@ def inspect #-------------------------------------------------- - # a task which invokes the specified method on the definition - # the task is executed independently of the process, so there isn't any further - # processing once it completes (or fails) - # the args must be intrinsic types, since they are serialized to YAML - class Hook < Task - attr_reader :method - attr_reader :args - - def initialize(process, method, args, options={}) - super(process, options) - - raise ArgumentError, 'method' if method.nil? - raise NoMethodError, method unless executor.respond_to?(method) - - @method = method - @args = args - end - - def enqueue - Taskinator.queue.enqueue_task(self) - end - - def start - executor.send(method, *args) - # ASSUMPTION: when the method returns, the task is considered to be complete - complete! - - rescue => e - Taskinator.logger.error(e) - Taskinator.logger.debug(e.backtrace) - fail!(e) - raise e - end - - def accept(visitor) - super - visitor.visit_attribute(:method) - visitor.visit_args(:args) - end - - def executor - @executor ||= Taskinator::Executor.new(definition, self) - end - - def incr_count? - false - end - - def notify_process? - false - end - - def inspect - %(#<#{self.class.name}:0x#{self.__id__.to_s(16)} uuid="#{uuid}", definition=:#{definition}, method=:#{method}, args=#{args}, current_state=:#{current_state}>) - end - end - - #-------------------------------------------------- - # a task which invokes the specified background job # the args must be intrinsic types, since they are serialized to YAML class Job < Task @@ -370,5 +311,64 @@ def inspect %(#<#{self.class.name}:0x#{self.__id__.to_s(16)} uuid="#{uuid}", definition=:#{definition}, sub_process=#{sub_process.inspect}, current_state=:#{current_state}>) end end + + #-------------------------------------------------- + + # a task which invokes the specified method on the definition + # the task is executed independently of the process, so there isn't any further + # processing once it completes (or fails) + # the args must be intrinsic types, since they are serialized to YAML + class Hook < Task + attr_reader :method + attr_reader :args + + def initialize(process, method, args, options={}) + super(process, options) + + raise ArgumentError, 'method' if method.nil? + raise NoMethodError, method unless executor.respond_to?(method) + + @method = method + @args = args + end + + def enqueue + Taskinator.queue.enqueue_task(self) + end + + def start + executor.send(method, *args) + # ASSUMPTION: when the method returns, the task is considered to be complete + complete! + + rescue => e + Taskinator.logger.error(e) + Taskinator.logger.debug(e.backtrace) + fail!(e) + raise e + end + + def accept(visitor) + super + visitor.visit_attribute(:method) + visitor.visit_args(:args) + end + + def executor + @executor ||= Taskinator::Executor.new(definition, self) + end + + def incr_count? + false + end + + def notify_process? + false + end + + def inspect + %(#<#{self.class.name}:0x#{self.__id__.to_s(16)} uuid="#{uuid}", definition=:#{definition}, method=:#{method}, args=#{args}, current_state=:#{current_state}>) + end + end end end diff --git a/spec/examples/visitor_examples.rb b/spec/examples/visitor_examples.rb new file mode 100644 index 0000000..8ae7330 --- /dev/null +++ b/spec/examples/visitor_examples.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +shared_examples_for "a visitor" do |visitor| + + visitor_methods = visitor.instance_methods + + # visit_process(attribute) + it { expect(visitor_methods.include?(:visit_process)).to be } + + # visit_tasks(tasks) + it { expect(visitor_methods.include?(:visit_tasks)).to be } + + # visit_before_started_tasks(tasks) + it { expect(visitor_methods.include?(:visit_before_started_tasks)).to be } + + # visit_after_completed_tasks(tasks) + it { expect(visitor_methods.include?(:visit_after_completed_tasks)).to be } + + # visit_after_failed_tasks(tasks) + it { expect(visitor_methods.include?(:visit_after_failed_tasks)).to be } + + # visit_attribute(attribute) + it { expect(visitor_methods.include?(:visit_attribute)).to be } + + # visit_attribute_time(attribute) + it { expect(visitor_methods.include?(:visit_attribute_time)).to be } + + # visit_attribute_enum(attribute, type) + it { expect(visitor_methods.include?(:visit_attribute_enum)).to be } + + # visit_process_reference(attribute) + it { expect(visitor_methods.include?(:visit_process_reference)).to be } + + # visit_task_reference(attribute) + it { expect(visitor_methods.include?(:visit_task_reference)).to be } + + # visit_type(attribute) + it { expect(visitor_methods.include?(:visit_type)).to be } + + # visit_args(attribute) + it { expect(visitor_methods.include?(:visit_args)).to be } + + # task_count + it { expect(visitor_methods.include?(:task_count)).to be } + +end diff --git a/spec/taskinator/persistence_spec.rb b/spec/taskinator/persistence_spec.rb index 385e232..392541b 100644 --- a/spec/taskinator/persistence_spec.rb +++ b/spec/taskinator/persistence_spec.rb @@ -554,4 +554,5 @@ def initialize end end + end diff --git a/spec/taskinator/visitor_spec.rb b/spec/taskinator/visitor_spec.rb index f8f3d1a..6934910 100644 --- a/spec/taskinator/visitor_spec.rb +++ b/spec/taskinator/visitor_spec.rb @@ -1,14 +1,11 @@ require 'spec_helper' -describe Taskinator::Visitor::Base do +describe "Visitors" do - it { respond_to(:visit_process) } - it { respond_to(:visit_tasks) } - it { respond_to(:visit_attribute) } - it { respond_to(:visit_process_reference) } - it { respond_to(:visit_task_reference) } - it { respond_to(:visit_type) } - it { respond_to(:visit_args) } - it { respond_to(:task_count) } + it_should_behave_like "a visitor", Taskinator::Visitor::Base + it_should_behave_like "a visitor", Taskinator::Persistence::RedisSerializationVisitor + it_should_behave_like "a visitor", Taskinator::Persistence::XmlSerializationVisitor + it_should_behave_like "a visitor", Taskinator::Persistence::RedisDeserializationVisitor + it_should_behave_like "a visitor", Taskinator::Persistence::RedisCleanupVisitor end From f08888ccb165cd5735a37106ba666fe808402385 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 11 Jan 2023 12:04:04 +0000 Subject: [PATCH 13/15] fix: resume status never used --- lib/taskinator/workflow.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/taskinator/workflow.rb b/lib/taskinator/workflow.rb index 0308607..3744184 100644 --- a/lib/taskinator/workflow.rb +++ b/lib/taskinator/workflow.rb @@ -21,7 +21,6 @@ def transition(new_state) enqueued processing paused - resumed completed cancelled failed From b9a591cf85dd2a0029e592f05856c430ebb5cf81 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Sat, 7 Jan 2023 17:39:38 +0000 Subject: [PATCH 14/15] bump version and update changelog --- CHANGELOG.md | 10 ++++++++++ Gemfile.lock | 6 +++--- lib/taskinator/version.rb | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b3e04..9488eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ v?.?.? - ?? ??? ???? --- +v0.6.0 - ?? ??? 2024 +--- + +* Add `before_started`, `after_completed` and `after_failed` functionality. +* Add `logger` helper method, available within task methods. +* Use `SecureRandom.hex(10)` instead of `uuid` for shorter process and tasks IDs. +* Bug fix for options on `sequential`, `concurrent`, `for_each` and `sub_process` methods. +* Bug fix instrumentation payload. +* Documentation updates. + v0.5.2 - 04 Oct 2024 --- * Time arguments fix for Redis 5.0. Fixes #28 diff --git a/Gemfile.lock b/Gemfile.lock index b097af4..eb63330 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - taskinator (0.5.2) + taskinator (0.6.0) builder (>= 3.2.2) connection_pool (>= 2.2.0) globalid (>= 0.3) @@ -115,10 +115,10 @@ GEM rspec-mocks (~> 3.12.0) rspec-core (3.12.0) rspec-support (~> 3.12.0) - rspec-expectations (3.12.1) + rspec-expectations (3.12.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.1) + rspec-mocks (3.12.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (5.1.2) diff --git a/lib/taskinator/version.rb b/lib/taskinator/version.rb index cb9213f..3f56e67 100644 --- a/lib/taskinator/version.rb +++ b/lib/taskinator/version.rb @@ -1,3 +1,3 @@ module Taskinator - VERSION = "0.5.2" + VERSION = "0.6.0" end From fa67dcf7b616289d143e2eff54b621087c63379a Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Fri, 4 Oct 2024 13:12:50 +0100 Subject: [PATCH 15/15] wip! to be completed --- lib/taskinator/create_process_worker.rb | 4 + lib/taskinator/executor.rb | 6 ++ lib/taskinator/process.rb | 27 +++++-- lib/taskinator/task.rb | 17 ++++- spec/support/test_definitions.rb | 72 ++++++++++++++---- spec/taskinator/persistence_spec.rb | 3 + spec/taskinator/test_definitions_spec.rb | 94 ++++++++++++++++++++++++ 7 files changed, 200 insertions(+), 23 deletions(-) diff --git a/lib/taskinator/create_process_worker.rb b/lib/taskinator/create_process_worker.rb index 2684774..55b6229 100644 --- a/lib/taskinator/create_process_worker.rb +++ b/lib/taskinator/create_process_worker.rb @@ -31,6 +31,10 @@ def perform process_args << { :uuid => uuid } end + # generate the process for the given definition and arguments + # and enqueue the processes tasks + # -> sequential processes - enqueues the first task + # -> concurrent processes - enqueues all the tasks @definition._create_process_(false, *process_args).enqueue! end diff --git a/lib/taskinator/executor.rb b/lib/taskinator/executor.rb index da359d1..e419777 100644 --- a/lib/taskinator/executor.rb +++ b/lib/taskinator/executor.rb @@ -26,9 +26,15 @@ def options end # helpers + def logger Taskinator.logger end + def error + # task.process.error + raise NoMethodError + end + end end diff --git a/lib/taskinator/process.rb b/lib/taskinator/process.rb index dadb3e5..e075736 100644 --- a/lib/taskinator/process.rb +++ b/lib/taskinator/process.rb @@ -51,6 +51,10 @@ def parent=(value) @key = nil # NB: invalidate memoized key end + def sub_process? + defined?(@parent) + end + def tasks @tasks ||= Tasks.new end @@ -155,9 +159,6 @@ def complete! after_completed_tasks.each(&:enqueue!) end - # TODO: add retry method - to pick up from a failed task - # e.g. like retrying a failed job in Resque Web - def tasks_completed? # TODO: optimize this tasks.all?(&:completed?) @@ -185,8 +186,25 @@ def fail!(error) after_failed_tasks.each(&:enqueue!) end + #-------------------------------------------------- + + # TODO: add retry method - to pick up from a failed task + # e.g. like retrying a failed job in Resque Web + + def task_started(task) + return if processing? || sub_process? + + transition(:processing) do + # enqueue before started tasks independently + before_started_tasks.each(&:enqueue!) + end + end + + def task_cancelled(task) + cancel! + end + def task_failed(task, error) - # for now, fail this process fail!(error) end @@ -271,7 +289,6 @@ def enqueue end end - # this method only called in-process (usually from the console) def start if tasks.empty? complete! # weren't any tasks to start with diff --git a/lib/taskinator/task.rb b/lib/taskinator/task.rb index 030baec..65e3487 100644 --- a/lib/taskinator/task.rb +++ b/lib/taskinator/task.rb @@ -83,6 +83,9 @@ def start! transition(:processing) do instrument('taskinator.task.processing', processing_payload) do + # notify the process that this task has started + process.task_started(self) if notify_process? + start end end @@ -99,10 +102,12 @@ def paused? end def complete! + self.incr_completed if incr_count? + transition(:completed) do - self.incr_completed if incr_count? instrument('taskinator.task.completed', completed_payload) do complete if respond_to?(:complete) + # notify the process that this task has completed process.task_completed(self) if notify_process? end @@ -110,10 +115,14 @@ def complete! end def cancel! + self.incr_cancelled if incr_count? + transition(:cancelled) do - self.incr_cancelled if incr_count? instrument('taskinator.task.cancelled', cancelled_payload) do cancel if respond_to?(:cancel) + + # notify the process that this task has cancelled + process.task_cancelled(self) if notify_process? end end end @@ -123,10 +132,12 @@ def cancelled? end def fail!(error) + self.incr_failed if incr_count? + transition(:failed) do - self.incr_failed if incr_count? instrument('taskinator.task.failed', failed_payload(error)) do fail(error) if respond_to?(:fail) + # notify the process that this task has failed process.task_failed(self, error) if notify_process? end diff --git a/spec/support/test_definitions.rb b/spec/support/test_definitions.rb index 290949a..1a3cc51 100644 --- a/spec/support/test_definitions.rb +++ b/spec/support/test_definitions.rb @@ -6,6 +6,9 @@ def self.perform(*args) end end + class TestTaskFailed < StandardError + end + module Support def iterator(task_count, *args) @@ -22,6 +25,22 @@ def iterator(task_count, *args) end end + def task_fail(*args) + raise TestTaskFailed + end + + def task_before_started(*args) + Taskinator.logger.info(">>> Executing before started task #{__method__} [#{uuid}]...") + end + + def task_after_completed(*args) + Taskinator.logger.info(">>> Executing after completed task #{__method__} [#{uuid}]...") + end + + def task_after_failed(*args) + Taskinator.logger.info(">>> Executing after failed task #{__method__} [#{uuid}]...") + end + end module Definition @@ -46,12 +65,19 @@ module TaskBeforeStarted extend Taskinator::Definition include Support - define_process :task_count do - before_started :task1, :queue => :foo + define_process do + before_started :task_before_started - for_each :iterator do - task :task2, :queue => :foo - end + task :task1 + end + end + + module TaskBeforeStartedSubProcess + extend Taskinator::Definition + include Support + + define_process do + sub_process TaskBeforeStarted end end @@ -60,12 +86,20 @@ module TaskAfterCompleted extend Taskinator::Definition include Support - define_process :task_count do - for_each :iterator do - task :task1, :queue => :foo - end + define_process do + task :task1 + + after_completed :task_after_completed + end + + end + + module TaskAfterCompletedSubProcess + extend Taskinator::Definition + include Support - after_completed :task2, :queue => :foo + define_process do + sub_process TaskAfterCompleted end end @@ -74,12 +108,20 @@ module TaskAfterFailed extend Taskinator::Definition include Support - define_process :task_count do - for_each :iterator do - task :task1, :queue => :foo - end + define_process do + task :task_fail + + after_failed :task_after_failed + end - after_failed :task2, :queue => :foo + end + + module TaskAfterFailedSubProcess + extend Taskinator::Definition + include Support + + define_process do + sub_process TaskAfterFailed end end diff --git a/spec/taskinator/persistence_spec.rb b/spec/taskinator/persistence_spec.rb index 392541b..32e2e2f 100644 --- a/spec/taskinator/persistence_spec.rb +++ b/spec/taskinator/persistence_spec.rb @@ -501,6 +501,9 @@ def initialize TestDefinitions::EmptySequentialProcessTest, TestDefinitions::EmptyConcurrentProcessTest, TestDefinitions::NestedTask, + TestDefinitions::TaskBeforeStarted, + TestDefinitions::TaskAfterCompleted, + TestDefinitions::TaskAfterFailed, ].each do |definition| describe "#{definition.name} expire immediately" do diff --git a/spec/taskinator/test_definitions_spec.rb b/spec/taskinator/test_definitions_spec.rb index 37bf2e9..7e9676f 100644 --- a/spec/taskinator/test_definitions_spec.rb +++ b/spec/taskinator/test_definitions_spec.rb @@ -92,6 +92,52 @@ Taskinator.queue_adapter = :test_queue_worker end + context "before_started" do + let(:definition) { TestDefinitions::TaskBeforeStarted } + subject { definition.create_process } + + it "invokes before_started task" do + expect(subject.before_started_tasks.count).to eq(1) + expect_any_instance_of(definition).to receive(:task_before_started) + + expect { + subject.enqueue! + }.to change { Taskinator.queue.tasks.length }.by(2) + end + end + + context "after_completed" do + let(:definition) { TestDefinitions::TaskAfterCompleted } + subject { definition.create_process } + + it "invokes after_completed task" do + expect(subject.after_completed_tasks.count).to eq(1) + expect_any_instance_of(definition).to receive(:task_after_completed) + + expect { + subject.enqueue! + }.to change { Taskinator.queue.tasks.length }.by(2) + end + end + + context "after_failed" do + let(:definition) { TestDefinitions::TaskAfterFailed } + subject { definition.create_process } + + it "invokes after_failed task" do + expect(subject.after_failed_tasks.count).to eq(1) + expect_any_instance_of(definition).to receive(:task_after_failed) + + expect { + begin + subject.enqueue! + rescue TestDefinitions::TestTaskFailed + # ignore error + end + }.to change { Taskinator.queue.tasks.length }.by(2) + end + end + context "empty subprocesses" do context "sequential" do @@ -133,6 +179,54 @@ end end + + context "subprocesses" do + + context "before_started" do + let(:definition) { TestDefinitions::TaskBeforeStartedSubProcess } + subject { definition.create_process } + + it "invokes before_started task" do + expect_any_instance_of(definition).to receive(:task_before_started) + + expect { + subject.enqueue! + }.to change { Taskinator.queue.tasks.length }.by(1) + end + end + + context "after_completed" do + let(:definition) { TestDefinitions::TaskAfterCompletedSubProcess } + subject { definition.create_process } + + it "invokes after_completed task" do + expect_any_instance_of(definition).to receive(:task_after_completed) + + expect { + subject.enqueue! + }.to change { Taskinator.queue.tasks.length }.by(2) + end + end + + context "after_failed" do + let(:definition) { TestDefinitions::TaskAfterFailedSubProcess } + subject { definition.create_process } + + it "invokes after_failed task" do + expect_any_instance_of(definition).to receive(:task_after_failed) + + expect { + begin + subject.enqueue! + rescue TestDefinitions::TestTaskFailed + # ignore error + end + }.to change { Taskinator.queue.tasks.length }.by(2) + end + end + + end + end describe "statuses" do