swarm 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.
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