nats_wave 1.1.7 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c6127f961d72ff48417d654e302d3bf4f7899071660920554e4184b0ac39b53
4
- data.tar.gz: 7e74bea372edd1f978f15e9d31ec3ce22f6b2c2a3d6af220ac17731d9e4d97d4
3
+ metadata.gz: 26e26c369d6d7d10d4f5c7bcd2f3cc9ba5560b759ff1c4478105c9e6e66853c7
4
+ data.tar.gz: 9786a3a34f636185b056ca4aed597efc69cd1e07897d57a10636fc51b754b3f3
5
5
  SHA512:
6
- metadata.gz: 05a61fe3dbcc2ca4d64a33de79296c609f32bfc752c6436ae79950b4c0479d121687b86410359e2d82fa39fc3330cae3a0b8b2f77c8be304f9212dcc3e060224
7
- data.tar.gz: 4e4e430505cd8dbf36586da200e5c6cdbf5b3fc086c484a88b90ad88dcd800fe846e0cc349ae68be0e281a4002984a3f5d736107d99253946a7649b97eae8902
6
+ metadata.gz: 10861846ae79ef25b5986752d5caa09a126c1b8d803f1a60df7cf95e2b2b4a288db150216cb827b2018ec7a2ea902188617dbd4beb1da7942ef0c6f052537e1c
7
+ data.tar.gz: 4e4bdf076e56692299090ad6f9f5dd22387a821f7795f4c5ebc4dc320dc864b5d9159b3a92747ca4b9bf6f2fbab981707d48056af9741a3c1c36ad9df7373032
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nats_wave (1.1.7)
4
+ nats_wave (1.1.8)
5
5
  activerecord (>= 6.1, < 8.0)
6
6
  activesupport (>= 6.1, < 8.0)
7
7
  concurrent-ruby (~> 1.1)
@@ -77,7 +77,7 @@ module NatsWave
77
77
  {
78
78
  service: @service_name,
79
79
  environment: ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'unknown',
80
- version: ENV['APP_VERSION'] || '1.1.7'
80
+ version: ENV['APP_VERSION'] || '1.1.8'
81
81
  }
82
82
  end
83
83
 
@@ -609,27 +609,90 @@ module NatsWave
609
609
  end
610
610
  end
611
611
 
612
+ def establish_nats_connection
613
+ NatsWave.logger.info "Attempting to connect to NATS at #{@config.nats_url}"
614
+
615
+ @client = NATS.connect(
616
+ @config.nats_url,
617
+ reconnect_time_wait: @config.retry_delay,
618
+ max_reconnect_attempts: @config.reconnect_attempts,
619
+ ping_interval: 20,
620
+ max_outstanding_pings: 2,
621
+ disconnected_cb: proc do |reason|
622
+ NatsWave.logger.warn "🔌 NATS disconnected: #{reason}"
623
+ end,
624
+ reconnected_cb: proc do
625
+ NatsWave.logger.info "🔌 NATS auto-reconnected"
626
+ end,
627
+ error_cb: proc do |error|
628
+ NatsWave.logger.error "🔌 NATS error: #{error}"
629
+ end
630
+ )
631
+
632
+ # Verify the connection actually worked
633
+ if @client&.connected?
634
+ NatsWave.logger.info "✅ Successfully connected to NATS at #{@config.nats_url}"
635
+ else
636
+ NatsWave.logger.error "❌ NATS client created but not connected"
637
+ @client = nil
638
+ end
639
+
640
+ rescue => e
641
+ NatsWave.logger.error "❌ Failed to connect to NATS: #{e.message}"
642
+ NatsWave.logger.error "NATS URL: #{@config.nats_url}"
643
+ NatsWave.logger.error "Error class: #{e.class}"
644
+
645
+ @client = nil
646
+
647
+ # Re-raise in development to catch issues early
648
+ if defined?(Rails) && Rails.env.development?
649
+ raise ConnectionError, "Failed to connect to NATS: #{e.message}"
650
+ end
651
+ end
652
+
612
653
  # def establish_nats_connection
613
654
  # NatsWave.logger.info "Attempting to connect to NATS at #{@config.nats_url}"
614
655
  #
615
- # @client = NATS.connect(
616
- # @config.nats_url,
656
+ # # Add more detailed connection options for debugging
657
+ # connection_options = {
617
658
  # reconnect_time_wait: @config.retry_delay,
618
- # max_reconnect_attempts: @config.reconnect_attempts
619
- # )
659
+ # max_reconnect_attempts: @config.reconnect_attempts,
660
+ # dont_randomize_servers: true,
661
+ # verbose: true,
662
+ # pedantic: false
663
+ # }
664
+ #
665
+ # NatsWave.logger.info "Connection options: #{connection_options}"
666
+ #
667
+ # @client = NATS.connect(@config.nats_url, connection_options)
620
668
  #
621
- # # Verify the connection actually worked
622
- # if @client&.connected?
623
- # NatsWave.logger.info "✅ Successfully connected to NATS at #{@config.nats_url}"
669
+ # # Wait a moment for connection to establish
670
+ # sleep 0.5
671
+ #
672
+ # # Check connection status with more detail
673
+ # if @client
674
+ # NatsWave.logger.info "NATS client created: #{@client.class}"
675
+ # NatsWave.logger.info "NATS client connected?: #{@client.connected?}"
676
+ # NatsWave.logger.info "NATS client status: #{@client.status rescue 'unknown'}"
677
+ #
678
+ # if @client.connected?
679
+ # NatsWave.logger.info "✅ Successfully connected to NATS at #{@config.nats_url}"
680
+ # else
681
+ # NatsWave.logger.error "❌ NATS client created but not connected"
682
+ # NatsWave.logger.error "Client last error: #{@client.last_error rescue 'none'}"
683
+ # @client = nil
684
+ # end
624
685
  # else
625
- # NatsWave.logger.error "❌ NATS client created but not connected"
626
- # @client = nil
686
+ # NatsWave.logger.error "❌ NATS client is nil after connection attempt"
627
687
  # end
628
688
  #
629
689
  # rescue => e
630
690
  # NatsWave.logger.error "❌ Failed to connect to NATS: #{e.message}"
631
- # NatsWave.logger.error "NATS URL: #{@config.nats_url}"
632
691
  # NatsWave.logger.error "Error class: #{e.class}"
692
+ # NatsWave.logger.error "Error backtrace: #{e.backtrace.first(5).join('\n')}"
693
+ # NatsWave.logger.error "NATS URL: #{@config.nats_url}"
694
+ # NatsWave.logger.error "Retry delay: #{@config.retry_delay}"
695
+ # NatsWave.logger.error "Max reconnect attempts: #{@config.reconnect_attempts}"
633
696
  #
634
697
  # @client = nil
635
698
  #
@@ -639,58 +702,6 @@ module NatsWave
639
702
  # end
640
703
  # end
641
704
 
642
- def establish_nats_connection
643
- NatsWave.logger.info "Attempting to connect to NATS at #{@config.nats_url}"
644
-
645
- # Add more detailed connection options for debugging
646
- connection_options = {
647
- reconnect_time_wait: @config.retry_delay,
648
- max_reconnect_attempts: @config.reconnect_attempts,
649
- dont_randomize_servers: true,
650
- verbose: true,
651
- pedantic: false
652
- }
653
-
654
- NatsWave.logger.info "Connection options: #{connection_options}"
655
-
656
- @client = NATS.connect(@config.nats_url, connection_options)
657
-
658
- # Wait a moment for connection to establish
659
- sleep 0.5
660
-
661
- # Check connection status with more detail
662
- if @client
663
- NatsWave.logger.info "NATS client created: #{@client.class}"
664
- NatsWave.logger.info "NATS client connected?: #{@client.connected?}"
665
- NatsWave.logger.info "NATS client status: #{@client.status rescue 'unknown'}"
666
-
667
- if @client.connected?
668
- NatsWave.logger.info "✅ Successfully connected to NATS at #{@config.nats_url}"
669
- else
670
- NatsWave.logger.error "❌ NATS client created but not connected"
671
- NatsWave.logger.error "Client last error: #{@client.last_error rescue 'none'}"
672
- @client = nil
673
- end
674
- else
675
- NatsWave.logger.error "❌ NATS client is nil after connection attempt"
676
- end
677
-
678
- rescue => e
679
- NatsWave.logger.error "❌ Failed to connect to NATS: #{e.message}"
680
- NatsWave.logger.error "Error class: #{e.class}"
681
- NatsWave.logger.error "Error backtrace: #{e.backtrace.first(5).join('\n')}"
682
- NatsWave.logger.error "NATS URL: #{@config.nats_url}"
683
- NatsWave.logger.error "Retry delay: #{@config.retry_delay}"
684
- NatsWave.logger.error "Max reconnect attempts: #{@config.reconnect_attempts}"
685
-
686
- @client = nil
687
-
688
- # Re-raise in development to catch issues early
689
- if defined?(Rails) && Rails.env.development?
690
- raise ConnectionError, "Failed to connect to NATS: #{e.message}"
691
- end
692
- end
693
-
694
705
  def ensure_connected!
695
706
  unless connected?
696
707
  error_msg = if @client.nil?
@@ -18,7 +18,7 @@ module NatsWave
18
18
  def initialize(options = {})
19
19
  @nats_url = ENV['NATS_URL'] || "nats://localhost:4222"
20
20
  @service_name = ENV['NATS_SERVICE_NAME'] || "purplewave"
21
- @version = ENV['NATS_SERVICE_VERSION'] || "1.1.7"
21
+ @version = ENV['NATS_SERVICE_VERSION'] || "1.1.8"
22
22
  @instance_id = ENV['NATS_INSTANCE_ID'] || Socket.gethostname
23
23
  @database_url = ENV['NATS_DATABASE_URL'] || nil
24
24
  @connection_pool_size = (ENV['NATS_CONNECTION_POOL_SIZE'] || 10).to_i
@@ -7,6 +7,7 @@ module NatsWave
7
7
  def initialize(config, client, middleware_stack = [])
8
8
  @config = config
9
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)
@@ -16,6 +17,17 @@ module NatsWave
16
17
  @nats_subscriptions = [] # NATS::Subscription objects
17
18
  @running = false
18
19
  @shutdown = false
20
+ @reconnecting = false
21
+ @connection_stats = {
22
+ reconnect_count: 0,
23
+ last_successful_connection: Time.current,
24
+ consecutive_failures: 0
25
+ }
26
+ end
27
+
28
+ # Add method to store client factory for reconnection
29
+ def set_client_factory(factory_proc)
30
+ @original_client_factory = factory_proc
19
31
  end
20
32
 
21
33
  def begin
@@ -27,12 +39,7 @@ module NatsWave
27
39
 
28
40
  NatsWave.logger.info "Starting NATS subscriber for #{@config.service_name}"
29
41
 
30
- # Use ModelRegistry subscriptions
31
- @registry_subscriptions.each do |subscription|
32
- subscription[:subjects].each do |subject|
33
- subscribe_to_subject(subject, subscription[:handler])
34
- end
35
- end
42
+ setup_subscriptions
36
43
 
37
44
  NatsWave.logger.info "Started #{@registry_subscriptions.size} subscriptions from Model Registry"
38
45
 
@@ -56,18 +63,12 @@ module NatsWave
56
63
  # Stop keep alive thread
57
64
  if @keep_alive_thread&.alive?
58
65
  @keep_alive_thread.kill
66
+ @keep_alive_thread.join(5) # Wait up to 5 seconds for graceful shutdown
59
67
  @keep_alive_thread = nil
60
68
  end
61
69
 
62
70
  # Unsubscribe from all subscriptions
63
- @nats_subscriptions.each do |subscription|
64
- begin
65
- subscription.unsubscribe if subscription.respond_to?(:unsubscribe)
66
- rescue => e
67
- NatsWave.logger.error "Error unsubscribing: #{e.message}"
68
- end
69
- end
70
- @nats_subscriptions.clear
71
+ cleanup_subscriptions
71
72
 
72
73
  NatsWave.logger.info "🛑 Subscriber shutdown complete"
73
74
  end
@@ -82,27 +83,60 @@ module NatsWave
82
83
 
83
84
  private
84
85
 
85
- def subscribe_to_subject(subject_pattern, custom_handler = nil, model_mappings = {})
86
- NatsWave.logger.info "🔍 Attempting to subscribe to: #{subject_pattern}"
86
+ def setup_subscriptions
87
+ return unless @client&.connected?
88
+
89
+ # Clear existing subscriptions first
90
+ cleanup_subscriptions
87
91
 
88
- # Create the NATS subscription
89
- nats_subscription = @client.subscribe(
90
- subject_pattern,
91
- queue: @config.queue_group
92
- ) do |msg|
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|
93
102
  begin
94
- NatsWave.logger.debug "📨 Received message on #{msg.subject}"
95
- process_message(msg.data, custom_handler, model_mappings)
103
+ subscription.unsubscribe if subscription.respond_to?(:unsubscribe)
96
104
  rescue => e
97
- NatsWave.logger.error "Error in subscription handler: #{e.message}"
98
- NatsWave.logger.error e.backtrace.join("\n")
99
- # Don't re-raise - this would kill the subscription
105
+ NatsWave.logger.error "Error unsubscribing: #{e.message}"
100
106
  end
101
107
  end
108
+ @nats_subscriptions.clear
109
+ end
110
+
111
+ def subscribe_to_subject(subject_pattern, custom_handler = nil, model_mappings = {})
112
+ return unless @client&.connected?
113
+
114
+ NatsWave.logger.info "🔍 Attempting to subscribe to: #{subject_pattern}"
115
+
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})"
102
135
 
103
- # Add to NATS subscriptions array
104
- @nats_subscriptions << nats_subscription
105
- 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
106
140
  end
107
141
 
108
142
  def process_message(raw_message, custom_handler, model_mappings)
@@ -141,52 +175,166 @@ module NatsWave
141
175
  end
142
176
 
143
177
  def keep_alive
144
- Rails.logger.info "🔄 Starting keep-alive thread for persistent JetStream connection"
178
+ NatsWave.logger.info "🔄 Starting keep-alive thread for persistent JetStream connection"
145
179
 
146
180
  @keep_alive_thread = Thread.new do
147
181
  while @running && !@shutdown
148
182
  begin
149
183
  sleep 30 # Check every 30 seconds
150
184
 
151
- if @client&.connected?
152
- Rails.logger.debug "💓 Subscriber connection healthy - #{@nats_subscriptions.size} active subscriptions"
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
153
191
  else
154
- Rails.logger.error "❌ Subscriber connection lost! Attempting reconnection..."
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
155
198
  attempt_reconnection
156
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
157
206
 
158
207
  rescue => e
159
- Rails.logger.error "Error in keep_alive thread: #{e.message}"
208
+ NatsWave.logger.error "Error in keep_alive thread: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
160
209
  sleep 10
161
210
  end
162
211
  end
163
212
 
164
- Rails.logger.info "🛑 Keep-alive thread shutting down"
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}"
165
253
  end
166
254
  end
167
255
 
168
256
  def attempt_reconnection
169
- return if @shutdown
257
+ return if @shutdown || @reconnecting
170
258
 
171
- NatsWave.logger.info "🔄 Attempting to reconnect to NATS..."
259
+ @reconnecting = true
260
+ @connection_stats[:reconnect_count] += 1
172
261
 
173
- # Reset subscriptions
174
- @nats_subscriptions.clear
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
175
303
 
176
- # Try to reestablish subscriptions
177
- sleep 5 # Wait before reconnecting
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"
178
314
 
179
- if @client&.connected?
180
- NatsWave.logger.info "✅ NATS reconnected, reestablishing subscriptions"
315
+ begin
316
+ # Force close everything
317
+ cleanup_subscriptions
318
+ @client&.close rescue nil
181
319
 
182
- @registry_subscriptions.each do |subscription|
183
- subscription[:subjects].each do |subject|
184
- subscribe_to_subject(subject, subscription[:handler])
185
- end
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?
186
333
  end
334
+
335
+ rescue => e
336
+ NatsWave.logger.error "Full reset failed: #{e.message}"
187
337
  end
188
- rescue => e
189
- NatsWave.logger.error "Failed to reconnect: #{e.message}"
190
338
  end
191
339
 
192
340
  # ... rest of your existing methods (parse_message, handle_model_sync, handle_error)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NatsWave
4
- VERSION = '1.1.7'
4
+ VERSION = '1.1.8'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nats_wave
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.7
4
+ version: 1.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeffrey Dabo