oflow 0.3.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +182 -0
  4. data/lib/oflow/actor.rb +76 -0
  5. data/lib/oflow/actors/errorhandler.rb +32 -0
  6. data/lib/oflow/actors/ignore.rb +22 -0
  7. data/lib/oflow/actors/log.rb +175 -0
  8. data/lib/oflow/actors/relay.rb +23 -0
  9. data/lib/oflow/actors/timer.rb +126 -0
  10. data/lib/oflow/actors.rb +11 -0
  11. data/lib/oflow/box.rb +195 -0
  12. data/lib/oflow/env.rb +52 -0
  13. data/lib/oflow/errors.rb +74 -0
  14. data/lib/oflow/flow.rb +75 -0
  15. data/lib/oflow/haserrorhandler.rb +48 -0
  16. data/lib/oflow/haslinks.rb +64 -0
  17. data/lib/oflow/haslog.rb +72 -0
  18. data/lib/oflow/hasname.rb +31 -0
  19. data/lib/oflow/hastasks.rb +209 -0
  20. data/lib/oflow/inspector.rb +501 -0
  21. data/lib/oflow/link.rb +43 -0
  22. data/lib/oflow/pattern.rb +8 -0
  23. data/lib/oflow/stamp.rb +39 -0
  24. data/lib/oflow/task.rb +415 -0
  25. data/lib/oflow/test/action.rb +21 -0
  26. data/lib/oflow/test/actorwrap.rb +62 -0
  27. data/lib/oflow/test.rb +8 -0
  28. data/lib/oflow/tracker.rb +109 -0
  29. data/lib/oflow/version.rb +5 -0
  30. data/lib/oflow.rb +23 -0
  31. data/test/actors/log_test.rb +57 -0
  32. data/test/actors/timer_test.rb +56 -0
  33. data/test/actorwrap_test.rb +48 -0
  34. data/test/all_tests.rb +27 -0
  35. data/test/box_test.rb +127 -0
  36. data/test/collector.rb +23 -0
  37. data/test/flow_basic_test.rb +93 -0
  38. data/test/flow_cfg_error_test.rb +94 -0
  39. data/test/flow_log_test.rb +87 -0
  40. data/test/flow_nest_test.rb +215 -0
  41. data/test/flow_rescue_test.rb +133 -0
  42. data/test/flow_tracker_test.rb +82 -0
  43. data/test/stutter.rb +21 -0
  44. data/test/task_test.rb +98 -0
  45. data/test/tracker_test.rb +59 -0
  46. metadata +93 -0
data/lib/oflow/box.rb ADDED
@@ -0,0 +1,195 @@
1
+
2
+ module OFlow
3
+
4
+ # A Box encapsulates data in the system. It provides a wrapper around the data
5
+ # which becomes immutable as it is frozen in transit between Tasks. The Box
6
+ # allows the contents to be modified by replacing the contents with thawed
7
+ # copies of the original data.
8
+ #
9
+ # Boxes are shipped between Tasks. A Tracker can also be attached to a Box to
10
+ # follow it and gather a history of it's movements.
11
+ class Box
12
+
13
+ # Tracker for the box if there is one.
14
+ attr_reader :tracker
15
+
16
+ # The contents of the Box.
17
+ attr_reader :contents
18
+
19
+ # Create a new Box withe the content provided. The value provided will be
20
+ # frozen to inhibit changes to the value after the Box is created.
21
+ # @param value contents of the Box
22
+ # @param tracker [Tracker] used to track the progress of the Box
23
+ def initialize(value, tracker=nil)
24
+ @tracker = tracker
25
+ @contents = value
26
+ end
27
+
28
+ # Receives a Box by creating a new Box whose contents is the same as the
29
+ # existing but with an updated tracker.
30
+ # @param location [String] where the Box was received, full name of Task
31
+ # @param op [Symbol] operation that the Box was received under
32
+ # @return [Box] new Box.
33
+ def receive(location, op)
34
+ return self if @tracker.nil?
35
+ Box.new(@contents, @tracker.receive(location, op))
36
+ end
37
+
38
+ # Sets or adds a value in inside the Box. The Box is changed with the new
39
+ # contents being thawed where necessary. A path is a set of element names in
40
+ # the case of a Hash or index numbers in the case of an Array joined with
41
+ # the ':' character as a separator.
42
+ # @param path [String] location of element to change or add.
43
+ # @param value value for the addition or change
44
+ def set(path, value)
45
+ return aset(nil, value) if path.nil?
46
+ aset(path.split(':'), value)
47
+ end
48
+
49
+ # Sets or adds a value in inside the Box where the path is an array of
50
+ # element names or indices. Indices can be Fixnum or Strings.
51
+ # @param path [Array] location of element to change or add.
52
+ # @param value value for the addition or change
53
+ def aset(path, value)
54
+ Box.new(_aset(path, @contents, value), @tracker)
55
+ end
56
+
57
+ # Returns the data element described by the path. A path is a set of element
58
+ # names in the case of a Hash or index numbers in the case of an Array
59
+ # joined with the ':' character as a separator.
60
+ # @param path [String] location of element to return
61
+ # @return the data element.
62
+ def get(path)
63
+ return @contents if path.nil?
64
+ aget(path.split(':'))
65
+ end
66
+
67
+ # Returns the data element described by the path which is an array of
68
+ # element names or indices. Indices can be Fixnum or Strings.
69
+ # @param path [Array] location of element to return
70
+ # @return the data element.
71
+ def aget(path)
72
+ _aget(path, @contents)
73
+ end
74
+
75
+ # Returns a string representation of the Box and contents.
76
+ def to_s()
77
+ "Box{#{@contents}, tracker: #{@tracker}}"
78
+ end
79
+ alias inspect to_s
80
+
81
+ # Called when passing to another Task. It freezes the contents recursively.
82
+ def freeze()
83
+ deep_freeze(@contents)
84
+ super
85
+ end
86
+
87
+ # Makes a copy of the frozen contents and the Box to allow modifications.
88
+ # @return [Box] new Box.
89
+ def thaw()
90
+ # Don't freeze the contents.
91
+ Box.new(thaw_value(@contents, true), @tracker)
92
+ end
93
+
94
+ # TBD make these module methods on a Freezer module
95
+
96
+ def deep_freeze(value)
97
+ case value
98
+ when Array
99
+ value.each { |v| deep_freeze(v) }
100
+ when Hash
101
+ # hash keys are frozen already
102
+ value.each { |k, v| deep_freeze(v) }
103
+ end
104
+ # Don't freeze other Objects. This leaves an out for special purpose
105
+ # functionality.
106
+ value.freeze
107
+ end
108
+
109
+ # Make a copy of the value, unfrozen.
110
+ def thaw_value(value, recurse)
111
+ return value unless value.frozen? || recurse
112
+ case value
113
+ when Array
114
+ # thaws the array itself but not the elements
115
+ value = Array.new(value)
116
+ value.map! { |v| thaw_value(v, true) } if recurse
117
+ when Hash
118
+ # thaws the hash itself but not the elements
119
+ orig = value
120
+ value = {}
121
+ if recurse
122
+ orig.each { |k, v| value[k] = thaw_value(v, true) }
123
+ else
124
+ orig.each { |k, v| value[k] = v }
125
+ end
126
+ when String
127
+ value = String.new(value)
128
+ end
129
+ value
130
+ end
131
+
132
+ private
133
+
134
+ def _aset(path, value, rv)
135
+ return rv if path.nil? || path.empty?
136
+ p = path[0]
137
+ case value
138
+ when Array
139
+ value = Array.new(value) if value.frozen?
140
+ i = p.to_i
141
+ value[p.to_i] = _aset(path[1..-1], value[i], rv)
142
+ when Hash
143
+ if value.frozen?
144
+ orig = value
145
+ value = {}
146
+ orig.each { |k, v| value[k] = v }
147
+ end
148
+ if value.has_key?(p)
149
+ value[p] = _aset(path[1..-1], value[p], rv)
150
+ else
151
+ ps = p.to_sym
152
+ value[ps] = _aset(path[1..-1], value[ps], rv)
153
+ end
154
+ when NilClass
155
+ begin
156
+ i = p.to_i
157
+ value = []
158
+ value[i] = _aset(path[1..-1], nil, rv)
159
+ rescue
160
+ ps = p.to_sym
161
+ value = {}
162
+ value[ps] = _aset(path[1..-1], nil, rv)
163
+ end
164
+ else
165
+ raise FrozenError.new(p, value)
166
+ end
167
+ value
168
+ end
169
+
170
+ def _aget(path, value)
171
+ return value if path.nil? || path.empty? || value.nil?
172
+ p = path[0]
173
+ case value
174
+ when Array
175
+ begin
176
+ _aget(path[1..-1], value[p.to_i])
177
+ rescue
178
+ nil
179
+ end
180
+ when Hash
181
+ v = value[p] || value[p.to_sym]
182
+ _aget(path[1..-1], v)
183
+ else
184
+ if value.respond_to?(p.to_sym)
185
+ _aget(path[1..-1], value.send(p))
186
+ else
187
+ nil
188
+ end
189
+ end
190
+ end
191
+
192
+ end # Box
193
+
194
+ end # OFlow
195
+
data/lib/oflow/env.rb ADDED
@@ -0,0 +1,52 @@
1
+
2
+ module OFlow
3
+
4
+ # The platform that Flows are created in. It is the outer most element of the
5
+ # OFlow system.
6
+ class Env
7
+
8
+ extend HasTasks
9
+ extend HasLog
10
+ extend HasName
11
+ extend HasErrorHandler
12
+
13
+ # The default logging level.
14
+ @@log_level = Logger::WARN
15
+
16
+ init_name(nil, '')
17
+ init_tasks()
18
+
19
+ # Returns the default log level.
20
+ # @return [Fixnum] the default log level which is one of the Logger::Severity values.
21
+ def self.log_level()
22
+ @@log_level
23
+ end
24
+
25
+ # Sets the default log level.
26
+ # @param level [Fixnum] Logger::Severity to set the default log level to
27
+ def self.log_level=(level)
28
+ @@log_level = level unless level < Logger::Severity::DEBUG || Logger::Severity::FATAL < level
29
+ end
30
+
31
+ # Resets the error handler and log. Usually called on init and by the
32
+ # clear() method.
33
+ def self._clear()
34
+ @error_handler = Task.new(self, :error, Actors::ErrorHandler)
35
+ @log = Task.new(self, :log, Actors::Log)
36
+ end
37
+
38
+ _clear()
39
+
40
+ # Describes all the Flows and Tasks in the system.
41
+ def self.describe(detail=0, indent=0)
42
+ i = ' ' * indent
43
+ lines = ["#{i}#{self} {"]
44
+ @tasks.each_value { |t|
45
+ lines << t.describe(detail, indent + 2)
46
+ }
47
+ lines << i + "}"
48
+ lines.join("\n")
49
+ end
50
+
51
+ end # Env
52
+ end # OFlow
@@ -0,0 +1,74 @@
1
+
2
+ module OFlow
3
+ # An Exception indicating a Task was currently not receiving new requests.
4
+ class BlockedError < Exception
5
+ def initialize()
6
+ super("Blocked, try again later")
7
+ end
8
+ end # BlockedError
9
+
10
+ # An Exception indicating a Task was too busy to complete the requested
11
+ # operation.
12
+ class BusyError < Exception
13
+ def initialize()
14
+ super("Busy, try again later")
15
+ end
16
+ end # BusyError
17
+
18
+ # An Exception indicating a data value is frozen and can not be modified.
19
+ class FrozenError < Exception
20
+ def initialize(name, value)
21
+ super("#{name}, a #{value.class} Object is frozen")
22
+ end
23
+ end # FrozenError
24
+
25
+ # An Exception indicating an error in setup or configuration.
26
+ class ConfigError < Exception
27
+ def initialize(msg)
28
+ super(msg)
29
+ end
30
+ end # ConfigError
31
+
32
+ # An Exception raised when no destination is found.
33
+ class LinkError < Exception
34
+ def initialize(dest)
35
+ super("No destination found for '#{dest}'.")
36
+ end
37
+ end # LinkError
38
+
39
+ # An Exception raised when there are validation errors.
40
+ class ValidateError < Exception
41
+ attr_accessor :problems
42
+
43
+ def initialize(errors)
44
+ @problems = errors
45
+ ma = ["#{errors.size} validation errors."]
46
+ errors.each { |e| ma << e.to_s }
47
+ super(ma.join("\n "))
48
+ end
49
+
50
+ class Problem
51
+ LINK_ERROR = 'link_error'
52
+ MISSING_ERROR = 'missing_link_error'
53
+ INPUT_ERROR = 'input_link_error'
54
+
55
+ attr_reader :task_name
56
+ attr_reader :kind
57
+ attr_reader :message
58
+
59
+ def initialize(task_name, kind, msg)
60
+ @task_name = task_name
61
+ @kind = kind
62
+ @message = msg
63
+ end
64
+
65
+ def to_s()
66
+ "#{@task_name}: #{@message}"
67
+ end
68
+ alias inpsect to_s
69
+
70
+ end # Problem
71
+
72
+ end # ValidateError
73
+
74
+ end # OFlow
data/lib/oflow/flow.rb ADDED
@@ -0,0 +1,75 @@
1
+
2
+ module OFlow
3
+
4
+ # The Class used to managing interactions between Tasks and sub-Flows. It can
5
+ # be thought of as a container for Tasks where the Flow keeps track of the
6
+ # Links between the Tasks.
7
+ class Flow
8
+ include HasTasks
9
+ include HasLinks
10
+ include HasName
11
+ include HasErrorHandler
12
+ include HasLog
13
+
14
+ # Create a new Flow.
15
+ # @param flow [Flow] Flow containing the Flow
16
+ # @param name [name] Flow base name
17
+ # @param options [Hash] additional options for the Flow
18
+ def initialize(flow, name, options)
19
+ init_name(flow, name)
20
+ init_tasks()
21
+ init_links()
22
+ end
23
+
24
+ # Add a Link from the edge of the Flow to a Task contained in the Flow.
25
+ # @param label [Symbol|String] identifier for the Link
26
+ # @param task_name [Symbol|String] _name base name of teh Task to link to
27
+ # @param op [Symbol|String] operation to call when forwarding a request to the target Task
28
+ def route(label, task_name, op)
29
+ op = op.to_sym unless op.nil?
30
+ label = label.to_sym unless label.nil?
31
+ raise ConfigError.new("Link #{label} already exists.") unless find_link(label).nil?
32
+ @links[label] = Link.new(task_name.to_sym, op, true)
33
+ end
34
+
35
+ # Receive a request which is redirected to a Linked target Task.
36
+ # @param op [Symbol] identifies the link that points to the destination Task or Flow
37
+ # @param box [Box] contents or data for the request
38
+ def receive(op, box)
39
+ box = box.receive(full_name, op)
40
+ lnk = find_link(op)
41
+ raise LinkError.new(op) if lnk.nil? || lnk.target.nil?
42
+ lnk.target.receive(lnk.op, box)
43
+ end
44
+
45
+ # Returns true if the Flow has a Link identified by the op.
46
+ # @param op [Symbol] identifies the Link in question
47
+ def has_input(op)
48
+ !find_link(op).nil?
49
+ end
50
+
51
+ # Returns a String describing the Flow.
52
+ # @param detail [Fixnum] higher values result in more detail in the description
53
+ # @param indent [Fixnum] the number of spaces to indent the description
54
+ def describe(detail=0, indent=0)
55
+ i = ' ' * indent
56
+ lines = ["#{i}#{name} (#{self.class}) {"]
57
+ @tasks.each_value { |t|
58
+ lines << t.describe(detail, indent + 2)
59
+ }
60
+ @links.each { |local,link|
61
+ if link.ingress
62
+ lines << " #{i}#{local} * #{link.target_name}:#{link.op}"
63
+ else
64
+ lines << " #{i}#{local} => #{link.target_name}:#{link.op}"
65
+ end
66
+ }
67
+ lines << i + "}"
68
+ lines.join("\n")
69
+ end
70
+
71
+ def _clear()
72
+ end
73
+
74
+ end # Flow
75
+ end # OFlow
@@ -0,0 +1,48 @@
1
+
2
+ module OFlow
3
+
4
+ # Provides functionality to find an error handler Task which is how error are
5
+ # handled in the system. Each Flow or Task can have a different error
6
+ # handler. If a Flow does not have an error handler the error bubbles up to
7
+ # the next Flow until an error handler is found.
8
+ module HasErrorHandler
9
+
10
+ # Returns an error handler Task by checking for an @error_handler variable,
11
+ # then looking for a Task with a base name of :error in itself or any of the
12
+ # containing Flows.
13
+ # @return [Task|nil] Task to handle errors
14
+ def error_handler()
15
+ return @error_handler if instance_variable_defined?(:@error_handler) && !@error_handler.nil?
16
+ if instance_variable_defined?(:@flow)
17
+ if @flow.respond_to?(:find_task)
18
+ eh = @flow.find_task(:error)
19
+ return eh unless eh.nil?
20
+ end
21
+ if @flow.respond_to?(:error_handler)
22
+ eh = @flow.error_handler()
23
+ return eh unless eh.nil?
24
+ end
25
+ end
26
+ nil
27
+ end
28
+
29
+ # Sets avaliable for handling errors.
30
+ # @param t [Task|nil] Task for handling error or nil to unset
31
+ def error_handler=(t)
32
+ @error_handler = t
33
+ end
34
+
35
+ # Handles errors by putting a requestion on the error handler Task.
36
+ # @param e [Exception] error to handle
37
+ def handle_error(e)
38
+ handler = error_handler()
39
+ unless handler.nil?
40
+ handler.receive(nil, Box.new([e, full_name()]))
41
+ else
42
+ puts "** [#{full_name()}] #{e.class}: #{e.message}"
43
+ e.backtrace.each { |line| puts " #{line}" }
44
+ end
45
+ end
46
+
47
+ end # HasErrorHandler
48
+ end # OFlow
@@ -0,0 +1,64 @@
1
+
2
+ module OFlow
3
+
4
+ # Adds support for Links. Used by Flow and Env.
5
+ module HasLinks
6
+
7
+ # Sets up the links attribute.
8
+ def init_links()
9
+ @links = {}
10
+ end
11
+
12
+ # Creates a Link identified by the label that has a target Task or Flow and
13
+ # operation.
14
+ # @param label [Symbol|String] identifer of the Link
15
+ # @param target [Symbol|String] identifer of the target Task
16
+ # @param op [Symbol|String] operation to perform on the target Task
17
+ def link(label, target, op)
18
+ label = label.to_sym unless label.nil?
19
+ op = op.to_sym unless op.nil?
20
+ raise ConfigError.new("Link #{label} already exists.") unless @links[label].nil?
21
+ label = label.to_sym unless label.nil?
22
+ @links[label] = Link.new(target.to_sym, op)
23
+ end
24
+
25
+ # Attempts to find and resolve the Link identified by the label. Resolving a
26
+ # Link uses the target identifier to find the target Task and save that in
27
+ # the Link.
28
+ # @param label [Symbol|String] identifer of the Link
29
+ # @return [Link] returns the Link for the label
30
+ def resolve_link(label)
31
+ label = label.to_sym unless label.nil?
32
+ lnk = @links[label] || @links[nil]
33
+ return nil if lnk.nil?
34
+ set_link_target(lnk) if lnk.target.nil?
35
+ lnk
36
+ end
37
+
38
+ # Sets the target Task for a Link.
39
+ # @param lnk [Link] Link to find the target Task for.
40
+ def set_link_target(lnk)
41
+ if lnk.ingress
42
+ task = find_task(lnk.target_name)
43
+ else
44
+ task = @flow.find_task(lnk.target_name)
45
+ end
46
+ lnk.instance_variable_set(:@target, task)
47
+ end
48
+
49
+ # Attempts to find the Link identified by the label.
50
+ # @param label [Symbol|String] identifer of the Link
51
+ # @return [Link] returns the Link for the label
52
+ def find_link(label)
53
+ label = label.to_sym unless label.nil?
54
+ @links[label] || @links[nil]
55
+ end
56
+
57
+ # Returns the Links.
58
+ # @return [Hash] Hash of Links with the keys as Symbols that are the labels of the Links.
59
+ def links()
60
+ @links
61
+ end
62
+
63
+ end # HasLinks
64
+ end # OFlow
@@ -0,0 +1,72 @@
1
+
2
+ module OFlow
3
+
4
+ # Adds the ability to log by sending log requests to a log Task.
5
+ module HasLog
6
+
7
+ # Returns a log Task by looking for that Task in an attribute and then in
8
+ # the contained Tasks or Tasks in outer Flows.
9
+ # @return [Task] log Task.
10
+ def log()
11
+ return @log if instance_variable_defined?(:@log) && !@log.nil?
12
+ # Log task take precedence over log variable.
13
+ if respond_to?(:find_task)
14
+ lg = find_task(:log)
15
+ return lg unless lg.nil?
16
+ end
17
+ return @flow.log if instance_variable_defined?(:@flow) && @flow.respond_to?(:log)
18
+ nil
19
+ end
20
+
21
+ # Sets the log attribute.
22
+ # @param t [Task] log Task
23
+ def log=(t)
24
+ @log = t
25
+ end
26
+
27
+ # Lower level logging method. Generally only used when one of the primary
28
+ # severity methods are called.
29
+ # @param level [String] message severity or level
30
+ # @param msg [String] message to log
31
+ # @param fn [String] full name of Task or Flow calling the log function
32
+ def log_msg(level, msg, fn)
33
+ lt = log()
34
+ unless lt.nil?
35
+ lt.receive(level, Box.new([msg, fn]))
36
+ else
37
+ puts "[#{fn}] #{msg}"
38
+ end
39
+ end
40
+
41
+ # Logs the message if logging level is at least debug.
42
+ # @param msg [String] message to log
43
+ def debug(msg)
44
+ log_msg(:debug, msg, full_name())
45
+ end
46
+
47
+ # Logs the message if logging level is at least info.
48
+ # @param msg [String] message to display or log
49
+ def info(msg)
50
+ log_msg(:info, msg, full_name())
51
+ end
52
+
53
+ # Logs the message if logging level is at least error.
54
+ # @param msg [String] message to display or log
55
+ def error(msg)
56
+ log_msg(:error, msg, full_name())
57
+ end
58
+
59
+ # Logs the message if logging level is at least warn.
60
+ # @param msg [String] message to display or log
61
+ def warn(msg)
62
+ log_msg(:warn, msg, full_name())
63
+ end
64
+
65
+ # Logs the message if logging level is at least fatal.
66
+ # @param msg [String] message to display or log
67
+ def fatal(msg)
68
+ log_msg(:fatal, msg, full_name())
69
+ end
70
+
71
+ end # HasLog
72
+ end # OFlow
@@ -0,0 +1,31 @@
1
+
2
+ module OFlow
3
+
4
+ # Adds support for a name attribute and the ability to form full name for a
5
+ # named item.
6
+ module HasName
7
+ # The name.
8
+ attr_reader :name
9
+
10
+ # The containing Flow is used to support the full_name() method otherwise it
11
+ # just sets the name.
12
+ # @param flow [Flow|Env] containing Flow
13
+ # @param name [Symbol|String] base name
14
+ def init_name(flow, name)
15
+ @flow = flow
16
+ @name = name.to_sym
17
+ end
18
+
19
+ # Similar to a full file path. The full_name described the containment of
20
+ # the named item.
21
+ # @return [String] full name of item
22
+ def full_name()
23
+ if @flow.respond_to?(:full_name)
24
+ @flow.full_name() + ':' + @name.to_s
25
+ else
26
+ @name.to_s
27
+ end
28
+ end
29
+
30
+ end # HasName
31
+ end # OFlow