rsmp 0.37.0 → 0.38.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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +22 -0
  3. data/.github/workflows/rubocop.yaml +17 -0
  4. data/.gitignore +5 -6
  5. data/.rubocop.yml +80 -0
  6. data/Gemfile +13 -1
  7. data/Gemfile.lock +34 -1
  8. data/Rakefile +3 -3
  9. data/lib/rsmp/cli.rb +147 -124
  10. data/lib/rsmp/collect/ack_collector.rb +8 -7
  11. data/lib/rsmp/collect/aggregated_status_collector.rb +4 -4
  12. data/lib/rsmp/collect/alarm_collector.rb +31 -23
  13. data/lib/rsmp/collect/alarm_matcher.rb +3 -3
  14. data/lib/rsmp/collect/collector/logging.rb +17 -0
  15. data/lib/rsmp/collect/collector/reporting.rb +44 -0
  16. data/lib/rsmp/collect/collector/status.rb +34 -0
  17. data/lib/rsmp/collect/collector.rb +69 -150
  18. data/lib/rsmp/collect/command_matcher.rb +19 -6
  19. data/lib/rsmp/collect/command_response_collector.rb +7 -7
  20. data/lib/rsmp/collect/distributor.rb +14 -11
  21. data/lib/rsmp/collect/filter.rb +31 -15
  22. data/lib/rsmp/collect/matcher.rb +7 -11
  23. data/lib/rsmp/collect/queue.rb +4 -4
  24. data/lib/rsmp/collect/receiver.rb +10 -12
  25. data/lib/rsmp/collect/state_collector.rb +116 -77
  26. data/lib/rsmp/collect/status_collector.rb +6 -6
  27. data/lib/rsmp/collect/status_matcher.rb +17 -7
  28. data/lib/rsmp/{alarm_state.rb → component/alarm_state.rb} +76 -37
  29. data/lib/rsmp/{component.rb → component/component.rb} +15 -15
  30. data/lib/rsmp/component/component_base.rb +89 -0
  31. data/lib/rsmp/component/component_proxy.rb +75 -0
  32. data/lib/rsmp/component/components.rb +63 -0
  33. data/lib/rsmp/convert/export/json_schema.rb +116 -110
  34. data/lib/rsmp/convert/import/yaml.rb +21 -18
  35. data/lib/rsmp/{rsmp.rb → helpers/clock.rb} +5 -6
  36. data/lib/rsmp/{deep_merge.rb → helpers/deep_merge.rb} +2 -1
  37. data/lib/rsmp/helpers/error.rb +71 -0
  38. data/lib/rsmp/{inspect.rb → helpers/inspect.rb} +6 -10
  39. data/lib/rsmp/log/archive.rb +98 -0
  40. data/lib/rsmp/log/colorization.rb +41 -0
  41. data/lib/rsmp/log/filtering.rb +54 -0
  42. data/lib/rsmp/log/logger.rb +206 -0
  43. data/lib/rsmp/{logging.rb → log/logging.rb} +5 -7
  44. data/lib/rsmp/message.rb +159 -148
  45. data/lib/rsmp/{node.rb → node/node.rb} +19 -17
  46. data/lib/rsmp/{protocol.rb → node/protocol.rb} +5 -3
  47. data/lib/rsmp/node/site/site.rb +195 -0
  48. data/lib/rsmp/node/supervisor/modules/configuration.rb +59 -0
  49. data/lib/rsmp/node/supervisor/modules/connection.rb +140 -0
  50. data/lib/rsmp/node/supervisor/modules/sites.rb +64 -0
  51. data/lib/rsmp/node/supervisor/supervisor.rb +72 -0
  52. data/lib/rsmp/{task.rb → node/task.rb} +12 -14
  53. data/lib/rsmp/proxy/modules/acknowledgements.rb +144 -0
  54. data/lib/rsmp/proxy/modules/receive.rb +119 -0
  55. data/lib/rsmp/proxy/modules/send.rb +76 -0
  56. data/lib/rsmp/proxy/modules/state.rb +25 -0
  57. data/lib/rsmp/proxy/modules/tasks.rb +105 -0
  58. data/lib/rsmp/proxy/modules/versions.rb +69 -0
  59. data/lib/rsmp/proxy/modules/watchdogs.rb +66 -0
  60. data/lib/rsmp/proxy/proxy.rb +199 -0
  61. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +52 -0
  62. data/lib/rsmp/proxy/site/modules/alarms.rb +27 -0
  63. data/lib/rsmp/proxy/site/modules/commands.rb +31 -0
  64. data/lib/rsmp/proxy/site/modules/status.rb +110 -0
  65. data/lib/rsmp/proxy/site/site_proxy.rb +205 -0
  66. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +47 -0
  67. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +73 -0
  68. data/lib/rsmp/proxy/supervisor/modules/commands.rb +53 -0
  69. data/lib/rsmp/proxy/supervisor/modules/status.rb +204 -0
  70. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +178 -0
  71. data/lib/rsmp/tlc/detector_logic.rb +18 -34
  72. data/lib/rsmp/tlc/input_states.rb +126 -0
  73. data/lib/rsmp/tlc/modules/detector_logics.rb +50 -0
  74. data/lib/rsmp/tlc/modules/display.rb +78 -0
  75. data/lib/rsmp/tlc/modules/helpers.rb +41 -0
  76. data/lib/rsmp/tlc/modules/inputs.rb +173 -0
  77. data/lib/rsmp/tlc/modules/modes.rb +253 -0
  78. data/lib/rsmp/tlc/modules/outputs.rb +30 -0
  79. data/lib/rsmp/tlc/modules/plans.rb +218 -0
  80. data/lib/rsmp/tlc/modules/signal_groups.rb +109 -0
  81. data/lib/rsmp/tlc/modules/startup_sequence.rb +22 -0
  82. data/lib/rsmp/tlc/modules/system.rb +140 -0
  83. data/lib/rsmp/tlc/modules/traffic_data.rb +49 -0
  84. data/lib/rsmp/tlc/signal_group.rb +37 -41
  85. data/lib/rsmp/tlc/signal_plan.rb +14 -11
  86. data/lib/rsmp/tlc/signal_priority.rb +39 -35
  87. data/lib/rsmp/tlc/startup_sequence.rb +59 -0
  88. data/lib/rsmp/tlc/traffic_controller.rb +38 -1010
  89. data/lib/rsmp/tlc/traffic_controller_site.rb +58 -57
  90. data/lib/rsmp/version.rb +1 -1
  91. data/lib/rsmp.rb +82 -48
  92. data/rsmp.gemspec +24 -31
  93. metadata +79 -139
  94. data/lib/rsmp/archive.rb +0 -76
  95. data/lib/rsmp/collect/message_matchers.rb +0 -0
  96. data/lib/rsmp/component_base.rb +0 -87
  97. data/lib/rsmp/component_proxy.rb +0 -57
  98. data/lib/rsmp/components.rb +0 -65
  99. data/lib/rsmp/error.rb +0 -71
  100. data/lib/rsmp/logger.rb +0 -216
  101. data/lib/rsmp/proxy.rb +0 -693
  102. data/lib/rsmp/site.rb +0 -188
  103. data/lib/rsmp/site_proxy.rb +0 -389
  104. data/lib/rsmp/supervisor.rb +0 -302
  105. data/lib/rsmp/supervisor_proxy.rb +0 -510
  106. data/lib/rsmp/tlc/inputs.rb +0 -134
@@ -0,0 +1,144 @@
1
+ module RSMP
2
+ class Proxy
3
+ module Modules
4
+ # Message acknowledgement handling
5
+ # Manages sending/receiving acks and nacks, and tracking acknowledged messages
6
+ module Acknowledgements
7
+ def acknowledge(original)
8
+ raise InvalidArgument unless original
9
+
10
+ ack = MessageAck.build_from(original)
11
+ ack.original = original.clone
12
+ send_message ack, "for #{ack.original.type} #{original.m_id_short}"
13
+ check_ingoing_acknowledged original
14
+ end
15
+
16
+ def dont_acknowledge(original, prefix = nil, reason = nil, force: true)
17
+ raise InvalidArgument unless original
18
+
19
+ str = [prefix, reason].join(' ')
20
+ log str, message: original, level: :warning if reason
21
+ message = MessageNotAck.new({
22
+ 'oMId' => original.m_id,
23
+ 'rea' => reason || 'Unknown reason'
24
+ })
25
+ message.original = original.clone
26
+ send_message message, "for #{original.type} #{original.m_id_short}", force: force
27
+ end
28
+
29
+ def expect_acknowledgement(message)
30
+ return if message.is_a?(MessageAck) || message.is_a?(MessageNotAck)
31
+
32
+ @awaiting_acknowledgement[message.m_id] = message
33
+ end
34
+
35
+ def dont_expect_acknowledgement(message)
36
+ @awaiting_acknowledgement.delete message.attribute('oMId')
37
+ end
38
+
39
+ def check_ack_timeout(now)
40
+ timeout = @site_settings['timeouts']['acknowledgement']
41
+ # hash cannot be modify during iteration, so clone it
42
+ @awaiting_acknowledgement.clone.each_pair do |_m_id, message|
43
+ latest = message.timestamp + timeout
44
+ next unless now > latest
45
+
46
+ str = "No acknowledgements for #{message.type} #{message.m_id_short} within #{timeout} seconds"
47
+ log str, level: :error
48
+ begin
49
+ close
50
+ ensure
51
+ distribute_error MissingAcknowledgment.new(str)
52
+ end
53
+ end
54
+ end
55
+
56
+ def find_original_for_message(message)
57
+ @awaiting_acknowledgement[message.attribute('oMId')]
58
+ end
59
+
60
+ # TODO: this might be better handled by a proper event machine using e.g. the EventMachine gem
61
+ def check_outgoing_acknowledged(message)
62
+ return if @outgoing_acknowledged[message.type]
63
+
64
+ @outgoing_acknowledged[message.type] = true
65
+ acknowledged_first_outgoing message
66
+ end
67
+
68
+ def check_ingoing_acknowledged(message)
69
+ return if @ingoing_acknowledged[message.type]
70
+
71
+ @ingoing_acknowledged[message.type] = true
72
+ acknowledged_first_ingoing message
73
+ end
74
+
75
+ def acknowledged_first_outgoing(message); end
76
+
77
+ def acknowledged_first_ingoing(message); end
78
+
79
+ def process_ack(message)
80
+ original = find_original_for_message message
81
+ if original
82
+ dont_expect_acknowledgement message
83
+ message.original = original
84
+ log_acknowledgement_for_original message, original
85
+
86
+ case original.type
87
+ when 'Version'
88
+ version_acknowledged
89
+ when 'StatusSubscribe'
90
+ status_subscribe_acknowledged original
91
+ end
92
+
93
+ check_outgoing_acknowledged original
94
+
95
+ @acknowledgements[original.m_id] = message
96
+ @acknowledgement_condition.signal message
97
+ else
98
+ log_acknowledgement_for_unknown message
99
+ end
100
+ end
101
+
102
+ def process_not_ack(message)
103
+ original = find_original_for_message message
104
+ if original
105
+ dont_expect_acknowledgement message
106
+ message.original = original
107
+ log_acknowledgement_for_original message, original
108
+ @acknowledgements[original.m_id] = message
109
+ @acknowledgement_condition.signal message
110
+ else
111
+ log_acknowledgement_for_unknown message
112
+ end
113
+ end
114
+
115
+ def log_acknowledgement_for_original(message, original)
116
+ str = "Received #{message.type} for #{original.type} #{message.attribute('oMId')[0..3]}"
117
+ if message.type == 'MessageNotAck'
118
+ reason = message.attributes['rea']
119
+ str = "#{str}: #{reason}" if reason
120
+ log str, message: message, level: :warning
121
+ else
122
+ log str, message: message, level: :log
123
+ end
124
+ end
125
+
126
+ def log_acknowledgement_for_unknown(message)
127
+ log "Received #{message.type} for unknown message #{message.attribute('oMId')[0..3]}", message: message,
128
+ level: :warning
129
+ end
130
+
131
+ def status_subscribe_acknowledged(original)
132
+ component = find_component original.attribute('cId')
133
+ return unless component
134
+
135
+ short = Message.shorten_m_id original.m_id
136
+ subscribe_list = original.attributes['sS']
137
+ log "StatusSubscribe #{short} acknowledged, allowing repeated status values for #{subscribe_list}",
138
+ level: :info
139
+ component.allow_repeat_updates subscribe_list
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,119 @@
1
+ module RSMP
2
+ class Proxy
3
+ module Modules
4
+ # Message processing functionality
5
+ # Handles receiving and processing incoming messages
6
+ module Receive
7
+ def should_validate_ingoing_message?(message)
8
+ return true unless @site_settings
9
+
10
+ skip = @site_settings['skip_validation']
11
+ return true unless skip
12
+
13
+ klass = message.class.name.split('::').last
14
+ !skip.include?(klass)
15
+ end
16
+
17
+ def process_deferred
18
+ @node.process_deferred
19
+ end
20
+
21
+ def verify_sequence(message)
22
+ expect_version_message(message) unless @version_determined
23
+ end
24
+
25
+ def handle_invalid_packet(json, error)
26
+ str = "Received invalid package, must be valid JSON but got #{json.size} bytes: #{error.message}"
27
+ distribute_error error.exception(str)
28
+ log str, level: :warning
29
+ nil
30
+ end
31
+
32
+ def handle_malformed_message(attributes, error)
33
+ str = "Received malformed message, #{error.message}"
34
+ distribute_error error.exception(str)
35
+ log str, message: Malformed.new(attributes), level: :warning
36
+ nil
37
+ end
38
+
39
+ def handle_schema_error(message, error)
40
+ schemas_string = error.schemas.map { |schema| "#{schema.first}: #{schema.last}" }.join(', ')
41
+ reason = "schema errors (#{schemas_string}): #{error.message}"
42
+ str = "Received invalid #{message.type}"
43
+ distribute_error error.exception(str), message: message
44
+ dont_acknowledge message, str, reason
45
+ message
46
+ end
47
+
48
+ def handle_invalid_message(message, error)
49
+ reason = error.message.to_s
50
+ str = "Received invalid #{message.type},"
51
+ distribute_error error.exception("#{str} #{message.json}"), message: message
52
+ dont_acknowledge message, str, reason
53
+ message
54
+ end
55
+
56
+ def handle_fatal_error(message, error)
57
+ reason = error.message
58
+ str = "Rejected #{message.type},"
59
+ distribute_error error.exception(str), message: message
60
+ dont_acknowledge message, str, reason
61
+ close
62
+ message
63
+ end
64
+
65
+ def process_packet(json)
66
+ attributes = Message.parse_attributes json
67
+ message = Message.build attributes, json
68
+ message.validate(schemas) if should_validate_ingoing_message?(message)
69
+ verify_sequence message
70
+ with_deferred_distribution do
71
+ distribute message
72
+ process_message message
73
+ end
74
+ process_deferred
75
+ message
76
+ rescue InvalidPacket => e
77
+ handle_invalid_packet(json, e)
78
+ rescue MalformedMessage => e
79
+ handle_malformed_message(attributes, e)
80
+ rescue SchemaError, RSMP::Schema::Error => e
81
+ handle_schema_error(message, e)
82
+ rescue InvalidMessage => e
83
+ handle_invalid_message(message, e)
84
+ rescue FatalError => e
85
+ handle_fatal_error(message, e)
86
+ ensure
87
+ @node.clear_deferred
88
+ end
89
+
90
+ def process_message(message)
91
+ case message
92
+ when MessageAck
93
+ process_ack message
94
+ when MessageNotAck
95
+ process_not_ack message
96
+ when Version
97
+ process_version message
98
+ when RSMP::Watchdog
99
+ process_watchdog message
100
+ else
101
+ dont_acknowledge message, 'Received', "unknown message (#{message.type})"
102
+ end
103
+ end
104
+
105
+ def will_not_handle(message)
106
+ reason ||= "since we're a #{self.class.name.downcase}"
107
+ log "Ignoring #{message.type}, #{reason}", message: message, level: :warning
108
+ dont_acknowledge message, nil, reason
109
+ end
110
+
111
+ def expect_version_message(message)
112
+ return if message.is_a?(Version) || message.is_a?(MessageAck) || message.is_a?(MessageNotAck)
113
+
114
+ raise HandshakeError, 'Version must be received first'
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,76 @@
1
+ module RSMP
2
+ class Proxy
3
+ module Modules
4
+ # Message sending functionality
5
+ # Handles sending messages, validation, and buffering
6
+ module Send
7
+ def handle_send_schema_error(message, error)
8
+ schemas_string = error.schemas.map { |schema| "#{schema.first}: #{schema.last}" }.join(', ')
9
+ str = "Could not send #{message.type} because schema validation failed (#{schemas_string}): #{error.message}"
10
+ log str, message: message, level: :error
11
+ distribute_error error.exception("#{str} #{message.json}")
12
+ end
13
+
14
+ def send_message(message, reason = nil, validate: true, force: false)
15
+ raise NotReady if !force && !connected?
16
+ raise IOError unless @protocol
17
+
18
+ message.direction = :out
19
+ message.generate_json
20
+ message.validate schemas unless validate == false
21
+ @protocol.write_lines message.json
22
+ expect_acknowledgement message
23
+ distribute message
24
+ log_send message, reason
25
+ rescue IOError
26
+ buffer_message message
27
+ rescue SchemaError, RSMP::Schema::Error => e
28
+ handle_send_schema_error(message, e)
29
+ end
30
+
31
+ def buffer_message(message)
32
+ # TODO
33
+ # log "Cannot send #{message.type} because the connection is closed.", message: message, level: :error
34
+ end
35
+
36
+ def log_send(message, reason = nil)
37
+ str = if reason
38
+ "Sent #{message.type} #{reason}"
39
+ else
40
+ "Sent #{message.type}"
41
+ end
42
+
43
+ if message.type == 'MessageNotAck'
44
+ log str, message: message, level: :warning
45
+ else
46
+ log str, message: message, level: :log
47
+ end
48
+ end
49
+
50
+ def send_and_optionally_collect(message, options)
51
+ collect_options = options[:collect] || options[:collect!]
52
+ if collect_options
53
+ task = @task.async do |task|
54
+ task.annotate 'send_and_optionally_collect'
55
+ collector = yield collect_options # call block to create collector
56
+ collector.collect
57
+ collector.ok! if options[:collect!] # raise any errors if the bang version was specified
58
+ collector
59
+ end
60
+
61
+ send_message message, validate: options[:validate]
62
+ { sent: message, collector: task.wait }
63
+ else
64
+ send_message message, validate: options[:validate]
65
+ { sent: message }
66
+ end
67
+ end
68
+
69
+ def apply_nts_message_attributes(message)
70
+ message.attributes['ntsOId'] = main && main.ntsoid ? main.ntsoid : ''
71
+ message.attributes['xNId'] = main && main.xnid ? main.xnid : ''
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,25 @@
1
+ module RSMP
2
+ class Proxy
3
+ module Modules
4
+ # State management helpers
5
+ # Utility methods for waiting on state changes
6
+ module State
7
+ def wait_for_state(state, timeout:)
8
+ states = [state].flatten
9
+ return true if states.include?(@state)
10
+
11
+ wait_for_condition(@state_condition, timeout: timeout) do
12
+ states.include?(@state)
13
+ end
14
+ true
15
+ rescue RSMP::TimeoutError
16
+ raise RSMP::TimeoutError, "Did not reach state #{state} within #{timeout}s"
17
+ end
18
+
19
+ def handshake_complete
20
+ self.state = :ready
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,105 @@
1
+ module RSMP
2
+ class Proxy
3
+ module Modules
4
+ # Reader and timer task management
5
+ # Handles async tasks for reading from socket and running periodic timers
6
+ module Tasks
7
+ # run an async task that reads from @socket
8
+ def start_reader
9
+ @reader = @task.async do |task|
10
+ task.annotate 'reader'
11
+ run_reader
12
+ end
13
+ end
14
+
15
+ def run_reader
16
+ @stream ||= IO::Stream::Buffered.new(@socket)
17
+ @protocol ||= RSMP::Protocol.new(@stream) # rsmp messages are json terminated with a form-feed
18
+ loop do
19
+ read_line
20
+ end
21
+ rescue Restart
22
+ log 'Closing connection', level: :warning
23
+ raise
24
+ rescue EOFError, Async::Stop
25
+ log 'Connection closed', level: :warning
26
+ rescue IOError => e
27
+ log "IOError: #{e}", level: :warning
28
+ rescue Errno::ECONNRESET
29
+ log 'Connection reset by peer', level: :warning
30
+ rescue Errno::EPIPE
31
+ log 'Broken pipe', level: :warning
32
+ rescue StandardError => e
33
+ distribute_error e, level: :internal
34
+ end
35
+
36
+ def read_line
37
+ json = @protocol.read_line
38
+ beginning = Time.now
39
+ message = process_packet json
40
+ duration = Time.now - beginning
41
+ ms = (duration * 1000).round(4)
42
+ per_second = if duration.positive?
43
+ (1.0 / duration).round
44
+ else
45
+ Float::INFINITY
46
+ end
47
+ if message
48
+ type = message.type
49
+ m_id = Logger.shorten_message_id(message.m_id)
50
+ else
51
+ type = 'Unknown'
52
+ m_id = nil
53
+ end
54
+ str = [type, m_id, "processed in #{ms}ms, #{per_second}req/s"].compact.join(' ')
55
+ log str, level: :statistics
56
+ end
57
+
58
+ def start_timer
59
+ return if @timer
60
+
61
+ name = 'timer'
62
+ interval = @site_settings['intervals']['timer'] || 1
63
+ log "Starting #{name} with interval #{interval} seconds", level: :debug
64
+ @latest_watchdog_received = Clock.now
65
+ @timer = @task.async do |task|
66
+ task.annotate 'timer'
67
+ run_timer task, interval
68
+ end
69
+ end
70
+
71
+ def run_timer(task, interval)
72
+ next_time = Time.now.to_f
73
+ loop do
74
+ begin
75
+ now = Clock.now
76
+ timer(now)
77
+ rescue RSMP::Schema::Error => e
78
+ log "Timer: Schema error: #{e}", level: :warning
79
+ rescue EOFError => e
80
+ log "Timer: Connection closed: #{e}", level: :warning
81
+ rescue IOError
82
+ log 'Timer: IOError', level: :warning
83
+ rescue Errno::ECONNRESET
84
+ log 'Timer: Connection reset by peer', level: :warning
85
+ rescue Errno::EPIPE
86
+ log 'Timer: Broken pipe', level: :warning
87
+ rescue StandardError => e
88
+ distribute_error e, level: :internal
89
+ end
90
+ ensure
91
+ next_time += interval
92
+ duration = next_time - Time.now.to_f
93
+ task.sleep duration
94
+ end
95
+ end
96
+
97
+ def timer(now)
98
+ watchdog_send_timer now
99
+ check_ack_timeout now
100
+ check_watchdog_timeout now
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,69 @@
1
+ module RSMP
2
+ class Proxy
3
+ module Modules
4
+ # Version negotiation and handling
5
+ # Manages RSMP version handshake between sites and supervisors
6
+ module Versions
7
+ def core_versions
8
+ version = @site_settings['core_version']
9
+ if version == 'latest'
10
+ [RSMP::Schema.latest_core_version]
11
+ elsif version
12
+ [version]
13
+ else
14
+ RSMP::Schema.core_versions
15
+ end
16
+ end
17
+
18
+ def check_core_version(message)
19
+ versions = core_versions
20
+ # find versions that both we and the client support
21
+ candidates = message.versions & versions
22
+ if candidates.any?
23
+ @core_version = candidates.max_by { |v| Gem::Version.new(v) } # pick latest version
24
+ else
25
+ reason = "RSMP versions [#{message.versions.join(', ')}] requested, " \
26
+ "but only [#{versions.join(', ')}] supported."
27
+ dont_acknowledge message, 'Version message rejected', reason, force: true
28
+ raise HandshakeError, reason
29
+ end
30
+ end
31
+
32
+ def process_version(message); end
33
+
34
+ def extraneous_version(message)
35
+ dont_acknowledge message, 'Received', 'extraneous Version message'
36
+ end
37
+
38
+ def send_version(site_id, core_versions)
39
+ versions = if core_versions == 'latest'
40
+ [RSMP::Schema.latest_core_version]
41
+ elsif core_versions == 'all'
42
+ RSMP::Schema.core_versions
43
+ else
44
+ [core_versions].flatten
45
+ end
46
+ versions_array = versions.map { |v| { 'vers' => v } }
47
+
48
+ site_id_array = [site_id].flatten.map { |id| { 'sId' => id } }
49
+
50
+ version_response = Version.new({
51
+ 'RSMP' => versions_array,
52
+ 'siteId' => site_id_array,
53
+ 'SXL' => sxl_version.to_s
54
+ })
55
+ send_message version_response
56
+ end
57
+
58
+ def version_acknowledged; end
59
+
60
+ # Use Gem class to check version requirement
61
+ # Requirement must be a string like '1.1', '>=1.0.3' or '<2.1.4',
62
+ # or list of strings, like ['<=1.4','<1.5']
63
+ def self.version_meets_requirement?(version, requirement)
64
+ Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new(version))
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,66 @@
1
+ module RSMP
2
+ class Proxy
3
+ module Modules
4
+ # Watchdog functionality for monitoring connection health
5
+ # Handles sending and receiving watchdog messages
6
+ module Watchdogs
7
+ def start_watchdog
8
+ log "Starting watchdog with interval #{@site_settings['intervals']['watchdog']} seconds", level: :debug
9
+ @watchdog_started = true
10
+ end
11
+
12
+ def stop_watchdog
13
+ log 'Stopping watchdog', level: :debug
14
+ @watchdog_started = false
15
+ end
16
+
17
+ def with_watchdog_disabled
18
+ was = @watchdog_started
19
+ stop_watchdog if was
20
+ yield
21
+ ensure
22
+ start_watchdog if was
23
+ end
24
+
25
+ def watchdog_send_timer(now)
26
+ return unless @watchdog_started
27
+ return if @site_settings['intervals']['watchdog'] == :never
28
+
29
+ if @latest_watchdog_send_at.nil?
30
+ send_watchdog now
31
+ else
32
+ # we add half the timer interval to pick the timer
33
+ # event closes to the wanted wathcdog interval
34
+ diff = now - @latest_watchdog_send_at
35
+ if (diff + (0.5 * @site_settings['intervals']['timer'])) >= @site_settings['intervals']['watchdog']
36
+ send_watchdog now
37
+ end
38
+ end
39
+ end
40
+
41
+ def send_watchdog(now = Clock.now)
42
+ message = RSMP::Watchdog.new({ 'wTs' => clock.to_s })
43
+ send_message message
44
+ @latest_watchdog_send_at = now
45
+ end
46
+
47
+ def check_watchdog_timeout(now)
48
+ timeout = @site_settings['timeouts']['watchdog']
49
+ latest = @latest_watchdog_received + timeout
50
+ left = latest - now
51
+ return unless left.negative?
52
+
53
+ str = "No Watchdog received within #{timeout} seconds"
54
+ log str, level: :warning
55
+ distribute MissingWatchdog.new(str)
56
+ end
57
+
58
+ def process_watchdog(message)
59
+ log "Received #{message.type}", message: message, level: :log
60
+ @latest_watchdog_received = Clock.now
61
+ acknowledge message
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end