rsmp 0.29.0 → 0.32.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.
@@ -1,40 +1,53 @@
1
1
  module RSMP
2
2
 
3
- # Collects messages from a notifier.
3
+ # Collects messages from a distributor.
4
4
  # Can filter by message type, componet and direction.
5
5
  # Wakes up the once the desired number of messages has been collected.
6
- class Collector < Listener
7
- attr_reader :condition, :messages, :status, :error, :task
6
+ class Collector
7
+ include Receiver
8
+ attr_reader :condition, :messages, :status, :error, :task, :m_id
8
9
 
9
- def initialize notifier, options={}
10
- super notifier, options
10
+ def initialize distributor, options={}
11
+ initialize_receiver distributor, filter: options[:filter]
11
12
  @options = {
12
13
  cancel: {
13
14
  schema_error: true,
14
15
  disconnect: false,
15
16
  }
16
17
  }.deep_merge options
17
- @ingoing = options[:ingoing] == nil ? true : options[:ingoing]
18
- @outgoing = options[:outgoing] == nil ? false : options[:outgoing]
18
+ @timeout = options[:timeout]
19
+ @num = options[:num]
20
+ @m_id = options[:m_id]
19
21
  @condition = Async::Notification.new
20
- @title = options[:title] || [@options[:type]].flatten.join('/')
21
- if options[:task]
22
- @task = options[:task]
22
+ make_title options[:title]
23
+
24
+ if task
25
+ @task = task
23
26
  else
24
- # if notifier is a Proxy, or some other object that implements task(),
27
+ # if distributor is a Proxy, or some other object that implements task(),
25
28
  # then try to get the task that way
26
- if notifier.respond_to? 'task'
27
- @task = notifier.task
29
+ if distributor.respond_to? 'task'
30
+ @task = distributor.task
28
31
  end
29
32
  end
30
33
  reset
31
34
  end
32
35
 
36
+ def make_title title
37
+ if title
38
+ @title = title
39
+ elsif @filter
40
+ @title = [@filter.type].flatten.join('/')
41
+ else
42
+ @title = ""
43
+ end
44
+ end
45
+
33
46
  def use_task task
34
47
  @task = task
35
48
  end
36
49
 
37
- # Clear all query results
50
+ # Clear all matcher results
38
51
  def reset
39
52
  @messages = []
40
53
  @error = nil
@@ -95,7 +108,7 @@ module RSMP
95
108
  wait
96
109
  @status
97
110
  ensure
98
- @notifier.remove_listener self if @notifier
111
+ @distributor.remove_receiver self if @distributor
99
112
  end
100
113
 
101
114
  # Collect message
@@ -110,8 +123,8 @@ module RSMP
110
123
  # the desired messages have been collected, or timeout is reached.
111
124
  def wait
112
125
  if collecting?
113
- if @options[:timeout]
114
- @task.with_timeout(@options[:timeout]) { @condition.wait }
126
+ if @timeout
127
+ @task.with_timeout(@timeout) { @condition.wait }
115
128
  else
116
129
  @condition.wait
117
130
  end
@@ -136,39 +149,41 @@ module RSMP
136
149
  def start &block
137
150
  raise RuntimeError.new("Can't start collectimng unless ready (currently #{@status})") unless ready?
138
151
  @block = block
139
- raise ArgumentError.new("Num, timeout or block must be provided") unless @options[:num] || @options[:timeout] || @block
152
+ raise ArgumentError.new("Num, timeout or block must be provided") unless @num || @timeout || @block
140
153
  reset
141
154
  @status = :collecting
142
155
  log_start
143
- @notifier.add_listener self if @notifier
156
+ @distributor.add_receiver self if @distributor
144
157
  end
145
158
 
146
159
  # Build a string describing how how progress reached before timeout
147
160
  def describe_progress
148
161
  str = "#{identifier}: #{@title.capitalize} collection "
149
- str << "in response to #{@options[:m_id]} " if @options[:m_id]
150
- str << "didn't complete within #{@options[:timeout]}s, "
151
- str << "reached #{@messages.size}/#{@options[:num]}"
162
+ str << "in response to #{@m_id} " if @m_id
163
+ str << "didn't complete within #{@timeout}s, "
164
+ str << "reached #{@messages.size}/#{@num}"
152
165
  str
153
166
  end
154
167
 
155
168
  # Check if we receive a NotAck related to initiating request, identified by @m_id.
156
169
  def reject_not_ack message
157
- return unless @options[:m_id]
170
+ return unless @m_id
158
171
  if message.is_a?(MessageNotAck)
159
- if message.attribute('oMId') == @options[:m_id]
160
- m_id_short = RSMP::Message.shorten_m_id @options[:m_id], 8
172
+ if message.attribute('oMId') == @m_id
173
+ m_id_short = RSMP::Message.shorten_m_id @m_id, 8
161
174
  cancel RSMP::MessageRejected.new("#{@title} #{m_id_short} was rejected with '#{message.attribute('rea')}'")
162
- @notifier.log "#{identifier}: cancelled due to a NotAck", level: :debug
175
+ @distributor.log "#{identifier}: cancelled due to a NotAck", level: :debug
163
176
  true
164
177
  end
165
178
  end
166
179
  end
167
180
 
168
181
  # Handle message. and return true when we're done collecting
169
- def notify message
182
+ def receive message
170
183
  raise ArgumentError unless message
171
- raise RuntimeError.new("can't process message when status is :#{@status}, title: #{@title}, desc: #{describe}") unless ready? || collecting?
184
+ unless ready? || collecting?
185
+ raise RuntimeError.new("can't process message when status is :#{@status}, title: #{@title}, desc: #{describe}")
186
+ end
172
187
  if perform_match message
173
188
  if done?
174
189
  complete
@@ -185,8 +200,8 @@ module RSMP
185
200
  # Match message against our collection criteria
186
201
  def perform_match message
187
202
  return false if reject_not_ack(message)
188
- return false unless type_match?(message)
189
- #@notifier.log "#{identifier}: Looking at #{message.type} #{message.m_id_short}", level: :collect
203
+ return false unless acceptable?(message)
204
+ #@distributor.log "#{identifier}: Looking at #{message.type} #{message.m_id_short}", level: :collect
190
205
  if @block
191
206
  status = [@block.call(message)].flatten
192
207
  return unless collecting?
@@ -198,10 +213,10 @@ module RSMP
198
213
 
199
214
  # Have we collected the required number of messages?
200
215
  def done?
201
- @options[:num] && @messages.size >= @options[:num]
216
+ @num && @messages.size >= @num
202
217
  end
203
218
 
204
- # Called when we're done collecting. Remove ourself as a listener,
219
+ # Called when we're done collecting. Remove ourself as a receiver,
205
220
  # se we don't receive message notifications anymore
206
221
  def complete
207
222
  @status = :ok
@@ -214,39 +229,39 @@ module RSMP
214
229
  log_incomplete
215
230
  end
216
231
 
217
- # Remove ourself as a listener, so we don't receive message notifications anymore,
232
+ # Remove ourself as a receiver, so we don't receive message notifications anymore,
218
233
  # and wake up the async condition
219
234
  def do_stop
220
- @notifier.remove_listener self
235
+ @distributor.remove_receiver self
221
236
  @condition.signal
222
237
  end
223
238
 
224
239
  # An error occured upstream.
225
240
  # Check if we should cancel.
226
- def notify_error error, options={}
241
+ def receive_error error, options={}
227
242
  case error
228
243
  when RSMP::SchemaError
229
- notify_schema_error error, options
244
+ receive_schema_error error, options
230
245
  when RSMP::DisconnectError
231
- notify_disconnect error, options
246
+ receive_disconnect error, options
232
247
  end
233
248
  end
234
249
 
235
250
  # Cancel if we received e schema error for a message type we're collecting
236
- def notify_schema_error error, options
251
+ def receive_schema_error error, options
237
252
  return unless @options.dig(:cancel,:schema_error)
238
253
  message = options[:message]
239
254
  return unless message
240
255
  klass = message.class.name.split('::').last
241
- return unless @options[:type] == nil || [@options[:type]].flatten.include?(klass)
242
- @notifier.log "#{identifier}: cancelled due to schema error in #{klass} #{message.m_id_short}", level: :debug
256
+ return unless @filter&.type == nil || [@filter&.type].flatten.include?(klass)
257
+ @distributor.log "#{identifier}: cancelled due to schema error in #{klass} #{message.m_id_short}", level: :debug
243
258
  cancel error
244
259
  end
245
260
 
246
261
  # Cancel if we received e notificaiton about a disconnect
247
- def notify_disconnect error, options
262
+ def receive_disconnect error, options
248
263
  return unless @options.dig(:cancel,:disconnect)
249
- @notifier.log "#{identifier}: cancelled due to a connection error: #{error.to_s}", level: :debug
264
+ @distributor.log "#{identifier}: cancelled due to a connection error: #{error.to_s}", level: :debug
250
265
  cancel error
251
266
  end
252
267
 
@@ -264,39 +279,27 @@ module RSMP
264
279
 
265
280
  # Check a message against our match criteria
266
281
  # Return true if there's a match, false if not
267
- def type_match? message
268
- return false if message.direction == :in && @ingoing == false
269
- return false if message.direction == :out && @outgoing == false
270
- if @options[:type]
271
- if @options[:type].is_a? Array
272
- return false unless @options[:type].include? message.type
273
- else
274
- return false unless message.type == @options[:type]
275
- end
276
- end
277
- if @options[:component]
278
- return false if message.attributes['cId'] && message.attributes['cId'] != @options[:component]
279
- end
280
- true
282
+ def acceptable? message
283
+ @filter == nil || @filter.accept?(message)
281
284
  end
282
285
 
283
286
  # return a string describing the types of messages we're collecting
284
287
  def describe_types
285
- [@options[:type]].flatten.join('/')
288
+ [@filter&.type].flatten.join('/')
286
289
  end
287
290
 
288
291
  # return a string that describes whe number of messages, and type of message we're collecting
289
292
  def describe_num_and_type
290
- if @options[:num] && @options[:num] > 1
291
- "#{@options[:num]} #{describe_types}s"
293
+ if @num && @num > 1
294
+ "#{@num} #{describe_types}s"
292
295
  else
293
296
  describe_types
294
297
  end
295
298
  end
296
299
 
297
300
  # return a string that describes the attributes that we're looking for
298
- def describe_query
299
- h = {component: @options[:component]}.compact
301
+ def describe_matcher
302
+ h = {component: @filter&.component}.compact
300
303
  if h.empty?
301
304
  describe_num_and_type
302
305
  else
@@ -306,8 +309,8 @@ module RSMP
306
309
 
307
310
  # return a string that describe how many many messages have been collected
308
311
  def describe_progress
309
- if @options[:num]
310
- "#{@messages.size} of #{@options[:num]} message#{'s' if @messages.size!=1} collected"
312
+ if @num
313
+ "#{@messages.size} of #{@num} message#{'s' if @messages.size!=1} collected"
311
314
  else
312
315
  "#{@messages.size} message#{'s' if @messages.size!=1} collected"
313
316
  end
@@ -315,17 +318,17 @@ module RSMP
315
318
 
316
319
  # log when we start collecting
317
320
  def log_start
318
- @notifier.log "#{identifier}: Waiting for #{describe_query}".strip, level: :collect
321
+ @distributor.log "#{identifier}: Waiting for #{describe_matcher}".strip, level: :collect
319
322
  end
320
323
 
321
324
  # log current progress
322
325
  def log_incomplete
323
- @notifier.log "#{identifier}: #{describe_progress}", level: :collect
326
+ @distributor.log "#{identifier}: #{describe_progress}", level: :collect
324
327
  end
325
328
 
326
329
  # log when we end collecting
327
330
  def log_complete
328
- @notifier.log "#{identifier}: Done", level: :collect
331
+ @distributor.log "#{identifier}: Done", level: :collect
329
332
  end
330
333
 
331
334
  # get a short id in hex format, identifying ourself
@@ -1,7 +1,7 @@
1
1
  module RSMP
2
2
  # Match a specific command responses
3
- class CommandQuery < Query
4
- # Match a return value item against a query
3
+ class CommandMatcher < Matcher
4
+ # Match a return value item against a matcher
5
5
  def match? item
6
6
  return nil if @want['cCI'] && @want['cCI'] != item['cCI']
7
7
  return nil if @want['n'] && @want['n'] != item['n']
@@ -3,13 +3,13 @@ module RSMP
3
3
  class CommandResponseCollector < StateCollector
4
4
  def initialize proxy, want, options={}
5
5
  super proxy, want, options.merge(
6
- type: 'CommandResponse',
6
+ filter: RSMP::Filter.new(ingoing: true, outgoing: false, type: 'CommandResponse'),
7
7
  title:'command response'
8
8
  )
9
9
  end
10
10
 
11
- def build_query want
12
- CommandQuery.new want
11
+ def build_matcher want
12
+ CommandMatcher.new want
13
13
  end
14
14
 
15
15
  # Get items, in our case the return values
@@ -0,0 +1,65 @@
1
+ # Distributes messages to receivers
2
+
3
+ module RSMP
4
+ module Distributor
5
+ attr_reader :receivers
6
+
7
+ include Inspect
8
+
9
+ def inspect
10
+ "#<#{self.class.name}:#{self.object_id}, #{inspector(:@receivers)}>"
11
+ end
12
+
13
+ def initialize_distributor
14
+ @receivers = []
15
+ @defer_distribution = false
16
+ @deferred_messages = []
17
+ end
18
+
19
+ def clear_deferred_distribution &block
20
+ @deferred_messages = []
21
+ end
22
+
23
+ def with_deferred_distribution &block
24
+ was, @defer_distribution = @defer_distribution, true
25
+ yield
26
+ distribute_queued
27
+ ensure
28
+ @defer_distribution = was
29
+ @deferred_messages = []
30
+ end
31
+
32
+ def distribute_queued
33
+ @deferred_messages.each { |message| distribute_immediately message }
34
+ ensure
35
+ @deferred_messages = []
36
+ end
37
+
38
+ def add_receiver receiver
39
+ raise ArgumentError unless receiver
40
+ @receivers << receiver unless @receivers.include? receiver
41
+ end
42
+
43
+ def remove_receiver receiver
44
+ raise ArgumentError unless receiver
45
+ @receivers.delete receiver
46
+ end
47
+
48
+ def distribute message
49
+ raise ArgumentError unless message
50
+ if @defer_distribution
51
+ @deferred_messages << message
52
+ else
53
+ distribute_immediately message
54
+ end
55
+ end
56
+
57
+ def distribute_immediately message
58
+ @receivers.each { |receiver| receiver.receive message }
59
+ end
60
+
61
+ def distribute_error error, options={}
62
+ @receivers.each { |receiver| receiver.receive_error error, options }
63
+ end
64
+ end
65
+ end
@@ -3,10 +3,13 @@ module RSMP
3
3
  # Filter messages based on type, direction and component id.
4
4
  # Used by Collectors.
5
5
  class Filter
6
- def initialize ingoing:true, outgoing:true, type:, component:nil
6
+
7
+ attr_reader :ingoing, :outgoing, :type, :component
8
+
9
+ def initialize ingoing:true, outgoing:true, type:nil, component:nil
7
10
  @ingoing = ingoing
8
11
  @outgoing = outgoing
9
- @type = type
12
+ @type = type ? [type].flatten : nil
10
13
  @component = component
11
14
  end
12
15
 
@@ -16,10 +19,8 @@ module RSMP
16
19
  return false if message.direction == :in && @ingoing == false
17
20
  return false if message.direction == :out && @outgoing == false
18
21
  if @type
19
- if @type.is_a? Array
22
+ unless message.is_a?(MessageNotAck)
20
23
  return false unless @type.include? message.type
21
- else
22
- return false unless message.type == @type
23
24
  end
24
25
  end
25
26
  if @component
@@ -27,5 +28,9 @@ module RSMP
27
28
  end
28
29
  true
29
30
  end
31
+
32
+ def reject? message
33
+ !accept?(message)
34
+ end
30
35
  end
31
36
  end
@@ -1,7 +1,7 @@
1
1
  module RSMP
2
2
 
3
3
  # Class that matches a single status or command item
4
- class Query
4
+ class Matcher
5
5
  attr_reader :want, :got, :message
6
6
 
7
7
  def initialize want
@@ -0,0 +1,39 @@
1
+ # Receives items from a Distributor and keeps them in a queue.
2
+ # The client can wait for mesages and will get them one by one.
3
+
4
+ module RSMP
5
+ class Queue
6
+ include Receiver
7
+
8
+ attr_reader :messages
9
+
10
+ def initialize distributor, filter: nil, task:
11
+ initialize_receiver distributor, filter: filter
12
+ @condition = Async::Notification.new
13
+ @task = task
14
+ clear
15
+ end
16
+
17
+ def clear
18
+ @messages = []
19
+ end
20
+
21
+ def wait_for_message timeout: nil
22
+ if @messages.empty?
23
+ if timeout
24
+ @task.with_timeout(timeout) { @condition.wait }
25
+ else
26
+ @condition.wait
27
+ end
28
+ end
29
+ @messages.shift
30
+ rescue Async::TimeoutError
31
+ raise RSMP::TimeoutError
32
+ end
33
+
34
+ def handle_message message
35
+ @messages << message
36
+ @condition.signal
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ # Receives items from a Distributor, as long as it's
2
+ # installed as a receiver.
3
+ # Optionally can filter mesage using a Filter.
4
+
5
+ module RSMP
6
+ module Receiver
7
+ include Inspect
8
+
9
+ def initialize_receiver distributor, filter: nil
10
+ @distributor = distributor
11
+ @filter = filter
12
+ end
13
+
14
+ def start_receiving
15
+ @distributor.add_receiver(self)
16
+ end
17
+
18
+ def stop_receiving
19
+ @distributor.remove_receiver(self)
20
+ end
21
+
22
+ def receive message
23
+ handle_message(message) if accept_message?(message)
24
+ end
25
+
26
+ def receive_error error, options={}
27
+ end
28
+
29
+ def accept_message? message
30
+ @filter == nil || @filter.accept?(message)
31
+ end
32
+
33
+ def reject_message? message
34
+ !accept_message?(message)
35
+ end
36
+
37
+ def handle_message message
38
+ end
39
+ end
40
+ end