nats_wave 1.1.5 → 1.1.8

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.
@@ -2,40 +2,52 @@
2
2
 
3
3
  module NatsWave
4
4
  class Subscriber
5
- attr_reader :config, :nats_client
5
+ attr_reader :config, :client
6
6
 
7
- def initialize(config, nats_client, middleware_stack = [])
7
+ def initialize(config, client, middleware_stack = [])
8
8
  @config = config
9
- @nats_client = nats_client
9
+ @client = client
10
+ @original_client_factory = nil # Store how to recreate the client
10
11
  @database_connector = DatabaseConnector.new(config)
11
12
  @model_mapper = ModelMapper.new(config)
12
13
  @message_transformer = MessageTransformer.new(config)
13
14
  @dead_letter_queue = DeadLetterQueue.new(config)
14
15
 
15
- # Separate the two types of subscriptions
16
- @registry_subscriptions = ModelRegistry.subscriptions # Hash objects
17
- @nats_subscriptions = [] # NATS::Subscription objects
16
+ @registry_subscriptions = ModelRegistry.subscriptions
17
+ @nats_subscriptions = [] # NATS::Subscription objects
18
18
  @running = false
19
+ @shutdown = false
20
+ @reconnecting = false
21
+ @connection_stats = {
22
+ reconnect_count: 0,
23
+ last_successful_connection: Time.current,
24
+ consecutive_failures: 0
25
+ }
19
26
  end
20
27
 
21
- def start
22
- return if @running
28
+ # Add method to store client factory for reconnection
29
+ def set_client_factory(factory_proc)
30
+ @original_client_factory = factory_proc
31
+ end
32
+
33
+ def begin
34
+ return if @running || @shutdown
23
35
  return unless @config.subscription_enabled
24
36
 
25
37
  @running = true
38
+ @shutdown = false
39
+
26
40
  NatsWave.logger.info "Starting NATS subscriber for #{@config.service_name}"
27
41
 
28
- # Use ModelRegistry subscriptions
29
- @registry_subscriptions.each do |subscription|
30
- subscription[:subjects].each do |subject|
31
- subscribe_to_subject(subject, subscription[:handler])
32
- end
33
- end
42
+ setup_subscriptions
34
43
 
35
- NatsWave.logger.info "Started #{@registry_subscriptions.size} subscriptions from ModelRegistry"
44
+ NatsWave.logger.info "Started #{@registry_subscriptions.size} subscriptions from Model Registry"
45
+
46
+ # Keep the subscriber alive
47
+ keep_alive
36
48
  end
37
49
 
38
- def subscribe(subjects:, model_mappings: {}, handler: nil)
50
+ def listen(subjects:, model_mappings: {}, handler: nil)
39
51
  subjects = Array(subjects)
40
52
 
41
53
  subjects.each do |subject|
@@ -44,11 +56,21 @@ module NatsWave
44
56
  end
45
57
  end
46
58
 
47
- def unsubscribe_all
48
- # Unsubscribe from NATS subscriptions (the actual NATS::Subscription objects)
49
- @nats_subscriptions.each(&:unsubscribe)
50
- @nats_subscriptions.clear
59
+ def reset
60
+ @shutdown = true
51
61
  @running = false
62
+
63
+ # Stop keep alive thread
64
+ if @keep_alive_thread&.alive?
65
+ @keep_alive_thread.kill
66
+ @keep_alive_thread.join(5) # Wait up to 5 seconds for graceful shutdown
67
+ @keep_alive_thread = nil
68
+ end
69
+
70
+ # Unsubscribe from all subscriptions
71
+ cleanup_subscriptions
72
+
73
+ NatsWave.logger.info "🛑 Subscriber shutdown complete"
52
74
  end
53
75
 
54
76
  def database_connected?
@@ -56,29 +78,72 @@ module NatsWave
56
78
  end
57
79
 
58
80
  def disconnect
59
- unsubscribe_all
81
+ reset
60
82
  end
61
83
 
62
84
  private
63
85
 
86
+ def setup_subscriptions
87
+ return unless @client&.connected?
88
+
89
+ # Clear existing subscriptions first
90
+ cleanup_subscriptions
91
+
92
+ # Use ModelRegistry subscriptions
93
+ @registry_subscriptions.each do |subscription|
94
+ subscription[:subjects].each do |subject|
95
+ subscribe_to_subject(subject, subscription[:handler])
96
+ end
97
+ end
98
+ end
99
+
100
+ def cleanup_subscriptions
101
+ @nats_subscriptions.each do |subscription|
102
+ begin
103
+ subscription.unsubscribe if subscription.respond_to?(:unsubscribe)
104
+ rescue => e
105
+ NatsWave.logger.error "Error unsubscribing: #{e.message}"
106
+ end
107
+ end
108
+ @nats_subscriptions.clear
109
+ end
110
+
64
111
  def subscribe_to_subject(subject_pattern, custom_handler = nil, model_mappings = {})
112
+ return unless @client&.connected?
113
+
65
114
  NatsWave.logger.info "🔍 Attempting to subscribe to: #{subject_pattern}"
66
115
 
67
- # Create the NATS subscription
68
- nats_subscription = @nats_client.subscribe(
69
- subject_pattern,
70
- queue: @config.queue_group
71
- ) do |msg|
72
- NatsWave.logger.info "📨 Received message on #{msg.subject}: #{msg.data}"
73
- process_message(msg.data, custom_handler, model_mappings)
74
- end
116
+ begin
117
+ # Create the NATS subscription
118
+ nats_subscription = @client.subscribe(
119
+ subject_pattern,
120
+ queue: @config.queue_group
121
+ ) do |msg|
122
+ begin
123
+ NatsWave.logger.debug "📨 Received message on #{msg.subject}"
124
+ process_message(msg.data, custom_handler, model_mappings)
125
+ rescue => e
126
+ NatsWave.logger.error "Error in subscription handler: #{e.message}"
127
+ NatsWave.logger.error e.backtrace.join("\n")
128
+ # Don't re-raise - this would kill the subscription
129
+ end
130
+ end
131
+
132
+ # Add to NATS subscriptions array
133
+ @nats_subscriptions << nats_subscription
134
+ NatsWave.logger.info "✅ Successfully subscribed to #{subject_pattern} (total: #{@nats_subscriptions.size})"
75
135
 
76
- # Add to NATS subscriptions array (not the registry subscriptions)
77
- @nats_subscriptions << nats_subscription
78
- NatsWave.logger.info "✅ Successfully subscribed to #{subject_pattern} (total: #{@nats_subscriptions.size})"
136
+ rescue => e
137
+ NatsWave.logger.error "Failed to subscribe to #{subject_pattern}: #{e.message}"
138
+ raise
139
+ end
79
140
  end
80
141
 
81
142
  def process_message(raw_message, custom_handler, model_mappings)
143
+ return unless should_process_message?(raw_message)
144
+
145
+ NatsWave.logger.debug "🔄 Processing message: #{raw_message[0..200]}..."
146
+
82
147
  message = parse_message(raw_message)
83
148
 
84
149
  if custom_handler
@@ -87,11 +152,192 @@ module NatsWave
87
152
  handle_model_sync(message, model_mappings)
88
153
  end
89
154
 
90
- NatsWave.logger.debug('Successfully processed message')
155
+ NatsWave.logger.debug('Successfully processed message')
91
156
  rescue StandardError => e
92
157
  handle_error(e, raw_message, message)
158
+ # Don't re-raise - this would kill the subscription
159
+ end
160
+
161
+ def should_process_message?(raw_message_data)
162
+ message = JSON.parse(raw_message_data)
163
+ source = message['source'] || {}
164
+
165
+ # Skip messages from same service instance
166
+ if source['service'] == @config.service_name && source['instance_id'] == @config.instance_id
167
+ NatsWave.logger.debug "🔄 Skipping message from same service instance"
168
+ return false
169
+ end
170
+
171
+ true
172
+ rescue JSON::ParserError => e
173
+ NatsWave.logger.error "Failed to parse message for filtering: #{e.message}"
174
+ false
175
+ end
176
+
177
+ def keep_alive
178
+ NatsWave.logger.info "🔄 Starting keep-alive thread for persistent JetStream connection"
179
+
180
+ @keep_alive_thread = Thread.new do
181
+ while @running && !@shutdown
182
+ begin
183
+ sleep 30 # Check every 30 seconds
184
+
185
+ connection_healthy = check_connection_health
186
+
187
+ if connection_healthy
188
+ NatsWave.logger.debug "💓 Subscriber connection healthy - #{@nats_subscriptions.size} active subscriptions"
189
+ @connection_stats[:consecutive_failures] = 0
190
+ @connection_stats[:last_successful_connection] = Time.current
191
+ else
192
+ @connection_stats[:consecutive_failures] += 1
193
+ time_since_last_good = Time.current - @connection_stats[:last_successful_connection]
194
+
195
+ NatsWave.logger.error "❌ Subscriber connection lost! (failure ##{@connection_stats[:consecutive_failures]}, last good: #{time_since_last_good.to_i}s ago)"
196
+
197
+ log_connection_diagnostics
198
+ attempt_reconnection
199
+ end
200
+
201
+ # If too many consecutive failures, escalate
202
+ if @connection_stats[:consecutive_failures] > 5
203
+ NatsWave.logger.error "🚨 Too many consecutive failures. Performing full reset."
204
+ perform_full_reset
205
+ end
206
+
207
+ rescue => e
208
+ NatsWave.logger.error "Error in keep_alive thread: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
209
+ sleep 10
210
+ end
211
+ end
212
+
213
+ NatsWave.logger.info "🛑 Keep-alive thread shutting down"
214
+ end
215
+ end
216
+
217
+ def check_connection_health
218
+ return false unless @client
219
+
220
+ begin
221
+ # First check basic connectivity
222
+ return false unless @client.connected?
223
+
224
+ # Try to perform a simple operation with timeout
225
+ Timeout.timeout(5) do
226
+ @client.flush
227
+ return true
228
+ end
229
+ rescue Timeout::Error
230
+ NatsWave.logger.warn "⏰ Connection health check timed out"
231
+ return false
232
+ rescue => e
233
+ NatsWave.logger.warn "🔍 Health check failed: #{e.message}"
234
+ return false
235
+ end
236
+ end
237
+
238
+ def log_connection_diagnostics
239
+ begin
240
+ NatsWave.logger.info "🔍 Connection Diagnostics:"
241
+ NatsWave.logger.info " - Client connected?: #{@client&.connected? rescue 'unknown'}"
242
+ NatsWave.logger.info " - Active subscriptions: #{@nats_subscriptions.size}"
243
+ NatsWave.logger.info " - Reconnect count: #{@connection_stats[:reconnect_count]}"
244
+ NatsWave.logger.info " - Thread count: #{Thread.list.count}"
245
+ NatsWave.logger.info " - Last successful: #{@connection_stats[:last_successful_connection]}"
246
+
247
+ # Check if subscriptions are actually valid
248
+ valid_subs = @nats_subscriptions.count { |sub| sub.respond_to?(:unsubscribe) }
249
+ NatsWave.logger.info " - Valid subscriptions: #{valid_subs}/#{@nats_subscriptions.size}"
250
+
251
+ rescue => e
252
+ NatsWave.logger.error "Failed to gather diagnostics: #{e.message}"
253
+ end
254
+ end
255
+
256
+ def attempt_reconnection
257
+ return if @shutdown || @reconnecting
258
+
259
+ @reconnecting = true
260
+ @connection_stats[:reconnect_count] += 1
261
+
262
+ begin
263
+ NatsWave.logger.info "🔄 Attempting to reconnect to NATS... (attempt ##{@connection_stats[:reconnect_count]})"
264
+
265
+ # First, clean up existing connection and subscriptions
266
+ cleanup_subscriptions
267
+
268
+ # Close the existing client connection
269
+ begin
270
+ @client&.close if @client&.connected?
271
+ rescue => e
272
+ NatsWave.logger.debug "Error closing existing connection: #{e.message}"
273
+ end
274
+
275
+ # Wait before attempting to reconnect
276
+ sleep_time = [2 ** [@connection_stats[:consecutive_failures] - 1, 4].min, 30].min
277
+ NatsWave.logger.debug "⏱️ Waiting #{sleep_time}s before reconnection attempt"
278
+ sleep sleep_time
279
+
280
+ # Attempt to recreate the client connection
281
+ if @original_client_factory
282
+ @client = @original_client_factory.call
283
+ NatsWave.logger.info "🔄 Created new NATS client using factory"
284
+ else
285
+ NatsWave.logger.warn "⚠️ No client factory available, hoping existing client recovers"
286
+ end
287
+
288
+ # Check if we have a working connection
289
+ if @client&.connected?
290
+ NatsWave.logger.info "✅ NATS client reconnected successfully"
291
+
292
+ # Re-establish all subscriptions
293
+ setup_subscriptions
294
+
295
+ NatsWave.logger.info "✅ Subscriptions restored (#{@nats_subscriptions.size} active)"
296
+ @connection_stats[:consecutive_failures] = 0
297
+ @connection_stats[:last_successful_connection] = Time.current
298
+
299
+ return true
300
+ else
301
+ raise "Connection failed - client not connected"
302
+ end
303
+
304
+ rescue => e
305
+ NatsWave.logger.error "Reconnection failed: #{e.message}"
306
+ return false
307
+ ensure
308
+ @reconnecting = false
309
+ end
310
+ end
311
+
312
+ def perform_full_reset
313
+ NatsWave.logger.info "🔄 Performing full connection reset"
314
+
315
+ begin
316
+ # Force close everything
317
+ cleanup_subscriptions
318
+ @client&.close rescue nil
319
+
320
+ # Clear connection stats
321
+ @connection_stats[:consecutive_failures] = 0
322
+
323
+ # Force garbage collection
324
+ GC.start
325
+
326
+ # Wait longer before attempting reset
327
+ sleep 10
328
+
329
+ # Try to recreate everything
330
+ if @original_client_factory
331
+ @client = @original_client_factory.call
332
+ setup_subscriptions if @client&.connected?
333
+ end
334
+
335
+ rescue => e
336
+ NatsWave.logger.error "Full reset failed: #{e.message}"
337
+ end
93
338
  end
94
339
 
340
+ # ... rest of your existing methods (parse_message, handle_model_sync, handle_error)
95
341
  def parse_message(raw_message)
96
342
  @message_transformer.parse_message(raw_message)
97
343
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NatsWave
4
- VERSION = '1.1.5'
4
+ VERSION = '1.1.8'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nats_wave
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.5
4
+ version: 1.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeffrey Dabo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-18 00:00:00.000000000 Z
11
+ date: 2025-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nats-pure