rsmp 0.1.11 → 0.1.21

Sign up to get free protection for your applications and to get access to all the features.
@@ -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