rsmp 0.1.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.
data/lib/rsmp/probe.rb ADDED
@@ -0,0 +1,104 @@
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 Probe
7
+ attr_reader :condition, :items, :done
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
+ def initialize archive
26
+ raise ArgumentError.new("Archive expected") unless archive.is_a? Archive
27
+ @archive = archive
28
+ @items = []
29
+ @condition = Async::Notification.new
30
+ end
31
+
32
+ def capture task, options={}, &block
33
+ @options = options
34
+ @block = block
35
+ @num = options[:num]
36
+
37
+ if options[:earliest]
38
+ from = find_timestamp_index options[:earliest]
39
+ backscan from
40
+ elsif options[:from]
41
+ backscan options[:from]
42
+ end
43
+
44
+ # if backscan didn't find enough items, then
45
+ # insert ourself as probe and sleep until enough items are captured
46
+ if @items.size < @num
47
+ begin
48
+ @archive.probes.add self
49
+ task.with_timeout(options[:timeout]) do
50
+ @condition.wait
51
+ end
52
+ ensure
53
+ @archive.probes.remove self
54
+ end
55
+ end
56
+
57
+ if @num == 1
58
+ @items.first # if one item was requested, return item instead of array
59
+ else
60
+ @items[0..@num-1] # return array, but ensure we never return more than requested
61
+ end
62
+ end
63
+
64
+ def find_timestamp_index earliest
65
+ (0..@archive.items.size).bsearch do |i| # use binary search to find item index
66
+ @archive.items[i][:timestamp] >= earliest
67
+ end
68
+ end
69
+
70
+ def backscan from
71
+ from.upto(@archive.items.size-1) do |i|
72
+ return if process @archive.items[i]
73
+ end
74
+ end
75
+
76
+ def reset
77
+ @items.clear
78
+ @done = false
79
+ end
80
+
81
+ def process item
82
+ raise ArgumentError unless item
83
+ return true if @done
84
+ if matches? item
85
+ @items << item
86
+ if @num && @items.size >= @num
87
+ @done = true
88
+ @condition.signal
89
+ return true
90
+ end
91
+ end
92
+ false
93
+ end
94
+
95
+ def matches? item
96
+ raise ArgumentError unless item
97
+ return false if @options[:type] && (item[:message] == nil || (item[:message].type != @options[:type]))
98
+ return if @options[:level] && item[:level] != @options[:level]
99
+ return false if @options[:with_message] && !(item[:direction] && item[:message])
100
+ return false if @block && @block.call(item) == false
101
+ true
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,28 @@
1
+ # Collection of probes
2
+
3
+ module RSMP
4
+ class ProbeCollection
5
+
6
+ def initialize
7
+ @probes = []
8
+ end
9
+
10
+ def add probe
11
+ raise ArgumentError unless probe
12
+ @probes << probe
13
+ end
14
+
15
+ def remove probe
16
+ raise ArgumentError unless probe
17
+ @probes.delete probe
18
+ end
19
+
20
+ def process item
21
+ @probes.each { |probe| probe.process item }
22
+ end
23
+
24
+ def clear
25
+ @probes.each { |probe| probe.clear }
26
+ end
27
+ end
28
+ end
data/lib/rsmp/proxy.rb ADDED
@@ -0,0 +1,508 @@
1
+ # Base class for a connection to a remote site or supervisor.
2
+
3
+ module RSMP
4
+ class Proxy < Base
5
+ attr_reader :site_ids, :state, :archive, :connection_info
6
+
7
+ def initialize options
8
+ super options
9
+ @settings = options[:settings]
10
+ @task = options[:task]
11
+ @socket = options[:socket]
12
+ @ip = options[:ip]
13
+ @connection_info = options[:info]
14
+ clear
15
+ end
16
+
17
+ def site_id
18
+ @site_ids.first #rsmp connection can represent multiple site ids. pick the first
19
+ end
20
+
21
+ def run
22
+ start
23
+ @reader.wait if @reader
24
+ stop
25
+ end
26
+
27
+ def ready?
28
+ @state == :ready
29
+ end
30
+
31
+ def start
32
+ set_state :starting
33
+ end
34
+
35
+ def stop
36
+ return if @state == :stopped
37
+ set_state :stopping
38
+ stop_tasks
39
+ ensure
40
+ close_socket
41
+ clear
42
+ set_state :stopped
43
+ end
44
+
45
+ def clear
46
+ @site_ids = []
47
+ @awaiting_acknowledgement = {}
48
+ @latest_watchdog_received = nil
49
+ @watchdog_started = false
50
+ @version_determined = false
51
+ @ingoing_acknowledged = {}
52
+ @outgoing_acknowledged = {}
53
+ @latest_watchdog_send_at = nil
54
+
55
+ @state_condition = Async::Notification.new
56
+ @acknowledgements = {}
57
+ @acknowledgement_condition = Async::Notification.new
58
+ end
59
+
60
+ def close_socket
61
+ if @stream
62
+ @stream.close
63
+ @stream = nil
64
+ end
65
+
66
+ if @socket
67
+ @socket.close
68
+ @socket = nil
69
+ end
70
+ end
71
+
72
+ def start_reader
73
+ @reader = @task.async do |task|
74
+ task.annotate "reader"
75
+ @stream = Async::IO::Stream.new(@socket)
76
+ @protocol = Async::IO::Protocol::Line.new(@stream,"\f") # rsmp messages are json terminated with a form-feed
77
+
78
+ while packet = @protocol.read_line
79
+ beginning = Time.now
80
+ message = process_packet packet
81
+ duration = Time.now - beginning
82
+ ms = (duration*1000).round(4)
83
+ per_second = (1.0 / duration).round
84
+ if message
85
+ type = message.type
86
+ m_id = Logger.shorten_message_id(message.m_id)
87
+ else
88
+ type = 'Unknown'
89
+ m_id = nil
90
+ end
91
+ str = [type,m_id,"processed in #{ms}ms, #{per_second}req/s"].compact.join(' ')
92
+ log str, level: :statistics
93
+ end
94
+ rescue Async::Wrapper::Cancelled
95
+ # ignore
96
+ rescue EOFError
97
+ log "Connection closed", level: :warning
98
+ rescue IOError => e
99
+ log "IOError: #{e}", level: :warning
100
+ rescue Errno::ECONNRESET
101
+ log "Connection reset by peer", level: :warning
102
+ rescue Errno::EPIPE
103
+ log "Broken pipe", level: :warning
104
+ rescue SystemCallError => e # all ERRNO errors
105
+ log "Proxy exception: #{e.to_s}", level: :error
106
+ rescue StandardError => e
107
+ log ["Proxy exception: #{e.inspect}",e.backtrace].flatten.join("\n"), level: :error
108
+ end
109
+ end
110
+
111
+ def start_watchdog
112
+ log "Starting watchdog with interval #{@settings["watchdog_interval"]} seconds", level: :debug
113
+ send_watchdog
114
+ @watchdog_started = true
115
+ end
116
+
117
+ def start_timer
118
+ name = "timer"
119
+ interval = @settings["timer_interval"] || 1
120
+ log "Starting #{name} with interval #{interval} seconds", level: :debug
121
+ @latest_watchdog_received = RSMP.now_object
122
+ @timer = @task.async do |task|
123
+ task.annotate "timer"
124
+ loop do
125
+ now = RSMP.now_object
126
+ break if timer(now) == false
127
+ rescue StandardError => e
128
+ log ["#{name} exception: #{e}",e.backtrace].flatten.join("\n"), level: :error
129
+ ensure
130
+ task.sleep interval
131
+ end
132
+ end
133
+ end
134
+
135
+ def timer now
136
+ check_watchdog_send_time now
137
+ return false if check_ack_timeout now
138
+ return false if check_watchdog_timeout now
139
+ end
140
+
141
+ def check_watchdog_send_time now
142
+ return unless @watchdog_started
143
+ return if @settings["watchdog_interval"] == :never
144
+
145
+ if @latest_watchdog_send_at == nil
146
+ send_watchdog now
147
+ else
148
+ # we add half the timer interval to pick the timer
149
+ # event closes to the wanted wathcdog interval
150
+ diff = now - @latest_watchdog_send_at
151
+ if (diff + 0.5*@settings["timer_interval"]) >= (@settings["watchdog_interval"])
152
+ send_watchdog now
153
+ end
154
+ end
155
+ rescue StandardError => e
156
+ log ["Watchdog error: #{e}",e.backtrace].flatten.join("\n"), level: :error
157
+ end
158
+
159
+ def send_watchdog now=nil
160
+ now = RSMP.now_object unless nil
161
+ message = Watchdog.new( {"wTs" => RSMP.now_object_to_string(now)})
162
+ send_message message
163
+ @latest_watchdog_send_at = now
164
+ end
165
+
166
+ def check_ack_timeout now
167
+ timeout = @settings["acknowledgement_timeout"]
168
+ # hash cannot be modify during iteration, so clone it
169
+ @awaiting_acknowledgement.clone.each_pair do |m_id, message|
170
+ latest = message.timestamp + timeout
171
+ if now > latest
172
+ log "No acknowledgements for #{message.type} within #{timeout} seconds", level: :error
173
+ stop
174
+ return true
175
+ end
176
+ end
177
+ false
178
+ end
179
+
180
+ def check_watchdog_timeout now
181
+ timeout = @settings["watchdog_timeout"]
182
+ latest = @latest_watchdog_received + timeout
183
+ if now > latest
184
+ log "No Watchdog within #{timeout} seconds, received at #{@latest_watchdog_received}, now is #{now}, diff #{now-latest}", level: :error
185
+ stop
186
+ return true
187
+ end
188
+ false
189
+ end
190
+
191
+ def stop_tasks
192
+ @timer.stop if @timer
193
+ @reader.stop if @reader
194
+ end
195
+
196
+ def log str, options={}
197
+ super str, options.merge(ip: @ip, port: @port, site_id: site_id)
198
+ end
199
+
200
+ def send_message message, reason=nil
201
+ raise IOError unless @protocol
202
+ message.validate
203
+ message.generate_json
204
+ message.direction = :out
205
+ expect_acknowledgement message
206
+ @protocol.write_lines message.out
207
+ log_send message, reason
208
+ rescue EOFError, IOError
209
+ buffer_message message
210
+ rescue SchemaError => e
211
+ log "Error sending #{message.type}, schema validation failed: #{e.message}", message: message, level: :error
212
+ end
213
+
214
+ def buffer_message message
215
+ # TODO
216
+ log "Cannot send #{message.type} because the connection is closed.", message: message, level: :error
217
+ end
218
+
219
+ def log_send message, reason=nil
220
+ if reason
221
+ str = "Sent #{message.type} #{reason}"
222
+ else
223
+ str = "Sent #{message.type}"
224
+ end
225
+
226
+ if message.type == "MessageNotAck"
227
+ log str, message: message, level: :warning
228
+ else
229
+ log str, message: message, level: :log
230
+ end
231
+ end
232
+
233
+ def process_packet packet
234
+ attributes = Message.parse_attributes packet
235
+ message = Message.build attributes, packet
236
+ message.validate
237
+ expect_version_message(message) unless @version_determined
238
+ process_message message
239
+ message
240
+ rescue InvalidPacket => e
241
+ warning "Received invalid package, must be valid JSON but got #{packet.size} bytes: #{e.message}"
242
+ nil
243
+ rescue MalformedMessage => e
244
+ warning "Received malformed message, #{e.message}", Malformed.new(attributes)
245
+ # cannot send NotAcknowledged for a malformed message since we can't read it, just ignore it
246
+ nil
247
+ rescue SchemaError => e
248
+ dont_acknowledge message, "Received", "invalid #{message.type}, schema errors: #{e.message}"
249
+ message
250
+ rescue InvalidMessage => e
251
+ dont_acknowledge message, "Received", "invalid #{message.type}, #{e.message}"
252
+ message
253
+ rescue FatalError => e
254
+ dont_acknowledge message, "Rejected #{message.type},", "#{e.message}"
255
+ stop
256
+ message
257
+ end
258
+
259
+ def process_message message
260
+ case message
261
+ when MessageAck
262
+ process_ack message
263
+ when MessageNotAck
264
+ process_not_ack message
265
+ when Version
266
+ process_version message
267
+ when Watchdog
268
+ process_watchdog message
269
+ else
270
+ dont_acknowledge message, "Received", "unknown message (#{message.type})"
271
+ end
272
+ end
273
+
274
+ def will_not_handle message
275
+ reason = "since we're a #{self.class.name.downcase}" unless reason
276
+ log "Ignoring #{message.type}, #{reason}", message: message, level: :warning
277
+ dont_acknowledge message, nil, reason
278
+ end
279
+
280
+ def expect_acknowledgement message
281
+ unless message.is_a?(MessageAck) || message.is_a?(MessageNotAck)
282
+ @awaiting_acknowledgement[message.m_id] = message
283
+ end
284
+ end
285
+
286
+ def dont_expect_acknowledgement message
287
+ @awaiting_acknowledgement.delete message.attribute("oMId")
288
+ end
289
+
290
+ def extraneous_version message
291
+ dont_acknowledge message, "Received", "extraneous Version message"
292
+ end
293
+
294
+ def check_rsmp_version message
295
+ # find versions that both we and the client support
296
+ candidates = message.versions & @settings["rsmp_versions"]
297
+ if candidates.any?
298
+ # pick latest version
299
+ version = candidates.sort.last
300
+ return version
301
+ else
302
+ raise FatalError.new "RSMP versions [#{message.versions.join(',')}] requested, but only [#{@settings["rsmp_versions"].join(',')}] supported."
303
+ end
304
+ end
305
+
306
+ def process_version message
307
+ return extraneous_version message if @version_determined
308
+ check_site_ids message
309
+ site_ids_changed
310
+ rsmp_version = check_rsmp_version message
311
+ set_state :version_determined
312
+ check_sxl_version
313
+ version_accepted message, rsmp_version
314
+ end
315
+
316
+ def site_ids_changed
317
+ end
318
+
319
+ def check_sxl_version
320
+ end
321
+
322
+ def acknowledge original
323
+ raise InvalidArgument unless original
324
+ ack = MessageAck.build_from(original)
325
+ ack.original = original.clone
326
+ send_message ack, "for #{ack.original.type} #{original.m_id_short}"
327
+ check_ingoing_acknowledged original
328
+ end
329
+
330
+ def dont_acknowledge original, prefix=nil, reason=nil
331
+ raise InvalidArgument unless original
332
+ str = [prefix,reason].join(' ')
333
+ log str, message: original, level: :warning if reason
334
+ message = MessageNotAck.new({
335
+ "oMId" => original.m_id,
336
+ "rea" => reason || "Unknown reason"
337
+ })
338
+ message.original = original.clone
339
+ send_message message, "for #{original.type} #{original.m_id_short}"
340
+ end
341
+
342
+ def set_state state
343
+ @state = state
344
+ @state_condition.signal @state
345
+ end
346
+
347
+ def wait_for_state state, timeout
348
+ states = [state].flatten
349
+ return if states.include?(@state)
350
+ RSMP::Wait.wait_for(@task,@state_condition,timeout) do |s|
351
+ states.include?(@state)
352
+ end
353
+ @state
354
+ end
355
+
356
+ def send_version rsmp_versions
357
+ versions_hash = [rsmp_versions].flatten.map {|v| {"vers" => v} }
358
+ version_response = Version.new({
359
+ "RSMP"=>versions_hash,
360
+ "siteId"=>[{"sId"=>@settings["site_id"]}],
361
+ "SXL"=>"1.1"
362
+ })
363
+ send_message version_response
364
+ end
365
+
366
+ def find_original_for_message message
367
+ @awaiting_acknowledgement[ message.attribute("oMId") ]
368
+ end
369
+
370
+ # TODO this might be better handled by a proper event machine using e.g. the EventMachine gem
371
+ def check_outgoing_acknowledged message
372
+ unless @outgoing_acknowledged[message.type]
373
+ @outgoing_acknowledged[message.type] = true
374
+ acknowledged_first_outgoing message
375
+ end
376
+ end
377
+
378
+ def check_ingoing_acknowledged message
379
+ unless @ingoing_acknowledged[message.type]
380
+ @ingoing_acknowledged[message.type] = true
381
+ acknowledged_first_ingoing message
382
+ end
383
+ end
384
+
385
+ def acknowledged_first_outgoing message
386
+ end
387
+
388
+ def acknowledged_first_ingoing message
389
+ end
390
+
391
+ def process_ack message
392
+ original = find_original_for_message message
393
+ if original
394
+ dont_expect_acknowledgement message
395
+ message.original = original
396
+ log_acknowledgement_for_original message, original
397
+
398
+ if original.type == "Version"
399
+ version_acknowledged
400
+ end
401
+
402
+ check_outgoing_acknowledged original
403
+
404
+ @acknowledgements[ original.m_id ] = message
405
+ @acknowledgement_condition.signal message
406
+ else
407
+ log_acknowledgement_for_unknown message
408
+ end
409
+ end
410
+
411
+ def process_not_ack message
412
+ original = find_original_for_message message
413
+ if original
414
+ dont_expect_acknowledgement message
415
+ message.original = original
416
+ log_acknowledgement_for_original message, original
417
+ @acknowledgements[ original.m_id ] = message
418
+ @acknowledgement_condition.signal message
419
+ else
420
+ log_acknowledgement_for_unknown message
421
+ end
422
+ end
423
+
424
+ def log_acknowledgement_for_original message, original
425
+ str = "Received #{message.type} for #{original.type} #{message.attribute("oMId")[0..3]}"
426
+ if message.type == 'MessageNotAck'
427
+ reason = message.attributes["rea"]
428
+ str = "#{str}: #{reason}" if reason
429
+ log str, message: message, level: :warning
430
+ else
431
+ log str, message: message, level: :log
432
+ end
433
+ end
434
+
435
+ def log_acknowledgement_for_unknown message
436
+ log "Received #{message.type} for unknown message #{message.attribute("oMId")[0..3]}", message: message, level: :warning
437
+ end
438
+
439
+ def process_watchdog message
440
+ log "Received #{message.type}", message: message, level: :log
441
+ @latest_watchdog_received = RSMP.now_object
442
+ acknowledge message
443
+ end
444
+
445
+ def expect_version_message message
446
+ unless message.is_a?(Version) || message.is_a?(MessageAck) || message.is_a?(MessageNotAck)
447
+ raise FatalError.new "Version must be received first"
448
+ end
449
+ end
450
+
451
+ def connection_complete
452
+ set_state :ready
453
+ end
454
+
455
+ def check_site_ids message
456
+ message.attribute("siteId").map { |item| item["sId"] }.each do |site_id|
457
+ check_site_id site_id
458
+ @site_ids << site_id
459
+ end
460
+ end
461
+
462
+ def check_site_id site_id
463
+ end
464
+
465
+ def site_id_accetable? site_id
466
+ true
467
+ end
468
+
469
+ def add_site_id site_id
470
+ @site_ids << site_id
471
+ end
472
+
473
+ def version_acknowledged
474
+ end
475
+
476
+ def wait_for_acknowledgement original, timeout, options={}
477
+ raise ArgumentError unless original
478
+ RSMP::Wait.wait_for(@task,@acknowledgement_condition,timeout) do |message|
479
+ message.is_a?(MessageAck) &&
480
+ message.attributes["oMId"] == original.m_id
481
+ end
482
+ end
483
+
484
+ def wait_for_not_acknowledged original, timeout
485
+ raise ArgumentError unless original
486
+ RSMP::Wait.wait_for(@task,@acknowledgement_condition,timeout) do |message|
487
+ message.is_a?(MessageNotAck) &&
488
+ message.attributes["oMId"] == original.m_id
489
+ end
490
+ end
491
+
492
+ def wait_for_acknowledgements timeout
493
+ return if @awaiting_acknowledgement.empty?
494
+ RSMP::Wait.wait_for(@task,@acknowledgement_condition,timeout) do |message|
495
+ @awaiting_acknowledgement.empty?
496
+ end
497
+ end
498
+
499
+ def node
500
+ raise 'Must be overridden'
501
+ end
502
+
503
+ def author
504
+ node.site_id
505
+ end
506
+
507
+ end
508
+ end
data/lib/rsmp/rsmp.rb ADDED
@@ -0,0 +1,31 @@
1
+ # RSMP module
2
+
3
+ module RSMP
4
+ WRAPPING_DELIMITER = "\f"
5
+
6
+ def self.now_object
7
+ # date using UTC time zone
8
+ Time.now.utc
9
+ end
10
+
11
+ def self.now_object_to_string now
12
+ # date in the format required by rsmp, using UTC time zone
13
+ # example: 2015-06-08T12:01:39.654Z
14
+ time ||= now.utc
15
+ time.strftime("%FT%T.%3NZ")
16
+ end
17
+
18
+ def self.now_string time=nil
19
+ time ||= Time.now
20
+ now_object_to_string time
21
+ end
22
+
23
+ def self.parse_time time_str
24
+ Time.parse time_str
25
+ end
26
+
27
+ def self.log_prefix ip
28
+ "#{now_string} #{ip.ljust(20)}"
29
+ end
30
+
31
+ end