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/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
@@ -0,0 +1,8 @@
1
+
2
+ module OFlow
3
+
4
+ # TBD
5
+ class Pattern
6
+
7
+ end # Pattern
8
+ end # OFlow
@@ -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,8 @@
1
+
2
+ module OFlow
3
+ module Test
4
+ end
5
+ end
6
+
7
+ require 'oflow/test/action'
8
+ require 'oflow/test/actorwrap'
@@ -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