jetstream_bridge 4.0.1 → 4.0.3

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: 358b977021f3b620cbbedb6af88bca009060f2da62ec932cbdcf5db1e3b0b375
4
- data.tar.gz: 4f0ca0e54a96aee5971460c2be2fdeac82e5f6df00fc70a17fb90e472e91e1ee
3
+ metadata.gz: fdb73e243bddbd9812d6d454d1975d15f6f8d98325c7759853d32a01c830e27e
4
+ data.tar.gz: 8c570f62d4e4b0efddf59e6a4033026b5cabaad83fe808ec0942ad57f3874d5e
5
5
  SHA512:
6
- metadata.gz: 550efc74885ce2b4d2fbc86742bf0db0dabdf402b64c6b072a3007b435946e09f460f5b759f1f027a3ba4ed8211b9a807ff6f157b075179d2901284703097b47
7
- data.tar.gz: '09478489b9e13b897979657cb5fa5dd6da90f9399ad4e09d7f5e0436c9fc881e02c1ffc2c7441834d72dff3c9549b846d199c4cb6bf5adee845babb4877235ae'
6
+ metadata.gz: c9b95208ecf116e52daa75e77d3edb62899e6724bec67c72c4322aab31778392906053f77e9eb420c855605ee6aca41eacad9c509b47e5123e710436d201718c
7
+ data.tar.gz: 588603c1946d72946c9ba990869869e5932c3f04e265cefbbc2d6f0c3829467e1e498eab307b84770b7d88ed134184d6c8f07596968d0b30da107204fa3bca5f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.0.3] - 2025-11-23
9
+
10
+ ### Added
11
+
12
+ - **Connection Validation** - Comprehensive NATS URL validation on initialization
13
+ - Validates URL format, scheme (nats/nats+tls), host presence, and port range (1-65535)
14
+ - Verifies NATS connection established after connect
15
+ - Verifies JetStream availability with account info check
16
+ - Helpful error messages for common misconfigurations
17
+ - All validation errors include specific guidance for fixes
18
+
19
+ - **Connection Logging** - Detailed logging throughout connection lifecycle
20
+ - Debug logs for URL validation progress and verification steps
21
+ - Info logs for successful validation and JetStream stats (streams, consumers, memory, storage)
22
+ - Error logs for all validation failures with specific details
23
+ - Automatic credential sanitization in all log messages
24
+ - Human-readable resource usage formatting (bytes to KB/MB/GB)
25
+
26
+ ### Fixed
27
+
28
+ - **Health Check** - Fixed `healthy` field returning `nil` instead of `false` when disconnected
29
+ - Changed `stream_info[:exists]` to `stream_info&.fetch(:exists, false)` for proper nil handling
30
+ - Ensures boolean values always returned for monitoring systems
31
+
32
+ ### Changed
33
+
34
+ - **Code Organization** - Moved all inline RuboCop rules to centralized `.rubocop.yml`
35
+ - Removed inline comments from 5 files (logging.rb, model_utils.rb, inbox_event.rb, outbox_event.rb, inbox_message.rb)
36
+ - Added exclusions to `.rubocop.yml` for metrics and naming rules
37
+ - Cleaner codebase with all style exceptions in one location
38
+
39
+ ## [4.0.2] - 2025-11-23
40
+
41
+ ### Fixed
42
+
43
+ - **Stream Updates** - Prevent retention policy change errors (NATS error 10052)
44
+ - Skip all stream updates when retention policy differs from expected 'workqueue'
45
+ - Prevents "stream configuration update can not change retention policy to/from workqueue" error
46
+ - Logs warning when stream has mismatched retention policy but skips update
47
+ - Ensures compatibility with existing streams that have different retention policies
48
+
8
49
  ## [4.0.1] - 2025-11-23
9
50
 
10
51
  ### Fixed
@@ -8,7 +8,6 @@ module JetstreamBridge
8
8
  class InboxMessage
9
9
  attr_reader :msg, :seq, :deliveries, :stream, :subject, :headers, :body, :raw, :event_id, :now
10
10
 
11
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
12
11
  def self.from_nats(msg)
13
12
  meta = (msg.respond_to?(:metadata) && msg.metadata) || nil
14
13
  seq = meta.respond_to?(:stream_sequence) ? meta.stream_sequence : nil
@@ -33,9 +32,7 @@ module JetstreamBridge
33
32
 
34
33
  new(msg, seq, deliveries, stream, subject, headers, body, raw, id, Time.now.utc, consumer)
35
34
  end
36
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
37
35
 
38
- # rubocop:disable Metrics/ParameterLists
39
36
  def initialize(msg, seq, deliveries, stream, subject, headers, body, raw, event_id, now, consumer = nil)
40
37
  @msg = msg
41
38
  @seq = seq
@@ -49,7 +46,6 @@ module JetstreamBridge
49
46
  @now = now
50
47
  @consumer = consumer
51
48
  end
52
- # rubocop:enable Metrics/ParameterLists
53
49
 
54
50
  def body_for_store
55
51
  body.empty? ? raw : body
@@ -34,6 +34,8 @@ module JetstreamBridge
34
34
  connect_timeout: 5
35
35
  }.freeze
36
36
 
37
+ VALID_NATS_SCHEMES = %w[nats nats+tls].freeze
38
+
37
39
  class << self
38
40
  # Thread-safe delegator to the singleton instance.
39
41
  # Returns a live JetStream context.
@@ -106,11 +108,14 @@ module JetstreamBridge
106
108
  end
107
109
 
108
110
  def nats_servers
109
- JetstreamBridge.config.nats_urls
110
- .to_s
111
- .split(',')
112
- .map(&:strip)
113
- .reject(&:empty?)
111
+ servers = JetstreamBridge.config.nats_urls
112
+ .to_s
113
+ .split(',')
114
+ .map(&:strip)
115
+ .reject(&:empty?)
116
+
117
+ validate_nats_urls!(servers)
118
+ servers
114
119
  end
115
120
 
116
121
  def establish_connection(servers)
@@ -141,9 +146,15 @@ module JetstreamBridge
141
146
 
142
147
  @nc.connect({ servers: servers }.merge(DEFAULT_CONN_OPTS))
143
148
 
149
+ # Verify connection is established
150
+ verify_connection!
151
+
144
152
  # Create JetStream context
145
153
  @jts = @nc.jetstream
146
154
 
155
+ # Verify JetStream is available
156
+ verify_jetstream!
157
+
147
158
  # Ensure JetStream responds to #nc
148
159
  return if @jts.respond_to?(:nc)
149
160
 
@@ -151,6 +162,129 @@ module JetstreamBridge
151
162
  @jts.define_singleton_method(:nc) { nc_ref }
152
163
  end
153
164
 
165
+ def validate_nats_urls!(servers)
166
+ Logging.debug(
167
+ "Validating #{servers.size} NATS URL(s): #{sanitize_urls(servers).join(', ')}",
168
+ tag: 'JetstreamBridge::Connection'
169
+ )
170
+
171
+ servers.each do |url|
172
+ # Check for basic URL format (scheme://host)
173
+ unless url.include?('://')
174
+ Logging.error(
175
+ "Invalid URL format (missing scheme): #{url}",
176
+ tag: 'JetstreamBridge::Connection'
177
+ )
178
+ raise ConnectionError, "Invalid NATS URL format: #{url}. Expected format: nats://host:port"
179
+ end
180
+
181
+ uri = URI.parse(url)
182
+
183
+ # Validate scheme
184
+ scheme = uri.scheme&.downcase
185
+ unless VALID_NATS_SCHEMES.include?(scheme)
186
+ Logging.error(
187
+ "Invalid URL scheme '#{uri.scheme}': #{Logging.sanitize_url(url)}",
188
+ tag: 'JetstreamBridge::Connection'
189
+ )
190
+ raise ConnectionError, "Invalid NATS URL scheme '#{uri.scheme}' in: #{url}. Expected 'nats' or 'nats+tls'"
191
+ end
192
+
193
+ # Validate host is present
194
+ if uri.host.nil? || uri.host.empty?
195
+ Logging.error(
196
+ "Missing host in URL: #{Logging.sanitize_url(url)}",
197
+ tag: 'JetstreamBridge::Connection'
198
+ )
199
+ raise ConnectionError, "Invalid NATS URL - missing host: #{url}"
200
+ end
201
+
202
+ # Validate port if present
203
+ if uri.port && (uri.port < 1 || uri.port > 65_535)
204
+ Logging.error(
205
+ "Invalid port #{uri.port} in URL: #{Logging.sanitize_url(url)}",
206
+ tag: 'JetstreamBridge::Connection'
207
+ )
208
+ raise ConnectionError, "Invalid NATS URL - port must be 1-65535: #{url}"
209
+ end
210
+
211
+ Logging.debug(
212
+ "URL validated: #{Logging.sanitize_url(url)}",
213
+ tag: 'JetstreamBridge::Connection'
214
+ )
215
+ rescue URI::InvalidURIError => e
216
+ Logging.error(
217
+ "Malformed URL: #{url} (#{e.message})",
218
+ tag: 'JetstreamBridge::Connection'
219
+ )
220
+ raise ConnectionError, "Invalid NATS URL format: #{url} (#{e.message})"
221
+ end
222
+
223
+ Logging.info(
224
+ 'All NATS URLs validated successfully',
225
+ tag: 'JetstreamBridge::Connection'
226
+ )
227
+ end
228
+
229
+ def verify_connection!
230
+ Logging.debug(
231
+ 'Verifying NATS connection...',
232
+ tag: 'JetstreamBridge::Connection'
233
+ )
234
+
235
+ unless @nc.connected?
236
+ Logging.error(
237
+ 'NATS connection verification failed - client not connected',
238
+ tag: 'JetstreamBridge::Connection'
239
+ )
240
+ raise ConnectionError, 'Failed to establish connection to NATS server(s)'
241
+ end
242
+
243
+ Logging.info(
244
+ 'NATS connection verified successfully',
245
+ tag: 'JetstreamBridge::Connection'
246
+ )
247
+ end
248
+
249
+ def verify_jetstream!
250
+ Logging.debug(
251
+ 'Verifying JetStream availability...',
252
+ tag: 'JetstreamBridge::Connection'
253
+ )
254
+
255
+ # Verify JetStream is enabled by checking account info
256
+ account_info = @jts.account_info
257
+
258
+ Logging.info(
259
+ "JetStream verified - Streams: #{account_info.streams}, " \
260
+ "Consumers: #{account_info.consumers}, " \
261
+ "Memory: #{format_bytes(account_info.memory)}, " \
262
+ "Storage: #{format_bytes(account_info.storage)}",
263
+ tag: 'JetstreamBridge::Connection'
264
+ )
265
+ rescue NATS::IO::NoRespondersError
266
+ Logging.error(
267
+ 'JetStream not available - no responders (JetStream not enabled)',
268
+ tag: 'JetstreamBridge::Connection'
269
+ )
270
+ raise ConnectionError, 'JetStream not enabled on NATS server. Please enable JetStream with -js flag'
271
+ rescue StandardError => e
272
+ Logging.error(
273
+ "JetStream verification failed: #{e.class} - #{e.message}",
274
+ tag: 'JetstreamBridge::Connection'
275
+ )
276
+ raise ConnectionError, "JetStream verification failed: #{e.message}"
277
+ end
278
+
279
+ def format_bytes(bytes)
280
+ return 'N/A' if bytes.nil? || bytes.zero?
281
+
282
+ units = %w[B KB MB GB TB]
283
+ exp = (Math.log(bytes) / Math.log(1024)).to_i
284
+ exp = [exp, units.length - 1].min
285
+ "#{(bytes / (1024.0**exp)).round(2)} #{units[exp]}"
286
+ end
287
+
154
288
  def refresh_jetstream_context
155
289
  @jts = @nc.jetstream
156
290
  nc_ref = @nc
@@ -40,7 +40,6 @@ module JetstreamBridge
40
40
  log(:error, msg, tag: tag)
41
41
  end
42
42
 
43
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
44
43
  def sanitize_url(url)
45
44
  uri = URI.parse(url)
46
45
  return url unless uri.user || uri.password
@@ -67,6 +66,5 @@ module JetstreamBridge
67
66
  "#{scheme}://#{masked}@"
68
67
  end
69
68
  end
70
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
71
69
  end
72
70
  end
@@ -14,13 +14,11 @@ module JetstreamBridge
14
14
  defined?(ActiveRecord::Base) && klass <= ActiveRecord::Base
15
15
  end
16
16
 
17
- # rubocop:disable Naming/PredicatePrefix
18
17
  def has_columns?(klass, *cols)
19
18
  return false unless ar_class?(klass)
20
19
 
21
20
  cols.flatten.all? { |c| klass.column_names.include?(c.to_s) }
22
21
  end
23
- # rubocop:enable Naming/PredicatePrefix
24
22
 
25
23
  def assign_known_attrs(record, attrs)
26
24
  attrs.each do |k, v|
@@ -15,7 +15,6 @@ module JetstreamBridge
15
15
 
16
16
  class << self
17
17
  # Safe column presence check that never boots a connection during class load.
18
- # rubocop:disable Naming/PredicatePrefix
19
18
  def has_column?(name)
20
19
  return false unless ar_connected?
21
20
 
@@ -23,7 +22,6 @@ module JetstreamBridge
23
22
  rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
24
23
  false
25
24
  end
26
- # rubocop:enable Naming/PredicatePrefix
27
25
 
28
26
  def ar_connected?
29
27
  ActiveRecord::Base.connected? && connection_pool.active_connection?
@@ -15,7 +15,6 @@ module JetstreamBridge
15
15
 
16
16
  class << self
17
17
  # Safe column presence check that never boots a connection during class load.
18
- # rubocop:disable Naming/PredicatePrefix
19
18
  def has_column?(name)
20
19
  return false unless ar_connected?
21
20
 
@@ -23,7 +22,6 @@ module JetstreamBridge
23
22
  rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
24
23
  false
25
24
  end
26
- # rubocop:enable Naming/PredicatePrefix
27
25
 
28
26
  def ar_connected?
29
27
  # Avoid creating a connection; rescue if pool isn't set yet.
@@ -129,11 +129,15 @@ module JetstreamBridge
129
129
  def ensure_update(jts, name, info, desired_subjects)
130
130
  existing = StreamSupport.normalize_subjects(info.config.subjects || [])
131
131
  to_add = StreamSupport.missing_subjects(existing, desired_subjects)
132
- add_subjects(jts, name, existing, to_add) if to_add.any?
133
132
 
134
- # Retention is immutable; warn if different and do not include on update.
133
+ # Retention is immutable; if different, skip all updates to avoid 10052 error.
135
134
  have_ret = info.config.retention.to_s.downcase
136
- StreamSupport.log_retention_mismatch(name, have: have_ret, want: RETENTION) if have_ret != RETENTION
135
+ if have_ret != RETENTION
136
+ StreamSupport.log_retention_mismatch(name, have: have_ret, want: RETENTION)
137
+ return
138
+ end
139
+
140
+ add_subjects(jts, name, existing, to_add) if to_add.any?
137
141
 
138
142
  # Storage can be updated; do it without passing retention.
139
143
  have_storage = info.config.storage.to_s.downcase
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '4.0.1'
7
+ VERSION = '4.0.3'
8
8
  end
@@ -126,7 +126,7 @@ module JetstreamBridge
126
126
  stream_info = fetch_stream_info if connected
127
127
 
128
128
  {
129
- healthy: connected && stream_info[:exists],
129
+ healthy: connected && stream_info&.fetch(:exists, false),
130
130
  nats_connected: connected,
131
131
  connected_at: connected_at&.iso8601,
132
132
  stream: stream_info,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jetstream_bridge
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 4.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Attara