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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +182 -0
- data/lib/oflow/actor.rb +76 -0
- data/lib/oflow/actors/errorhandler.rb +32 -0
- data/lib/oflow/actors/ignore.rb +22 -0
- data/lib/oflow/actors/log.rb +175 -0
- data/lib/oflow/actors/relay.rb +23 -0
- data/lib/oflow/actors/timer.rb +126 -0
- data/lib/oflow/actors.rb +11 -0
- data/lib/oflow/box.rb +195 -0
- data/lib/oflow/env.rb +52 -0
- data/lib/oflow/errors.rb +74 -0
- data/lib/oflow/flow.rb +75 -0
- data/lib/oflow/haserrorhandler.rb +48 -0
- data/lib/oflow/haslinks.rb +64 -0
- data/lib/oflow/haslog.rb +72 -0
- data/lib/oflow/hasname.rb +31 -0
- data/lib/oflow/hastasks.rb +209 -0
- data/lib/oflow/inspector.rb +501 -0
- data/lib/oflow/link.rb +43 -0
- data/lib/oflow/pattern.rb +8 -0
- data/lib/oflow/stamp.rb +39 -0
- data/lib/oflow/task.rb +415 -0
- data/lib/oflow/test/action.rb +21 -0
- data/lib/oflow/test/actorwrap.rb +62 -0
- data/lib/oflow/test.rb +8 -0
- data/lib/oflow/tracker.rb +109 -0
- data/lib/oflow/version.rb +5 -0
- data/lib/oflow.rb +23 -0
- data/test/actors/log_test.rb +57 -0
- data/test/actors/timer_test.rb +56 -0
- data/test/actorwrap_test.rb +48 -0
- data/test/all_tests.rb +27 -0
- data/test/box_test.rb +127 -0
- data/test/collector.rb +23 -0
- data/test/flow_basic_test.rb +93 -0
- data/test/flow_cfg_error_test.rb +94 -0
- data/test/flow_log_test.rb +87 -0
- data/test/flow_nest_test.rb +215 -0
- data/test/flow_rescue_test.rb +133 -0
- data/test/flow_tracker_test.rb +82 -0
- data/test/stutter.rb +21 -0
- data/test/task_test.rb +98 -0
- data/test/tracker_test.rb +59 -0
- metadata +93 -0
data/lib/oflow/link.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
module OFlow
|
3
|
+
|
4
|
+
# A Link is the data needed to link one Task with another so that when the
|
5
|
+
# ship() method is called the data can be delivered to the destination Task.
|
6
|
+
class Link
|
7
|
+
|
8
|
+
# Name of the target.
|
9
|
+
attr_reader :target_name
|
10
|
+
# Operation to provide the target.
|
11
|
+
attr_reader :op
|
12
|
+
# The actual target Task or Flow.
|
13
|
+
attr_reader :target
|
14
|
+
# Flag indicating the Link is from a Flow to a Task contained in the Flow.
|
15
|
+
attr_reader :ingress
|
16
|
+
|
17
|
+
# Creates a new Link. This is called from link() and route() methods on
|
18
|
+
# Tasks and Flows.
|
19
|
+
# @param target_name [Symbol] target Task base name
|
20
|
+
# @param op [Symbol] operation to use on the target
|
21
|
+
# @param ingress [Boolean] indicates the Link is internal
|
22
|
+
# @return [Link] new Link
|
23
|
+
def initialize(target_name, op, ingress=false)
|
24
|
+
@target_name = target_name
|
25
|
+
@op = op
|
26
|
+
@target = nil
|
27
|
+
@ingress = ingress
|
28
|
+
end
|
29
|
+
|
30
|
+
# Delivers a package (Box) to the target.
|
31
|
+
# @param box [Box] package to deliver
|
32
|
+
def ship(box)
|
33
|
+
@target.receive(@op, box)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns a string representation of the Link.
|
37
|
+
def to_s()
|
38
|
+
"Link{ingress: #{@ingress}, target_name: #{@target_name}, op: #{op}, target: #{@target}}"
|
39
|
+
end
|
40
|
+
alias inspect to_s
|
41
|
+
|
42
|
+
end # Link
|
43
|
+
end # OFlow
|
data/lib/oflow/stamp.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module OFlow
|
5
|
+
|
6
|
+
# Information used to identify a location and time that a Box was
|
7
|
+
# received. Stamps are placed in Trackers.
|
8
|
+
class Stamp
|
9
|
+
|
10
|
+
# Full name of Task that created the Stamp in a Tracker.
|
11
|
+
attr_reader :location
|
12
|
+
# Operation that caused the Stamp to be created.
|
13
|
+
attr_reader :op
|
14
|
+
# The time the Stamp was created.
|
15
|
+
attr_reader :time
|
16
|
+
|
17
|
+
# Create a new Stamp.
|
18
|
+
# @param location [String] full name of Task that created the Stamp in a Tracker
|
19
|
+
# @param op [Symbol] operation that caused the Stamp to be created
|
20
|
+
# @param time [Time] time the Stamp was created
|
21
|
+
def initialize(location, op=nil, time=nil)
|
22
|
+
@location = location
|
23
|
+
@op = op
|
24
|
+
@time = (time || Time.now).utc
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns a string composed of the location and operation.
|
28
|
+
def where()
|
29
|
+
"#{@location}-#{@op}"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a String representation of the Stamp.
|
33
|
+
def to_s()
|
34
|
+
"#{@location}-#{@op}@#{@time.iso8601(9)}"
|
35
|
+
end
|
36
|
+
alias inspect to_s
|
37
|
+
|
38
|
+
end # Stamp
|
39
|
+
end # OFlow
|
data/lib/oflow/task.rb
ADDED
@@ -0,0 +1,415 @@
|
|
1
|
+
|
2
|
+
module OFlow
|
3
|
+
|
4
|
+
# The Task class provides the asynchronous functionality for the system. Each
|
5
|
+
# Task has it's own thread and receives requests as an operation and Box of
|
6
|
+
# data by the receive() method. The request is put on a queue and popped off
|
7
|
+
# one at a time and handed to an Actor associatesd with the Task.
|
8
|
+
class Task
|
9
|
+
include HasName
|
10
|
+
include HasLinks
|
11
|
+
include HasErrorHandler
|
12
|
+
include HasLog
|
13
|
+
|
14
|
+
# value of @state that indicates the Task is being created.
|
15
|
+
STARTING = 0
|
16
|
+
# value of @state that indicates the Task is not currently processing requests
|
17
|
+
STOPPED = 1
|
18
|
+
# value of @state that indicates the Task is currently ready to process requests
|
19
|
+
RUNNING = 2
|
20
|
+
# value of @state that indicates the Task is shutting down
|
21
|
+
CLOSING = 3
|
22
|
+
# value of @state that indicates the Task is not receiving new requests.
|
23
|
+
BLOCKED = 4
|
24
|
+
# value of @state that indicates the Task is processing one request and will
|
25
|
+
# stop after that processing is complete
|
26
|
+
STEP = 5
|
27
|
+
|
28
|
+
# The current processing state of the Task
|
29
|
+
attr_reader :state
|
30
|
+
# the Actor
|
31
|
+
attr_reader :actor
|
32
|
+
|
33
|
+
# A Task is initialized by specifying a class to create an instance of.
|
34
|
+
# @param flow [Flow] Flow containing the Task
|
35
|
+
# @param name [name] Task base name
|
36
|
+
# @param actor_class [Class] _class Class of the Actor to create
|
37
|
+
# @param options [Hash] additional options for the Task or Actor
|
38
|
+
def initialize(flow, name, actor_class, options={})
|
39
|
+
@state = STARTING
|
40
|
+
@queue = []
|
41
|
+
@req_mutex = Mutex.new()
|
42
|
+
@req_thread = nil
|
43
|
+
@step_thread = nil
|
44
|
+
@waiting_thread = nil
|
45
|
+
@req_timeout = 0.0
|
46
|
+
@max_queue_count = nil
|
47
|
+
@current_req = nil
|
48
|
+
@proc_cnt = 0
|
49
|
+
@loop = nil
|
50
|
+
|
51
|
+
init_name(flow, name)
|
52
|
+
init_links()
|
53
|
+
set_options(options)
|
54
|
+
|
55
|
+
@actor = actor_class.new(self, options)
|
56
|
+
raise Exception.new("#{actor} does not respond to the perform() method.") unless @actor.respond_to?(:perform)
|
57
|
+
|
58
|
+
@state = RUNNING
|
59
|
+
return unless @actor.with_own_thread()
|
60
|
+
|
61
|
+
@loop = Thread.start(self) do |me|
|
62
|
+
Thread.current[:name] = me.full_name()
|
63
|
+
while CLOSING != @state
|
64
|
+
begin
|
65
|
+
if RUNNING == @state || STEP == @state || BLOCKED == @state
|
66
|
+
req = nil
|
67
|
+
if @queue.empty?
|
68
|
+
@waiting_thread.wakeup() unless @waiting_thread.nil?
|
69
|
+
sleep(1.0)
|
70
|
+
next
|
71
|
+
end
|
72
|
+
@req_mutex.synchronize {
|
73
|
+
req = @queue.pop()
|
74
|
+
}
|
75
|
+
@req_thread.wakeup() unless @req_thread.nil?
|
76
|
+
|
77
|
+
@current_req = req
|
78
|
+
begin
|
79
|
+
@actor.perform(req.op, req.box) unless req.nil?
|
80
|
+
rescue Exception => e
|
81
|
+
handle_error(e)
|
82
|
+
end
|
83
|
+
@proc_cnt += 1
|
84
|
+
@current_req = nil
|
85
|
+
if STEP == @state
|
86
|
+
@step_thread.wakeup() unless @step_thread.nil?
|
87
|
+
@state = STOPPED
|
88
|
+
end
|
89
|
+
elsif STOPPED == @state
|
90
|
+
sleep(1.0)
|
91
|
+
end
|
92
|
+
rescue Exception => e
|
93
|
+
puts "*** #{full_name} #{e.class}: #{e.message}"
|
94
|
+
@current_req = nil
|
95
|
+
# TBD Env.rescue(e)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns the state of the Task as a String.
|
102
|
+
# @return [String] String representation of the state
|
103
|
+
def state_string()
|
104
|
+
ss = 'UNKNOWN'
|
105
|
+
case @state
|
106
|
+
when STOPPED
|
107
|
+
ss = 'STOPPED'
|
108
|
+
when RUNNING
|
109
|
+
ss = 'RUNNING'
|
110
|
+
when CLOSING
|
111
|
+
ss = 'CLOSING'
|
112
|
+
when STEP
|
113
|
+
ss = 'STEP'
|
114
|
+
end
|
115
|
+
ss
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns a String that describes the Task.
|
119
|
+
# @param detail [Fixnum] higher values result in more detail in the description
|
120
|
+
# @param indent [Fixnum] the number of spaces to indent the description
|
121
|
+
def describe(detail=0, indent=0)
|
122
|
+
i = ' ' * indent
|
123
|
+
lines = ["#{i}#{name} (#{actor.class}) {"]
|
124
|
+
@links.each { |local,link|
|
125
|
+
lines << " #{i}#{local} => #{link.target_name}:#{link.op}"
|
126
|
+
}
|
127
|
+
if 1 <= detail
|
128
|
+
lines << " #{i}queued: #{queue_count()} (#{busy? ? 'busy' : 'idle'})"
|
129
|
+
lines << " #{i}state: #{state_string()}"
|
130
|
+
@actor.options.each do |key,value|
|
131
|
+
lines << " #{i}#{key} = #{value} (#{value.class})"
|
132
|
+
end
|
133
|
+
if 2 <= detail
|
134
|
+
lines << " #{i}processing: #{@current_req.describe(detail)}" unless @current_req.nil?
|
135
|
+
# Copy queue so it doesn't change while working with it and don't
|
136
|
+
# block the queue. It is okay for the requests to be stale as it is
|
137
|
+
# for a display that will be out of date as soon as it is displayed.
|
138
|
+
reqs = []
|
139
|
+
@req_mutex.synchronize {
|
140
|
+
reqs = Array.new(@queue)
|
141
|
+
}
|
142
|
+
lines << " #{i}queue:"
|
143
|
+
reqs.each do |req|
|
144
|
+
lines << " #{i}#{req.describe(detail)}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
lines << i + "}"
|
149
|
+
lines.join("\n")
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns the number of requests on the queue.
|
153
|
+
# @return [Fixnum] number of queued requests
|
154
|
+
def queue_count()
|
155
|
+
@queue.length
|
156
|
+
end
|
157
|
+
|
158
|
+
# Returns a score indicating how backed up the queue is. This is used for
|
159
|
+
# selecting an Actor when stepping from the Inspector.
|
160
|
+
# @return [Fixnum] a measure of how backed up a Task is
|
161
|
+
def backed_up()
|
162
|
+
cnt = @queue.size()
|
163
|
+
return 0 if 0 == cnt
|
164
|
+
if @max_queue_count.nil? || 0 == @max_queue_count
|
165
|
+
cnt = 80 if 80 < cnt
|
166
|
+
cnt
|
167
|
+
else
|
168
|
+
cnt * 100 / @max_queue_count
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns the true if any requests are queued or a request is being processed.
|
173
|
+
# @return [true|false] true if busy, false otherwise
|
174
|
+
def busy?()
|
175
|
+
!@current_req.nil? || !@queue.empty?
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns the default timeout for the time to wait for the Task to be
|
179
|
+
# ready to accept a request using the receive() method.
|
180
|
+
# @return [Float] current timeout for the receive() method
|
181
|
+
def request_timeout()
|
182
|
+
@req_timeout
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the maximum number of requests allowed on the normal processing
|
186
|
+
# queue. A value of nil indicates there is no limit.
|
187
|
+
# @return [NilClass|Fixnum] maximum number of request that can be queued
|
188
|
+
def max_queue_count()
|
189
|
+
@max_queue_count
|
190
|
+
end
|
191
|
+
|
192
|
+
# Returns the total number of requested processed.
|
193
|
+
# @return [Fixnum] number of request processed
|
194
|
+
def proc_count()
|
195
|
+
@proc_cnt
|
196
|
+
end
|
197
|
+
|
198
|
+
# Causes the Actor to stop processing any more requests after the current
|
199
|
+
# request has finished.
|
200
|
+
def stop()
|
201
|
+
@state = STOPPED
|
202
|
+
end
|
203
|
+
|
204
|
+
# Causes the Task to process one request and then stop. The max_wait is
|
205
|
+
# used to avoid getting stuck if the processing takes too long.
|
206
|
+
# @param max_wait [Float|Fixnum] _wait maximum time to wait for the step to complete
|
207
|
+
def step(max_wait=5)
|
208
|
+
return nil if @loop.nil?
|
209
|
+
return nil if STOPPED != @state || @queue.empty?
|
210
|
+
@state = STEP
|
211
|
+
@step_thread = Thread.current
|
212
|
+
@loop.wakeup()
|
213
|
+
sleep(max_wait)
|
214
|
+
@step_thread = nil
|
215
|
+
self
|
216
|
+
end
|
217
|
+
|
218
|
+
# Wakes up the Task if it has been stopped or if Env.shutdown() has been called.
|
219
|
+
def wakeup()
|
220
|
+
@loop.wakeup() unless @loop.nil?
|
221
|
+
end
|
222
|
+
|
223
|
+
# Restarts the Task's processing thread.
|
224
|
+
def start()
|
225
|
+
@state = RUNNING
|
226
|
+
@loop.wakeup() unless @loop.nil?
|
227
|
+
end
|
228
|
+
|
229
|
+
# Closes the Task by exiting the processing thread. If flush is true then
|
230
|
+
# all requests in the queue are processed first.
|
231
|
+
def shutdown(flush_first=false)
|
232
|
+
return if @loop.nil?
|
233
|
+
if flush_first
|
234
|
+
@state = BLOCKED
|
235
|
+
flush()
|
236
|
+
end
|
237
|
+
@state = CLOSING
|
238
|
+
begin
|
239
|
+
# if the loop has already exited this will raise an Exception that can be ignored
|
240
|
+
@loop.wakeup()
|
241
|
+
rescue
|
242
|
+
# ignore
|
243
|
+
end
|
244
|
+
@loop.join()
|
245
|
+
end
|
246
|
+
|
247
|
+
# Waits for all processing to complete before returning.
|
248
|
+
def flush()
|
249
|
+
return if @loop.nil?
|
250
|
+
@waiting_thread = Thread.current
|
251
|
+
begin
|
252
|
+
@loop.wakeup()
|
253
|
+
rescue
|
254
|
+
# ignore
|
255
|
+
end
|
256
|
+
while busy?
|
257
|
+
sleep(2.0)
|
258
|
+
end
|
259
|
+
@waiting_thread = nil
|
260
|
+
end
|
261
|
+
|
262
|
+
# The current state of the Task.
|
263
|
+
def state=(s)
|
264
|
+
@state = s
|
265
|
+
end
|
266
|
+
|
267
|
+
# The expected inputs the Task supports or nil if not implemented.
|
268
|
+
def inputs()
|
269
|
+
@actor.inputs()
|
270
|
+
end
|
271
|
+
|
272
|
+
# The expected outputs the Task supports or nil if not implemented.
|
273
|
+
def outputs()
|
274
|
+
@actor.outputs()
|
275
|
+
end
|
276
|
+
|
277
|
+
# Creates a Request and adds it to the queue.
|
278
|
+
# @param op [Symbol] operation to perform
|
279
|
+
# @param box [Box] contents or data for the request
|
280
|
+
def receive(op, box)
|
281
|
+
return if CLOSING == @state
|
282
|
+
|
283
|
+
raise BlockedError.new() if BLOCKED == @state
|
284
|
+
|
285
|
+
box = box.receive(full_name, op) unless box.nil?
|
286
|
+
# Special case for starting state so that an Actor can place an item on
|
287
|
+
# the queue before the loop is started.
|
288
|
+
if @loop.nil? && STARTING != @state # no thread task
|
289
|
+
begin
|
290
|
+
@actor.perform(op, box)
|
291
|
+
rescue Exception => e
|
292
|
+
handle_error(e)
|
293
|
+
end
|
294
|
+
return
|
295
|
+
end
|
296
|
+
unless @max_queue_count.nil? || 0 == @max_queue_count || @queue.size() < @max_queue_count
|
297
|
+
@req_thread = Thread.current
|
298
|
+
sleep(timeout) unless @req_timeout.nil? || 0 == @req_timeout
|
299
|
+
@req_thread = nil
|
300
|
+
raise BusyError.new() unless @queue.size() < @max_queue_count
|
301
|
+
end
|
302
|
+
@req_mutex.synchronize {
|
303
|
+
@queue.insert(0, Request.new(op, box))
|
304
|
+
}
|
305
|
+
@loop.wakeup() if RUNNING == @state
|
306
|
+
end
|
307
|
+
|
308
|
+
# Sends a message to another Task or Flow.
|
309
|
+
# @param dest [Symbol] identifies the link that points to the destination Task or Flow
|
310
|
+
# @param box [Box] contents or data for the request
|
311
|
+
def ship(dest, box)
|
312
|
+
box.freeze() unless box.nil?
|
313
|
+
link = resolve_link(dest)
|
314
|
+
raise LinkError.new(dest) if link.nil? || link.target.nil?
|
315
|
+
link.target.receive(link.op, box)
|
316
|
+
link
|
317
|
+
end
|
318
|
+
|
319
|
+
# Processes the initialize() options. Subclasses should call super.
|
320
|
+
# @param options [Hash] options to be used for initialization
|
321
|
+
# @option options [Fixnum] :max_queue_count maximum number of requests
|
322
|
+
# that can be queued before backpressure is applied to the caller.
|
323
|
+
# @option options [Float] :request_timeout timeout in seconds to wait
|
324
|
+
# before raising a BusyError if the request queue is too long.
|
325
|
+
def set_options(options)
|
326
|
+
@max_queue_count = options.fetch(:max_queue_count, @max_queue_count)
|
327
|
+
@req_timeout = options.fetch(:request_timeout, @req_timeout).to_f
|
328
|
+
end
|
329
|
+
|
330
|
+
def _validation_errors()
|
331
|
+
errors = []
|
332
|
+
@links.each_value { |lnk| _check_link(lnk, errors) }
|
333
|
+
|
334
|
+
unless (outs = @actor.outputs()).nil?
|
335
|
+
outs.each do |spec|
|
336
|
+
if find_link(spec.dest).nil?
|
337
|
+
errors << ValidateError::Problem.new(full_name, ValidateError::Problem::MISSING_ERROR, "Missing link for '#{spec.dest}'.")
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
errors
|
342
|
+
end
|
343
|
+
|
344
|
+
def has_input(op)
|
345
|
+
ins = @actor.inputs()
|
346
|
+
return true if ins.nil?
|
347
|
+
op = op.to_sym unless op.nil?
|
348
|
+
ins.each { |spec| return true if spec.op.nil? || spec.op == op }
|
349
|
+
false
|
350
|
+
end
|
351
|
+
|
352
|
+
def _check_link(lnk, errors)
|
353
|
+
if lnk.target.nil?
|
354
|
+
errors << ValidateError::Problem.new(full_name, ValidateError::Problem::LINK_ERROR, "Failed to find task '#{lnk.target_name}'.")
|
355
|
+
return
|
356
|
+
end
|
357
|
+
unless lnk.target.has_input(lnk.op)
|
358
|
+
errors << ValidateError::Problem.new(full_name, ValidateError::Problem::INPUT_ERROR, "'#{lnk.op}' not allowed on '#{lnk.target.full_name}'.")
|
359
|
+
return
|
360
|
+
end
|
361
|
+
|
362
|
+
# TBD
|
363
|
+
# Verify target has link.op as input if input is specified.
|
364
|
+
|
365
|
+
end
|
366
|
+
|
367
|
+
# Resolves all links. Called by the system.
|
368
|
+
def resolve_all_links()
|
369
|
+
@links.each_value { |lnk|
|
370
|
+
set_link_target(lnk) if lnk.target.nil?
|
371
|
+
}
|
372
|
+
end
|
373
|
+
|
374
|
+
private
|
375
|
+
|
376
|
+
# Internal class used to store information about asynchronous method
|
377
|
+
# invocations.
|
378
|
+
class Request
|
379
|
+
attr_accessor :op
|
380
|
+
attr_accessor :box
|
381
|
+
|
382
|
+
def initialize(op, box)
|
383
|
+
@op = op
|
384
|
+
@box = box
|
385
|
+
end
|
386
|
+
|
387
|
+
def describe(detail=3)
|
388
|
+
if @box.nil?
|
389
|
+
"#{@op}()"
|
390
|
+
elsif 2 >= detail
|
391
|
+
@op
|
392
|
+
elsif 3 >= detail || @box.tracker.nil?
|
393
|
+
"#{@op}(#{@box.contents})"
|
394
|
+
else
|
395
|
+
"#{@op}(#{@box.contents}) #{@box.tracker}"
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
end # Request
|
400
|
+
|
401
|
+
class Link
|
402
|
+
attr_reader :name
|
403
|
+
attr_reader :task
|
404
|
+
attr_reader :op
|
405
|
+
|
406
|
+
def initialize(name, task, op)
|
407
|
+
@name = name
|
408
|
+
@task = task
|
409
|
+
@op = op
|
410
|
+
end
|
411
|
+
|
412
|
+
end # Link
|
413
|
+
|
414
|
+
end # Task
|
415
|
+
end # OFlow
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
module OFlow
|
3
|
+
module Test
|
4
|
+
|
5
|
+
class Action
|
6
|
+
attr_reader :dest
|
7
|
+
attr_reader :box
|
8
|
+
|
9
|
+
def initialize(dest, box)
|
10
|
+
@dest = dest
|
11
|
+
@box = box
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s()
|
15
|
+
"#{@dest}: #{box.contents}"
|
16
|
+
end
|
17
|
+
alias inspect to_s
|
18
|
+
|
19
|
+
end # Action
|
20
|
+
end # Test
|
21
|
+
end # OFlow
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
module OFlow
|
3
|
+
module Test
|
4
|
+
class ActorWrap
|
5
|
+
|
6
|
+
attr_reader :actor
|
7
|
+
attr_accessor :name
|
8
|
+
|
9
|
+
# Array of Actions. Log entries appear with a destination of :log.
|
10
|
+
attr_reader :history
|
11
|
+
|
12
|
+
def initialize(name, actor_class, options={})
|
13
|
+
@name = name
|
14
|
+
@actor = actor_class.new(self, options)
|
15
|
+
@history = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def reset()
|
19
|
+
@history = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def full_name()
|
23
|
+
":test:#{@name}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Calls perform on the actor instance
|
27
|
+
def receive(op, box)
|
28
|
+
@actor.perform(op, box)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Task API that adds entry to history.
|
32
|
+
def ship(dest, box)
|
33
|
+
@history << Action.new(dest, box)
|
34
|
+
end
|
35
|
+
|
36
|
+
def log_msg(level, msg, fn)
|
37
|
+
@history << Action.new(:log, Box.new([level, msg, fn]))
|
38
|
+
end
|
39
|
+
|
40
|
+
def debug(msg)
|
41
|
+
log_msg(:debug, msg, full_name())
|
42
|
+
end
|
43
|
+
|
44
|
+
def info(msg)
|
45
|
+
log_msg(:info, msg, full_name())
|
46
|
+
end
|
47
|
+
|
48
|
+
def error(msg)
|
49
|
+
log_msg(:error, msg, full_name())
|
50
|
+
end
|
51
|
+
|
52
|
+
def warn(msg)
|
53
|
+
log_msg(:warn, msg, full_name())
|
54
|
+
end
|
55
|
+
|
56
|
+
def fatal(msg)
|
57
|
+
log_msg(:fatal, msg, full_name())
|
58
|
+
end
|
59
|
+
|
60
|
+
end # ActorWrap
|
61
|
+
end # Test
|
62
|
+
end # OFlow
|
data/lib/oflow/test.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module OFlow
|
5
|
+
|
6
|
+
# A Tracker is used to track data through the system. They are attached to
|
7
|
+
# Boxes and are updated when they are received by Flows and Tasks.
|
8
|
+
class Tracker
|
9
|
+
# Gets the machine address. This is used for generating unique identifiers
|
10
|
+
# for the Tracker instances.
|
11
|
+
def self.get_machine()
|
12
|
+
machine = "unknown"
|
13
|
+
Socket.ip_address_list.each do |addr|
|
14
|
+
next unless addr.ip?
|
15
|
+
next if addr.ipv6_linklocal?
|
16
|
+
next if addr.ipv4_loopback? || addr.ipv6_loopback?
|
17
|
+
next if addr.ipv4_multicast? || addr.ipv6_multicast?
|
18
|
+
machine = addr.ip_address
|
19
|
+
break
|
20
|
+
end
|
21
|
+
machine
|
22
|
+
end
|
23
|
+
|
24
|
+
@@machine = self.get_machine()
|
25
|
+
@@pid = Process.pid
|
26
|
+
@@last_nano = 0
|
27
|
+
@@nano_mutex = Mutex.new()
|
28
|
+
|
29
|
+
# The identifier of the Tracker.
|
30
|
+
attr_reader :id
|
31
|
+
# The Stamps that were placed in the Tracker as it is received.
|
32
|
+
attr_reader :track
|
33
|
+
|
34
|
+
def self.create(location, op=nil)
|
35
|
+
t = Tracker.new(gen_id(), [Stamp.new(location, op).freeze()])
|
36
|
+
t.track.freeze()
|
37
|
+
t.freeze()
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns an updated instance with a Stamp for the location, operation, and
|
41
|
+
# current time.
|
42
|
+
# @param location [String] full name of Task where the Tracker was received
|
43
|
+
# @param op [Symbol] operation that caused the Stamp to be created
|
44
|
+
# @return [Tracker] updated Tracker
|
45
|
+
def receive(location, op)
|
46
|
+
t = Tracker.new(@id, Array.new(@track) << Stamp.new(location, op).freeze())
|
47
|
+
t.track.freeze()
|
48
|
+
t.freeze()
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a String representation of the Tracker.
|
52
|
+
def to_s()
|
53
|
+
"Tracker{#{@id}, track: #{@track}}"
|
54
|
+
end
|
55
|
+
alias inspect to_s
|
56
|
+
|
57
|
+
# When a package is split and travels on more than one route the Tracker can
|
58
|
+
# be merged with this method. The returned Tracker contains both tracks.
|
59
|
+
# @param t2 [Tracker] other Tracker
|
60
|
+
# @return [Tracker] merged Tracker
|
61
|
+
def merge(t2)
|
62
|
+
raise Exception.new("Can not merge #{t2.id} into #{@id}. Different IDs.") if t2.id != @id
|
63
|
+
comb = []
|
64
|
+
s2 = t2.track.size
|
65
|
+
for i in 0..@track.size
|
66
|
+
break if s2 <= i
|
67
|
+
unless @track[i] == t2.track[i]
|
68
|
+
if @track[-1].location == t2.track[-1].location
|
69
|
+
comb << [@track[i..-2], t2.track[i..-2]]
|
70
|
+
comb << @track[-1]
|
71
|
+
else
|
72
|
+
comb << [@track[i..-1], t2.track[i..-1]]
|
73
|
+
end
|
74
|
+
break
|
75
|
+
end
|
76
|
+
comb << @track[i]
|
77
|
+
end
|
78
|
+
comb.freeze
|
79
|
+
Tracker.new(@id, comb)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def self.gen_id()
|
85
|
+
nano = (Time.now.to_f * 1000000000.0).to_i
|
86
|
+
@@nano_mutex.synchronize do
|
87
|
+
while nano <= @@last_nano
|
88
|
+
nano += 1
|
89
|
+
end
|
90
|
+
@@last_nano = nano
|
91
|
+
end
|
92
|
+
"#{@@machine}.#{@@pid}.#{nano}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize(id, track)
|
96
|
+
@id = id
|
97
|
+
@track = track
|
98
|
+
end
|
99
|
+
|
100
|
+
def id=(i)
|
101
|
+
@id = i
|
102
|
+
end
|
103
|
+
|
104
|
+
def track=(t)
|
105
|
+
@track = t
|
106
|
+
end
|
107
|
+
|
108
|
+
end # Tracker
|
109
|
+
end # OFlow
|