swarm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +13 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +39 -0
  9. data/Rakefile +13 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +7 -0
  12. data/lib/swarm/engine/base/job.rb +31 -0
  13. data/lib/swarm/engine/base/queue.rb +60 -0
  14. data/lib/swarm/engine/volatile/job.rb +57 -0
  15. data/lib/swarm/engine/volatile/queue.rb +85 -0
  16. data/lib/swarm/engine/worker/command.rb +61 -0
  17. data/lib/swarm/engine/worker.rb +73 -0
  18. data/lib/swarm/evaluation/expression_evaluator.rb +40 -0
  19. data/lib/swarm/evaluation/workitem_context.rb +17 -0
  20. data/lib/swarm/expression.rb +107 -0
  21. data/lib/swarm/expressions/activity_expression.rb +11 -0
  22. data/lib/swarm/expressions/branch_expression.rb +44 -0
  23. data/lib/swarm/expressions/concurrence_expression.rb +41 -0
  24. data/lib/swarm/expressions/conditional_expression.rb +36 -0
  25. data/lib/swarm/expressions/sequence_expression.rb +16 -0
  26. data/lib/swarm/expressions/subprocess_expression.rb +14 -0
  27. data/lib/swarm/hive.rb +69 -0
  28. data/lib/swarm/hive_dweller.rb +170 -0
  29. data/lib/swarm/observers/base.rb +17 -0
  30. data/lib/swarm/participant.rb +18 -0
  31. data/lib/swarm/participants/storage_participant.rb +12 -0
  32. data/lib/swarm/participants/trace_participant.rb +27 -0
  33. data/lib/swarm/pollen/parser.rb +95 -0
  34. data/lib/swarm/pollen/reader.rb +22 -0
  35. data/lib/swarm/pollen/transformer.rb +66 -0
  36. data/lib/swarm/process.rb +57 -0
  37. data/lib/swarm/process_definition.rb +53 -0
  38. data/lib/swarm/router.rb +18 -0
  39. data/lib/swarm/storage.rb +56 -0
  40. data/lib/swarm/stored_workitem.rb +15 -0
  41. data/lib/swarm/support.rb +81 -0
  42. data/lib/swarm/version.rb +3 -0
  43. data/lib/swarm.rb +24 -0
  44. data/swarm.gemspec +31 -0
  45. metadata +199 -0
@@ -0,0 +1,44 @@
1
+ require_relative "../router"
2
+
3
+ module Swarm
4
+ class BranchExpression < Expression
5
+ class InvalidPositionError < StandardError; end;
6
+
7
+ def children
8
+ (child_ids || []).map { |child_id|
9
+ Expression.fetch(child_id, hive: hive)
10
+ }
11
+ end
12
+
13
+ def kick_off_children(at_positions)
14
+ at_positions.each do |at_position|
15
+ add_and_apply_child(at_position)
16
+ end
17
+ save
18
+ end
19
+
20
+ def add_and_apply_child(at_position)
21
+ new_child = add_child(at_position)
22
+ new_child.apply
23
+ end
24
+
25
+ def add_child(at_position)
26
+ node = tree[at_position]
27
+ raise InvalidPositionError unless node
28
+ expression = create_child_expression(node: node, at_position: at_position)
29
+ (self.child_ids ||= []) << expression.id
30
+ expression
31
+ end
32
+
33
+ def create_child_expression(node:, at_position:)
34
+ klass = Router.expression_class_for_node(node)
35
+ expression = klass.create(
36
+ :hive => hive,
37
+ :parent_id => id,
38
+ :position => position + [at_position],
39
+ :workitem => workitem,
40
+ :process_id => process_id
41
+ )
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,41 @@
1
+ require_relative "branch_expression"
2
+
3
+ module Swarm
4
+ class ConcurrenceExpression < BranchExpression
5
+ def work
6
+ kick_off_children(tree.each_index.to_a)
7
+ end
8
+
9
+ def replied_children
10
+ children.select(&:replied_at)
11
+ end
12
+
13
+ def ready_to_proceed?
14
+ required_replies = arguments.fetch("required_replies", nil)
15
+ return all_children_replied? unless required_replies
16
+ replied_children.count >= required_replies
17
+ end
18
+
19
+ def all_children_replied?
20
+ replied_children.count == tree.size
21
+ end
22
+
23
+ def move_on_from(child)
24
+ merge_child_workitem(child)
25
+ save
26
+ if all_children_replied?
27
+ reply
28
+ end
29
+ end
30
+
31
+ def merge_child_workitem(child)
32
+ self.workitem = Swarm::Support.deep_merge(
33
+ workitem, child.workitem, :combine_arrays => array_combination_method
34
+ )
35
+ end
36
+
37
+ def array_combination_method
38
+ arguments.fetch("combine_arrays", "uniq")
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,36 @@
1
+ require_relative "branch_expression"
2
+
3
+ module Swarm
4
+ class ConditionalExpression < BranchExpression
5
+ alias_method :original_tree, :tree
6
+
7
+ def work
8
+ if tree.empty?
9
+ reply
10
+ else
11
+ kick_off_children([0])
12
+ end
13
+ end
14
+
15
+ def move_on_from(child)
16
+ self.workitem = child.workitem
17
+ reply
18
+ end
19
+
20
+ def tree
21
+ @tree ||= select_branch || []
22
+ end
23
+
24
+ def select_branch
25
+ if branch_condition_met?
26
+ original_tree["true"]
27
+ else
28
+ original_tree["false"]
29
+ end
30
+ end
31
+
32
+ def branch_condition_met?
33
+ evaluator.check_condition(command, arguments["condition"])
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ require_relative "branch_expression"
2
+
3
+ module Swarm
4
+ class SequenceExpression < BranchExpression
5
+ def work
6
+ kick_off_children([0])
7
+ end
8
+
9
+ def move_on_from(child)
10
+ self.workitem = child.workitem
11
+ kick_off_children([child.branch_position + 1])
12
+ rescue InvalidPositionError => e
13
+ reply
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module Swarm
2
+ class SubprocessExpression < Expression
3
+ def work
4
+ definition = ProcessDefinition.find_by_name(arguments.fetch("name", nil))
5
+ raise Swarm::ProcessDefinition::RecordNotFoundError unless definition
6
+ process = definition.launch_process(workitem: workitem, parent_expression_id: id)
7
+ end
8
+
9
+ def move_on_from(process)
10
+ self.workitem = process.workitem
11
+ reply
12
+ end
13
+ end
14
+ end
data/lib/swarm/hive.rb ADDED
@@ -0,0 +1,69 @@
1
+ require_relative "storage"
2
+
3
+ module Swarm
4
+ class Hive
5
+ class MissingTypeError < StandardError; end
6
+ class IllegalDefaultError < StandardError; end
7
+ class NoDefaultSetError < StandardError; end
8
+
9
+ class << self
10
+ def default=(default)
11
+ unless default.is_a?(self)
12
+ raise IllegalDefaultError.new("Default must be a Swarm::Hive")
13
+ end
14
+ @default = default
15
+ end
16
+
17
+ def default
18
+ unless @default
19
+ raise NoDefaultSetError.new("No default Hive defined yet")
20
+ end
21
+ @default
22
+ end
23
+ end
24
+
25
+ attr_reader :storage, :work_queue
26
+
27
+ def initialize(storage:, work_queue:)
28
+ @storage = storage
29
+ @work_queue = work_queue
30
+ end
31
+
32
+ def registered_observers
33
+ @registered_observers ||= []
34
+ end
35
+
36
+ def inspect
37
+ "#<Swarm::Hive storage: #{storage.backend.class}, work_queue: #{work_queue.name}>"
38
+ end
39
+
40
+ def traced
41
+ storage["trace"] ||= []
42
+ end
43
+
44
+ def trace(new_element)
45
+ storage["trace"] = traced + [new_element]
46
+ end
47
+
48
+ def queue(action, object)
49
+ @work_queue.add_job({
50
+ :action => action,
51
+ :metadata => object.to_hash
52
+ })
53
+ end
54
+
55
+ def fetch(klass, id)
56
+ Swarm::Support.constantize(klass).fetch(id, hive: self)
57
+ end
58
+
59
+ def reify_from_hash(hsh)
60
+ Support.symbolize_keys!(hsh)
61
+ raise MissingTypeError.new(hsh.inspect) unless hsh[:type]
62
+ Swarm::Support.constantize(hsh.delete(:type)).new_from_storage(
63
+ hsh.merge(
64
+ :hive => self
65
+ )
66
+ )
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,170 @@
1
+ module Swarm
2
+ class HiveDweller
3
+ class RecordNotFoundError < StandardError; end
4
+
5
+ attr_reader :hive, :id
6
+
7
+ def initialize(hive: Hive.default, **args)
8
+ @hive = hive
9
+ @changed_attributes = {}
10
+ set_attributes(args, record_changes: false)
11
+ end
12
+
13
+ def new?
14
+ id.nil?
15
+ end
16
+
17
+ def changed?
18
+ !@changed_attributes.empty?
19
+ end
20
+
21
+ def set_attributes(args, record_changes: true)
22
+ unknown_arguments = args.keys - self.class.columns
23
+ unless unknown_arguments.empty?
24
+ raise ArgumentError, "unknown keywords: #{unknown_arguments.join(', ')}"
25
+ end
26
+ args.each do |key, value|
27
+ change_attribute(key, value, record: record_changes)
28
+ end
29
+ end
30
+
31
+ def change_attribute(key, value, record: true)
32
+ if record
33
+ @changed_attributes[key] = [send(key), value]
34
+ end
35
+ instance_variable_set(:"@#{key}", value)
36
+ end
37
+
38
+ def ==(other)
39
+ other.is_a?(self.class) && other.to_hash == to_hash
40
+ end
41
+
42
+ def storage_id
43
+ self.class.storage_id_for_key(id)
44
+ end
45
+
46
+ def storage
47
+ @hive.storage
48
+ end
49
+
50
+ def delete
51
+ storage.delete(storage_id)
52
+ self
53
+ end
54
+
55
+ def save
56
+ if new? || changed?
57
+ @id ||= Swarm::Support.uuid_with_timestamp
58
+ storage[storage_id] = to_hash
59
+ end
60
+ self
61
+ end
62
+
63
+ def attributes
64
+ self.class.columns.each_with_object({}) { |col_name, hsh|
65
+ hsh[col_name.to_sym] = send(:"#{col_name}")
66
+ }
67
+ end
68
+
69
+ def to_hash
70
+ hsh = {
71
+ :id => id,
72
+ :type => self.class.name
73
+ }
74
+ hsh.merge(attributes)
75
+ end
76
+
77
+ def reload!
78
+ hsh = hive.storage[storage_id]
79
+ self.class.columns.each do |column|
80
+ instance_variable_set(:"@#{column}", hsh[column.to_s])
81
+ end
82
+ self.class.associations.each do |type|
83
+ instance_variable_set(:"@#{type}", nil)
84
+ end
85
+ @changed_attributes = {}
86
+ self
87
+ end
88
+
89
+ class << self
90
+ include Enumerable
91
+
92
+ attr_reader :columns, :associations
93
+
94
+ def inherited(subclass)
95
+ super
96
+ subclass.instance_variable_set(:@columns, [])
97
+ subclass.instance_variable_set(:@associations, [])
98
+ end
99
+
100
+ def set_columns(*args)
101
+ attr_reader *args
102
+ args.each do |arg|
103
+ define_method("#{arg}=") { |value|
104
+ change_attribute(arg, value)
105
+ }
106
+ end
107
+ @columns = @columns | args
108
+ end
109
+
110
+ def many_to_one(type, class_name: nil)
111
+ define_method(type) do
112
+ memo = instance_variable_get(:"@#{type}")
113
+ memo || begin
114
+ key = self.send(:"#{type}_id")
115
+ return nil unless key
116
+ klass = Swarm::Support.constantize("#{class_name || type}")
117
+ instance_variable_set(:"@#{type}", klass.fetch(key, :hive => hive))
118
+ end
119
+ end
120
+ @associations << type
121
+ end
122
+
123
+ def create(hive: Hive.default, **args)
124
+ new(hive: hive, **args).save
125
+ end
126
+
127
+ def storage_type
128
+ name.split("::").last
129
+ end
130
+
131
+ def storage_id_for_key(key)
132
+ if key.match(/^#{storage_type}\:/)
133
+ key
134
+ else
135
+ "#{storage_type}:#{key}"
136
+ end
137
+ end
138
+
139
+ def new_from_storage(**args)
140
+ id = args.delete(:id)
141
+ new(**args).tap { |instance|
142
+ instance.instance_variable_set(:@id, id)
143
+ }
144
+ end
145
+
146
+ def fetch(key, hive: Hive.default)
147
+ hsh = hive.storage[storage_id_for_key(key)].dup
148
+ hive.reify_from_hash(hsh)
149
+ end
150
+
151
+ def ids(hive: Hive.default)
152
+ hive.storage.ids_for_type(storage_type)
153
+ end
154
+
155
+ def each(hive: Hive.default, subtypes: true, &block)
156
+ return to_enum(__method__, hive: hive, subtypes: subtypes) unless block_given?
157
+ ids(hive: hive).each do |id|
158
+ object = fetch(id, hive: hive)
159
+ if (subtypes && object.is_a?(self)) || object.class == self
160
+ yield object
161
+ end
162
+ end
163
+ end
164
+
165
+ def all(hive: Hive.default, subtypes: true)
166
+ to_a(hive: hive, subtypes: subtypes)
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,17 @@
1
+ module Swarm
2
+ module Observers
3
+ class Base
4
+ extend Forwardable
5
+
6
+ def_delegators :command, :action, :metadata, :object
7
+ attr_reader :command
8
+
9
+ def initialize(command)
10
+ @command = command
11
+ end
12
+
13
+ def before_action; end
14
+ def after_action; end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Swarm
2
+ class Participant
3
+ attr_reader :hive, :expression
4
+
5
+ def initialize(hive: Hive.default, expression:)
6
+ @hive = hive
7
+ @expression = expression
8
+ end
9
+
10
+ def workitem
11
+ expression.workitem
12
+ end
13
+
14
+ def arguments
15
+ expression.arguments
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "../participant"
2
+
3
+ module Swarm
4
+ class StorageParticipant < Participant
5
+ def work
6
+ StoredWorkitem.create({
7
+ :hive => hive,
8
+ :expression_id => expression.id
9
+ })
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "../participant"
2
+
3
+ module Swarm
4
+ class TraceParticipant < Participant
5
+ def work
6
+ if text
7
+ append_to_workitem_trace
8
+ append_to_hive_trace
9
+ end
10
+ expression.reply
11
+ end
12
+
13
+ def text
14
+ @text ||= arguments.fetch("text", nil)
15
+ end
16
+
17
+ def append_to_workitem_trace
18
+ traced = workitem["traced"] || []
19
+ traced << text
20
+ expression.workitem = workitem.merge("traced" => traced)
21
+ end
22
+
23
+ def append_to_hive_trace
24
+ hive.trace(text)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,95 @@
1
+ require "parslet"
2
+
3
+ module Swarm
4
+ module Pollen
5
+ class Parser < Parslet::Parser
6
+ def optionally_spaced(atom)
7
+ spaces? >> atom >> spaces?
8
+ end
9
+
10
+ rule(:eol) { (optionally_spaced(match['\n'])).repeat(1) }
11
+ rule(:whitespace) { match('\s').repeat(0) }
12
+ rule(:spaces) { match[' \t'].repeat(1) }
13
+ rule(:spaces?) { spaces.maybe }
14
+ rule(:comma) { optionally_spaced(str(',')) }
15
+
16
+ rule(:integer) {
17
+ match['0-9'].repeat(1).as(:integer)
18
+ }
19
+
20
+ rule(:float) {
21
+ (match['0-9'].repeat(1) >> str('.') >> match['0-9'].repeat(1)).as(:float)
22
+ }
23
+
24
+ rule(:line) {
25
+ (match['\n'].absent? >> any).repeat(1).as(:line)
26
+ }
27
+
28
+ rule(:string) {
29
+ (str("'") | str('"')).capture(:q) >>
30
+ (str('\\') >> any |
31
+ dynamic { |s,c| str(c.captures[:q]) }.absent? >> any
32
+ ).repeat.as(:string) >> dynamic { |s,c| str(c.captures[:q]) }
33
+ }
34
+
35
+ rule(:colon_pair) {
36
+ token.as(:key) >> str(':') >> spaces? >> string.as(:value)
37
+ }
38
+
39
+ rule(:symbol) { str(':') >> token.as(:symbol) }
40
+ rule(:token) { (match('[a-z_]') >> match('[a-zA-Z0-9_]').repeat(0)).as(:token) }
41
+
42
+ rule(:rocket_pair) {
43
+ (symbol | string).as(:key) >> optionally_spaced(str('=>')) >> string.as(:value)
44
+ }
45
+
46
+ rule(:key_value_pair) { rocket_pair | colon_pair }
47
+
48
+ rule(:key_value_list) {
49
+ key_value_pair >> (comma >> key_value_pair).repeat(0)
50
+ }
51
+
52
+ rule(:arguments) {
53
+ key_value_list.as(:arguments) | string.as(:text_argument)
54
+ }
55
+
56
+ rule(:reserved_word) {
57
+ %w(if unless else end).map { |w| str(w) }.reduce(:|)
58
+ }
59
+
60
+ rule(:expression) {
61
+ reserved_word.absent? >> token.as(:command) >> (spaces >> arguments).maybe
62
+ }
63
+
64
+ rule(:tree) {
65
+ ((conditional_block | branch_block | expression) >> eol).repeat(0)
66
+ }
67
+
68
+ rule(:conditional_block) {
69
+ (str('if') | str('unless')).as(:conditional) >>
70
+ spaces >> string.as(:conditional_clause) >> eol >>
71
+ tree.as(:true_tree) >>
72
+ (str('else') >> eol >> tree.as(:false_tree)).maybe >>
73
+ str('end')
74
+ }
75
+
76
+ rule(:branch_block) {
77
+ expression >> spaces >> str('do') >> eol >>
78
+ tree.as(:tree) >>
79
+ str('end')
80
+ }
81
+
82
+ rule(:metadata_entry) {
83
+ token.as(:key) >> str(':') >> spaces? >>
84
+ (string | float | integer | line).as(:value)
85
+ }
86
+
87
+ rule(:metadata) {
88
+ str('---') >> eol >> (metadata_entry >> eol).repeat(0) >> str('---') >> eol
89
+ }
90
+
91
+ rule(:document) { whitespace >> metadata.maybe.as(:metadata) >> branch_block.as(:tree) >> whitespace }
92
+ root(:document)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,22 @@
1
+ require_relative "parser"
2
+ require_relative "transformer"
3
+
4
+ module Swarm
5
+ module Pollen
6
+ class Reader
7
+ def initialize(pollen)
8
+ @pollen = pollen
9
+ end
10
+
11
+ def to_hash
12
+ Transformer.new.apply(
13
+ Parser.new.parse(@pollen, :reporter => Parslet::ErrorReporter::Deepest.new)
14
+ )
15
+ end
16
+
17
+ def to_json
18
+ to_hash.to_json
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,66 @@
1
+ require "parslet"
2
+
3
+ module Swarm
4
+ module Pollen
5
+ class Transformer < Parslet::Transform
6
+ class << self
7
+ def transform_arguments(args)
8
+ if args.is_a?(Array) && !args.empty?
9
+ args.reduce(:merge)
10
+ else
11
+ args
12
+ end
13
+ end
14
+ end
15
+
16
+ rule(:symbol => simple(:sym)) { sym.to_s }
17
+ rule(:token => simple(:token)) { token.to_s }
18
+ rule(:string => simple(:st)) { st.to_s }
19
+ rule(:line => simple(:line)) { line.to_s }
20
+ rule(:float => simple(:float)) { float.to_f }
21
+ rule(:integer => simple(:int)) { int.to_i }
22
+
23
+ rule(:key => simple(:key), :value => simple(:value)) {
24
+ { key => value }
25
+ }
26
+ rule(:conditional => simple(:conditional), :conditional_clause => simple(:clause), :true_tree => subtree(:true_tree), :false_tree => subtree(:false_tree)) {
27
+ [conditional.to_s, { "condition" => clause }, {
28
+ "true" => [
29
+ ["sequence", {}, true_tree]
30
+ ],
31
+ "false" => [
32
+ ["sequence", {}, false_tree]
33
+ ],
34
+ }]
35
+ }
36
+
37
+ rule(:conditional => simple(:conditional), :conditional_clause => simple(:clause), :true_tree => subtree(:true_tree)) {
38
+ [conditional.to_s, { "condition" => clause }, {
39
+ "true" => [
40
+ ["sequence", {}, true_tree]
41
+ ]
42
+ }]
43
+ }
44
+
45
+ rule(:command => simple(:command)) {
46
+ [command.to_s, {}, []]
47
+ }
48
+ rule(:command => simple(:command), :tree => subtree(:tree)) {
49
+ [command.to_s, {}, tree]
50
+ }
51
+ rule(:command => simple(:command), :arguments => subtree(:args)) { |captures|
52
+ [captures[:command].to_s, transform_arguments(captures[:args]), []]
53
+ }
54
+ rule(:command => simple(:command), :arguments => subtree(:args), :tree => subtree(:tree)) { |captures|
55
+ [captures[:command].to_s, transform_arguments(captures[:args]), captures[:tree]]
56
+ }
57
+ rule(:command => simple(:command), :text_argument => simple(:ta)) {
58
+ [command.to_s, { "text" => ta }, []]
59
+ }
60
+ rule(:metadata => subtree(:metadata), :tree => subtree(:tree)) { |captures|
61
+ metadata = (captures[:metadata] || {}).reduce(:merge)
62
+ (metadata || {}).merge("definition" => captures[:tree])
63
+ }
64
+ end
65
+ end
66
+ end