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.
- checksums.yaml +4 -4
- data/.idea/nats_wave.iml +5 -5
- data/Gemfile.lock +1 -1
- data/README.md +1153 -401
- data/lib/generators/nats_wave/templates/initializer.rb +76 -19
- data/lib/nats_wave/adapters/datadog_metrics.rb +1 -1
- data/lib/nats_wave/client.rb +646 -26
- data/lib/nats_wave/concerns/mappable.rb +9 -3
- data/lib/nats_wave/configuration.rb +2 -2
- data/lib/nats_wave/dead_letter_queue.rb +4 -4
- data/lib/nats_wave/model_mapper.rb +1 -1
- data/lib/nats_wave/model_registry.rb +39 -15
- data/lib/nats_wave/publisher.rb +50 -47
- data/lib/nats_wave/subscriber.rb +279 -33
- data/lib/nats_wave/version.rb +1 -1
- metadata +2 -2
data/lib/nats_wave/subscriber.rb
CHANGED
@@ -2,40 +2,52 @@
|
|
2
2
|
|
3
3
|
module NatsWave
|
4
4
|
class Subscriber
|
5
|
-
attr_reader :config, :
|
5
|
+
attr_reader :config, :client
|
6
6
|
|
7
|
-
def initialize(config,
|
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)
|
13
14
|
@dead_letter_queue = DeadLetterQueue.new(config)
|
14
15
|
|
15
|
-
|
16
|
-
@
|
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
|
-
|
22
|
-
|
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
|
-
|
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
|
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
|
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
|
48
|
-
|
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
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
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
|
data/lib/nats_wave/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2025-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nats-pure
|