rsmp 0.1.0

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