rsmp 0.1.10 → 0.1.19

Sign up to get free protection for your applications and to get access to all the features.
@@ -23,7 +23,7 @@ module RSMP
23
23
  class MissingWatchdog < Error
24
24
  end
25
25
 
26
- class MissingAcknowledgment < Error
26
+ class MessageRejected < Error
27
27
  end
28
28
 
29
29
  class MissingAttribute < InvalidMessage
@@ -43,4 +43,13 @@ module RSMP
43
43
 
44
44
  class UnknownComponent < Error
45
45
  end
46
+
47
+ class UnknownCommand < Error
48
+ end
49
+
50
+ class UnknownStatus < Error
51
+ end
52
+
53
+ class ConfigurationError < Error
54
+ end
46
55
  end
@@ -27,6 +27,9 @@ module RSMP
27
27
 
28
28
  @@schemas = load_schemas
29
29
 
30
+ def self.make_m_id
31
+ SecureRandom.uuid()
32
+ end
30
33
 
31
34
  def self.parse_attributes json
32
35
  raise ArgumentError unless json
@@ -80,8 +83,12 @@ module RSMP
80
83
  @attributes["mId"]
81
84
  end
82
85
 
86
+ def self.shorten_m_id m_id, length=4
87
+ m_id[0..length-1]
88
+ end
89
+
83
90
  def m_id_short
84
- @attributes["mId"][0..3]
91
+ Message.shorten_m_id @attributes["mId"]
85
92
  end
86
93
 
87
94
  def attribute key
@@ -126,7 +133,7 @@ module RSMP
126
133
 
127
134
  def ensure_message_id
128
135
  # if message id is empty, generate a new one
129
- @attributes["mId"] ||= SecureRandom.uuid()
136
+ @attributes["mId"] ||= Message.make_m_id
130
137
  end
131
138
 
132
139
  def validate sxl=nil
@@ -197,6 +204,22 @@ module RSMP
197
204
  end
198
205
  end
199
206
 
207
+ class AlarmRequest < Message
208
+ def initialize attributes = {}
209
+ super({
210
+ "type" => "Alarm",
211
+ }.merge attributes)
212
+ end
213
+ end
214
+
215
+ class AlarmAcknowledged < Message
216
+ def initialize attributes = {}
217
+ super({
218
+ "type" => "Alarm",
219
+ }.merge attributes)
220
+ end
221
+ end
222
+
200
223
  class Watchdog < Message
201
224
  def initialize attributes = {}
202
225
  super({
@@ -5,19 +5,46 @@
5
5
 
6
6
  module RSMP
7
7
  class Node < Base
8
- attr_reader :archive, :logger, :task
8
+ include Wait
9
+
10
+ attr_reader :archive, :logger, :task, :deferred
9
11
 
10
12
  def initialize options
11
13
  super options
14
+ @task = options[:task]
15
+ @deferred = []
16
+ end
17
+
18
+ def defer item
19
+ @deferred << item
20
+ end
21
+
22
+ def process_deferred
23
+ cloned = @deferred.clone # clone in case do_deferred restarts the current task
24
+ @deferred.clear
25
+ cloned.each do |item|
26
+ do_deferred item
27
+ end
28
+ end
29
+
30
+ def do_deferred item
31
+ end
32
+
33
+ def do_start task
34
+ task.annotate self.class.to_s
35
+ @task = task
36
+ start_action
37
+ idle
12
38
  end
13
39
 
14
40
  def start
15
41
  starting
16
- Async do |task|
17
- task.annotate self.class
18
- @task = task
19
- start_action
20
- idle
42
+ if @task
43
+ do_start @task
44
+ else
45
+ Async do |task|
46
+ do_start task
47
+ end
21
48
  end
22
49
  rescue Errno::EADDRINUSE => e
23
50
  log "Cannot start: #{e.to_s}", level: :error
@@ -6,22 +6,6 @@ module RSMP
6
6
  class Probe
7
7
  attr_reader :condition, :items, :done
8
8
 
9
- # block should send a message and return message just sent
10
- def self.collect_response proxy, options={}, &block
11
- from = proxy.archive.current_index
12
- sent = yield
13
- raise RuntimeError unless sent && sent[:message].is_a?(RSMP::Message)
14
- item = proxy.archive.capture(options.merge(from: from+1, num: 1, with_message: true)) do |item|
15
- ["CommandResponse","StatusResponse","MessageNotAck"].include?(item[:message].type)
16
- end
17
- if item
18
- item[:message]
19
- else
20
- nil
21
- end
22
- end
23
-
24
-
25
9
  def initialize archive
26
10
  raise ArgumentError.new("Archive expected") unless archive.is_a? Archive
27
11
  @archive = archive
@@ -30,6 +14,7 @@ module RSMP
30
14
  end
31
15
 
32
16
  def capture task, options={}, &block
17
+ raise ArgumentError.new("timeout option is missing") unless options[:timeout]
33
18
  @options = options
34
19
  @block = block
35
20
  @num = options[:num]
@@ -37,8 +22,6 @@ module RSMP
37
22
  if options[:earliest]
38
23
  from = find_timestamp_index options[:earliest]
39
24
  backscan from
40
- elsif options[:from]
41
- backscan options[:from]
42
25
  end
43
26
 
44
27
  # if backscan didn't find enough items, then
@@ -62,6 +45,7 @@ module RSMP
62
45
  end
63
46
 
64
47
  def find_timestamp_index earliest
48
+ return 0 if earliest == :start
65
49
  (0..@archive.items.size).bsearch do |i| # use binary search to find item index
66
50
  @archive.items[i][:timestamp] >= earliest
67
51
  end
@@ -105,6 +89,9 @@ module RSMP
105
89
  end
106
90
  return if @options[:level] && item[:level] != @options[:level]
107
91
  return false if @options[:with_message] && !(item[:direction] && item[:message])
92
+ if @options[:component]
93
+ return false if item[:message].attributes['cId'] && item[:message].attributes['cId'] != @options[:component]
94
+ end
108
95
  if @block
109
96
  return false if @block.call(item) == false
110
97
  end
@@ -2,7 +2,9 @@
2
2
 
3
3
  module RSMP
4
4
  class Proxy < Base
5
- attr_reader :state, :archive, :connection_info, :sxl
5
+ include Wait
6
+
7
+ attr_reader :state, :archive, :connection_info, :sxl, :task
6
8
 
7
9
  def initialize options
8
10
  super options
@@ -18,7 +20,7 @@ module RSMP
18
20
  def run
19
21
  start
20
22
  @reader.wait if @reader
21
- stop
23
+ stop unless [:stopped, :stopping].include? @state
22
24
  end
23
25
 
24
26
  def ready?
@@ -115,26 +117,42 @@ module RSMP
115
117
  interval = @settings["timer_interval"] || 1
116
118
  log "Starting #{name} with interval #{interval} seconds", level: :debug
117
119
  @latest_watchdog_received = RSMP.now_object
120
+
118
121
  @timer = @task.async do |task|
119
122
  task.annotate "timer"
123
+ next_time = Time.now.to_f
120
124
  loop do
121
- now = RSMP.now_object
122
- break if timer(now) == false
123
- rescue StandardError => e
124
- log ["#{name} exception: #{e}",e.backtrace].flatten.join("\n"), level: :error
125
+ begin
126
+ now = RSMP.now_object
127
+ timer(now)
128
+ rescue EOFError => e
129
+ log "Timer: Connection closed: #{e}", level: :warning
130
+ rescue IOError => e
131
+ log "Timer: IOError", level: :warning
132
+ rescue Errno::ECONNRESET
133
+ log "Timer: Connection reset by peer", level: :warning
134
+ rescue Errno::EPIPE => e
135
+ log "Timer: Broken pipe", level: :warning
136
+ rescue StandardError => e
137
+ log "Error: #{e}", level: :debug
138
+ #rescue StandardError => e
139
+ # log ["Timer error: #{e}",e.backtrace].flatten.join("\n"), level: :error
140
+ end
125
141
  ensure
126
- task.sleep interval
142
+ next_time += interval
143
+ duration = next_time - Time.now.to_f
144
+ task.sleep duration
127
145
  end
128
146
  end
129
147
  end
130
148
 
131
149
  def timer now
132
- check_watchdog_send_time now
133
- return false if check_ack_timeout now
134
- return false if check_watchdog_timeout now
150
+ watchdog_send_timer now
151
+ check_ack_timeout now
152
+ check_watchdog_timeout now
135
153
  end
136
154
 
137
- def check_watchdog_send_time now
155
+ def watchdog_send_timer now
138
156
  return unless @watchdog_started
139
157
  return if @settings["watchdog_interval"] == :never
140
158
 
@@ -148,8 +166,6 @@ module RSMP
148
166
  send_watchdog now
149
167
  end
150
168
  end
151
- rescue StandardError => e
152
- log ["Watchdog error: #{e}",e.backtrace].flatten.join("\n"), level: :error
153
169
  end
154
170
 
155
171
  def send_watchdog now=nil
@@ -167,24 +183,18 @@ module RSMP
167
183
  if now > latest
168
184
  log "No acknowledgements for #{message.type} #{message.m_id_short} within #{timeout} seconds", level: :error
169
185
  stop
170
- return true
171
186
  end
172
187
  end
173
- false
174
188
  end
175
189
 
176
190
  def check_watchdog_timeout now
177
-
178
191
  timeout = @settings["watchdog_timeout"]
179
192
  latest = @latest_watchdog_received + timeout
180
193
  left = latest - now
181
- log "Check watchdog, time:#{timeout}, last:#{@latest_watchdog_received}, now: #{now}, latest:#{latest}, left #{left}, fail:#{left<0}", level: :debug
182
194
  if left < 0
183
- log "No Watchdog within #{timeout} seconds, received at #{@latest_watchdog_received}, now is #{now}, diff #{now-latest}", level: :error
195
+ log "No Watchdog within #{timeout} seconds", level: :error
184
196
  stop
185
- return true
186
197
  end
187
- false
188
198
  end
189
199
 
190
200
  def stop_tasks
@@ -212,7 +222,7 @@ module RSMP
212
222
 
213
223
  def buffer_message message
214
224
  # TODO
215
- log "Cannot send #{message.type} because the connection is closed.", message: message, level: :error
225
+ #log "Cannot send #{message.type} because the connection is closed.", message: message, level: :error
216
226
  end
217
227
 
218
228
  def log_send message, reason=nil
@@ -235,12 +245,13 @@ module RSMP
235
245
  message.validate sxl
236
246
  expect_version_message(message) unless @version_determined
237
247
  process_message message
248
+ process_deferred
238
249
  message
239
250
  rescue InvalidPacket => e
240
- warning "Received invalid package, must be valid JSON but got #{json.size} bytes: #{e.message}"
251
+ log "Received invalid package, must be valid JSON but got #{json.size} bytes: #{e.message}", level: :warning
241
252
  nil
242
253
  rescue MalformedMessage => e
243
- warning "Received malformed message, #{e.message}", Malformed.new(attributes)
254
+ log "Received malformed message, #{e.message}", message: Malformed.new(attributes), level: :warning
244
255
  # cannot send NotAcknowledged for a malformed message since we can't read it, just ignore it
245
256
  nil
246
257
  rescue SchemaError => e
@@ -331,7 +342,7 @@ module RSMP
331
342
  def wait_for_state state, timeout
332
343
  states = [state].flatten
333
344
  return if states.include?(@state)
334
- RSMP::Wait.wait_for(@task,@state_condition,timeout) do |s|
345
+ wait_for(@state_condition,timeout) do |s|
335
346
  states.include?(@state)
336
347
  end
337
348
  @state
@@ -441,27 +452,16 @@ module RSMP
441
452
  def version_acknowledged
442
453
  end
443
454
 
444
- def wait_for_acknowledgement original, timeout, options={}
455
+ def wait_for_acknowledgement original, timeout
445
456
  raise ArgumentError unless original
446
- RSMP::Wait.wait_for(@task,@acknowledgement_condition,timeout) do |message|
447
- message.is_a?(MessageAck) &&
448
- message.attributes["oMId"] == original.m_id
449
- end
450
- end
451
-
452
- def wait_for_not_acknowledged original, timeout
453
- raise ArgumentError unless original
454
- RSMP::Wait.wait_for(@task,@acknowledgement_condition,timeout) do |message|
455
- message.is_a?(MessageNotAck) &&
456
- message.attributes["oMId"] == original.m_id
457
- end
458
- end
459
-
460
- def wait_for_acknowledgements timeout
461
- return if @awaiting_acknowledgement.empty?
462
- RSMP::Wait.wait_for(@task,@acknowledgement_condition,timeout) do |message|
463
- @awaiting_acknowledgement.empty?
457
+ wait_for(@acknowledgement_condition,timeout) do |message|
458
+ if message.is_a?(MessageNotAck) && message.attributes["oMId"] == original.m_id
459
+ raise RSMP::MessageRejected.new(message.attributes['rea'])
460
+ end
461
+ message.is_a?(MessageAck) && message.attributes["oMId"] == original.m_id
464
462
  end
463
+ rescue Async::TimeoutError
464
+ raise RSMP::TimeoutError.new("Acknowledgement for #{original.type} #{original.m_id} not received within #{timeout}s")
465
465
  end
466
466
 
467
467
  def node
@@ -27,6 +27,7 @@ module RSMP
27
27
  { 'ip' => '127.0.0.1', 'port' => 12111 }
28
28
  ],
29
29
  'rsmp_versions' => ['3.1.1','3.1.2','3.1.3','3.1.4'],
30
+ 'sxl' => 'traffic_light_controller',
30
31
  'sxl_version' => '1.0.7',
31
32
  'timer_interval' => 0.1,
32
33
  'watchdog_interval' => 1,
@@ -69,7 +70,7 @@ module RSMP
69
70
  end
70
71
  end
71
72
 
72
- def build_connector settings
73
+ def build_proxy settings
73
74
  SupervisorProxy.new settings
74
75
  end
75
76
 
@@ -80,7 +81,7 @@ module RSMP
80
81
  end
81
82
 
82
83
  def connect_to_supervisor task, supervisor_settings
83
- proxy = build_connector({
84
+ proxy = build_proxy({
84
85
  site: self,
85
86
  task: @task,
86
87
  settings: @site_settings,
@@ -109,7 +110,9 @@ module RSMP
109
110
  if @site_settings["reconnect_interval"] != :no
110
111
  # sleep until waken by reconnect() or the reconnect interval passed
111
112
  proxy.set_state :wait_for_reconnect
112
- task.with_timeout(@site_settings["reconnect_interval"]) { @sleep_condition.wait }
113
+ task.with_timeout(@site_settings["reconnect_interval"]) do
114
+ @sleep_condition.wait
115
+ end
113
116
  else
114
117
  proxy.set_state :cannot_connect
115
118
  break
@@ -140,5 +143,6 @@ module RSMP
140
143
  proxy.stop
141
144
  end
142
145
  end
146
+
143
147
  end
144
148
  end
@@ -13,8 +13,10 @@ module RSMP
13
13
 
14
14
  def setup_components settings
15
15
  return unless settings
16
- settings.each_pair do |id,settings|
17
- @components[id] = build_component(id,settings)
16
+ settings.each_pair do |type,components_by_type|
17
+ components_by_type.each_pair do |id,settings|
18
+ @components[id] = build_component(id:id, type:type, settings:settings)
19
+ end
18
20
  end
19
21
  end
20
22
 
@@ -22,8 +24,8 @@ module RSMP
22
24
  @components[component.c_id] = component
23
25
  end
24
26
 
25
- def build_component id, settings={}
26
- Component.new id: id, node: self, grouped: true
27
+ def build_component id:, type:, settings:{}
28
+ Component.new id:id, node: self, grouped: true
27
29
  end
28
30
 
29
31
  def find_component component_id
@@ -23,6 +23,11 @@ module RSMP
23
23
  start_reader
24
24
  end
25
25
 
26
+ def stop
27
+ log "Closing connection to site", level: :info
28
+ super
29
+ end
30
+
26
31
  def connection_complete
27
32
  super
28
33
  log "Connection to site #{@site_id} established", level: :info
@@ -50,11 +55,15 @@ module RSMP
50
55
  end
51
56
  end
52
57
 
58
+ def process_deferred
59
+ supervisor.process_deferred
60
+ end
61
+
53
62
  def version_accepted message
54
63
  log "Received Version message for site #{@site_id} using RSMP #{@rsmp_version}", message: message, level: :log
55
64
  start_timer
56
65
  acknowledge message
57
- send_version @site_id, @rsmp_version
66
+ send_version @site_id, @settings['rsmp_versions']
58
67
  @version_determined = true
59
68
 
60
69
  if @settings['sites']
@@ -81,7 +90,7 @@ module RSMP
81
90
  component = @components[c_id]
82
91
  if component == nil
83
92
  if @site_settings == nil || @site_settings['components'] == nil
84
- component = build_component c_id
93
+ component = build_component(id:c_id, type:nil)
85
94
  @components[c_id] = component
86
95
  log "Adding component #{c_id} to site #{@site_id}", level: :info
87
96
  else
@@ -123,16 +132,38 @@ module RSMP
123
132
  @supervisor.site_ids_changed
124
133
  end
125
134
 
126
- def request_status component, status_list, timeout=nil
127
- raise NotReady unless @state == :ready
135
+ def fetch_status parent_task, options
136
+ wait_for_status_responses(parent_task,options) do |m_id|
137
+ request_status options.merge(m_id: m_id)
138
+ end
139
+ end
140
+
141
+ # Convert from a short ruby hash:
142
+ # {:S0001=>[:signalgroupstatus, :cyclecounter, :basecyclecounter, :stage]}
143
+ # to an rsmp-style list:
144
+ # [{"sCI"=>"S0001", "n"=>"signalgroupstatus"}, {"sCI"=>"S0001", "n"=>"cyclecounter"}, {"sCI"=>"S0001", "n"=>"basecyclecounter"}, {"sCI"=>"S0001", "n"=>"stage"}]
145
+ #
146
+ # If the input is already an array, just return it
147
+ def convert_status_list list
148
+ return list.clone if list.is_a? Array
149
+ list.map do |status_code_id,names|
150
+ names.map do |name|
151
+ { 'sCI' => status_code_id.to_s, 'n' => name.to_s }
152
+ end
153
+ end.flatten
154
+ end
155
+
156
+ def request_status options
157
+ raise NotReady unless ready?
128
158
  message = RSMP::StatusRequest.new({
129
159
  "ntsOId" => '',
130
160
  "xNId" => '',
131
- "cId" => component,
132
- "sS" => status_list
161
+ "cId" => options[:component],
162
+ "sS" => convert_status_list(options[:status_list]),
163
+ "mId" => options[:m_id]
133
164
  })
134
165
  send_message message
135
- return message, wait_for_status_response(message: message, timeout: timeout)
166
+ message
136
167
  end
137
168
 
138
169
  def process_status_response message
@@ -140,41 +171,26 @@ module RSMP
140
171
  acknowledge message
141
172
  end
142
173
 
143
- def wait_for_status_response options
144
- raise ArgumentError unless options[:message]
145
- item = @archive.capture(@task, options.merge(
146
- type: ['StatusResponse','MessageNotAck'],
147
- with_message: true,
148
- num: 1
149
- )) do |item|
150
- if item[:message].type == 'MessageNotAck'
151
- next item[:message].attribute('oMId') == options[:message].m_id
152
- elsif item[:message].type == 'StatusResponse'
153
- next item[:message].attribute('cId') == options[:message].attribute('cId')
154
- end
155
- end
156
- item[:message] if item
157
- end
158
-
159
- def subscribe_to_status component, status_list, timeout
160
- raise NotReady unless @state == :ready
174
+ def subscribe_to_status component, status_list, options={}
175
+ raise NotReady unless ready?
161
176
  message = RSMP::StatusSubscribe.new({
162
177
  "ntsOId" => '',
163
178
  "xNId" => '',
164
179
  "cId" => component,
165
- "sS" => status_list
180
+ "sS" => convert_status_list(status_list),
181
+ 'mId'=>options[:m_id]
166
182
  })
167
183
  send_message message
168
- return message, wait_for_status_update(component: component, timeout: timeout)
184
+ return message
169
185
  end
170
186
 
171
187
  def unsubscribe_to_status component, status_list
172
- raise NotReady unless @state == :ready
188
+ raise NotReady unless ready?
173
189
  message = RSMP::StatusUnsubscribe.new({
174
190
  "ntsOId" => '',
175
191
  "xNId" => '',
176
192
  "cId" => component,
177
- "sS" => status_list
193
+ "sS" => convert_status_list(status_list)
178
194
  })
179
195
  send_message message
180
196
  message
@@ -185,50 +201,79 @@ module RSMP
185
201
  acknowledge message
186
202
  end
187
203
 
188
- def wait_for_status_update options={}
189
- raise ArgumentError unless options[:component]
190
- item = @archive.capture(@task,options.merge(type: "StatusUpdate", with_message: true, num: 1)) do |item|
204
+ def status_match? query, item
205
+ return false if query[:sCI] && query[:sCI] != item['sCI']
206
+ return false if query[:n] && query[:n] != item['n']
207
+ return false if query[:q] && query[:q] != item['q']
208
+ if query[:s].is_a? Regexp
209
+ return false if query[:s] && item['s'] !~ query[:s]
210
+ else
211
+ return false if query[:s] && item['s'] != query[:s]
212
+ end
213
+ true
214
+ end
215
+
216
+ def wait_for_alarm options={}
217
+ raise ArgumentError.new("component argument is missing") unless options[:component]
218
+ matching_alarm = nil
219
+ item = @archive.capture(@task,options.merge(type: "Alarm", with_message: true, num: 1)) do |item|
191
220
  # TODO check components
192
- found = false
193
- sS = item[:message].attributes['sS']
194
- sS.each do |status|
195
- break if options[:sCI] && options[:sCI] != status['sCI']
196
- break if options[:n] && options[:n] != status['n']
197
- break if options[:q] && options[:q] != status['q']
198
- break if options[:s] && options[:s] != status['s']
199
- found = true
200
- break
201
- end
202
- found
221
+ matching_alarm = nil
222
+ alarm = item[:message]
223
+ next if options[:aCId] && options[:aCId] != alarm.attribute("aCId")
224
+ next if options[:aSp] && options[:aSp] != alarm.attribute("aSp")
225
+ next if options[:aS] && options[:aS] != alarm.attribute("aS")
226
+ matching_alarm = alarm
227
+ break
228
+ end
229
+ if item
230
+ { message: item[:message], status: matching_alarm }
203
231
  end
204
- item[:message] if item
205
232
  end
206
233
 
207
- def send_command component, args
208
- raise NotReady unless @state == :ready
209
- message = RSMP::CommandRequest.new({
234
+ def send_alarm_acknowledgement component, alarm_code
235
+ message = RSMP::AlarmAcknowledged.new({
210
236
  "ntsOId" => '',
211
237
  "xNId" => '',
212
238
  "cId" => component,
213
- "arg" => args
239
+ "aCId" => alarm_code,
240
+ "xACId" => '',
241
+ "xNACId" => '',
242
+ "aSp" => 'Acknowledge'
214
243
  })
215
244
  send_message message
216
245
  message
217
246
  end
218
247
 
219
- def process_command_response message
220
- log "Received #{message.type}", message: message, level: :log
221
- acknowledge message
222
- end
223
-
224
- def wait_for_command_response options
225
- raise ArgumentError unless options[:component]
226
- item = @archive.capture(@task,options.merge(num: 1, type: "CommandResponse", with_message: true)) do |item|
227
- # check component
248
+ def wait_for_alarm_acknowledgement_response options
249
+ raise ArgumentError.new("component argument is missing") unless options[:component]
250
+ item = @archive.capture(@task,options.merge(
251
+ num: 1,
252
+ type: ['AlarmAcknowledgedResponse','MessageNotAck'],
253
+ with_message: true
254
+ )) do |item|
255
+ if item[:message].type == 'MessageNotAck'
256
+ next item[:message].attribute('oMId') == options[:message].m_id
257
+ elsif item[:message].type == 'AlarmAcknowledgedResponse'
258
+ next item[:message].attribute('cId') == options[:message].attribute('cId')
259
+ end
228
260
  end
229
261
  item[:message] if item
230
262
  end
231
263
 
264
+ def send_command component, args, options={}
265
+ raise NotReady unless ready?
266
+ message = RSMP::CommandRequest.new({
267
+ "ntsOId" => '',
268
+ "xNId" => '',
269
+ "cId" => component,
270
+ "arg" => args,
271
+ "mId" => options[:m_id]
272
+ })
273
+ send_message message
274
+ message
275
+ end
276
+
232
277
  def set_watchdog_interval interval
233
278
  @settings["watchdog_interval"] = interval
234
279
  end
@@ -261,6 +306,5 @@ module RSMP
261
306
  site_ids_changed
262
307
  end
263
308
 
264
-
265
309
  end
266
- end
310
+ end