oflow 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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