rsmp 0.45.2 → 0.46.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ceec0bb561c61873341a4af09334ed0c6e0eb7bb1909b6fb38f2a350aaae8ca9
4
- data.tar.gz: 41f8df99b563e086770f502e69ff8bc8a8dc91b075aa9e50c2f0dd451b8bc0ad
3
+ metadata.gz: a5a5d66114ac44b96bc823b0366eddc9225abeb1686ec75cdd60d25373c8e53b
4
+ data.tar.gz: 49387a2b58f4aa7cfd0e36226240cbb1e93d2f49e5fd5636eb9cf3cabd0e3b53
5
5
  SHA512:
6
- metadata.gz: 46ee01791490e180283de79f9568b4d37ad8bef6bd228f2cee5c6eca7d95f568fad715f5f48314c1ebfec8610185bd027b7cc5e65701af248d2253d3569ff491
7
- data.tar.gz: 26b27b919cc42a3fdf337720bfb31c3168e8a2a95a0361b642efe27973352eaffbcb4e4c07ea3d55a6f0d4e59d57650c226ed603edcf6a9c45747bb1b3439c98
6
+ metadata.gz: f0cf699c114f7c61b03e42e199ecddee74270bb6a3fabe980c95f89951e7edc557cebc9b7c3eb1668614dc73a2c69a6c849be3010c6cdcb3210459a506ca04ef
7
+ data.tar.gz: 7b53bd11a26ad80c150862fbd28fe98b6a9fb0be238a613e2500ac0ffb439c34188b10f962f120c54fa491a371db05688a98d0f9ca27549eb9658a498f1d2d2a
data/CHANGELOG.md CHANGED
@@ -670,3 +670,18 @@ Initial release.
670
670
  - fix issues with config normalization
671
671
  - make collector cancellation raise pending errors
672
672
  - replace Cucumber/Aruba CLI tests with sus tests that call the Thor CLI directly
673
+
674
+ ## 0.45.1
675
+ - fix CLI execution by using relative requires
676
+ - update schema generation and generated schemas
677
+ - support patterns in list item types
678
+ - resolve SXL schema core definitions dynamically
679
+ - add CLI tests and split message tests
680
+
681
+ ## 0.45.2
682
+ - update gems
683
+ - add AGENTS.md
684
+ - replace em dashes to avoid UTF issues
685
+
686
+ ## 0.46.0
687
+ - add outgoing message buffering for sites
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rsmp (0.45.2)
4
+ rsmp (0.46.0)
5
5
  async (~> 2.39)
6
6
  colorize (~> 1.1)
7
7
  io-endpoint (~> 0.17)
@@ -96,10 +96,9 @@ module RSMP
96
96
 
97
97
  def run
98
98
  log_site_starting
99
- @proxies.each do |proxy|
100
- proxy.start
101
- proxy.wait
102
- end
99
+ start_status_timer
100
+ @proxies.each(&:start)
101
+ @proxies.each(&:wait)
103
102
  end
104
103
 
105
104
  def build_proxies
@@ -119,7 +118,7 @@ module RSMP
119
118
 
120
119
  def aggregated_status_changed(component, _options = {})
121
120
  @proxies.each do |proxy|
122
- proxy.send_aggregated_status component if proxy.ready?
121
+ proxy.send_aggregated_status component
123
122
  end
124
123
  end
125
124
 
@@ -137,10 +136,50 @@ module RSMP
137
136
 
138
137
  def send_alarm(alarm)
139
138
  @proxies.each do |proxy|
140
- proxy.send_message alarm if proxy.ready? && proxy.receive_alarms?
139
+ proxy.send_message alarm if proxy.receive_alarms?
140
+ end
141
+ end
142
+
143
+ def start_status_timer
144
+ return if @status_timer
145
+
146
+ interval = @site_settings['intervals']['timer'] || 1
147
+ log "Starting site status timer with interval #{interval} seconds", level: :debug
148
+ @status_timer = @task.async do |task|
149
+ task.annotate 'site status timer'
150
+ run_status_timer task, interval
151
+ end
152
+ end
153
+
154
+ def run_status_timer(task, interval)
155
+ next_time = Time.now.to_f
156
+ loop do
157
+ now = Clock.now
158
+ tick_status_subscriptions now
159
+ rescue StandardError => e
160
+ distribute_error e, level: :internal
161
+ ensure
162
+ next_time += interval
163
+ duration = next_time - Time.now.to_f
164
+ task.sleep duration
141
165
  end
142
166
  end
143
167
 
168
+ def tick_status_subscriptions(now)
169
+ @proxies.each { |proxy| proxy.status_update_timer now }
170
+ end
171
+
172
+ def stop_status_timer
173
+ @status_timer&.stop
174
+ ensure
175
+ @status_timer = nil
176
+ end
177
+
178
+ def stop_subtasks
179
+ stop_status_timer
180
+ super
181
+ end
182
+
144
183
  def connect_to_supervisor(_task, supervisor_settings)
145
184
  proxy = build_proxy({
146
185
  site: self,
@@ -41,6 +41,32 @@
41
41
  "additionalProperties": true
42
42
  },
43
43
  "send_after_connect": { "type": "boolean" },
44
+ "message_buffer": {
45
+ "type": "object",
46
+ "properties": {
47
+ "enabled": { "type": "boolean" },
48
+ "max_messages": { "type": "integer", "minimum": 1 },
49
+ "statuses": {
50
+ "oneOf": [
51
+ { "type": "boolean" },
52
+ {
53
+ "type": "array",
54
+ "items": {
55
+ "type": "object",
56
+ "properties": {
57
+ "cId": { "type": "string" },
58
+ "sCI": { "type": "string" },
59
+ "n": { "type": "string" }
60
+ },
61
+ "required": ["sCI"],
62
+ "additionalProperties": false
63
+ }
64
+ }
65
+ ]
66
+ }
67
+ },
68
+ "additionalProperties": false
69
+ },
44
70
  "components": { "type": "object" },
45
71
  "security_codes": { "type": "object" },
46
72
  "startup_sequence": { "type": "string" },
@@ -21,6 +21,7 @@ module RSMP
21
21
  'acknowledgement' => 2
22
22
  },
23
23
  'send_after_connect' => true,
24
+ 'message_buffer' => default_message_buffer,
24
25
  'components' => {
25
26
  'main' => {
26
27
  'C1' => {}
@@ -35,6 +36,14 @@ module RSMP
35
36
 
36
37
  private
37
38
 
39
+ def default_message_buffer
40
+ {
41
+ 'enabled' => true,
42
+ 'max_messages' => 10_000,
43
+ 'statuses' => []
44
+ }
45
+ end
46
+
38
47
  def apply_defaults(options)
39
48
  defaults = defaults()
40
49
  defaults['components']['main'] = options['components']['main'] if options.dig('components', 'main')
@@ -11,8 +11,13 @@ module RSMP
11
11
  distribute_error error.exception("#{str} #{message.json}")
12
12
  end
13
13
 
14
- def send_message(message, reason = nil, validate: true, force: false)
15
- raise NotReady if !force && !connected?
14
+ def send_message(message, reason = nil, validate: true, force: false, buffer: true)
15
+ unless force || connected?
16
+ error = NotReady.new
17
+ raise error unless buffer
18
+
19
+ return buffer_message(message, error)
20
+ end
16
21
  raise IOError unless @protocol
17
22
 
18
23
  message.direction = :out
@@ -22,15 +27,18 @@ module RSMP
22
27
  expect_acknowledgement message
23
28
  distribute message
24
29
  log_send message, reason
25
- rescue IOError
26
- buffer_message message
30
+ rescue NotReady, IOError => e
31
+ raise e unless buffer
32
+
33
+ buffer_message message, e
27
34
  rescue SchemaError, RSMP::Schema::Error => e
28
35
  handle_send_schema_error(message, e)
29
36
  end
30
37
 
31
- def buffer_message(message)
32
- # TODO
33
- # log "Cannot send #{message.type} because the connection is closed.", message: message, level: :error
38
+ def buffer_message(message, error = nil)
39
+ str = "Cannot send #{message.type} because the connection is closed."
40
+ log str, message: message, level: :error
41
+ raise error if error
34
42
  end
35
43
 
36
44
  def log_send(message, reason = nil)
@@ -125,13 +125,14 @@ module RSMP
125
125
  end
126
126
 
127
127
  def unsubscribe_to_status(status_list, component: nil, validate: nil)
128
- validate_ready 'unsubscribe to status'
129
128
  component ||= main.c_id
130
129
 
131
130
  status_list.each do |item|
132
131
  remove_subscription_item(component, item['sCI'], item['n'])
133
132
  end
134
133
 
134
+ return unless ready? # if the connection is don't we skip sending
135
+
135
136
  message = RSMP::StatusUnsubscribe.new({
136
137
  'cId' => component,
137
138
  'sS' => status_list
@@ -13,7 +13,7 @@ module RSMP
13
13
  def send_aggregated_status(component, m_id: nil)
14
14
  m_id ||= RSMP::Message.make_m_id
15
15
 
16
- se = if Proxy.version_meets_requirement?(core_version, '<=3.1.2')
16
+ se = if core_version && Proxy.version_meets_requirement?(core_version, '<=3.1.2')
17
17
  component.aggregated_status_bools.map { |bool| bool ? 'true' : 'false' }
18
18
  else
19
19
  component.aggregated_status_bools
@@ -0,0 +1,144 @@
1
+ module RSMP
2
+ class SupervisorProxy < Proxy
3
+ module Modules
4
+ # In-memory outgoing communication buffer for site-originated messages.
5
+ module MessageBuffer
6
+ def message_buffer_settings
7
+ @site_settings['message_buffer'] || {}
8
+ end
9
+
10
+ def message_buffer_enabled?
11
+ message_buffer_settings['enabled'] != false
12
+ end
13
+
14
+ def message_buffer_max_messages
15
+ message_buffer_settings['max_messages'] || 10_000
16
+ end
17
+
18
+ def status_buffer_selectors
19
+ return message_buffer_settings['statuses'] if message_buffer_settings.key? 'statuses'
20
+
21
+ []
22
+ end
23
+
24
+ def status_buffer_selector?(component_id, status)
25
+ selectors = status_buffer_selectors
26
+ return true if selectors == true
27
+ return false unless selectors.is_a?(Array)
28
+
29
+ selectors.any? { |selector| status_buffer_selector_matches?(selector, component_id, status) }
30
+ end
31
+
32
+ def status_buffer_selector_matches?(selector, component_id, status)
33
+ selector = selector.transform_keys(&:to_s)
34
+ component_matches = !selector['cId'] || selector['cId'] == component_id
35
+ code_matches = !selector['sCI'] || selector['sCI'] == status['sCI']
36
+ name_matches = !selector['n'] || selector['n'] == status['n']
37
+ component_matches && code_matches && name_matches
38
+ end
39
+
40
+ def clone_message(message, attributes = message.attributes)
41
+ message.class.new(JSON.parse(JSON.generate(attributes)))
42
+ end
43
+
44
+ def site_originated_buffer_candidate?(message)
45
+ message.is_a?(RSMP::AggregatedStatus) ||
46
+ message.is_a?(RSMP::AlarmIssue) ||
47
+ message.is_a?(RSMP::AlarmSuspended) ||
48
+ message.is_a?(RSMP::AlarmResumed) ||
49
+ message.is_a?(RSMP::AlarmAcknowledged) ||
50
+ message.is_a?(RSMP::StatusUpdate)
51
+ end
52
+
53
+ def prepare_status_update_for_buffer(message, core_version:, for_send:)
54
+ attributes = JSON.parse(JSON.generate(message.attributes))
55
+ component_id = attributes['cId']
56
+ attributes['sS'] = attributes['sS'].select { |status| status_buffer_selector?(component_id, status) }
57
+ return if attributes['sS'].empty?
58
+
59
+ if for_send && core_version && version_meets_requirement?(core_version, '>=3.2.0')
60
+ attributes['sS'].each { |status| status['q'] = 'old' }
61
+ end
62
+ clone_message message, attributes
63
+ end
64
+
65
+ def normalize_aggregated_status_buffer(states, core_version)
66
+ if version_meets_requirement?(core_version, '<=3.1.2')
67
+ states.map { |item| item == true || item.to_s == 'true' ? 'true' : 'false' }
68
+ else
69
+ states.map { |item| item == true || item.to_s == 'true' }
70
+ end
71
+ end
72
+
73
+ def prepare_aggregated_status_for_buffer(message, core_version:, for_send:)
74
+ attributes = JSON.parse(JSON.generate(message.attributes))
75
+ if for_send && core_version && attributes['se']
76
+ attributes['se'] = normalize_aggregated_status_buffer(attributes['se'], core_version)
77
+ end
78
+ clone_message message, attributes
79
+ end
80
+
81
+ def prepare_message_for_buffer(message, core_version: @core_version, for_send: false)
82
+ return unless message_buffer_enabled?
83
+ return unless site_originated_buffer_candidate? message
84
+ return false if message.is_a?(RSMP::StatusUpdate) && status_buffer_selectors == false
85
+ return false if message.is_a?(RSMP::Alarm) && !receive_alarms?
86
+
87
+ if message.is_a? RSMP::AggregatedStatus
88
+ prepare_aggregated_status_for_buffer message, core_version: core_version, for_send: for_send
89
+ elsif message.is_a? RSMP::StatusUpdate
90
+ prepare_status_update_for_buffer message, core_version: core_version, for_send: for_send
91
+ else
92
+ clone_message message
93
+ end
94
+ end
95
+
96
+ def buffer_message(message, error = nil)
97
+ prepared = prepare_message_for_buffer message, core_version: @core_version
98
+ if prepared
99
+ enqueue_buffered_message prepared
100
+ elsif site_originated_buffer_candidate? message
101
+ log "Discarded #{message.type}; it is not configured for buffering", message: message, level: :warning
102
+ else
103
+ super
104
+ end
105
+ rescue NotReady, IOError
106
+ raise error if error
107
+ end
108
+
109
+ def enqueue_buffered_message(message)
110
+ while @message_buffer.size >= message_buffer_max_messages
111
+ dropped = @message_buffer.shift
112
+ log "Dropped buffered #{dropped.type}; message buffer is full", message: dropped, level: :warning
113
+ end
114
+ @message_buffer << message
115
+ log "Buffered #{message.type}; #{message_buffer.size} message(s) queued", message: message, level: :warning
116
+ message
117
+ end
118
+
119
+ def flush_message_buffer
120
+ return if @message_buffer.empty?
121
+
122
+ queued = @message_buffer
123
+ @message_buffer = []
124
+ log "Sending #{queued.size} buffered message(s)", level: :info
125
+ queued.each_with_index do |message, index|
126
+ break unless flush_buffered_message(message, queued, index)
127
+ end
128
+ end
129
+
130
+ def flush_buffered_message(message, queued, index)
131
+ prepared = prepare_message_for_buffer message, core_version: @core_version, for_send: true
132
+ return true unless prepared
133
+
134
+ send_message prepared, 'from buffer', buffer: false
135
+ true
136
+ rescue NotReady, IOError
137
+ @message_buffer = queued[index..] + @message_buffer
138
+ log "Stopped sending buffered messages; #{message_buffer.size} message(s) remain queued", level: :warning
139
+ false
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -101,6 +101,21 @@ module RSMP
101
101
  acknowledge message
102
102
  end
103
103
 
104
+ def prune_unbuffered_status_subscriptions
105
+ @status_subscriptions.each_key.to_a.each do |component_id|
106
+ by_code = @status_subscriptions[component_id]
107
+ by_code.each_key.to_a.each do |code|
108
+ by_name = by_code[code]
109
+ by_name.delete_if do |name, _subscription|
110
+ status = { 'sCI' => code, 'n' => name }
111
+ !status_buffer_selector?(component_id, status)
112
+ end
113
+ by_code.delete(code) if by_name.empty?
114
+ end
115
+ @status_subscriptions.delete(component_id) if by_code.empty?
116
+ end
117
+ end
118
+
104
119
  def fetch_last_sent_status(component, code, name)
105
120
  @last_status_sent&.dig component, code, name
106
121
  end
@@ -7,8 +7,9 @@ module RSMP
7
7
  include Modules::Commands
8
8
  include Modules::Alarms
9
9
  include Modules::AggregatedStatus
10
+ include Modules::MessageBuffer
10
11
 
11
- attr_reader :supervisor_id, :site
12
+ attr_reader :supervisor_id, :site, :message_buffer
12
13
 
13
14
  def initialize(options)
14
15
  super(options.merge(node: options[:site]))
@@ -21,6 +22,7 @@ module RSMP
21
22
  @accepted_sxls = @sxls.dup
22
23
  @rejected_sxls = []
23
24
  @synthetic_id = Supervisor.build_id_from_ip_port @ip, @port
25
+ @message_buffer = []
24
26
  end
25
27
 
26
28
  # handle communication
@@ -51,6 +53,11 @@ module RSMP
51
53
  send_version_request @site_settings['site_id'], core_versions
52
54
  end
53
55
 
56
+ def close
57
+ prune_unbuffered_status_subscriptions
58
+ super
59
+ end
60
+
54
61
  # connect to the supervisor and initiate handshake supervisor
55
62
  def connect
56
63
  log "Connecting to supervisor at #{@ip}:#{@port}", level: :info
@@ -98,6 +105,7 @@ module RSMP
98
105
  send_all_aggregated_status
99
106
  send_active_alarms if receive_alarms?
100
107
  end
108
+ flush_message_buffer
101
109
  super
102
110
  end
103
111
 
@@ -170,11 +178,6 @@ module RSMP
170
178
  send_watchdog
171
179
  end
172
180
 
173
- def timer(now)
174
- super
175
- status_update_timer now if ready?
176
- end
177
-
178
181
  def process_version(message)
179
182
  return extraneous_version message if @version_determined
180
183
 
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = '0.45.2'.freeze
2
+ VERSION = '0.46.0'.freeze
3
3
  end
data/lib/rsmp.rb CHANGED
@@ -77,6 +77,7 @@ require_relative 'rsmp/proxy/supervisor/modules/status'
77
77
  require_relative 'rsmp/proxy/supervisor/modules/commands'
78
78
  require_relative 'rsmp/proxy/supervisor/modules/alarms'
79
79
  require_relative 'rsmp/proxy/supervisor/modules/aggregated_status'
80
+ require_relative 'rsmp/proxy/supervisor/modules/message_buffer'
80
81
  require_relative 'rsmp/proxy/supervisor/supervisor_proxy'
81
82
  require_relative 'rsmp/proxy/site/modules/status'
82
83
  require_relative 'rsmp/proxy/site/modules/aggregated_status'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsmp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.45.2
4
+ version: 0.46.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Tin
@@ -228,6 +228,7 @@ files:
228
228
  - lib/rsmp/proxy/supervisor/modules/aggregated_status.rb
229
229
  - lib/rsmp/proxy/supervisor/modules/alarms.rb
230
230
  - lib/rsmp/proxy/supervisor/modules/commands.rb
231
+ - lib/rsmp/proxy/supervisor/modules/message_buffer.rb
231
232
  - lib/rsmp/proxy/supervisor/modules/status.rb
232
233
  - lib/rsmp/proxy/supervisor/supervisor_proxy.rb
233
234
  - lib/rsmp/schema.rb