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 +4 -4
- data/CHANGELOG.md +42 -0
- data/lib/jetstream_bridge/consumer/inbox/inbox_message.rb +0 -4
- data/lib/jetstream_bridge/core/connection.rb +139 -5
- data/lib/jetstream_bridge/core/debug_helper.rb +24 -12
- data/lib/jetstream_bridge/core/logging.rb +0 -2
- data/lib/jetstream_bridge/core/model_utils.rb +0 -2
- data/lib/jetstream_bridge/models/inbox_event.rb +0 -2
- data/lib/jetstream_bridge/models/outbox_event.rb +0 -2
- data/lib/jetstream_bridge/topology/overlap_guard.rb +4 -1
- data/lib/jetstream_bridge/topology/stream.rb +8 -3
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +43 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d2af28b3492eb97a04b9337390f71f90786321baf7c50f3f49de3a2cbe1b5c3
|
|
4
|
+
data.tar.gz: 3746cb676e9739a087c295488d85b51a6acec82c4ffca81027726cf33a9ada53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
data/lib/jetstream_bridge.rb
CHANGED
|
@@ -115,21 +115,39 @@ module JetstreamBridge
|
|
|
115
115
|
Connection.jetstream
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
#
|
|
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
|
|
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:
|
|
288
|
-
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)
|