rsmp 0.51.0 → 0.51.1

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: 363ee5c8eb68f060a0f464939cc6a4444265568821fab38186992b189de29dd0
4
- data.tar.gz: f53b952f5877fd3c17b8d90ee82b7e1412432ad34a68cdcac3736320674cce8f
3
+ metadata.gz: b41aa5a1cbba1d8803f5c579b06b91472e0f9aed82db0c5ed70b14881f1faef4
4
+ data.tar.gz: 7d487e564d6626c0fc306e328d45dcff7576cbac442f31971784de1b0be7bdd6
5
5
  SHA512:
6
- metadata.gz: ed82fd73d09b5bcbd83c5c675a9e8ff9ed8d925e23f7cd059dd8efd9ca413e672f53ca0f3f76717114686d541c88b2a84f6dcd204a57338688b4c5db11dec1c9
7
- data.tar.gz: 2598d67cfd2d5a66bf466b6c8901d2013689160b18cf4fead369111c41dd0cc2ee760874551fc0b86d70f284b21a38b68aa5810c534a0a3563b79ef028f972a9
6
+ metadata.gz: 37b1f43e03c6ef6c4b41c95d8b3f2b0c394f656c7285bde99b14d4d4c8bfabdd6c0230fccd0861115d02eb6a4d04a97ecc012a2824efd3b8fdf5f48b6a0a53fb
7
+ data.tar.gz: fae75a302258af53de4308eba92dc39d3eb4ee3d00cdceead6c12e7ccc9773a03085bb2df9df21e6c2dcb658532044caa1f83a5fcd564e74c0e31560b53abf38
data/CHANGELOG.md CHANGED
@@ -711,3 +711,11 @@ Initial release.
711
711
  - publish typed events for expected peer and connection failures
712
712
  - preserve unexpected exceptions and backtraces through structured Async task trees
713
713
  - add explicit bang variants for callers that prefer exception-based handling
714
+
715
+ ## 0.51.0
716
+ - fix Async listener and reconnect lifecycle cleanup
717
+ - prevent teardown races when acknowledgement and proxy tasks stop concurrently
718
+ - fix RuboCop violations
719
+
720
+ ## 0.51.1
721
+ - cleanup semantic versioning handling of version strings and use canonical schema versions internally
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rsmp (0.51.0)
4
+ rsmp (0.51.1)
5
5
  async (~> 2.45)
6
6
  colorize (~> 1.1)
7
7
  io-endpoint (~> 0.17)
@@ -16,6 +16,14 @@ 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
+ ## Version strings
20
+
21
+ Core 3.3.0 and later use exact `MAJOR.MINOR.PATCH` version strings for both Core and SXL negotiation.
22
+
23
+ Earlier Core versions retain their historical wire formats for compatibility. In particular, the Core 3.2 release may be written as either `3.2` or `3.2.0`. The library resolves both to the `3.2.0` schema internally, but a supervisor responding to a legacy Version message uses the exact Core and SXL strings sent by the site. Ambiguous versions such as `3.1` are rejected because they do not identify one of the supported 3.1.x releases.
24
+
25
+ Legacy SXL strings such as `1.1` and `1.2` remain supported for connections using a Core version before 3.3.0. Their three-part equivalents are used only for internal schema lookup.
26
+
19
27
  ## Loading a configuration file
20
28
 
21
29
  Use the option classes to load a YAML configuration file:
@@ -4,13 +4,12 @@ module RSMP
4
4
  # Converts SXL definitions to JSON Schema files.
5
5
  module JSONSchema
6
6
  def self.output_sxl_index(out, sxl)
7
+ legacy_types = legacy_types?(sxl)
7
8
  out['sxl_index.json'] = output_json({
8
9
  'meta' => sxl[:meta],
9
- 'statuses' => index_items(sxl[:statuses],
10
- legacy_types: legacy_types?(sxl)),
11
- 'commands' => index_items(sxl[:commands],
12
- legacy_types: legacy_types?(sxl)),
13
- 'alarms' => index_items(sxl[:alarms], legacy_types: legacy_types?(sxl))
10
+ 'statuses' => index_items(sxl[:statuses], legacy_types: legacy_types),
11
+ 'commands' => index_items(sxl[:commands], legacy_types: legacy_types),
12
+ 'alarms' => index_items(sxl[:alarms], legacy_types: legacy_types)
14
13
  })
15
14
  end
16
15
 
data/lib/rsmp/message.rb CHANGED
@@ -202,10 +202,9 @@ module RSMP
202
202
 
203
203
  # Represents a malformed message with invalid attributes.
204
204
  class Malformed < Message
205
- # rubocop:disable-next Lint/MissingSuper
206
205
  def initialize(attributes = {})
207
- # don't call super, just copy (potentially invalid) attributes
208
- @attributes = {}
206
+ super({})
207
+ @attributes.clear
209
208
  @invalid_attributes = attributes
210
209
  end
211
210
  end
@@ -81,8 +81,9 @@ module RSMP
81
81
  def check_core_versions
82
82
  version = @site_settings['core_version']
83
83
  return unless version
84
+ return if %w[all latest].include? version
84
85
 
85
- return if RSMP::Schema.core_versions.include? version
86
+ return if RSMP::Schema.normalize_core_version version
86
87
 
87
88
  error_str = "Unknown core version: #{version}"
88
89
  raise RSMP::ConfigurationError, error_str
@@ -35,23 +35,36 @@ module RSMP
35
35
  sites.each do |site_id, settings|
36
36
  raise RSMP::ConfigurationError, "Configuration for site '#{site_id}' is empty" unless settings
37
37
 
38
- sxls = settings['sxls']
39
- raise RSMP::ConfigurationError, "Configuration error for site '#{site_id}': No SXLs specified" unless sxls
38
+ check_configured_sxls site_id, settings['sxls']
39
+ check_configured_core_version site_id, settings['core_version']
40
+ rescue RSMP::Schema::UnknownSchemaError => e
41
+ raise RSMP::ConfigurationError, "Configuration error for site '#{site_id}': #{e}"
42
+ end
43
+ end
40
44
 
41
- sxls.each do |sxl|
42
- name = sxl['name']
43
- if name.to_s == 'core'
44
- raise RSMP::ConfigurationError,
45
- "Configuration error for site '#{site_id}': SXL name cannot be core"
46
- end
45
+ def check_configured_sxls(site_id, sxls)
46
+ raise RSMP::ConfigurationError, "Configuration error for site '#{site_id}': No SXLs specified" unless sxls
47
47
 
48
- RSMP::Schema.find_schema! name, sxl['version'], lenient: true
48
+ sxls.each do |sxl|
49
+ name = sxl['name']
50
+ if name.to_s == 'core'
51
+ raise RSMP::ConfigurationError,
52
+ "Configuration error for site '#{site_id}': SXL name cannot be core"
49
53
  end
50
- rescue RSMP::Schema::UnknownSchemaError => e
51
- raise RSMP::ConfigurationError, "Configuration error for site '#{site_id}': #{e}"
54
+
55
+ RSMP::Schema.find_schema! name, sxl['version'], lenient: true
52
56
  end
53
57
  end
54
58
 
59
+ def check_configured_core_version(site_id, core_version)
60
+ return unless core_version
61
+ return if %w[all latest].include? core_version
62
+ return if RSMP::Schema.normalize_core_version core_version
63
+
64
+ raise RSMP::ConfigurationError,
65
+ "Configuration error for site '#{site_id}': Unknown core version: #{core_version}"
66
+ end
67
+
55
68
  def site_id_to_site_setting(site_id)
56
69
  base = @supervisor_settings['default'] || {}
57
70
 
@@ -8,13 +8,23 @@ module RSMP
8
8
  version = @site_settings['core_version']
9
9
  if version == 'latest'
10
10
  [RSMP::Schema.latest_core_version]
11
- elsif version
12
- [version]
13
- else
11
+ elsif version.nil? || version == 'all'
14
12
  RSMP::Schema.core_versions
13
+ else
14
+ [RSMP::Schema.normalize_core_version(version)].compact
15
15
  end
16
16
  end
17
17
 
18
+ # Version strings to advertise before a Core version has been selected.
19
+ # Include known legacy spellings so peers using exact string matching
20
+ # can find a common pre-3.3 version.
21
+ def advertised_core_versions
22
+ versions = core_versions.flat_map { |version| wire_core_version_aliases(version) }
23
+ configured = @site_settings['core_version']
24
+ versions.unshift configured if RSMP::Schema.normalize_core_version(configured)
25
+ versions.uniq
26
+ end
27
+
18
28
  def core_3_3?
19
29
  core_version && version_meets_requirement?(core_version, '>=3.3.0')
20
30
  end
@@ -29,8 +39,9 @@ module RSMP
29
39
 
30
40
  def sxl_request_items
31
41
  configured_sxls.map do |sxl|
32
- item = { 'name' => sxl['name'], 'version' => sxl['version'].to_s }
33
- prefix = RSMP::Schema.sxl_prefix(sxl['name'], sxl['version'], lenient: true)
42
+ version = RSMP::Schema.sanitize_version(sxl['version'].to_s)
43
+ item = { 'name' => sxl['name'], 'version' => version }
44
+ prefix = RSMP::Schema.sxl_prefix(sxl['name'], version)
34
45
  item['prefix'] = prefix if prefix
35
46
  item
36
47
  end
@@ -38,10 +49,14 @@ module RSMP
38
49
 
39
50
  def check_core_version(message)
40
51
  versions = core_versions
41
- # find versions that both we and the client support
42
- candidates = message.versions & versions
52
+ candidates = message.versions.filter_map do |wire_version|
53
+ normalized = RSMP::Schema.normalize_core_version(wire_version)
54
+ [wire_version, normalized] if normalized && versions.include?(normalized)
55
+ end
43
56
  if candidates.any?
44
- @core_version = candidates.max_by { |v| Gem::Version.new(v) } # pick latest version
57
+ @core_version_string, @core_version = candidates.max_by do |_wire_version, normalized|
58
+ Gem::Version.new(normalized)
59
+ end
45
60
  else
46
61
  reason = "RSMP versions [#{message.versions.join(', ')}] requested, " \
47
62
  "but only [#{versions.join(', ')}] supported."
@@ -74,10 +89,21 @@ module RSMP
74
89
  'receiveAlarms' => @site_settings['receive_alarms'] != false
75
90
  }), validate: false
76
91
  else
77
- send_version_message(site_id, core_versions, step: nil)
92
+ send_legacy_version_response(site_id, core_versions)
78
93
  end
79
94
  end
80
95
 
96
+ def send_legacy_version_response(site_id, core_versions)
97
+ response_versions = normalized_core_versions(core_versions).map do |version|
98
+ normalized = RSMP::Schema.normalize_core_version(version)
99
+ normalized == core_version ? core_version_string : version
100
+ end.uniq
101
+ attributes = version_message_attributes(site_id, response_versions)
102
+ primary = accepted_sxls.first
103
+ attributes['SXL'] = primary['version'].to_s if primary
104
+ send_message Version.new(attributes), validate: false
105
+ end
106
+
81
107
  def send_version_message(site_id, core_versions, step:)
82
108
  attributes = version_message_attributes(site_id, core_versions)
83
109
  attributes.merge!(version_request_attributes) if step == 'Request'
@@ -109,6 +135,15 @@ module RSMP
109
135
  end
110
136
  end
111
137
 
138
+ def wire_core_version_aliases(version)
139
+ normalized = RSMP::Schema.normalize_core_version(version)
140
+ return [version] unless normalized
141
+ return [normalized] if Gem::Version.new(normalized) >= Gem::Version.new('3.3.0')
142
+ return [normalized] unless normalized.end_with?('.0')
143
+
144
+ [normalized, normalized.delete_suffix('.0')]
145
+ end
146
+
112
147
  def site_id_items(site_id)
113
148
  [site_id].flatten.map { |id| { 'sId' => id } }
114
149
  end
@@ -124,13 +159,28 @@ module RSMP
124
159
  accepted_sxls + rejected_sxls
125
160
  end
126
161
 
162
+ def validate_sxl_response!(sxls)
163
+ invalid = sxls.find { |accepted| !valid_sxl_response?(accepted) }
164
+ return unless invalid
165
+
166
+ raise HandshakeError,
167
+ "Invalid SXL version #{invalid['name']} #{invalid['version']}; " \
168
+ 'Core 3.3 requires an exact MAJOR.MINOR.PATCH match'
169
+ end
170
+
171
+ def valid_sxl_response?(accepted)
172
+ requested = sxl_request_items.find { |item| item['name'] == accepted['name'] }
173
+ RSMP::Schema.strict_version?(accepted['version']) && requested &&
174
+ requested['version'] == accepted['version']
175
+ end
176
+
127
177
  def version_acknowledged; end
128
178
 
129
179
  def component_list_acknowledged; end
130
180
 
131
181
  # Use Gem class to check version requirement
132
- # Requirement must be a string like '1.1', '>=1.0.3' or '<2.1.4',
133
- # or list of strings, like ['<=1.4','<1.5']
182
+ # Requirement must be a string like '1.1.0', '>=1.0.3' or '<2.1.4',
183
+ # or list of strings, like ['<=1.4.0','<1.5.0']
134
184
  def self.version_meets_requirement?(version, requirement)
135
185
  Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new(version))
136
186
  end
@@ -19,7 +19,7 @@ module RSMP
19
19
  include Modules::Lifecycle
20
20
 
21
21
  attr_reader :state, :archive, :connection_info, :sxls, :accepted_sxls, :rejected_sxls,
22
- :collector, :ip, :port, :node, :core_version, :sxl_interfaces,
22
+ :collector, :ip, :port, :node, :core_version, :core_version_string, :sxl_interfaces,
23
23
  :site_settings, :session_id
24
24
 
25
25
  def initialize(options)
@@ -190,8 +190,8 @@ module RSMP
190
190
  end
191
191
 
192
192
  # Use Gem class to check version requirement
193
- # Requirement must be a string like '1.1', '>=1.0.3' or '<2.1.4',
194
- # or list of strings, like ['<=1.4','<1.5']
193
+ # Requirement must be a string like '1.1.0', '>=1.0.3' or '<2.1.4',
194
+ # or list of strings, like ['<=1.4.0','<1.5.0']
195
195
  def self.version_meets_requirement?(version, requirement)
196
196
  Modules::Versions.version_meets_requirement?(version, requirement)
197
197
  end
@@ -34,8 +34,13 @@ module RSMP
34
34
  configured = configured_sxls.find { |item| item['name'] == requested['name'] }
35
35
  return rejected_sxl(requested, 1, 'SXL not supported') unless configured
36
36
 
37
- if configured['version'].to_s == requested['version'].to_s
38
- RSMP::Schema.find_schema! requested['name'], requested['version'], lenient: true
37
+ unless RSMP::Schema.strict_version?(requested['version'])
38
+ return rejected_sxl(requested, 2, 'Core 3.3 requires SXL versions to use MAJOR.MINOR.PATCH')
39
+ end
40
+
41
+ configured_version = RSMP::Schema.sanitize_version(configured['version'].to_s)
42
+ if configured_version == requested['version']
43
+ RSMP::Schema.find_schema! requested['name'], requested['version']
39
44
  requested.slice('name', 'version', 'prefix')
40
45
  else
41
46
  rejected_sxl(requested, 2, "Supervisor only supports #{configured['version']}")
@@ -63,7 +63,7 @@ module RSMP
63
63
  end
64
64
 
65
65
  def start_handshake
66
- send_version_request @site_settings['site_id'], core_versions
66
+ send_version_request @site_settings['site_id'], advertised_core_versions
67
67
  end
68
68
 
69
69
  def close(...)
@@ -213,6 +213,7 @@ module RSMP
213
213
  def check_sxl_version(message)
214
214
  if core_3_3?
215
215
  @rejected_sxls, @accepted_sxls = message.sxls.partition { |item| item['rejected'] }
216
+ validate_sxl_response! @accepted_sxls
216
217
  @receive_alarms = message.attributes.fetch('receiveAlarms', true)
217
218
  else
218
219
  primary = primary_configured_sxl
@@ -0,0 +1,27 @@
1
+ module RSMP
2
+ # Provides JSON Schema validation for RSMP messages across core and SXL versions.
3
+ module Schema
4
+ STRICT_VERSION_PATTERN = /\A(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\z/
5
+ LEGACY_SHORT_VERSION_PATTERN = /\A(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\z/
6
+
7
+ # Return the schema version for a supported Core version string.
8
+ # Before Core 3.3, some releases used a two-part version on the wire.
9
+ # Only accept such a version when adding .0 identifies a known legacy
10
+ # schema unambiguously. Core 3.3 and later require all three parts.
11
+ def self.normalize_core_version(version)
12
+ return unless version.is_a?(String)
13
+ return version if core_versions.include? version
14
+ return unless LEGACY_SHORT_VERSION_PATTERN.match? version
15
+
16
+ normalized = "#{version}.0"
17
+ return unless core_versions.include? normalized
18
+ return unless Gem::Version.new(normalized) < Gem::Version.new('3.3.0')
19
+
20
+ normalized
21
+ end
22
+
23
+ def self.strict_version?(version)
24
+ version.is_a?(String) && STRICT_VERSION_PATTERN.match?(version)
25
+ end
26
+ end
27
+ end
data/lib/rsmp/schema.rb CHANGED
@@ -299,4 +299,5 @@ end
299
299
 
300
300
  require_relative 'schema/core_sxl_resolution'
301
301
  require_relative 'schema/message_resolution'
302
+ require_relative 'schema/version'
302
303
  require_relative 'schema/validation'
@@ -64,7 +64,7 @@ module RSMP
64
64
 
65
65
  # S0091 - Operator logged in/out OP-panel
66
66
  def handle_s0091(_status_code, status_name = nil, options = {})
67
- if RSMP::Proxy.version_meets_requirement?(options[:sxl_version], '>=1.1')
67
+ if RSMP::Proxy.version_meets_requirement?(options[:sxl_version], '>=1.1.0')
68
68
  case status_name
69
69
  when 'user'
70
70
  TrafficControllerSite.make_status 0
@@ -81,7 +81,7 @@ module RSMP
81
81
 
82
82
  # S0092 - Operator logged in/out web-interface
83
83
  def handle_s0092(_status_code, status_name = nil, options = {})
84
- if RSMP::Proxy.version_meets_requirement?(options[:sxl_version], '>=1.1')
84
+ if RSMP::Proxy.version_meets_requirement?(options[:sxl_version], '>=1.1.0')
85
85
  case status_name
86
86
  when 'user'
87
87
  TrafficControllerSite.make_status 0
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = '0.51.0'.freeze
2
+ VERSION = '0.51.1'.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.0
4
+ version: 0.51.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Tin
@@ -253,6 +253,7 @@ files:
253
253
  - lib/rsmp/schema/core_sxl_resolution.rb
254
254
  - lib/rsmp/schema/message_resolution.rb
255
255
  - lib/rsmp/schema/validation.rb
256
+ - lib/rsmp/schema/version.rb
256
257
  - lib/rsmp/schema_error.rb
257
258
  - lib/rsmp/status_list.rb
258
259
  - lib/rsmp/sxl/interface.rb