rsmp 0.51.3 → 0.51.4

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: 883735d537c281828ffe79225cf667fef84b88e2055e39820cc4213168f715ec
4
- data.tar.gz: 7c577e4ee8998cf1dbcb7308aa486122ba430d967375e4827030dd89c075f408
3
+ metadata.gz: 653475280335ad2b867657cada46ef95d9cb61808cb12f3302762b0e2bcda790
4
+ data.tar.gz: 2d8b6cfe3c5b46623329cb1dbcb1a01544dd293ab93f5398b650fea8121d31fd
5
5
  SHA512:
6
- metadata.gz: ae9f566fd7a1f2bc9c16f59507abfd1becacb0326489fa5308363daaee89122cb32863cbabf187f033fa34d18dfb6648a6e9f1150678e1c32d60aaa2f898be55
7
- data.tar.gz: 6c2348ed50374b5d9f1b1b99313420d3190c588ac70bb562815c0b418e46ebd697fcdf8f8809915392ad8f42f70f5637b7154eb33df40d9d6695ce52b25c131a
6
+ metadata.gz: 82018c56d70a3cfa59db7d60b30c172f324765a01be8d650906d64c92109d68c52d80f75efdf388fd8332a9fa5028bc0ee1bed51e4293a1ffa6c5f441bfec8db
7
+ data.tar.gz: a1b83604f3b516da1dc0e8a3071cddd71cd4cadbc78f22406fe5ea0f73d21c06663ef1739de1666818d88ea85445cc6cc045ee024987fe2120a4ddc6e179b539
data/CHANGELOG.md CHANGED
@@ -719,3 +719,13 @@ Initial release.
719
719
 
720
720
  ## 0.51.1
721
721
  - cleanup semantic versioning handling of version strings and use canonical schema versions internally
722
+
723
+ ## 0.51.2
724
+ - bumb patch version for test purposes, no changes othewise
725
+
726
+ ## 0.51.3
727
+ - ensure socket shutdown when stopping node
728
+
729
+ ## 0.51.4
730
+ - don't update subscription list until unsubscribe is acknowledged
731
+
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rsmp (0.51.3)
4
+ rsmp (0.51.4)
5
5
  async (~> 2.45)
6
6
  colorize (~> 1.1)
7
7
  io-endpoint (~> 0.17)
@@ -16,6 +16,18 @@ RSMP uses option classes to handle configuration for sites and supervisors. Thes
16
16
 
17
17
  Each class applies defaults and validates the configuration against a JSON Schema located in `lib/rsmp/options/schemas/`.
18
18
 
19
+ Field types and unknown keys are checked before defaults are applied. The `required_settings.json`
20
+ and `required_supervisor_settings.json` schemas then check required settings after defaults and
21
+ supervisor site inheritance are resolved:
22
+
23
+ - Every configured signal plan must have a `cycle_time`.
24
+ - Configured components must include exactly one `main` component. Sites supply a default main
25
+ component when omitted; supervisor site settings can inherit it from `default`.
26
+ - A site in `server` role must have a `port`, either explicitly or from its first supervisor endpoint.
27
+ With `supervisors: []`, the port must be explicit.
28
+
29
+ `inputs.programming: {}` is allowed and means no programmed input actions.
30
+
19
31
  ## Version strings
20
32
 
21
33
  Core 3.3.0 and later use exact `MAJOR.MINOR.PATCH` version strings for both Core and SXL negotiation.
@@ -34,7 +34,9 @@ module RSMP
34
34
  validate_log_settings! if validate
35
35
  config = normalize_config(options || {})
36
36
  validate!(config) if validate
37
- @data = normalize(apply_defaults(config))
37
+ config = apply_defaults(config)
38
+ validate_effective!(config) if validate
39
+ @data = normalize(config)
38
40
  end
39
41
 
40
42
  def defaults = {}
@@ -56,10 +58,10 @@ module RSMP
56
58
  raise RSMP::ConfigurationError, "Invalid log configuration#{source_suffix}:\n#{message}"
57
59
  end
58
60
 
59
- def validate!(data = @data)
60
- return unless schema_path && File.exist?(schema_path)
61
+ def validate!(data = @data, path: schema_path)
62
+ return unless path && File.exist?(path)
61
63
 
62
- schemer = JSONSchemer.schema(Pathname.new(schema_path))
64
+ schemer = JSONSchemer.schema(Pathname.new(path))
63
65
  errors = schemer.validate(data).to_a
64
66
  return if errors.empty?
65
67
 
@@ -86,6 +88,8 @@ module RSMP
86
88
 
87
89
  private
88
90
 
91
+ def validate_effective!(_config); end
92
+
89
93
  def apply_defaults(options)
90
94
  defaults.deep_merge(options)
91
95
  end
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$comment": "Validate after applying defaults and inherited site settings; structural validation uses the other config schemas.",
4
+ "type": "object",
5
+ "properties": {
6
+ "components": {
7
+ "type": "object",
8
+ "required": ["main"],
9
+ "properties": {
10
+ "main": {
11
+ "type": "object",
12
+ "minProperties": 1,
13
+ "maxProperties": 1
14
+ }
15
+ }
16
+ },
17
+ "signal_plans": {
18
+ "type": "object",
19
+ "additionalProperties": {
20
+ "type": "object",
21
+ "required": ["cycle_time"]
22
+ }
23
+ }
24
+ },
25
+ "if": {
26
+ "required": ["connection_role"],
27
+ "properties": { "connection_role": { "const": "server" } }
28
+ },
29
+ "then": { "required": ["port"] }
30
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$comment": "Validate each site after merging supervisor defaults, without changing the stored overrides.",
4
+ "type": "object",
5
+ "properties": {
6
+ "default": { "$ref": "required_settings.json" },
7
+ "sites": {
8
+ "type": "object",
9
+ "additionalProperties": { "$ref": "required_settings.json" }
10
+ }
11
+ }
12
+ }
@@ -23,6 +23,10 @@ module RSMP
23
23
 
24
24
  private
25
25
 
26
+ def validate_effective!(config)
27
+ validate!(config, path: File.join(SCHEMAS_PATH, 'required_settings.json'))
28
+ end
29
+
26
30
  def default_message_buffer
27
31
  {
28
32
  'max_messages' => 10_000,
@@ -54,7 +58,8 @@ module RSMP
54
58
  defaults = defaults()
55
59
  defaults['components']['main'] = options['components']['main'] if options.dig('components', 'main')
56
60
  data = defaults.deep_merge(options)
57
- data['port'] ||= data.dig('supervisors', 0, 'port')
61
+ port = data.dig('supervisors', 0, 'port')
62
+ data['port'] ||= port if port
58
63
  data
59
64
  end
60
65
  end
@@ -28,6 +28,15 @@ module RSMP
28
28
  def schema_file
29
29
  'supervisor.json'
30
30
  end
31
+
32
+ private
33
+
34
+ def validate_effective!(config)
35
+ base = config['default'] || {}
36
+ sites = (config['sites'] || {}).transform_values { |settings| base.deep_merge(settings) }
37
+ effective = config.merge('sites' => sites)
38
+ validate!(effective, path: File.join(SCHEMAS_PATH, 'required_supervisor_settings.json'))
39
+ end
31
40
  end
32
41
  end
33
42
  end
@@ -103,6 +103,8 @@ module RSMP
103
103
  component_list_acknowledged
104
104
  when 'StatusSubscribe'
105
105
  status_subscribe_acknowledged original
106
+ when 'StatusUnsubscribe'
107
+ status_unsubscribe_acknowledged original
106
108
  end
107
109
 
108
110
  check_outgoing_acknowledged original
@@ -3,6 +3,16 @@ module RSMP
3
3
  module Modules
4
4
  # Handles status requests, responses, subscriptions and updates
5
5
  module Status
6
+ def clear
7
+ super
8
+ @pending_status_unsubscriptions = {}
9
+ end
10
+
11
+ def process_not_ack(message)
12
+ @pending_status_unsubscriptions.delete(message.attribute('oMId'))
13
+ super
14
+ end
15
+
6
16
  # Build and send a StatusRequest. Returns Result<StatusRequest>.
7
17
  def request_status(status_list, component: nil, m_id: nil, validate: true)
8
18
  readiness = validate_ready 'request status'
@@ -75,9 +85,9 @@ module RSMP
75
85
  subscribe_list.each do |item|
76
86
  code = item['sCI']
77
87
  name = item['n']
78
- sub = ensure_subscription_path(component_id, code, name)
79
- sub['uRt'] = item['uRt']
80
- sub['sOc'] = item['sOc']
88
+ ensure_subscription_path(component_id, code, name)
89
+ # Replace the record so an older unsubscribe ACK cannot remove it.
90
+ @status_subscriptions[component_id][code][name] = item.slice('uRt', 'sOc')
81
91
  end
82
92
  end
83
93
 
@@ -147,20 +157,46 @@ module RSMP
147
157
  def unsubscribe_to_status(status_list, component: nil, validate: nil)
148
158
  component ||= main.c_id
149
159
 
150
- status_list.each do |item|
151
- remove_subscription_item(component, item['sCI'], item['n'])
152
- end
153
-
154
160
  # Local subscription state must be cleaned up even when the peer has
155
161
  # already disconnected. In that case there is nothing left to send.
156
- return Result.success(nil) unless ready?
162
+ unless ready?
163
+ status_list.each do |item|
164
+ remove_subscription_item(component, item['sCI'], item['n'])
165
+ end
166
+ return Result.success(nil)
167
+ end
157
168
 
158
169
  message = RSMP::StatusUnsubscribe.new({
159
170
  'cId' => component,
160
171
  'sS' => status_list
161
172
  })
162
173
  apply_nts_message_attributes message
163
- send_message(message, validate: validate).map(&:message)
174
+ track_status_unsubscribe(message)
175
+ delivery = send_message(message, validate: validate)
176
+ delivery.map(&:message)
177
+ ensure
178
+ @pending_status_unsubscriptions.delete(message.m_id) if message && !delivery&.success?
179
+ end
180
+
181
+ def track_status_unsubscribe(message)
182
+ component = message.attribute('cId')
183
+ @pending_status_unsubscriptions[message.m_id] = message.attribute('sS').map do |item|
184
+ code = item['sCI']
185
+ name = item['n']
186
+ [code, name, @status_subscriptions.dig(component, code, name)]
187
+ end
188
+ end
189
+
190
+ def status_unsubscribe_acknowledged(original)
191
+ component = original.attribute('cId')
192
+ subscriptions = @pending_status_unsubscriptions.delete(original.m_id)
193
+ return unless subscriptions
194
+
195
+ subscriptions.each do |code, name, subscription|
196
+ next unless @status_subscriptions.dig(component, code, name).equal?(subscription)
197
+
198
+ remove_subscription_item(component, code, name)
199
+ end
164
200
  end
165
201
 
166
202
  def unsubscribe_to_status!(...)
@@ -190,6 +190,8 @@ module RSMP
190
190
  end
191
191
 
192
192
  def programming_hash_to_array(normalized)
193
+ return [] if normalized.empty?
194
+
193
195
  max_key = normalized.keys.max
194
196
  program_array = Array.new(max_key + 1)
195
197
  normalized.each do |index, value|
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = '0.51.3'.freeze
2
+ VERSION = '0.51.4'.freeze
3
3
  end
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.51.3
4
+ version: 0.51.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Tin
@@ -218,6 +218,8 @@ files:
218
218
  - lib/rsmp/options/options.rb
219
219
  - lib/rsmp/options/schemas/config_common.json
220
220
  - lib/rsmp/options/schemas/log.json
221
+ - lib/rsmp/options/schemas/required_settings.json
222
+ - lib/rsmp/options/schemas/required_supervisor_settings.json
221
223
  - lib/rsmp/options/schemas/site.json
222
224
  - lib/rsmp/options/schemas/supervisor.json
223
225
  - lib/rsmp/options/schemas/supervisor_site.json