rsmp 0.8.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yaml +14 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile.lock +46 -67
  5. data/README.md +2 -2
  6. data/bin/console +1 -1
  7. data/config/tlc.yaml +8 -6
  8. data/documentation/classes_and_modules.md +4 -4
  9. data/documentation/collecting_message.md +2 -2
  10. data/documentation/tasks.md +149 -0
  11. data/lib/rsmp/archive.rb +3 -3
  12. data/lib/rsmp/cli.rb +27 -4
  13. data/lib/rsmp/collect/aggregated_status_collector.rb +1 -1
  14. data/lib/rsmp/collect/collector.rb +13 -6
  15. data/lib/rsmp/collect/command_response_collector.rb +1 -1
  16. data/lib/rsmp/collect/state_collector.rb +1 -1
  17. data/lib/rsmp/collect/status_collector.rb +2 -1
  18. data/lib/rsmp/components.rb +3 -3
  19. data/lib/rsmp/convert/export/json_schema.rb +4 -4
  20. data/lib/rsmp/convert/import/yaml.rb +1 -1
  21. data/lib/rsmp/error.rb +0 -3
  22. data/lib/rsmp/inspect.rb +1 -1
  23. data/lib/rsmp/logger.rb +5 -5
  24. data/lib/rsmp/logging.rb +1 -1
  25. data/lib/rsmp/message.rb +1 -1
  26. data/lib/rsmp/node.rb +10 -45
  27. data/lib/rsmp/proxy.rb +184 -134
  28. data/lib/rsmp/rsmp.rb +1 -1
  29. data/lib/rsmp/site.rb +24 -61
  30. data/lib/rsmp/site_proxy.rb +33 -37
  31. data/lib/rsmp/supervisor.rb +25 -21
  32. data/lib/rsmp/supervisor_proxy.rb +55 -29
  33. data/lib/rsmp/task.rb +84 -0
  34. data/lib/rsmp/tlc/signal_group.rb +17 -7
  35. data/lib/rsmp/tlc/signal_plan.rb +2 -2
  36. data/lib/rsmp/tlc/traffic_controller.rb +125 -39
  37. data/lib/rsmp/tlc/traffic_controller_site.rb +51 -35
  38. data/lib/rsmp/version.rb +1 -1
  39. data/lib/rsmp.rb +1 -1
  40. data/rsmp.gemspec +7 -7
  41. metadata +20 -20
  42. data/lib/rsmp/site_proxy_wait.rb +0 -0
  43. data/lib/rsmp/wait.rb +0 -16
  44. data/test.rb +0 -27
@@ -23,7 +23,7 @@ module RSMP
23
23
 
24
24
  def self.build_value item
25
25
  out = {}
26
-
26
+
27
27
  if item['description']
28
28
  out["description"] = item['description']
29
29
  end
@@ -95,7 +95,7 @@ module RSMP
95
95
  }
96
96
  end
97
97
  json = {
98
- "properties" => {
98
+ "properties" => {
99
99
  "aCId" => { "enum" => items.keys.sort },
100
100
  "rvs" => { "items" => { "allOf" => list } }
101
101
  }
@@ -175,7 +175,7 @@ module RSMP
175
175
  }
176
176
  ]
177
177
  }
178
- out["sxl.json"] = output_json json
178
+ out["sxl.json"] = output_json json
179
179
  end
180
180
 
181
181
  def self.generate sxl
@@ -192,7 +192,7 @@ module RSMP
192
192
  out.each_pair do |relative_path,str|
193
193
  path = File.join(folder, relative_path)
194
194
  FileUtils.mkdir_p File.dirname(path) # create folders if needed
195
- file = File.open(path, 'w+') # w+ means truncate or create new file
195
+ file = File.open(path, 'w+') # w+ means truncate or create new file
196
196
  file.puts str
197
197
  end
198
198
  end
@@ -24,7 +24,7 @@ module RSMP
24
24
  commands: {}
25
25
  }
26
26
 
27
- yaml['objects'].each_pair do |type,object|
27
+ yaml['objects'].each_pair do |type,object|
28
28
  object["alarms"].each { |id,item| sxl[:alarms][id] = item }
29
29
  object["statuses"].each { |id,item| sxl[:statuses][id] = item }
30
30
  object["commands"].each { |id,item| sxl[:commands][id] = item }
data/lib/rsmp/error.rb CHANGED
@@ -64,7 +64,4 @@ module RSMP
64
64
 
65
65
  class RepeatedStatusError < Error
66
66
  end
67
-
68
- class TimestampError < Error
69
- end
70
67
  end
data/lib/rsmp/inspect.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # Costume inspect, to reduce noise
2
- #
2
+ #
3
3
  # Instance variables of classes starting with Async or RSMP are shown
4
4
  # with only their class name and object id, which reduces output,
5
5
  # especially for deep object structures.
data/lib/rsmp/logger.rb CHANGED
@@ -2,7 +2,7 @@ module RSMP
2
2
  class Logger
3
3
 
4
4
  attr_accessor :settings
5
-
5
+
6
6
  def initialize settings={}
7
7
  defaults = {
8
8
  'active'=>true,
@@ -115,7 +115,7 @@ module RSMP
115
115
  end
116
116
  end
117
117
  end
118
- return false if ack && @settings["acknowledgements"] == false &&
118
+ return false if ack && @settings["acknowledgements"] == false &&
119
119
  [:not_acknowledged,:warning,:error].include?(item[:level]) == false
120
120
  end
121
121
  true
@@ -159,7 +159,7 @@ module RSMP
159
159
 
160
160
  def log item, force:false
161
161
  if output?(item, force)
162
- output item[:level], build_output(item)
162
+ output item[:level], build_output(item)
163
163
  end
164
164
  end
165
165
 
@@ -169,7 +169,7 @@ module RSMP
169
169
  else
170
170
  ' '*length
171
171
  end
172
- end
172
+ end
173
173
 
174
174
  def dump archive, force:false, num:nil
175
175
  num ||= archive.items.size
@@ -183,7 +183,7 @@ module RSMP
183
183
  def build_part parts, item, key, &block
184
184
  skey = key.to_s
185
185
  return unless @settings[skey]
186
-
186
+
187
187
  part = item[key]
188
188
  part = yield part if block
189
189
  part = part.to_s
data/lib/rsmp/logging.rb CHANGED
@@ -8,7 +8,7 @@ module RSMP
8
8
 
9
9
  def initialize_logging options
10
10
  @archive = options[:archive] || RSMP::Archive.new
11
- @logger = options[:logger] || RSMP::Logger.new(options[:log_settings])
11
+ @logger = options[:logger] || RSMP::Logger.new(options[:log_settings])
12
12
  end
13
13
 
14
14
  def author
data/lib/rsmp/message.rb CHANGED
@@ -188,7 +188,7 @@ module RSMP
188
188
  class Unknown < Message
189
189
  end
190
190
 
191
- class AggregatedStatus < Message
191
+ class AggregatedStatus < Message
192
192
  def initialize attributes = {}
193
193
  super({
194
194
  "type" => "AggregatedStatus",
data/lib/rsmp/node.rb CHANGED
@@ -3,14 +3,14 @@
3
3
  module RSMP
4
4
  class Node
5
5
  include Logging
6
- include Wait
7
6
  include Inspect
7
+ include Task
8
8
 
9
9
  attr_reader :archive, :logger, :task, :deferred, :error_queue, :clock, :collector
10
10
 
11
11
  def initialize options
12
12
  initialize_logging options
13
- @task = options[:task]
13
+ initialize_task
14
14
  @deferred = []
15
15
  @clock = Clock.new
16
16
  @error_queue = Async::Queue.new
@@ -18,6 +18,13 @@ module RSMP
18
18
  @collect = options[:collect]
19
19
  end
20
20
 
21
+ # stop proxies, then call super
22
+ def stop_subtasks
23
+ @proxies.each { |proxy| proxy.stop }
24
+ @proxies.clear
25
+ super
26
+ end
27
+
21
28
  def ignore_errors classes, &block
22
29
  was, @ignore_errors = @ignore_errors, [classes].flatten
23
30
  yield
@@ -50,55 +57,13 @@ module RSMP
50
57
 
51
58
  def clear_deferred
52
59
  @deferred.clear
53
- end
54
-
55
- def do_start task
56
- task.annotate self.class.to_s
57
- @task = task
58
- start_action
59
- idle
60
- end
61
-
62
- def start
63
- starting
64
- if @task
65
- do_start @task
66
- else
67
- Async do |task|
68
- do_start task
69
- end
70
- end
71
- rescue Errno::EADDRINUSE => e
72
- log "Cannot start: #{e.to_s}", level: :error
73
- rescue SystemExit, SignalException, Interrupt
74
- @logger.unmute_all
75
- exiting
76
- end
77
-
78
- def idle
79
- loop do
80
- @task.sleep 60
81
- end
82
- end
83
-
84
- def stop
85
- @task.stop if @task
86
- end
87
-
88
- def restart
89
- stop
90
- start
91
- end
92
-
93
- def exiting
94
- log "Exiting", level: :info
95
60
  end
96
61
 
97
62
  def check_required_settings settings, required
98
63
  raise ArgumentError.new "Settings is empty" unless settings
99
64
  required.each do |setting|
100
65
  raise ArgumentError.new "Missing setting: #{setting}" unless settings.include? setting.to_s
101
- end
66
+ end
102
67
  end
103
68
 
104
69
  def author
data/lib/rsmp/proxy.rb CHANGED
@@ -1,32 +1,104 @@
1
- # Logging class for a connection to a remote site or supervisor.
1
+ # A connection to a remote site or supervisor.
2
+ # Uses the Task module to handle asyncronous work, but adds
3
+ # the concept of a connection that can be connected or disconnected.
2
4
 
3
5
  require 'rubygems'
4
6
 
5
- module RSMP
7
+ module RSMP
6
8
  class Proxy
7
9
  WRAPPING_DELIMITER = "\f"
8
10
 
9
11
  include Logging
10
- include Wait
11
12
  include Notifier
12
13
  include Inspect
14
+ include Task
13
15
 
14
- attr_reader :state, :archive, :connection_info, :sxl, :task, :collector, :ip, :port
16
+ attr_reader :state, :archive, :connection_info, :sxl, :collector, :ip, :port
15
17
 
16
18
  def initialize options
17
19
  initialize_logging options
18
20
  initialize_distributor
21
+ initialize_task
19
22
  setup options
20
23
  clear
24
+ @state = :disconnected
21
25
  end
22
26
 
27
+ def disconnect
28
+ end
29
+
30
+ # wait for the reader task to complete,
31
+ # which is not expected to happen before the connection is closed
32
+ def wait_for_reader
33
+ @reader.wait if @reader
34
+ end
35
+
36
+ # close connection, but keep our main task running so we can reconnect
37
+ def close
38
+ log "Closing connection", level: :warning
39
+ close_stream
40
+ close_socket
41
+ set_state :disconnected
42
+ notify_error DisconnectError.new("Connection was closed")
43
+ stop_timer
44
+ end
45
+
46
+ def stop_subtasks
47
+ stop_timer
48
+ stop_reader
49
+ clear
50
+ super
51
+ end
52
+
53
+ def stop_timer
54
+ return unless @timer
55
+ @timer.stop
56
+ @timer = nil
57
+ end
58
+
59
+ def stop_reader
60
+ return unless @reader
61
+ @reader.stop
62
+ @reader = nil
63
+ end
64
+
65
+ def close_stream
66
+ return unless @stream
67
+ @stream.close
68
+ @stream = nil
69
+ end
70
+
71
+ def close_socket
72
+ return unless @socket
73
+ @socket.close
74
+ @socket = nil
75
+ end
76
+
77
+ def stop_task
78
+ close
79
+ super
80
+ end
81
+
82
+ # change our state
83
+ def set_state state
84
+ return if state == @state
85
+ @state = state
86
+ state_changed
87
+ end
88
+
89
+ # the state changed
90
+ # override to to things like notifications
91
+ def state_changed
92
+ @state_condition.signal @state
93
+ end
94
+
95
+ # revive after a reconnect
23
96
  def revive options
24
97
  setup options
25
98
  end
26
99
 
27
100
  def setup options
28
101
  @settings = options[:settings]
29
- @task = options[:task]
30
102
  @socket = options[:socket]
31
103
  @stream = options[:stream]
32
104
  @protocol = options[:protocol]
@@ -35,7 +107,6 @@ module RSMP
35
107
  @connection_info = options[:info]
36
108
  @sxl = nil
37
109
  @site_settings = nil # can't pick until we know the site id
38
- @state = :stopped
39
110
  if options[:collect]
40
111
  @collector = RSMP::Collector.new self, options[:collect]
41
112
  @collector.start
@@ -52,35 +123,16 @@ module RSMP
52
123
  node.clock
53
124
  end
54
125
 
55
- def run
56
- start
57
- @reader.wait if @reader
58
- ensure
59
- stop unless [:stopped, :stopping].include? @state
60
- end
61
-
62
126
  def ready?
63
127
  @state == :ready
64
128
  end
65
129
 
66
130
  def connected?
67
- @state == :starting || @state == :ready
131
+ @state == :connected || @state == :ready
68
132
  end
69
133
 
70
-
71
- def start
72
- set_state :starting
73
- end
74
-
75
- def stop
76
- return if @state == :stopped
77
- set_state :stopping
78
- stop_tasks
79
- notify_error DisconnectError.new("Connection was closed")
80
- ensure
81
- close_socket
82
- clear
83
- set_state :stopped
134
+ def disconnected?
135
+ @state == :disconnected
84
136
  end
85
137
 
86
138
  def clear
@@ -97,56 +149,57 @@ module RSMP
97
149
  @acknowledgement_condition = Async::Notification.new
98
150
  end
99
151
 
100
- def close_socket
101
- if @stream
102
- @stream.close
103
- @stream = nil
104
- end
105
-
106
- if @socket
107
- @socket.close
108
- @socket = nil
152
+ # run an async task that reads from @socket
153
+ def start_reader
154
+ @reader = @task.async do |task|
155
+ task.annotate "reader"
156
+ run_reader
109
157
  end
110
158
  end
111
159
 
112
- def start_reader
113
- @reader = @task.async do |task|
114
- task.annotate "reader"
115
- @stream ||= Async::IO::Stream.new(@socket)
116
- @protocol ||= Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
117
- while json = @protocol.read_line
118
- beginning = Time.now
119
- message = process_packet json
120
- duration = Time.now - beginning
121
- ms = (duration*1000).round(4)
122
- if duration > 0
123
- per_second = (1.0 / duration).round
124
- else
125
- per_second = Float::INFINITY
126
- end
127
- if message
128
- type = message.type
129
- m_id = Logger.shorten_message_id(message.m_id)
130
- else
131
- type = 'Unknown'
132
- m_id = nil
133
- end
134
- str = [type,m_id,"processed in #{ms}ms, #{per_second}req/s"].compact.join(' ')
135
- log str, level: :statistics
136
- end
137
- rescue Async::Wrapper::Cancelled
138
- # ignore
139
- rescue EOFError
140
- log "Connection closed", level: :warning
141
- rescue IOError => e
142
- log "IOError: #{e}", level: :warning
143
- rescue Errno::ECONNRESET
144
- log "Connection reset by peer", level: :warning
145
- rescue Errno::EPIPE
146
- log "Broken pipe", level: :warning
147
- rescue StandardError => e
148
- notify_error e, level: :internal
160
+ def run_reader
161
+ @stream ||= Async::IO::Stream.new(@socket)
162
+ @protocol ||= Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
163
+ loop do
164
+ read_line
165
+ end
166
+ rescue Restart
167
+ log "Closing connection", level: :warning
168
+ raise
169
+ rescue Async::Wrapper::Cancelled
170
+ # ignore exceptions raised when a wait is aborted because a task is stopped
171
+ rescue EOFError, Async::Stop
172
+ log "Connection closed", level: :warning
173
+ rescue IOError => e
174
+ log "IOError: #{e}", level: :warning
175
+ rescue Errno::ECONNRESET
176
+ log "Connection reset by peer", level: :warning
177
+ rescue Errno::EPIPE
178
+ log "Broken pipe", level: :warning
179
+ rescue StandardError => e
180
+ notify_error e, level: :internal
181
+ end
182
+
183
+ def read_line
184
+ json = @protocol.read_line
185
+ beginning = Time.now
186
+ message = process_packet json
187
+ duration = Time.now - beginning
188
+ ms = (duration*1000).round(4)
189
+ if duration > 0
190
+ per_second = (1.0 / duration).round
191
+ else
192
+ per_second = Float::INFINITY
193
+ end
194
+ if message
195
+ type = message.type
196
+ m_id = Logger.shorten_message_id(message.m_id)
197
+ else
198
+ type = 'Unknown'
199
+ m_id = nil
149
200
  end
201
+ str = [type,m_id,"processed in #{ms}ms, #{per_second}req/s"].compact.join(' ')
202
+ log str, level: :statistics
150
203
  end
151
204
 
152
205
  def notify_error e, options={}
@@ -160,36 +213,40 @@ module RSMP
160
213
  end
161
214
 
162
215
  def start_timer
216
+ return if @timer
163
217
  name = "timer"
164
218
  interval = @site_settings['intervals']['timer'] || 1
165
219
  log "Starting #{name} with interval #{interval} seconds", level: :debug
166
220
  @latest_watchdog_received = Clock.now
167
-
168
221
  @timer = @task.async do |task|
169
222
  task.annotate "timer"
170
- next_time = Time.now.to_f
171
- loop do
172
- begin
173
- now = Clock.now
174
- timer(now)
175
- rescue RSMP::Schemer::Error => e
176
- puts "Timer: Schema error: #{e}"
177
- rescue EOFError => e
178
- log "Timer: Connection closed: #{e}", level: :warning
179
- rescue IOError => e
180
- log "Timer: IOError", level: :warning
181
- rescue Errno::ECONNRESET
182
- log "Timer: Connection reset by peer", level: :warning
183
- rescue Errno::EPIPE => e
184
- log "Timer: Broken pipe", level: :warning
185
- rescue StandardError => e
186
- notify_error e, level: :internal
187
- end
188
- ensure
189
- next_time += interval
190
- duration = next_time - Time.now.to_f
191
- task.sleep duration
223
+ run_timer task, interval
224
+ end
225
+ end
226
+
227
+ def run_timer task, interval
228
+ next_time = Time.now.to_f
229
+ loop do
230
+ begin
231
+ now = Clock.now
232
+ timer(now)
233
+ rescue RSMP::Schemer::Error => e
234
+ log "Timer: Schema error: #{e}", level: :warning
235
+ rescue EOFError => e
236
+ log "Timer: Connection closed: #{e}", level: :warning
237
+ rescue IOError => e
238
+ log "Timer: IOError", level: :warning
239
+ rescue Errno::ECONNRESET
240
+ log "Timer: Connection reset by peer", level: :warning
241
+ rescue Errno::EPIPE => e
242
+ log "Timer: Broken pipe", level: :warning
243
+ rescue StandardError => e
244
+ notify_error e, level: :internal
192
245
  end
246
+ ensure
247
+ next_time += interval
248
+ duration = next_time - Time.now.to_f
249
+ task.sleep duration
193
250
  end
194
251
  end
195
252
 
@@ -200,7 +257,7 @@ module RSMP
200
257
  end
201
258
 
202
259
  def watchdog_send_timer now
203
- return unless @watchdog_started
260
+ return unless @watchdog_started
204
261
  return if @site_settings['intervals']['watchdog'] == :never
205
262
  if @latest_watchdog_send_at == nil
206
263
  send_watchdog now
@@ -226,9 +283,13 @@ module RSMP
226
283
  @awaiting_acknowledgement.clone.each_pair do |m_id, message|
227
284
  latest = message.timestamp + timeout
228
285
  if now > latest
229
- log "No acknowledgements for #{message.type} #{message.m_id_short} within #{timeout} seconds", level: :error
230
- stop
231
- notify_error MissingAcknowledgment.new('No ack')
286
+ str = "No acknowledgements for #{message.type} #{message.m_id_short} within #{timeout} seconds"
287
+ log str, level: :error
288
+ begin
289
+ close
290
+ ensure
291
+ notify_error MissingAcknowledgment.new(str)
292
+ end
232
293
  end
233
294
  end
234
295
  end
@@ -238,16 +299,16 @@ module RSMP
238
299
  latest = @latest_watchdog_received + timeout
239
300
  left = latest - now
240
301
  if left < 0
241
- log "No Watchdog within #{timeout} seconds", level: :error
242
- stop
302
+ str = "No Watchdog within #{timeout} seconds"
303
+ log str, level: :error
304
+ begin
305
+ close # this will stop the current task (ourself)
306
+ ensure
307
+ notify_error MissingWatchdog.new(str) # but ensure block will still be reached
308
+ end
243
309
  end
244
310
  end
245
311
 
246
- def stop_tasks
247
- @timer.stop if @timer
248
- @reader.stop if @reader
249
- end
250
-
251
312
  def log str, options={}
252
313
  super str, options.merge(ip: @ip, port: @port, site_id: @site_id)
253
314
  end
@@ -355,7 +416,7 @@ module RSMP
355
416
  str = "Rejected #{message.type},"
356
417
  notify_error e.exception(str), message: message
357
418
  dont_acknowledge message, str, reason
358
- stop
419
+ close
359
420
  message
360
421
  ensure
361
422
  node.clear_deferred
@@ -436,19 +497,14 @@ module RSMP
436
497
  send_message message, "for #{original.type} #{original.m_id_short}"
437
498
  end
438
499
 
439
- def set_state state
440
- @state = state
441
- @state_condition.signal @state
442
- end
443
-
444
- def wait_for_state state, timeout
500
+ def wait_for_state state, timeout:
445
501
  states = [state].flatten
446
502
  return if states.include?(@state)
447
- wait_for(@state_condition,timeout) do
503
+ wait_for_condition(@state_condition,timeout: timeout) do
448
504
  states.include?(@state)
449
505
  end
450
506
  @state
451
- rescue Async::TimeoutError
507
+ rescue RSMP::TimeoutError
452
508
  raise RSMP::TimeoutError.new "Did not reach state #{state} within #{timeout}s"
453
509
  end
454
510
 
@@ -557,10 +613,10 @@ module RSMP
557
613
  end
558
614
  end
559
615
 
560
- def connection_complete
616
+ def handshake_complete
561
617
  set_state :ready
562
618
  end
563
-
619
+
564
620
  def version_acknowledged
565
621
  end
566
622
 
@@ -572,23 +628,17 @@ module RSMP
572
628
  node.site_id
573
629
  end
574
630
 
575
- def wait_for_acknowledgement parent_task, options={}, m_id
576
- collector = Collector.new self, options.merge(task: parent_task, type: ['MessageAck','MessageNotAck'])
577
- collector.collect do |message|
578
- if message.is_a?(MessageNotAck)
579
- if message.attribute('oMId') == m_id
580
- m_id_short = RSMP::Message.shorten_m_id m_id, 8
581
- raise RSMP::MessageRejected.new "Aggregated status request #{m_id_short} was rejected with '#{message.attribute('rea')}'"
582
- end
583
- elsif message.is_a?(MessageAck)
584
- collector.complete if message.attribute('oMId') == m_id
631
+ def send_and_optionally_collect message, options, &block
632
+ collect_options = options[:collect] || options[:collect!]
633
+ if collect_options
634
+ task = @task.async do |task|
635
+ task.annotate 'send_and_optionally_collect'
636
+ collector = yield collect_options # call block to create collector
637
+ collector.collect
638
+ collector.ok! if options[:collect!] # raise any errors if the bang version was specified
639
+ collector
585
640
  end
586
- end
587
- end
588
641
 
589
- def send_and_optionally_collect message, options, &block
590
- if options[:collect]
591
- task = @task.async { |task| yield task }
592
642
  send_message message, validate: options[:validate]
593
643
  { sent: message, collector: task.wait }
594
644
  else
data/lib/rsmp/rsmp.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # Get the current time in UTC, with optional adjustment
2
- # Convertion to string uses the RSMP format 2015-06-08T12:01:39.654Z
2
+ # Convertion to string uses the RSMP format 2015-06-08T12:01:39.654Z
3
3
  # Note that using to_s on a my_clock.to_s will not produce an RSMP formatted timestamp,
4
4
  # you need to use Clock.to_s my_clock
5
5