workflow_rb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 378c7aa6543eccd7a1706b1b4248d045fb68d705
4
+ data.tar.gz: 2520ebcac7a53516bb207ef4365e17b346528e18
5
+ SHA512:
6
+ metadata.gz: 1949842e74f1007bf3b422f586e7dc2c118ae96702949a019ad677766c6536c5ce547ad4566adc76ba3a11ac11f9e3b2bc520d03ecf72667ee1e6e2fd9a5d504
7
+ data.tar.gz: cafba682c685829de508f3bb0c9ccf6b159f0ad4613b00b218df7c6da1d1b051eedf563b48e8c4d0962547eb5416ac1652f9568cb19704090dea361a75f4d2fc
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.4
5
+ before_install: gem install bundler -v 1.13.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in workflow_rb.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # WorkflowRb
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/workflow_rb`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'workflow_rb'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install workflow_rb
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/workflow_rb.
36
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "workflow_rb"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,10 @@
1
+ module WorkflowRb
2
+ class EventPublication
3
+ attr_accessor :id
4
+ attr_accessor :workflow_id
5
+ attr_accessor :step_id
6
+ attr_accessor :event_name
7
+ attr_accessor :event_key
8
+ attr_accessor :event_data
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module WorkflowRb
2
+ class EventSubscription
3
+ attr_accessor :id
4
+ attr_accessor :workflow_id
5
+ attr_accessor :step_id
6
+ attr_accessor :event_name
7
+ attr_accessor :event_key
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module WorkflowRb
2
+
3
+ class ExecutionPointer
4
+ attr_accessor :step_id
5
+ attr_accessor :active
6
+ attr_accessor :persistence_data
7
+ attr_accessor :start_time
8
+ attr_accessor :end_time
9
+ attr_accessor :sleep_until
10
+ attr_accessor :event_name
11
+ attr_accessor :event_key
12
+ attr_accessor :event_published
13
+ attr_accessor :event_data
14
+ attr_accessor :concurrent_fork
15
+ attr_accessor :path_terminator
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,25 @@
1
+ module WorkflowRb
2
+
3
+ class ExecutionResult
4
+ attr_accessor :proceed
5
+ attr_accessor :outcome_value
6
+ attr_accessor :persistence_data
7
+ attr_accessor :sleep_for
8
+
9
+ def self.NextStep
10
+ result = ExecutionResult.new
11
+ result.proceed = true
12
+ result.outcome_value = nil
13
+ result
14
+ end
15
+
16
+ def self.Outcome(value)
17
+ result = ExecutionResult.new
18
+ result.proceed = true
19
+ result.outcome_value = value
20
+ result
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,6 @@
1
+ module WorkflowRb
2
+ class IOMapping
3
+ attr_accessor :property
4
+ attr_accessor :value
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+
2
+ module WorkflowRb
3
+
4
+ class StepBody
5
+ def run(context)
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,9 @@
1
+ module WorkflowRb
2
+
3
+ class StepExecutionContext
4
+ attr_accessor :workflow
5
+ attr_accessor :step
6
+ attr_accessor :persistence_data
7
+ end
8
+
9
+ end
@@ -0,0 +1,9 @@
1
+
2
+ module WorkflowRb
3
+
4
+ class StepOutcome
5
+ attr_accessor :value
6
+ attr_accessor :next_step
7
+ end
8
+
9
+ end
@@ -0,0 +1,15 @@
1
+ require 'workflow_rb/models/workflow_step'
2
+ require 'workflow_rb/models/subscription_step_body'
3
+
4
+ module WorkflowRb
5
+ class SubscriptionStep < WorkflowStep
6
+ attr_accessor :event_name
7
+ attr_accessor :event_key
8
+
9
+ def initialize
10
+ super
11
+ @body = SubscriptionStepBody
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ require 'workflow_rb/models/step_body'
2
+ require 'workflow_rb/models/execution_result'
3
+
4
+ module WorkflowRb
5
+ class SubscriptionStepBody < StepBody
6
+ attr_accessor :event_data
7
+ def run(context)
8
+ WorkflowRb::ExecutionResult.NextStep
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module WorkflowRb
2
+
3
+ class WorkflowDefinition
4
+ attr_accessor :id
5
+ attr_accessor :version
6
+ attr_accessor :data_class
7
+ attr_accessor :initial_step
8
+ attr_accessor :steps
9
+
10
+ def initialize
11
+ @steps = []
12
+ end
13
+
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,30 @@
1
+ module WorkflowRb
2
+
3
+ class WorkflowInstance
4
+ attr_accessor :id
5
+ attr_accessor :definition_id
6
+ attr_accessor :version
7
+ attr_accessor :description
8
+ attr_accessor :execution_pointers
9
+ attr_accessor :next_execution
10
+ attr_accessor :status
11
+ attr_accessor :data
12
+ attr_accessor :create_time
13
+ attr_accessor :complete_time
14
+
15
+ def initialize
16
+ @execution_pointers = []
17
+
18
+ end
19
+
20
+ end
21
+
22
+ class WorkflowStatus
23
+ RUNNABLE = 0
24
+ SUSPENDED = 1
25
+ COMPLETE = 2
26
+ TERMINATED = 3
27
+ end
28
+
29
+
30
+ end
@@ -0,0 +1,21 @@
1
+ module WorkflowRb
2
+
3
+ class WorkflowStep
4
+ attr_accessor :id
5
+ attr_accessor :name
6
+
7
+ attr_accessor :body
8
+ attr_accessor :outcomes
9
+ attr_accessor :inputs
10
+ attr_accessor :outputs
11
+
12
+ def initialize
13
+ @outcomes = []
14
+ @inputs = []
15
+ @outputs = []
16
+ end
17
+
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,66 @@
1
+ require 'securerandom'
2
+ require 'workflow_rb/models/workflow_instance'
3
+
4
+ module WorkflowRb
5
+
6
+ class MemoryPersistenceProvider
7
+
8
+ def initialize
9
+ @instances = []
10
+ @subscriptions = []
11
+ @unpublished_events = []
12
+ @semaphore = Mutex.new
13
+ end
14
+
15
+ def create_new_workflow(workflow)
16
+ workflow.id = SecureRandom.uuid
17
+ @instances << workflow
18
+ workflow.id
19
+ end
20
+
21
+ def persist_workflow(workflow)
22
+ @semaphore.synchronize do
23
+ @instances.delete_if {|item| item.id == workflow.id }
24
+ @instances << workflow
25
+ end
26
+ end
27
+
28
+ def get_workflow_instance(id)
29
+ @instances.select {|item| item.id == id}.first
30
+ end
31
+
32
+ def get_runnable_instances
33
+ @instances.select {|item| item.next_execution and (item.next_execution <= Time.new) and (item.status == WorkflowStatus::RUNNABLE)}
34
+ .map {|item| item.id}
35
+ end
36
+
37
+ def create_subscription(subscription)
38
+ subscription.id = SecureRandom.uuid
39
+ @subscriptions << subscription
40
+ subscription.id
41
+ end
42
+
43
+ def get_subscriptions(event_name, event_key)
44
+ @subscriptions.select { |sub| sub.event_name == event_name and sub.event_key == event_key }
45
+ end
46
+
47
+ def terminate_subscription(id)
48
+ @semaphore.synchronize do
49
+ @subscriptions.delete_if { |sub| sub.id == id }
50
+ end
51
+ end
52
+
53
+ def create_unpublished_event(pub)
54
+ @unpublished_events << pub
55
+ end
56
+
57
+ def remove_unpublished_event(id)
58
+ @unpublished_events.delete_if { |pub| pub.id == id }
59
+ end
60
+
61
+ def get_unpublished_events
62
+ @unpublished_events
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,28 @@
1
+ require 'thread'
2
+
3
+ module WorkflowRb
4
+ class SingleNodeLockProvider
5
+
6
+ def initialize
7
+ @semaphore = Mutex.new
8
+ @named_locks = []
9
+ end
10
+
11
+ def acquire_lock(id)
12
+ @semaphore.synchronize do
13
+ if @named_locks.include?(id)
14
+ return false
15
+ end
16
+ @named_locks << id
17
+ return true
18
+ end
19
+ end
20
+
21
+ def release_lock(id)
22
+ @semaphore.synchronize do
23
+ @named_locks.delete(id)
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ require 'thread'
2
+
3
+ module WorkflowRb
4
+ class SingleNodeQueueProvider
5
+
6
+ def initialize
7
+ @process_queue = Queue.new
8
+ @publish_queue = Queue.new
9
+ end
10
+
11
+ def queue_for_processing(id)
12
+ @process_queue << id
13
+ end
14
+
15
+ def dequeue_for_processing
16
+ begin
17
+ return @process_queue.pop(true)
18
+ rescue
19
+ return nil
20
+ end
21
+ end
22
+
23
+ def queue_for_publish(pub)
24
+ @publish_queue << pub
25
+ end
26
+
27
+ def dequeue_for_publish
28
+ begin
29
+ return @publish_queue.pop(true)
30
+ rescue
31
+ return nil
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,172 @@
1
+ require 'workflow_rb'
2
+
3
+ module WorkflowRb
4
+ class WorkflowBuilder
5
+
6
+ attr_accessor :initial_step
7
+ attr_accessor :steps
8
+
9
+ def initialize
10
+ @steps = []
11
+ end
12
+
13
+ def build(id, version, data_class)
14
+ result = WorkflowDefinition.new
15
+ result.id = id
16
+ result.version = version
17
+ result.data_class = data_class
18
+ result.steps = @steps
19
+ result.initial_step = @initial_step
20
+ result
21
+ end
22
+
23
+ def add_step(step)
24
+ step.id = @steps.length
25
+ @steps << step
26
+ end
27
+
28
+ def start_with(body, &setup)
29
+ new_step = WorkflowStep.new
30
+ new_step.body = body
31
+
32
+ if body.kind_of?(Class)
33
+ new_step.name = body.name
34
+ end
35
+
36
+ add_step(new_step)
37
+ @initial_step = new_step.id
38
+ new_builder = StepBuilder.new(self, new_step)
39
+ if setup
40
+ setup.call(new_builder)
41
+ end
42
+ new_builder
43
+ end
44
+
45
+ def start_step(&body)
46
+ start_with(body)
47
+ end
48
+
49
+ end
50
+
51
+ class StepBuilder
52
+
53
+ attr_accessor :step
54
+
55
+ def initialize(workflow_builder, step)
56
+ @workflow_builder = workflow_builder
57
+ @step = step
58
+ end
59
+
60
+ def name(name)
61
+ @step.name = name
62
+ end
63
+
64
+ # Adds a new step to the workflow
65
+ #
66
+ # @param body [Class] the step body implementation class
67
+ def then(body, &setup)
68
+ new_step = WorkflowStep.new
69
+ new_step.body = body
70
+
71
+ @workflow_builder.add_step(new_step)
72
+ new_builder = StepBuilder.new(@workflow_builder, new_step)
73
+
74
+ if body.kind_of?(Class)
75
+ new_step.name = body.name
76
+ end
77
+
78
+ if setup
79
+ setup.call(new_builder)
80
+ end
81
+
82
+ new_outcome = StepOutcome.new
83
+ new_outcome.next_step = new_step.id
84
+ new_outcome.value = nil
85
+ @step.outcomes << new_outcome
86
+
87
+ new_builder
88
+ end
89
+
90
+ def then_step(&body)
91
+ self.then(body)
92
+ end
93
+
94
+ def when(value)
95
+ new_outcome = StepOutcome.new
96
+ new_outcome.value = value
97
+ @step.outcomes << new_outcome
98
+ new_builder = OutcomeBuilder.new(@workflow_builder, new_outcome)
99
+ new_builder
100
+ end
101
+
102
+ # Map workflow instance data to a property on the step
103
+ #
104
+ # @param step_property [Symbol] the attribute on the step body class
105
+ def input(step_property, &value)
106
+ mapping = IOMapping.new
107
+ mapping.property = step_property
108
+ mapping.value = value
109
+ @step.inputs << mapping
110
+ self
111
+ end
112
+
113
+ def output(data_property, &value)
114
+ mapping = IOMapping.new
115
+ mapping.property = data_property
116
+ mapping.value = value
117
+ @step.outputs << mapping
118
+ self
119
+ end
120
+
121
+ def wait_for(event_name, event_key)
122
+ new_step = SubscriptionStep.new
123
+ new_step.event_name = event_name
124
+ new_step.event_key = event_key
125
+
126
+ @workflow_builder.add_step(new_step)
127
+ new_builder = StepBuilder.new(@workflow_builder, new_step)
128
+ new_step.name = 'WaitFor'
129
+
130
+ new_outcome = StepOutcome.new
131
+ new_outcome.next_step = new_step.id
132
+ new_outcome.value = nil
133
+ @step.outcomes << new_outcome
134
+
135
+ new_builder
136
+ end
137
+
138
+ end
139
+
140
+ class OutcomeBuilder
141
+
142
+ attr_accessor :outcome
143
+
144
+ def initialize(workflow_builder, outcome)
145
+ @workflow_builder = workflow_builder
146
+ @outcome = outcome
147
+ end
148
+
149
+ def then(body, &setup)
150
+ new_step = WorkflowStep.new
151
+ new_step.body = body
152
+
153
+ @workflow_builder.add_step(new_step)
154
+ new_builder = StepBuilder.new(@workflow_builder, new_step)
155
+
156
+ if setup
157
+ setup.call(new_builder)
158
+ end
159
+
160
+ @outcome.next_step = new_step.id
161
+ new_builder
162
+ end
163
+
164
+ def then_step(&body)
165
+ self.then(body)
166
+ end
167
+
168
+ end
169
+
170
+
171
+ end
172
+
@@ -0,0 +1,146 @@
1
+ require 'workflow_rb/services/workflow_registry'
2
+ require 'workflow_rb/models/execution_pointer'
3
+ require 'workflow_rb/models/workflow_instance'
4
+ require 'workflow_rb/models/step_execution_context'
5
+ require 'workflow_rb/models/subscription_step'
6
+
7
+ module WorkflowRb
8
+
9
+
10
+ class WorkflowExecutor
11
+
12
+
13
+ public
14
+ def initialize(registry, persistence, host, logger)
15
+ @registry = registry
16
+ @persistence = persistence
17
+ @host = host
18
+ @logger = logger
19
+ end
20
+
21
+ def execute(workflow)
22
+ @logger.debug("Executing workflow #{workflow.id}")
23
+ exe_pointers = workflow.execution_pointers.select { |x| x.active }
24
+ definition = @registry.get_definition(workflow.definition_id, workflow.version)
25
+ if not definition
26
+ raise Exception "Workflow definition #{workflow.definition_id}"
27
+ end
28
+
29
+ exe_pointers.each do |pointer|
30
+ step = definition.steps.select { |x| x.id == pointer.step_id }.first
31
+ if not step
32
+ raise Exception "Step #{pointer.step_id} not found in definition"
33
+ end
34
+
35
+ if (step.kind_of?(SubscriptionStep)) and (not pointer.event_published)
36
+ pointer.event_name = step.event_name
37
+ pointer.event_key = step.event_key
38
+ pointer.active = false
39
+ @host.subscribe_event(workflow.id, step.id, step.event_name, step.event_key)
40
+ next
41
+ end
42
+
43
+
44
+ if not pointer.start_time
45
+ pointer.start_time = Time.new
46
+ end
47
+
48
+ execution_context = StepExecutionContext.new
49
+ execution_context.persistence_data = pointer.persistence_data
50
+ execution_context.workflow = workflow
51
+ execution_context.step = step
52
+
53
+
54
+ if step.body.kind_of?(Proc)
55
+ body_class = Class.new(StepBody) do
56
+ def initialize(body)
57
+ @body = body
58
+ end
59
+ def run(context)
60
+ @body.call(context)
61
+ end
62
+ end
63
+ body_obj = body_class.new(step.body)
64
+ else
65
+ if step.body <= StepBody
66
+ body_obj = step.body.new
67
+ end
68
+ end
69
+
70
+ if not body_obj
71
+ raise "Cannot construct step body #{step.body}"
72
+ end
73
+
74
+ step.inputs.each do |input|
75
+ io_value = input.value.call(workflow.data)
76
+ body_obj.send("#{input.property}=", io_value)
77
+ end
78
+
79
+ if (body_obj.kind_of?(SubscriptionStepBody)) and (pointer.event_published)
80
+ body_obj.event_data = pointer.event_data
81
+ end
82
+
83
+ result = body_obj.run(execution_context)
84
+
85
+ if (result.proceed)
86
+
87
+ step.outputs.each do |output|
88
+ io_value = output.value.call(body_obj)
89
+ workflow.data.send("#{output.property}=", io_value)
90
+ end
91
+
92
+ pointer.active = false
93
+ pointer.end_time = Time.new
94
+ fork_counter = 1
95
+ pointer.path_terminator = true
96
+
97
+ step.outcomes.select {|x| x.value == result.outcome_value}.each do |outcome|
98
+ new_pointer = ExecutionPointer.new
99
+ new_pointer.active = true
100
+ new_pointer.step_id = outcome.next_step
101
+ new_pointer.concurrent_fork = fork_counter * pointer.concurrent_fork
102
+ workflow.execution_pointers << new_pointer
103
+ pointer.path_terminator = false
104
+ fork_counter += 1
105
+ end
106
+ else
107
+ pointer.persistence_data = result.persistence_data
108
+ pointer.sleep_until = result.sleep_until
109
+ end
110
+
111
+ end
112
+ determine_next_execution(workflow)
113
+ @persistence.persist_workflow(workflow)
114
+ end
115
+
116
+ private
117
+ def determine_next_execution(workflow)
118
+ workflow.next_execution = nil
119
+ workflow.execution_pointers.select {|item| item.active }.each do |pointer|
120
+ if not pointer.sleep_until
121
+ workflow.next_execution = Time.new
122
+ return
123
+ end
124
+ workflow.next_execution = [pointer.sleep_until, workflow.next_execution ? workflow.next_execution : pointer.sleep_until].min
125
+ end
126
+
127
+ if not workflow.next_execution
128
+ forks = 1
129
+ terminals = 0
130
+ workflow.execution_pointers.each do |pointer|
131
+ forks = [forks, pointer.concurrent_fork].max
132
+ if pointer.path_terminator
133
+ terminals += 1
134
+ end
135
+ end
136
+ if forks <= terminals
137
+ workflow.status = WorkflowStatus::COMPLETE
138
+ workflow.complete_time = Time.new
139
+ end
140
+ end
141
+ end
142
+
143
+
144
+ end
145
+
146
+ end
@@ -0,0 +1,276 @@
1
+ require 'securerandom'
2
+ require 'logger'
3
+ require 'etc'
4
+ require 'workflow_rb'
5
+ require 'workflow_rb/services/memory_persistence_provider'
6
+ require 'workflow_rb/services/single_node_queue_provider'
7
+ require 'workflow_rb/services/single_node_lock_provider'
8
+
9
+ module WorkflowRb
10
+ class WorkflowHost
11
+
12
+ def initialize
13
+ @persistence = MemoryPersistenceProvider.new
14
+ @queue_provider = SingleNodeQueueProvider.new
15
+ @lock_provider = SingleNodeLockProvider.new
16
+ @registry = WorkflowRegistry.new
17
+ @is_shutdown = true;
18
+ @logger = Logger.new(STDOUT)
19
+ @logger.level = Logger::WARN
20
+ @thread_count = Etc.nprocessors
21
+ @threads = []
22
+ @poll_interval = 5
23
+ @poll_tick = 0
24
+ end
25
+
26
+ def use_logger(logger)
27
+ @logger = logger
28
+ end
29
+
30
+ def use_persistence(persistence)
31
+ @persistence = persistence
32
+ end
33
+
34
+ def register_workflow(workflow_class)
35
+ builder = WorkflowRb::WorkflowBuilder.new
36
+ workflow_obj = workflow_class.new
37
+ workflow_obj.build(builder)
38
+ definition = builder.build(workflow_class::ID, workflow_class::VERSION, workflow_class::DATA_CLASS)
39
+ @registry.register_workflow(definition)
40
+ end
41
+
42
+ def start_workflow(definition_id, version, data = nil)
43
+ wf_def = @registry.get_definition(definition_id, version)
44
+
45
+ wf = WorkflowInstance.new
46
+ wf.definition_id = definition_id
47
+ wf.version = version
48
+ wf.next_execution = Time.new
49
+ wf.create_time = Time.new
50
+ wf.status = WorkflowStatus::RUNNABLE
51
+
52
+ if data
53
+ wf.data = data
54
+ else
55
+ if wf_def.data_class
56
+ wf.data = wf_def.data_class.new
57
+ end
58
+ end
59
+
60
+ ep = ExecutionPointer.new
61
+ ep.active = true
62
+ ep.step_id = wf_def.initial_step
63
+ ep.concurrent_fork = 1
64
+ wf.execution_pointers << ep
65
+
66
+ id = @persistence.create_new_workflow(wf)
67
+ @queue_provider.queue_for_processing(id)
68
+ id
69
+ end
70
+
71
+ def start
72
+ if (@is_shutdown)
73
+ @is_shutdown = false;
74
+ @logger.info('Starting worker thread pool')
75
+
76
+ @thread_count.times do
77
+ @threads << Thread.new { run_workflows }
78
+ end
79
+
80
+ @threads << Thread.new { run_publications }
81
+ @threads << Thread.new { house_keeping }
82
+
83
+ end
84
+ end
85
+
86
+ def stop
87
+ @is_shutdown = true;
88
+ @logger.info('Stopping worker thread pool')
89
+ @threads.each do |thread|
90
+ thread.join(10)
91
+ end
92
+ end
93
+
94
+ def subscribe_event(workflow_id, step_id, event_name, event_key)
95
+ @logger.info("Subscribing to event #{event_name} #{event_key} for workflow #{workflow_id} step #{step_id}")
96
+ sub = EventSubscription.new
97
+ sub.workflow_id = workflow_id
98
+ sub.step_id = step_id
99
+ sub.event_name = event_name
100
+ sub.event_key = event_key
101
+ @persistence.create_subscription(sub)
102
+ end
103
+
104
+ def publish_event(event_name, event_key, event_data)
105
+ if @is_shutdown
106
+ raise Exception 'Host is not running'
107
+ end
108
+ @logger.info("Publishing event #{event_name} #{event_key}")
109
+ subs = @persistence.get_subscriptions(event_name, event_key)
110
+ subs.each do |sub|
111
+ pub = EventPublication.new
112
+ pub.id = SecureRandom.uuid
113
+ pub.event_data = event_data
114
+ pub.event_key = event_key
115
+ pub.event_name = event_name
116
+ pub.step_id = sub.step_id
117
+ pub.workflow_id = sub.workflow_id
118
+ @queue_provider.queue_for_publish(pub)
119
+ @persistence.terminate_subscription(sub.id)
120
+ end
121
+ end
122
+
123
+ def suspend_workflow(id)
124
+ if @lock_provider.acquire_lock(id)
125
+ begin
126
+ workflow = @persistence.get_workflow_instance(id)
127
+ if workflow.status == WorkflowStatus::RUNNABLE
128
+ workflow.status = WorkflowStatus::SUSPENDED
129
+ @persistence.persist_workflow(workflow)
130
+ return true
131
+ else
132
+ return false
133
+ end
134
+ rescue Exception => e
135
+ @logger.error("#{e.message} #{e.backtrace}")
136
+ ensure
137
+ @lock_provider.release_lock(id)
138
+ end
139
+ else
140
+ false
141
+ end
142
+ end
143
+
144
+ def resume_workflow(id)
145
+ if @lock_provider.acquire_lock(id)
146
+ begin
147
+ workflow = @persistence.get_workflow_instance(id)
148
+ if workflow.status == WorkflowStatus::SUSPENDED
149
+ workflow.status = WorkflowStatus::RUNNABLE
150
+ @persistence.persist_workflow(workflow)
151
+ return true
152
+ else
153
+ return false
154
+ end
155
+ rescue Exception => e
156
+ @logger.error("#{e.message} #{e.backtrace}")
157
+ ensure
158
+ @lock_provider.release_lock(id)
159
+ end
160
+ else
161
+ false
162
+ end
163
+ end
164
+
165
+ def terminate_workflow(id)
166
+ if @lock_provider.acquire_lock(id)
167
+ begin
168
+ workflow = @persistence.get_workflow_instance(id)
169
+ workflow.status = WorkflowStatus::TERMINATED
170
+ @persistence.persist_workflow(workflow)
171
+ return true
172
+ rescue Exception => e
173
+ @logger.error("#{e.message} #{e.backtrace}")
174
+ ensure
175
+ @lock_provider.release_lock(id)
176
+ end
177
+ else
178
+ false
179
+ end
180
+ end
181
+
182
+ private
183
+
184
+ def run_workflows
185
+ executor = WorkflowExecutor.new(@registry, @persistence, self, @logger)
186
+ while not @is_shutdown
187
+ begin
188
+ workflow_id = @queue_provider.dequeue_for_processing
189
+ if (workflow_id)
190
+
191
+ if @lock_provider.acquire_lock(workflow_id)
192
+ begin
193
+ workflow = @persistence.get_workflow_instance(workflow_id)
194
+ executor.execute(workflow)
195
+ ensure
196
+ @lock_provider.release_lock(workflow_id)
197
+ end
198
+
199
+ if workflow.next_execution
200
+ if (workflow.status == WorkflowStatus::RUNNABLE) and (workflow.next_execution <= Time.new)
201
+ @queue_provider.queue_for_processing(workflow_id)
202
+ end
203
+ end
204
+ else
205
+ @logger.info("Workflow #{workflow_id} is locked")
206
+ end
207
+ else
208
+ sleep(0.2) #no work
209
+ end
210
+ rescue Exception => e
211
+ @logger.error("#{e.message} #{e.backtrace}")
212
+ end
213
+ end
214
+ end
215
+
216
+ def run_publications
217
+ while not @is_shutdown
218
+ begin
219
+ pub = @queue_provider.dequeue_for_publish
220
+ if (pub)
221
+ if @lock_provider.acquire_lock(pub.workflow_id)
222
+ begin
223
+ workflow = @persistence.get_workflow_instance(pub.workflow_id)
224
+ pointers = workflow.execution_pointers.select { |ep| ep.event_name == pub.event_name and ep.event_key == pub.event_key and not ep.event_published}
225
+ pointers.each do |pointer|
226
+ pointer.event_data = pub.event_data
227
+ pointer.event_published = true
228
+ pointer.active = true
229
+ end
230
+ workflow.next_execution = Time.new
231
+ @persistence.persist_workflow(workflow)
232
+ rescue Exception => e
233
+ @logger.error(e)
234
+ @persistence.create_unpublished_event(pub)
235
+ ensure
236
+ @lock_provider.release_lock(pub.workflow_id)
237
+ @queue_provider.queue_for_processing(pub.workflow_id)
238
+ end
239
+
240
+ if workflow.next_execution
241
+ if (workflow.status == WorkflowStatus::RUNNABLE) and (workflow.next_execution <= Time.new)
242
+ @queue_provider.queue_for_processing(pub.workflow_id)
243
+ end
244
+ end
245
+ else
246
+ @logger.info("Workflow #{workflow_id} is locked")
247
+ end
248
+ else
249
+ sleep(0.5) #no work
250
+ end
251
+ rescue Exception => e
252
+ @logger.error("#{e.message} #{e.backtrace}")
253
+ end
254
+ end
255
+ end
256
+
257
+ def house_keeping
258
+ while (!@is_shutdown)
259
+ begin
260
+ if (@poll_tick >= @poll_interval)
261
+ @poll_tick = 0
262
+ @logger.debug('Polling for runnable instances')
263
+ @persistence.get_runnable_instances.each do |item|
264
+ @queue_provider.queue_for_processing(item)
265
+ end
266
+ end
267
+ @poll_tick += 1
268
+ sleep(1)
269
+ rescue Exception => e
270
+ @logger.error("#{e.message} #{e.backtrace}")
271
+ end
272
+ end
273
+ end
274
+
275
+ end
276
+ end
@@ -0,0 +1,24 @@
1
+ module WorkflowRb
2
+
3
+ class WorkflowRegistry
4
+
5
+ def initialize
6
+ @registry = []
7
+ end
8
+
9
+ def get_definition(id, version)
10
+ @registry.each do |item|
11
+ if (item.id == id) and (item.version == version)
12
+ return item
13
+ end
14
+ end
15
+
16
+ end
17
+
18
+ def register_workflow(definition)
19
+ @registry << definition
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,3 @@
1
+ module WorkflowRb
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,21 @@
1
+ require 'workflow_rb/version'
2
+ require 'workflow_rb/services/workflow_executor'
3
+ require 'workflow_rb/services/workflow_registry'
4
+ require 'workflow_rb/services/workflow_host'
5
+ require 'workflow_rb/services/workflow_builder'
6
+ require 'workflow_rb/models/workflow_instance'
7
+ require 'workflow_rb/models/execution_result'
8
+ require 'workflow_rb/models/workflow_definition'
9
+ require 'workflow_rb/models/workflow_step'
10
+ require 'workflow_rb/models/step_outcome'
11
+ require 'workflow_rb/models/step_body'
12
+ require 'workflow_rb/models/io_mapping'
13
+ require 'workflow_rb/models/subscription_step'
14
+ require 'workflow_rb/models/subscription_step_body'
15
+ require 'workflow_rb/models/event_publication'
16
+ require 'workflow_rb/models/event_subscription'
17
+
18
+ module WorkflowRb
19
+ # Your code goes here...
20
+ end
21
+
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'workflow_rb/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "workflow_rb"
8
+ spec.version = WorkflowRb::VERSION
9
+ spec.authors = ["Daniel Gerlag"]
10
+ spec.email = ["daniel@gerlag.ca"]
11
+
12
+ spec.summary = %q{Lightweight workflow library}
13
+ spec.homepage = "http://github.com/danielgerlag/workflow_rb"
14
+
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.13"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 3.0"
26
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: workflow_rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Gerlag
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-12-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description:
56
+ email:
57
+ - daniel@gerlag.ca
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - ".travis.yml"
65
+ - Gemfile
66
+ - README.md
67
+ - Rakefile
68
+ - bin/console
69
+ - bin/setup
70
+ - lib/workflow_rb.rb
71
+ - lib/workflow_rb/models/event_publication.rb
72
+ - lib/workflow_rb/models/event_subscription.rb
73
+ - lib/workflow_rb/models/execution_pointer.rb
74
+ - lib/workflow_rb/models/execution_result.rb
75
+ - lib/workflow_rb/models/io_mapping.rb
76
+ - lib/workflow_rb/models/step_body.rb
77
+ - lib/workflow_rb/models/step_execution_context.rb
78
+ - lib/workflow_rb/models/step_outcome.rb
79
+ - lib/workflow_rb/models/subscription_step.rb
80
+ - lib/workflow_rb/models/subscription_step_body.rb
81
+ - lib/workflow_rb/models/workflow_definition.rb
82
+ - lib/workflow_rb/models/workflow_instance.rb
83
+ - lib/workflow_rb/models/workflow_step.rb
84
+ - lib/workflow_rb/services/memory_persistence_provider.rb
85
+ - lib/workflow_rb/services/single_node_lock_provider.rb
86
+ - lib/workflow_rb/services/single_node_queue_provider.rb
87
+ - lib/workflow_rb/services/workflow_builder.rb
88
+ - lib/workflow_rb/services/workflow_executor.rb
89
+ - lib/workflow_rb/services/workflow_host.rb
90
+ - lib/workflow_rb/services/workflow_registry.rb
91
+ - lib/workflow_rb/version.rb
92
+ - workflow_rb.gemspec
93
+ homepage: http://github.com/danielgerlag/workflow_rb
94
+ licenses: []
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.6.7
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Lightweight workflow library
116
+ test_files: []