rsmp 0.1.11 → 0.1.21

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.
@@ -0,0 +1,90 @@
1
+ # A probe checks incoming messages and store matches
2
+ # Once it has collected what it needs, it triggers a condition variable
3
+ # and the client wakes up.
4
+
5
+ module RSMP
6
+ class Collector < Listener
7
+ attr_reader :condition, :items, :done
8
+
9
+ def initialize proxy, options={}
10
+ #raise ArgumentError.new("timeout option is missing") unless options[:timeout]
11
+ super proxy, options
12
+ @items = []
13
+ @condition = Async::Notification.new
14
+ @done = false
15
+ @options = options
16
+ @num = options[:num]
17
+ end
18
+
19
+ def wait
20
+ @condition.wait
21
+ end
22
+
23
+ def collect_for task, duration
24
+ siphon do
25
+ task.sleep duration
26
+ end
27
+ end
28
+
29
+ def collect task, options={}, &block
30
+ @num = options[:num] if options[:num]
31
+ @options[:timeout] = options[:timeout] if options[:timeout]
32
+ @block = block
33
+
34
+ siphon do
35
+ task.with_timeout(@options[:timeout]) do
36
+ @condition.wait
37
+ end
38
+ end
39
+
40
+ #if @num == 1
41
+ # @items = @items.first # if one item was requested, return item instead of array
42
+ #else
43
+ # @items = @items.first @num # return array, but ensure we never return more than requested
44
+ #end
45
+ #@items
46
+ end
47
+
48
+ def reset
49
+ @items.clear
50
+ @done = false
51
+ end
52
+
53
+ def notify item
54
+ raise ArgumentError unless item
55
+ return true if @done
56
+ return if item[:message].direction == :in && @ingoing == false
57
+ return if item[:message].direction == :out && @outgoing == false
58
+ if matches? item
59
+ @items << item
60
+ if @num && @items.size >= @num
61
+ @done = true
62
+ @proxy.remove_listener self
63
+ @condition.signal
64
+ end
65
+ end
66
+ end
67
+
68
+ def matches? item
69
+ raise ArgumentError unless item
70
+
71
+ if @options[:type]
72
+ return false if item[:message] == nil
73
+ if @options[:type].is_a? Array
74
+ return false unless @options[:type].include? item[:message].type
75
+ else
76
+ return false unless item[:message].type == @options[:type]
77
+ end
78
+ end
79
+ return if @options[:level] && item[:level] != @options[:level]
80
+ return false if @options[:with_message] && !(item[:direction] && item[:message])
81
+ if @options[:component]
82
+ return false if item[:message].attributes['cId'] && item[:message].attributes['cId'] != @options[:component]
83
+ end
84
+ if @block
85
+ return false if @block.call(item) == false
86
+ end
87
+ true
88
+ end
89
+ end
90
+ end
@@ -1,6 +1,6 @@
1
1
  module RSMP
2
2
  class Component
3
- attr_reader :c_id, :alarms, :statuses, :aggregated_status, :aggregated_status_bools, :grouped
3
+ attr_reader :c_id, :node, :alarms, :statuses, :aggregated_status, :aggregated_status_bools, :grouped
4
4
 
5
5
  AGGREGATED_STATUS_KEYS = [ :local_control,
6
6
  :communication_distruption,
@@ -23,6 +23,7 @@ module RSMP
23
23
  def clear_aggregated_status
24
24
  @aggregated_status = []
25
25
  @aggregated_status_bools = Array.new(8,false)
26
+ @aggregated_status_bools[5] = true
26
27
  end
27
28
 
28
29
  def set_aggregated_status status
@@ -58,7 +59,16 @@ module RSMP
58
59
  def alarm code:, status:
59
60
  end
60
61
 
61
- def status code:, value:
62
+ def log str, options
63
+ @node.log str, options
64
+ end
65
+
66
+ def handle_command command_code, arg
67
+ raise UnknownCommand.new "Command #{command_code} not implemented by #{self.class}"
68
+ end
69
+
70
+ def get_status status_code, status_name=nil
71
+ raise UnknownStatus.new "Status #{status_code}/#{status_name} not implemented by #{self.class}"
62
72
  end
63
73
 
64
74
  end
@@ -1,10 +1,10 @@
1
1
  # Things shared between sites and site proxies
2
2
 
3
3
  module RSMP
4
- module SiteBase
4
+ module Components
5
5
  attr_reader :components
6
6
 
7
- def initialize_site
7
+ def initialize_components
8
8
  @components = {}
9
9
  end
10
10
 
@@ -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,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
@@ -0,0 +1,33 @@
1
+ # Receives messages from a Notifier, as long as it's
2
+ # installed as a listener.
3
+ # Can listen for ingoing and/or outgoing messages.
4
+
5
+ module RSMP
6
+ class Listener
7
+
8
+ def initialize proxy, options={}
9
+ @proxy = proxy
10
+ @ingoing = options[:ingoing] == nil ? true : options[:ingoing]
11
+ @outgoing = options[:outgoing] == nil ? false : options[:outgoing]
12
+ end
13
+
14
+ def ingoing?
15
+ ingoing == true
16
+ end
17
+
18
+ def outgoing?
19
+ outgoing == true
20
+ end
21
+
22
+ def notify item
23
+ end
24
+
25
+ def siphon &block
26
+ @proxy.add_listener self
27
+ yield
28
+ ensure
29
+ @proxy.remove_listener self
30
+ end
31
+
32
+ end
33
+ end
@@ -82,7 +82,6 @@ module RSMP
82
82
  end
83
83
 
84
84
  def colorize level, str
85
- #p String.color_samples
86
85
  if @settings["color"] == false || @settings["color"] == nil
87
86
  str
88
87
  elsif @settings["color"] == true
@@ -125,8 +124,9 @@ module RSMP
125
124
  end
126
125
  end
127
126
 
128
- def dump archive, force:false
129
- log = archive.items.map do |item|
127
+ def dump archive, force:false, num:nil
128
+ num ||= archive.items.size
129
+ log = archive.items.last(num).map do |item|
130
130
  str = build_output item
131
131
  str = colorize item[:level], str
132
132
  end
@@ -3,10 +3,10 @@
3
3
  #
4
4
 
5
5
  module RSMP
6
- class Base
6
+ module Logging
7
7
  attr_reader :archive, :logger
8
8
 
9
- def initialize options
9
+ def initialize_logging options
10
10
  @archive = options[:archive] || RSMP::Archive.new
11
11
  @logger = options[:logger] || RSMP::Logger.new(options[:log_settings])
12
12
  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({
@@ -1,23 +1,48 @@
1
- # RSMP site
2
- #
3
- # Handles a single connection to a supervisor.
4
- # We connect to the supervisor.
1
+ # Base class for sites and supervisors
5
2
 
6
3
  module RSMP
7
- class Node < Base
8
- attr_reader :archive, :logger, :task
4
+ class Node
5
+ include Logging
6
+ include Wait
7
+
8
+ attr_reader :archive, :logger, :task, :deferred
9
9
 
10
10
  def initialize options
11
- super options
11
+ initialize_logging options
12
+ @task = options[:task]
13
+ @deferred = []
14
+ end
15
+
16
+ def defer item
17
+ @deferred << item
18
+ end
19
+
20
+ def process_deferred
21
+ cloned = @deferred.clone # clone in case do_deferred restarts the current task
22
+ @deferred.clear
23
+ cloned.each do |item|
24
+ do_deferred item
25
+ end
26
+ end
27
+
28
+ def do_deferred item
29
+ end
30
+
31
+ def do_start task
32
+ task.annotate self.class.to_s
33
+ @task = task
34
+ start_action
35
+ idle
12
36
  end
13
37
 
14
38
  def start
15
39
  starting
16
- Async do |task|
17
- task.annotate self.class
18
- @task = task
19
- start_action
20
- idle
40
+ if @task
41
+ do_start @task
42
+ else
43
+ Async do |task|
44
+ do_start task
45
+ end
21
46
  end
22
47
  rescue Errno::EADDRINUSE => e
23
48
  log "Cannot start: #{e.to_s}", level: :error
@@ -0,0 +1,24 @@
1
+ # Distributes messages to listeners
2
+
3
+ module RSMP
4
+ module Notifier
5
+
6
+ def initialize_distributor
7
+ @listeners = []
8
+ end
9
+
10
+ def add_listener listener
11
+ raise ArgumentError unless listener
12
+ @listeners << listener unless @listeners.include? listener
13
+ end
14
+
15
+ def remove_listener listener
16
+ raise ArgumentError unless listener
17
+ @listeners.delete listener
18
+ end
19
+
20
+ def notify item
21
+ @listeners.each { |listener| listener.notify item }
22
+ end
23
+ end
24
+ end
@@ -1,24 +1,44 @@
1
- # Base class for a connection to a remote site or supervisor.
1
+ # Logging class for a connection to a remote site or supervisor.
2
2
 
3
3
  module RSMP
4
- class Proxy < Base
5
- attr_reader :state, :archive, :connection_info, :sxl
4
+ class Proxy
5
+ include Logging
6
+ include Wait
7
+ include Notifier
8
+
9
+ attr_reader :state, :archive, :connection_info, :sxl, :task, :collector
6
10
 
7
11
  def initialize options
8
- super options
12
+ initialize_logging options
9
13
  @settings = options[:settings]
10
14
  @task = options[:task]
11
15
  @socket = options[:socket]
12
16
  @ip = options[:ip]
13
17
  @connection_info = options[:info]
14
18
  @sxl = nil
19
+ initialize_distributor
20
+
21
+ prepare_collection options[:settings]['collect']
22
+
15
23
  clear
16
24
  end
17
25
 
26
+ def prepare_collection num
27
+ if num
28
+ @collector = RSMP::Collector.new self, num: num, ingoing: true, outgoing: true
29
+ add_listener @collector
30
+ end
31
+ end
32
+
33
+ def collect task, options, &block
34
+ probe = RSMP::Collector.new self, options
35
+ probe.collect task, &block
36
+ end
37
+
18
38
  def run
19
39
  start
20
40
  @reader.wait if @reader
21
- stop
41
+ stop unless [:stopped, :stopping].include? @state
22
42
  end
23
43
 
24
44
  def ready?
@@ -115,26 +135,42 @@ module RSMP
115
135
  interval = @settings["timer_interval"] || 1
116
136
  log "Starting #{name} with interval #{interval} seconds", level: :debug
117
137
  @latest_watchdog_received = RSMP.now_object
138
+
118
139
  @timer = @task.async do |task|
119
140
  task.annotate "timer"
141
+ next_time = Time.now.to_f
120
142
  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
143
+ begin
144
+ now = RSMP.now_object
145
+ timer(now)
146
+ rescue EOFError => e
147
+ log "Timer: Connection closed: #{e}", level: :warning
148
+ rescue IOError => e
149
+ log "Timer: IOError", level: :warning
150
+ rescue Errno::ECONNRESET
151
+ log "Timer: Connection reset by peer", level: :warning
152
+ rescue Errno::EPIPE => e
153
+ log "Timer: Broken pipe", level: :warning
154
+ rescue StandardError => e
155
+ log "Error: #{e}", level: :debug
156
+ #rescue StandardError => e
157
+ # log ["Timer error: #{e}",e.backtrace].flatten.join("\n"), level: :error
158
+ end
125
159
  ensure
126
- task.sleep interval
160
+ next_time += interval
161
+ duration = next_time - Time.now.to_f
162
+ task.sleep duration
127
163
  end
128
164
  end
129
165
  end
130
166
 
131
167
  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
168
+ watchdog_send_timer now
169
+ check_ack_timeout now
170
+ check_watchdog_timeout now
135
171
  end
136
172
 
137
- def check_watchdog_send_time now
173
+ def watchdog_send_timer now
138
174
  return unless @watchdog_started
139
175
  return if @settings["watchdog_interval"] == :never
140
176
 
@@ -148,8 +184,6 @@ module RSMP
148
184
  send_watchdog now
149
185
  end
150
186
  end
151
- rescue StandardError => e
152
- log ["Watchdog error: #{e}",e.backtrace].flatten.join("\n"), level: :error
153
187
  end
154
188
 
155
189
  def send_watchdog now=nil
@@ -167,24 +201,18 @@ module RSMP
167
201
  if now > latest
168
202
  log "No acknowledgements for #{message.type} #{message.m_id_short} within #{timeout} seconds", level: :error
169
203
  stop
170
- return true
171
204
  end
172
205
  end
173
- false
174
206
  end
175
207
 
176
208
  def check_watchdog_timeout now
177
-
178
209
  timeout = @settings["watchdog_timeout"]
179
210
  latest = @latest_watchdog_received + timeout
180
211
  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
212
  if left < 0
183
213
  log "No Watchdog within #{timeout} seconds", level: :error
184
214
  stop
185
- return true
186
215
  end
187
- false
188
216
  end
189
217
 
190
218
  def stop_tasks
@@ -203,6 +231,7 @@ module RSMP
203
231
  message.direction = :out
204
232
  expect_acknowledgement message
205
233
  @protocol.write_lines message.json
234
+ notify message: message
206
235
  log_send message, reason
207
236
  rescue EOFError, IOError
208
237
  buffer_message message
@@ -212,7 +241,7 @@ module RSMP
212
241
 
213
242
  def buffer_message message
214
243
  # TODO
215
- log "Cannot send #{message.type} because the connection is closed.", message: message, level: :error
244
+ #log "Cannot send #{message.type} because the connection is closed.", message: message, level: :error
216
245
  end
217
246
 
218
247
  def log_send message, reason=nil
@@ -233,14 +262,16 @@ module RSMP
233
262
  attributes = Message.parse_attributes json
234
263
  message = Message.build attributes, json
235
264
  message.validate sxl
265
+ notify message: message
236
266
  expect_version_message(message) unless @version_determined
237
267
  process_message message
268
+ process_deferred
238
269
  message
239
270
  rescue InvalidPacket => e
240
- warning "Received invalid package, must be valid JSON but got #{json.size} bytes: #{e.message}"
271
+ log "Received invalid package, must be valid JSON but got #{json.size} bytes: #{e.message}", level: :warning
241
272
  nil
242
273
  rescue MalformedMessage => e
243
- warning "Received malformed message, #{e.message}", Malformed.new(attributes)
274
+ log "Received malformed message, #{e.message}", message: Malformed.new(attributes), level: :warning
244
275
  # cannot send NotAcknowledged for a malformed message since we can't read it, just ignore it
245
276
  nil
246
277
  rescue SchemaError => e
@@ -331,7 +362,7 @@ module RSMP
331
362
  def wait_for_state state, timeout
332
363
  states = [state].flatten
333
364
  return if states.include?(@state)
334
- RSMP::Wait.wait_for(@task,@state_condition,timeout) do |s|
365
+ wait_for(@state_condition,timeout) do
335
366
  states.include?(@state)
336
367
  end
337
368
  @state
@@ -441,27 +472,16 @@ module RSMP
441
472
  def version_acknowledged
442
473
  end
443
474
 
444
- def wait_for_acknowledgement original, timeout, options={}
445
- 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
475
+ def wait_for_acknowledgement original, timeout
453
476
  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?
477
+ wait_for(@acknowledgement_condition,timeout) do |message|
478
+ if message.is_a?(MessageNotAck) && message.attributes["oMId"] == original.m_id
479
+ raise RSMP::MessageRejected.new(message.attributes['rea'])
480
+ end
481
+ message.is_a?(MessageAck) && message.attributes["oMId"] == original.m_id
464
482
  end
483
+ rescue Async::TimeoutError
484
+ raise RSMP::TimeoutError.new("Acknowledgement for #{original.type} #{original.m_id} not received within #{timeout}s")
465
485
  end
466
486
 
467
487
  def node