jetstream_bridge 4.0.2 → 4.0.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: fa4616ea064fcf04741b61ed07981980f47944e4cae10624605e5b5aed990990
4
- data.tar.gz: 5eacc8c962149d70e1b6b8dc05588fd6b568dbc6112cbf04eaa70895d0d3969f
3
+ metadata.gz: 3d2af28b3492eb97a04b9337390f71f90786321baf7c50f3f49de3a2cbe1b5c3
4
+ data.tar.gz: 3746cb676e9739a087c295488d85b51a6acec82c4ffca81027726cf33a9ada53
5
5
  SHA512:
6
- metadata.gz: 16f82dca9bc584f85b01ec9fc5934980c2ffd6be87a8eaa8691af5fb8902ce7c737eaf5ee638d41b0a62396870f4d51d9b0684015a959893b4c9c2677b6ae84b
7
- data.tar.gz: db6b1268c39dfe997ad95f805c9a2553655e75cb2004cc78c1ed09968797ce7a9668a1e6bbe24fa1c98877b5bb9986cce21e4da7212297a947b2c554e30fb876
6
+ metadata.gz: 91cff40e1f076eaee83bf937d10cbbcaaaf0e311fabb347e3ae757d7e7bd0645bcea338cd222cbfcb9d42c37deefd88076d618af2185572ca4cdeb4fa85dea27
7
+ data.tar.gz: 5fc680881fb91c550b8f6561babfd2227a3a9e263d3c07f1bd89a0de61fd835a8bf74b0731603384a16f078b401f22afbdbf31e94d62bac88ed1473f8c0aa15b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,48 @@ 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.4] - 2025-11-23
9
+
10
+ ### Fixed
11
+
12
+ - **NATS Compatibility** - Fix connection failure with nats-pure 2.5.0
13
+ - Handle both object-style and hash-style access for stream_info responses
14
+ - Fixes "undefined method 'streams' for Hash" error during connection establishment
15
+ - Adds compatibility checks using `respond_to?` for config and state attributes
16
+ - Updated 4 files: jetstream_bridge.rb, topology/stream.rb, topology/overlap_guard.rb, debug_helper.rb
17
+ - Maintains backward compatibility with older nats-pure versions
18
+
19
+ ## [4.0.3] - 2025-11-23
20
+
21
+ ### Added
22
+
23
+ - **Connection Validation** - Comprehensive NATS URL validation on initialization
24
+ - Validates URL format, scheme (nats/nats+tls), host presence, and port range (1-65535)
25
+ - Verifies NATS connection established after connect
26
+ - Verifies JetStream availability with account info check
27
+ - Helpful error messages for common misconfigurations
28
+ - All validation errors include specific guidance for fixes
29
+
30
+ - **Connection Logging** - Detailed logging throughout connection lifecycle
31
+ - Debug logs for URL validation progress and verification steps
32
+ - Info logs for successful validation and JetStream stats (streams, consumers, memory, storage)
33
+ - Error logs for all validation failures with specific details
34
+ - Automatic credential sanitization in all log messages
35
+ - Human-readable resource usage formatting (bytes to KB/MB/GB)
36
+
37
+ ### Fixed
38
+
39
+ - **Health Check** - Fixed `healthy` field returning `nil` instead of `false` when disconnected
40
+ - Changed `stream_info[:exists]` to `stream_info&.fetch(:exists, false)` for proper nil handling
41
+ - Ensures boolean values always returned for monitoring systems
42
+
43
+ ### Changed
44
+
45
+ - **Code Organization** - Moved all inline RuboCop rules to centralized `.rubocop.yml`
46
+ - Removed inline comments from 5 files (logging.rb, model_utils.rb, inbox_event.rb, outbox_event.rb, inbox_message.rb)
47
+ - Added exclusions to `.rubocop.yml` for metrics and naming rules
48
+ - Cleaner codebase with all style exceptions in one location
49
+
8
50
  ## [4.0.2] - 2025-11-23
9
51
 
10
52
  ### 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
@@ -81,18 +81,7 @@ module JetstreamBridge
81
81
  cfg = JetstreamBridge.config
82
82
  info = jts.stream_info(cfg.stream_name)
83
83
 
84
- {
85
- name: cfg.stream_name,
86
- exists: true,
87
- subjects: info.config.subjects,
88
- retention: info.config.retention,
89
- storage: info.config.storage,
90
- max_consumers: info.config.max_consumers,
91
- messages: info.state.messages,
92
- bytes: info.state.bytes,
93
- first_seq: info.state.first_seq,
94
- last_seq: info.state.last_seq
95
- }
84
+ build_stream_info(cfg, info)
96
85
  rescue StandardError => e
97
86
  {
98
87
  name: JetstreamBridge.config.stream_name,
@@ -101,6 +90,29 @@ module JetstreamBridge
101
90
  }
102
91
  end
103
92
 
93
+ def build_stream_info(cfg, info)
94
+ # Handle both object-style and hash-style access for compatibility
95
+ config_data = info.config
96
+ state_data = info.state
97
+
98
+ {
99
+ name: cfg.stream_name,
100
+ exists: true,
101
+ subjects: safe_attr(config_data, :subjects),
102
+ retention: safe_attr(config_data, :retention),
103
+ storage: safe_attr(config_data, :storage),
104
+ max_consumers: safe_attr(config_data, :max_consumers),
105
+ messages: safe_attr(state_data, :messages),
106
+ bytes: safe_attr(state_data, :bytes),
107
+ first_seq: safe_attr(state_data, :first_seq),
108
+ last_seq: safe_attr(state_data, :last_seq)
109
+ }
110
+ end
111
+
112
+ def safe_attr(obj, attr)
113
+ obj.respond_to?(attr) ? obj.public_send(attr) : obj[attr]
114
+ end
115
+
104
116
  def log_hash(hash, indent: 0)
105
117
  prefix = ' ' * indent
106
118
  hash.each do |key, value|
@@ -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.
@@ -48,7 +48,10 @@ module JetstreamBridge
48
48
  def list_streams_with_subjects(jts)
49
49
  list_stream_names(jts).map do |name|
50
50
  info = jts.stream_info(name)
51
- { name: name, subjects: Array(info.config.subjects || []) }
51
+ # Handle both object-style and hash-style access for compatibility
52
+ config_data = info.config
53
+ subjects = config_data.respond_to?(:subjects) ? config_data.subjects : config_data[:subjects]
54
+ { name: name, subjects: Array(subjects || []) }
52
55
  end
53
56
  end
54
57
 
@@ -127,11 +127,15 @@ module JetstreamBridge
127
127
  private
128
128
 
129
129
  def ensure_update(jts, name, info, desired_subjects)
130
- existing = StreamSupport.normalize_subjects(info.config.subjects || [])
130
+ # Handle both object-style and hash-style access for compatibility
131
+ config_data = info.config
132
+ subjects = config_data.respond_to?(:subjects) ? config_data.subjects : config_data[:subjects]
133
+ existing = StreamSupport.normalize_subjects(subjects || [])
131
134
  to_add = StreamSupport.missing_subjects(existing, desired_subjects)
132
135
 
133
136
  # Retention is immutable; if different, skip all updates to avoid 10052 error.
134
- have_ret = info.config.retention.to_s.downcase
137
+ retention = config_data.respond_to?(:retention) ? config_data.retention : config_data[:retention]
138
+ have_ret = retention.to_s.downcase
135
139
  if have_ret != RETENTION
136
140
  StreamSupport.log_retention_mismatch(name, have: have_ret, want: RETENTION)
137
141
  return
@@ -140,7 +144,8 @@ module JetstreamBridge
140
144
  add_subjects(jts, name, existing, to_add) if to_add.any?
141
145
 
142
146
  # Storage can be updated; do it without passing retention.
143
- have_storage = info.config.storage.to_s.downcase
147
+ storage = config_data.respond_to?(:storage) ? config_data.storage : config_data[:storage]
148
+ have_storage = storage.to_s.downcase
144
149
  if have_storage != STORAGE
145
150
  apply_update(jts, name, existing, storage: STORAGE)
146
151
  StreamSupport.log_config_updated(name, storage: STORAGE)
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '4.0.2'
7
+ VERSION = '4.0.4'
8
8
  end
@@ -115,21 +115,39 @@ module JetstreamBridge
115
115
  Connection.jetstream
116
116
  end
117
117
 
118
- # Health check for monitoring and readiness probes
118
+ # Active health check for monitoring and readiness probes
119
+ #
120
+ # Performs actual operations to verify system health:
121
+ # - Checks NATS connection (active: calls account_info API)
122
+ # - Verifies stream exists and is accessible (active: queries stream info)
123
+ # - Tests NATS round-trip communication (active: RTT measurement)
119
124
  #
120
125
  # @return [Hash] Health status including NATS connection, stream, and version
121
126
  def health_check
127
+ start_time = Time.now
122
128
  conn_instance = Connection.instance
129
+
130
+ # Active check: calls @jts.account_info internally
123
131
  connected = conn_instance.connected?
124
132
  connected_at = conn_instance.connected_at
125
133
 
134
+ # Active check: queries actual stream from NATS server
126
135
  stream_info = fetch_stream_info if connected
127
136
 
137
+ # Active check: measure NATS round-trip time
138
+ rtt_ms = measure_nats_rtt if connected
139
+
140
+ health_check_duration_ms = ((Time.now - start_time) * 1000).round(2)
141
+
128
142
  {
129
- healthy: connected && stream_info[:exists],
143
+ healthy: connected && stream_info&.fetch(:exists, false),
130
144
  nats_connected: connected,
131
145
  connected_at: connected_at&.iso8601,
132
146
  stream: stream_info,
147
+ performance: {
148
+ nats_rtt_ms: rtt_ms,
149
+ health_check_duration_ms: health_check_duration_ms
150
+ },
133
151
  config: {
134
152
  env: config.env,
135
153
  app_name: config.app_name,
@@ -281,11 +299,18 @@ module JetstreamBridge
281
299
  def fetch_stream_info
282
300
  jts = Connection.jetstream
283
301
  info = jts.stream_info(config.stream_name)
302
+
303
+ # Handle both object-style and hash-style access for compatibility
304
+ config_data = info.config
305
+ state_data = info.state
306
+ subjects = config_data.respond_to?(:subjects) ? config_data.subjects : config_data[:subjects]
307
+ messages = state_data.respond_to?(:messages) ? state_data.messages : state_data[:messages]
308
+
284
309
  {
285
310
  exists: true,
286
311
  name: config.stream_name,
287
- subjects: info.config.subjects,
288
- messages: info.state.messages
312
+ subjects: subjects,
313
+ messages: messages
289
314
  }
290
315
  rescue StandardError => e
291
316
  {
@@ -295,6 +320,20 @@ module JetstreamBridge
295
320
  }
296
321
  end
297
322
 
323
+ def measure_nats_rtt
324
+ # Measure round-trip time using NATS RTT method
325
+ nc = Connection.nc
326
+ start = Time.now
327
+ nc.rtt
328
+ ((Time.now - start) * 1000).round(2)
329
+ rescue StandardError => e
330
+ Logging.warn(
331
+ "Failed to measure NATS RTT: #{e.class} #{e.message}",
332
+ tag: 'JetstreamBridge'
333
+ )
334
+ nil
335
+ end
336
+
298
337
  def assign!(cfg, key, val)
299
338
  setter = :"#{key}="
300
339
  raise ArgumentError, "Unknown configuration option: #{key}" unless cfg.respond_to?(setter)
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.2
4
+ version: 4.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Attara