orchestrated 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -1,17 +1,32 @@
1
1
  Orchestrated
2
2
  ============
3
3
 
4
- The [delayed_job](https://github.com/collectiveidea/delayed_job) Ruby Gem provides a job queuing system for Ruby. It implements an elegant API for delaying execution of any object method. Not only is the execution of the method (message delivery) delayed in time, it is potentially shifted in space too. By shifting in space, i.e. running in a separate virtual machine, possibly on a separate computer, multiple CPUs can be brought to bear on a computing problem.
4
+ The [delayed_job](https://github.com/collectiveidea/delayed_job) Ruby Gem provides a restartable queuing system for Ruby. It implements an elegant API for delaying execution of any Ruby object method invocation. Not only is the message delivery delayed in time, it is potentially shifted in space too. By shifting in space, i.e. running in a different virtual machine, possibly on a separate computer, multiple CPUs can be brought to bear on a computing problem.
5
5
 
6
6
  By breaking up otherwise serial execution into multiple queued jobs, a program can be made more scalable. This sort of distributed queue-processing architecture has a long and successful history in data processing.
7
7
 
8
- Queuing works well for simple tasks. By simple we mean the task can be done all at once, in one piece. It has no dependencies on other tasks. This works well for performing a file upload task in the background (to avoid tying up a Ruby virtual machine process/thread). More complex (compound) multi-part tasks, however, do not fit this model. Examples of complex (compound) tasks include:
8
+ Queuing works well for simple, independent tasks. By simple we mean the task can be done all at once, in one piece, with no inter-task dependencies. This works well for performing a file upload task in the background (to avoid tying up a Ruby virtual machine process/thread). More complex (compound) multi-part tasks, however, do not fit this model. Examples of complex (compound) tasks include:
9
9
 
10
- 1. pipelined (multi-step) generation of complex PDF documents
11
- 2. extract/transfer/load (ETL) jobs that may load thousands of database records
10
+ 1. pipelined (multi-step) generation of a complex PDF document
11
+ 2. an extract/transfer/load (ETL) job that must acquire data from source systems, transform it and load it into the target system
12
12
 
13
13
  If we would like to scale these compound operations, breaking them into smaller parts, and managing the execution of those parts across many computers, we need an "orchestrator". This project implements just such a framework, called "Orchestrated".
14
14
 
15
+ Orchestrated introduces the ```acts_as_orchestrated``` Object class method. When invoked on your class, this will define the ```orchestrated``` instance method. You use ```orchestrated``` in a mannner similar to [delayed_job](https://github.com/collectiveidea/delayed_job)'s ```delay```—the difference being that ```orchestrated``` takes a parameter that lets you specify dependencies between your jobs.
16
+
17
+ The reason we refer to [delayed_job](https://github.com/collectiveidea/delayed_job) as a restartable queueing system is because, even if computers (database host, worker hosts) in the cluster crash, the work on the queues progresses. If no worker is servicing a particular queue, then work accumulates there. Once workers are available, they consume the jobs. This is a resilient architecture.
18
+
19
+ With Orchestrated you can create restartable workflows, a workflow consisting of one or more dependent, queueable, tasks. This means that your workflows will continue to make progress even in the face of database and (queue) worker crashes.
20
+
21
+ In summary, orchestrated workflows running atop [active_record](https://github.com/rails/rails/tree/master/activerecord) and [delayed_job](https://github.com/collectiveidea/delayed_job) have these characteristics:
22
+
23
+ 1. restartable—the workflows make progress even though (queue worker, and database) hosts are not always available
24
+ 2. scalable—compound workflows are broken into steps which can be executed on separate computers. Results can be accumulated from the disparate steps as needed.
25
+ 3. forgiving of external system failures—workflow steps that communicate with an external system can simply throw an exception when the system is unavailable, assured that the step will be automatically retried again later.
26
+ 4. composable—compound tasks can be defined in terms of simpler ones
27
+
28
+ Read on to get started.
29
+
15
30
  Installation
16
31
  ------------
17
32
 
@@ -37,7 +52,13 @@ If you do not already have [delayed_job](https://github.com/collectiveidea/delay
37
52
  The API
38
53
  -------
39
54
 
40
- To orchestrate (methods) on your own classes you simply call ```acts_as_orchestrated``` in the class definition like this:
55
+ To orchestrate (methods) on your own classes you simply call ```acts_as_orchestrated``` in the class definition. Declaring ```acts_as_orchestrated``` on your class defines the ```orchestrated``` method:
56
+
57
+ * ```orchestrated```—call this to specify your workflow prerequisite, and designate a workflow step
58
+
59
+ Use ```orchestrated``` to orchestrate any method on your class.
60
+
61
+ Let's say for example you needed to download a couple files from remote systems (a slow process), merge their content and then load the results into your system. Imagine you have a ```Downloader``` class that knows how to download and an ```Xform``` class that knows how to merge the content and load the results into your system. Your ```Xform``` class might look something like this:
41
62
 
42
63
  ```ruby
43
64
  class Xform
@@ -55,12 +76,7 @@ class Xform
55
76
  end
56
77
  ```
57
78
 
58
- Declaring ```acts_as_orchestrated``` on your class gives it two methods:
59
-
60
- * ```orchestrated```—call this to specify your workflow prerequisite, and designate a workflow step
61
- * ```orchestration```—call this in the context of a workflow step (execution) to access orchestration (and prerequisite) context
62
-
63
- After that you can orchestrate any method on such a class. Let's say you needed to download a couple files from remote systems (a slow process), merge their content and then load the results into your system. Imagine you had had a Downloader class that knew how to download and an Xform class that knew how to merge the content and load the results into your system. You might write an orchestration like this:
79
+ You might write an orchestration like this:
64
80
 
65
81
  ```ruby
66
82
  xform = Xform.new
@@ -78,15 +94,16 @@ xform.orchestrated(
78
94
  ).load('combined_records')
79
95
  ```
80
96
 
81
- The next time you process delayed jobs, the :download messages will be delivered to a couple Downloaders. After the last download completes, the next time a delayed job is processed, the :merge message will be delivered to an Xform object. And on it goes…
97
+ The next time you process delayed jobs, the ```download``` messages will be delivered to a couple Downloaders. After the last download completes, the next time a delayed job is processed, the ```merge``` message will be delivered to an Xform object. And on it goes…
82
98
 
83
99
  What happened there? The pattern is:
84
100
 
85
101
  1. create an orchestrated object (instantiate it)
86
- 2. call orchestrated on it: this returns an "orchestration"
87
- 3. send a message to the orchestration (returned in the second step)
102
+ 2. call ```orchestrated``` on it: this returns a *magic proxy* object that can respond to any of the messages your object can respond to
103
+ 3. send any message to the *magic proxy* (returned in the second step) and the framework will delay delivery of that message and immediately return a "completion expression" you can use as a prerequisite for other orchestrations
104
+ 4. (optionally) use the "completion expression" returned in (3) as a prerequisite for other orchestrations
88
105
 
89
- Now the messages you can send in (3) are limited to the messages that your object can respond to. The message will be remembered by the framework and "replayed" (on a new instance of your object) somewhere on the network (later).
106
+ Now the messages you can send in (3) can be anything that your object can respond to. The message will be remembered by the framework and "replayed" (on a new instance of your object) somewhere on the network (later).
90
107
 
91
108
  Not accidentally, this is similar to the way [delayed_job](https://github.com/collectiveidea/delayed_job)'s delay method works. Under the covers, orchestrated is conspiring with [delayed_job](https://github.com/collectiveidea/delayed_job) when it comes time to actually execute a workflow step. Before that time though, orchestrated keeps track of everything.
92
109
 
@@ -95,16 +112,15 @@ Key Concept: Prerequisites (Completion Expressions)
95
112
 
96
113
  Unlike [delayed_job](https://github.com/collectiveidea/delayed_job) ```delay```, the orchestrated ```orchestrated``` method takes an optional parameter: the prerequisite. The prerequisite determines when your workflow step is ready to run.
97
114
 
98
- The return value from "orchestrate" is itself a ready-to-use prerequisite. You saw this in the statement generation example above. The result of the first ```orchestrated``` call was sent as an argument to the second. In this way, the second workflow step was suspended until after the first one finished. You may have also noticed from that example that if you specify no prerequisite then the step will be ready to run immediately, as was the case for the "generate" call).
115
+ The return value from messaging the *magic proxy* is itself a ready-to-use prerequisite. You saw this in the statement generation example above. The result of the first call to ```orchestrated``` calls (to "download") were sent as an argument to the third ("merge"). In this way, the "merge" workflow step was suspended until after the "download"s finished. You may have also noticed from that example that if you specify no prerequisite then the step will be ready to run immediately, as was the case for the "download" calls).
99
116
 
100
- There are five kinds of prerequisite in all. Some of them are used for combining others. The prerequisites types, also known as "completion expressions" are:
117
+ Users of the framework deal directly with three kinds of prerequisite or "completion expression":
101
118
 
102
- 1. ```OrchestrationCompletion```—returned by "orchestrate", complete when its associated orchestration is complete
103
- 2. ```Complete```—always complete
104
- 3. ```FirstCompletion```—aggregates other completions: complete after the first one completes
105
- 4. ```LastCompletion```—aggregates other completions: complete after all of them are complete
119
+ 1. ```OrchestrationCompletion```—returned messages to the *magic proxy*, complete when its associated orchestration is complete
120
+ 2. ```FirstCompletion```—aggregates other completions: complete after the first one completes
121
+ 3. ```LastCompletion```—aggregates other completions: complete after all of them are complete
106
122
 
107
- See the completion_spec for examples of how to combine these different prerequisite types into completion expressions.
123
+ There are other kinds of completion expression used internally by the framework but these three are the important ones for users to understand. See the completion_spec for examples of how to combine these different prerequisite types into completion expressions.
108
124
 
109
125
  Key Concept: Orchestration State
110
126
  --------------------------------
@@ -115,15 +131,15 @@ An orchestration can be in one of a few states:
115
131
 
116
132
  When you create a new orchestration that is waiting on a prerequisite that is not complete yet, the orchestration will be in the "waiting" state. Some time later, if that prerequisite completes, then your orchestration will become "ready". A "ready" orchestration is automatically queued to run by the framework (via [delayed_job](https://github.com/collectiveidea/delayed_job)).
117
133
 
118
- A "ready" orchestration will use [delayed_job](https://github.com/collectiveidea/delayed_job) to delivery its (delayed) message. In the context of such a message delivery (inside your object method e.g. StatementGenerator#generate or StatementGenerator#render) you can rely on the ability to access the current Orchestration (context) object via the "orchestration" accessor.
134
+ A "ready" orchestration will use [delayed_job](https://github.com/collectiveidea/delayed_job) to deliver its (delayed) message. In the context of such a message delivery (inside your object method e.g. ```Xform#merge``` or ```Xform#load``` in our example) you can rely on the ability to access the current Orchestration (context) object via the ```orchestration``` accessor. Be careful with that one though. You really shouldn't need it very often, and to use it, you have to understand framework internals.
119
135
 
120
136
  After your workflow step executes, the orchestration moves into either the "succeeded" or "failed" state.
121
137
 
122
- When an orchestration is "ready" or "waiting" it may be canceled by sending it the ```cancel!``` message. This moves it to the "canceled" state and prevents delivery of the orchestrated message (in the future).
138
+ When an orchestration is "ready" or "waiting" it may be canceled by sending it the ```cancel!``` message (i.e. a ```cancel!``` message to the ```OrchestrationCompletion```. This moves it to the orchestration to the "canceled" state and prevents delivery of the orchestrated message (in the future).
123
139
 
124
140
  It is important to understand that both of the states: "succeeded" and "failed" are part of a "super-state": "complete". When an orchestration is in either of those two states, it will return ```true``` in response to the ```complete?``` message.
125
141
 
126
- It is not just successful completion of orchestrated methods that causes dependent ones to run—a "failed" orchestration is complete too! If you have an orchestration that actually requires successful completion of its prerequisite then it can inspect the prerequisite as needed. It's accessible through the ```orchestration`` accessor (on the orchestrated object).
142
+ It is not just successful completion of orchestrated methods that causes dependent ones to run—a "failed" orchestration is complete too! If you have an orchestration that actually requires successful completion of its prerequisite then your method can inspect the prerequisite as needed, by accessing it via ```self.orchestration.prerequisite.prerequisite```.
127
143
 
128
144
  Failure (An Option)
129
145
  -------------------
@@ -7,11 +7,7 @@ module Orchestrated
7
7
  end
8
8
  def method_missing(sym, *args)
9
9
  raise 'cannot orchestrate with blocks because they are not portable across processes' if block_given?
10
- OrchestrationCompletion.new do |completion|
11
- completion.orchestration = Orchestration.create( @target, sym, args, @prerequisite)
12
- end.tap do |completion|
13
- completion.save!
14
- end
10
+ Orchestration.create( @target, sym, args, @prerequisite)
15
11
  end
16
12
  end
17
13
 
@@ -44,10 +44,10 @@ module Orchestrated
44
44
  def +(c); self << c; end # synonymc
45
45
  end
46
46
  class LastCompletion < CompositeCompletion
47
- def complete?; prerequisites.all?(&:complete?); end
48
- def always_complete?; prerequisites.empty?; end
47
+ def complete?; prerequisite_associations.all?(&:complete?); end
48
+ def always_complete?; prerequisite_associations.empty?; end
49
49
  def never_complete?; prerequisites.any?(&:never_complete?); end
50
- def canceled?; prerequisites.any?(&:canceled?); end
50
+ def canceled?; prerequisite_associations.any?(&:canceled?); end
51
51
  def <<(c)
52
52
  prerequisites << c unless c.always_complete?
53
53
  self
@@ -60,10 +60,10 @@ module Orchestrated
60
60
  end
61
61
  end
62
62
  class FirstCompletion < CompositeCompletion
63
- def complete?; prerequisites.any?(&:complete?); end
63
+ def complete?; prerequisite_associations.any?(&:complete?); end
64
64
  def always_complete?; prerequisites.any?(&:always_complete?); end
65
- def never_complete?; prerequisites.empty?; end
66
- def canceled?; prerequisites.all?(&:canceled?); end
65
+ def never_complete?; prerequisite_associations.empty?; end
66
+ def canceled?; prerequisite_associations.all?(&:canceled?); end
67
67
  def <<(c)
68
68
  prerequisites << c unless c.never_complete?
69
69
  self
@@ -4,9 +4,12 @@ module Orchestrated
4
4
  class OrchestrationDependency < ActiveRecord::Base
5
5
  # TODO: figure out why Rails thinks I'm mass-assigning this over in Orchestration when I'm not really!
6
6
  attr_accessible :prerequisite_id
7
- belongs_to :dependent, :class_name => 'CompletionExpression'
8
7
  belongs_to :prerequisite, :class_name => 'CompletionExpression'
9
- state_machine :initial => :incomplete do
8
+ belongs_to :dependent, :class_name => 'CompletionExpression'
9
+
10
+ before_validation :constrain
11
+
12
+ state_machine :initial => :incomplete, :action => :save_avoiding_recursion do
10
13
  state :incomplete
11
14
  state :complete
12
15
  state :canceled
@@ -17,10 +20,50 @@ module Orchestrated
17
20
  transition :incomplete => :canceled
18
21
  end
19
22
  after_transition any => :complete do |dependency, transition|
20
- dependency.dependent.prerequisite_complete
23
+ dependency.call_dependent{|d| d.prerequisite_complete}
21
24
  end
22
25
  after_transition any => :canceled do |dependency, transition|
23
- dependency.dependent.prerequisite_canceled
26
+ dependency.call_dependent{|d| d.prerequisite_canceled}
27
+ end
28
+ end
29
+ def call_dependent(&block)
30
+ yield dependent unless dependent.nil?
31
+ end
32
+ def constrain
33
+ @saving = true
34
+ _constrain.tap{@saving = false}
35
+ end
36
+ def _constrain
37
+ if prerequisite.present?
38
+ if prerequisite_id_changed? || new_record?
39
+ # this may be our first prerequisite, or our prerequisite may
40
+ # have changed—either way we must initialize our state
41
+
42
+ # This method can be called more than once in general since it is called
43
+ # as part of validation. Rather than loosening the state machine (to allow
44
+ # e.g. complete=>complete) we explicitly avoid re-submitting events here.
45
+ prerequisite_completed if prerequisite.complete? && can_prerequisite_completed?
46
+ prerequisite_canceled if prerequisite.canceled? && can_prerequisite_canceled?
47
+ else
48
+ # prerequisite has not changed so our state is already correct
49
+ if dependent_id_changed?
50
+ # dependent has been set for the first time—propigate state
51
+ call_dependent{|d| d.prerequisite_complete} if prerequisite.complete?
52
+ call_dependent{|d| d.prerequisite_canceled} if prerequisite.canceled?
53
+ end
54
+ end
55
+ end
56
+ true
57
+ end
58
+ def save_avoiding_recursion
59
+ # Default action of state_machine is "save", however that is
60
+ # a problem when we need to transition state during validation
61
+ # (see constrain method above). If were are validating then we
62
+ # dursnt call save again.
63
+ if @saving
64
+ true # allow state transition but don't save ActiveRecord
65
+ else
66
+ save # save ActiveRecord as usual and return true/false
24
67
  end
25
68
  end
26
69
  end
@@ -73,26 +73,23 @@ module Orchestrated
73
73
 
74
74
  end
75
75
 
76
+ # Actually creates a completion (wrapper). Not _exactly_ an orchestration—ssh…
76
77
  def self.create( value, sym, args, prerequisite)
77
- # set prerequisite in new call so it is passed to state_machine :initial proc
78
- new.tap do |orchestration|
79
-
80
- orchestration.handler = Handler.new( value, sym, args)
81
-
82
- # wee! static analysis FTW!
83
- raise 'prerequisite can never be complete' if prerequisite.never_complete?
84
-
85
- prerequisite.save!
86
- orchestration.save!
87
- interest = OrchestrationInterest.new.tap do |interest|
88
- interest.prerequisite = prerequisite
89
- interest.orchestration = orchestration
90
- end
91
- interest.save!
92
-
93
- # prime the pump for a constant prerequisite
94
- orchestration.prerequisite_complete! if prerequisite.complete?
95
- end
78
+ # wee! static analysis FTW!
79
+ raise 'prerequisite can never be complete' if prerequisite.never_complete?
80
+ prerequisite.save!
81
+ OrchestrationCompletion.new.tap do |completion|
82
+ completion.orchestration = new.tap do |orchestration|
83
+ orchestration.handler = Handler.new( value, sym, args)
84
+ orchestration.save!
85
+ interest = OrchestrationInterest.new.tap do |interest|
86
+ interest.prerequisite = prerequisite
87
+ interest.orchestration = orchestration
88
+ interest.save!
89
+ end # interest
90
+ end # orchestration
91
+ completion.save!
92
+ end # completion
96
93
  end
97
94
 
98
95
  def enqueue
@@ -1,3 +1,3 @@
1
1
  module Orchestrated
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.6"
3
3
  end
data/orchestrated.gemspec CHANGED
@@ -18,19 +18,19 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_runtime_dependency 'delayed_job_active_record', '>= 0.3'
22
- gem.add_runtime_dependency 'activerecord', ['~> 3']
23
- gem.add_runtime_dependency 'state_machine', ['>= 1']
21
+ gem.add_runtime_dependency 'delayed_job_active_record', '~> 0.3.3'
22
+ gem.add_runtime_dependency 'activerecord', ['~> 3.2']
23
+ gem.add_runtime_dependency 'state_machine', ['~> 1.1.2']
24
24
 
25
25
  gem.add_development_dependency 'rake', ['>= 10']
26
- gem.add_development_dependency 'rails', ['~> 3'] # for rspec-rails
27
- gem.add_development_dependency 'rspec-rails', ['>= 2.12']
26
+ gem.add_development_dependency 'rails', ['~> 3.2'] # for rspec-rails
27
+ gem.add_development_dependency 'rspec-rails', ['~> 2.12']
28
28
  # I couldn't get rspecs transactional fixtures setting to do savepoints
29
29
  # in this project (which is not _really_ a Rails app). database_cleaner
30
30
  # claims it'll help us clean up the database so let's try it!
31
- gem.add_development_dependency 'database_cleaner', ['>= 0.9']
32
- gem.add_development_dependency 'sqlite3', ['>= 1.3.6']
31
+ gem.add_development_dependency 'database_cleaner', ['~> 0.9']
32
+ gem.add_development_dependency 'sqlite3', ['~> 1.3']
33
33
  gem.add_development_dependency 'debugger'
34
34
  # The state_machine:draw rake task needs this
35
- gem.add_development_dependency 'ruby-graphviz', ['>= 0.9.0']
35
+ gem.add_development_dependency 'ruby-graphviz', ['~> 0.9']
36
36
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: orchestrated
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,24 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-04 00:00:00.000000000 Z
12
+ date: 2013-01-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: delayed_job_active_record
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
- - - ! '>='
19
+ - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: '0.3'
21
+ version: 0.3.3
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  none: false
26
26
  requirements:
27
- - - ! '>='
27
+ - - ~>
28
28
  - !ruby/object:Gem::Version
29
- version: '0.3'
29
+ version: 0.3.3
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: activerecord
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -34,7 +34,7 @@ dependencies:
34
34
  requirements:
35
35
  - - ~>
36
36
  - !ruby/object:Gem::Version
37
- version: '3'
37
+ version: '3.2'
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,23 +42,23 @@ dependencies:
42
42
  requirements:
43
43
  - - ~>
44
44
  - !ruby/object:Gem::Version
45
- version: '3'
45
+ version: '3.2'
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: state_machine
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  none: false
50
50
  requirements:
51
- - - ! '>='
51
+ - - ~>
52
52
  - !ruby/object:Gem::Version
53
- version: '1'
53
+ version: 1.1.2
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  none: false
58
58
  requirements:
59
- - - ! '>='
59
+ - - ~>
60
60
  - !ruby/object:Gem::Version
61
- version: '1'
61
+ version: 1.1.2
62
62
  - !ruby/object:Gem::Dependency
63
63
  name: rake
64
64
  requirement: !ruby/object:Gem::Requirement
@@ -82,7 +82,7 @@ dependencies:
82
82
  requirements:
83
83
  - - ~>
84
84
  - !ruby/object:Gem::Version
85
- version: '3'
85
+ version: '3.2'
86
86
  type: :development
87
87
  prerelease: false
88
88
  version_requirements: !ruby/object:Gem::Requirement
@@ -90,13 +90,13 @@ dependencies:
90
90
  requirements:
91
91
  - - ~>
92
92
  - !ruby/object:Gem::Version
93
- version: '3'
93
+ version: '3.2'
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: rspec-rails
96
96
  requirement: !ruby/object:Gem::Requirement
97
97
  none: false
98
98
  requirements:
99
- - - ! '>='
99
+ - - ~>
100
100
  - !ruby/object:Gem::Version
101
101
  version: '2.12'
102
102
  type: :development
@@ -104,7 +104,7 @@ dependencies:
104
104
  version_requirements: !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
- - - ! '>='
107
+ - - ~>
108
108
  - !ruby/object:Gem::Version
109
109
  version: '2.12'
110
110
  - !ruby/object:Gem::Dependency
@@ -112,7 +112,7 @@ dependencies:
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  none: false
114
114
  requirements:
115
- - - ! '>='
115
+ - - ~>
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0.9'
118
118
  type: :development
@@ -120,7 +120,7 @@ dependencies:
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  none: false
122
122
  requirements:
123
- - - ! '>='
123
+ - - ~>
124
124
  - !ruby/object:Gem::Version
125
125
  version: '0.9'
126
126
  - !ruby/object:Gem::Dependency
@@ -128,17 +128,17 @@ dependencies:
128
128
  requirement: !ruby/object:Gem::Requirement
129
129
  none: false
130
130
  requirements:
131
- - - ! '>='
131
+ - - ~>
132
132
  - !ruby/object:Gem::Version
133
- version: 1.3.6
133
+ version: '1.3'
134
134
  type: :development
135
135
  prerelease: false
136
136
  version_requirements: !ruby/object:Gem::Requirement
137
137
  none: false
138
138
  requirements:
139
- - - ! '>='
139
+ - - ~>
140
140
  - !ruby/object:Gem::Version
141
- version: 1.3.6
141
+ version: '1.3'
142
142
  - !ruby/object:Gem::Dependency
143
143
  name: debugger
144
144
  requirement: !ruby/object:Gem::Requirement
@@ -160,17 +160,17 @@ dependencies:
160
160
  requirement: !ruby/object:Gem::Requirement
161
161
  none: false
162
162
  requirements:
163
- - - ! '>='
163
+ - - ~>
164
164
  - !ruby/object:Gem::Version
165
- version: 0.9.0
165
+ version: '0.9'
166
166
  type: :development
167
167
  prerelease: false
168
168
  version_requirements: !ruby/object:Gem::Requirement
169
169
  none: false
170
170
  requirements:
171
- - - ! '>='
171
+ - - ~>
172
172
  - !ruby/object:Gem::Version
173
- version: 0.9.0
173
+ version: '0.9'
174
174
  description: a workflow orchestration framework running on delayed_job and active_record
175
175
  email:
176
176
  - bill@paydici.com