nats_wave 1.1.20 → 1.2.1
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/lib/nats_wave/adapters/active_record.rb +7 -1
- data/lib/nats_wave/adapters/datadog_metrics.rb +1 -1
- data/lib/nats_wave/concerns/mappable.rb +26 -11
- data/lib/nats_wave/configuration.rb +1 -1
- data/lib/nats_wave/subscriber.rb +38 -396
- data/lib/nats_wave/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8cfe71b3c054507c991ba39dfe44cfd275ceec15143053c92278b6dad000243a
|
4
|
+
data.tar.gz: 18fe41af6bfb86c573167e048fd9604e248fd9dec3a1e4d3f4ebf62ec3a9bf20
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54010d3177f0be905380f3b932fa0546cfd31665c6da161a2f25b34234948506750b62cc61e97aed931d5367c27cbfcee6b60e42bffc23296df2c49bec9556c8
|
7
|
+
data.tar.gz: 87ea4a45236016e1ceceb0a3386feaaff09707532b5a8937a83296da5b34617f4259dea7f1c2a08477f6cccbc6ba854d4d3ccee1c78c2264ac9d34682d2dd296
|
data/.idea/nats_wave.iml
CHANGED
@@ -129,21 +129,21 @@
|
|
129
129
|
<option name="myRootTask">
|
130
130
|
<RakeTaskImpl id="rake">
|
131
131
|
<subtasks>
|
132
|
-
<RakeTaskImpl description="Build nats_wave-1.1.
|
132
|
+
<RakeTaskImpl description="Build nats_wave-1.1.20.gem into the pkg directory" fullCommand="build" id="build" />
|
133
133
|
<RakeTaskImpl id="build">
|
134
134
|
<subtasks>
|
135
|
-
<RakeTaskImpl description="Generate SHA512 checksum of nats_wave-1.1.
|
135
|
+
<RakeTaskImpl description="Generate SHA512 checksum of nats_wave-1.1.20.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
|
136
136
|
</subtasks>
|
137
137
|
</RakeTaskImpl>
|
138
138
|
<RakeTaskImpl description="Remove any temporary products" fullCommand="clean" id="clean" />
|
139
139
|
<RakeTaskImpl description="Remove any generated files" fullCommand="clobber" id="clobber" />
|
140
|
-
<RakeTaskImpl description="Build and install nats_wave-1.1.
|
140
|
+
<RakeTaskImpl description="Build and install nats_wave-1.1.20.gem into system gems" fullCommand="install" id="install" />
|
141
141
|
<RakeTaskImpl id="install">
|
142
142
|
<subtasks>
|
143
|
-
<RakeTaskImpl description="Build and install nats_wave-1.1.
|
143
|
+
<RakeTaskImpl description="Build and install nats_wave-1.1.20.gem into system gems without network access" fullCommand="install:local" id="local" />
|
144
144
|
</subtasks>
|
145
145
|
</RakeTaskImpl>
|
146
|
-
<RakeTaskImpl description="Create tag v1.1.
|
146
|
+
<RakeTaskImpl description="Create tag v1.1.20 and build and push nats_wave-1.1.20.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
|
147
147
|
<RakeTaskImpl description="Run RuboCop" fullCommand="rubocop" id="rubocop" />
|
148
148
|
<RakeTaskImpl id="rubocop">
|
149
149
|
<subtasks>
|
data/Gemfile.lock
CHANGED
@@ -403,10 +403,16 @@ module NatsWave
|
|
403
403
|
end
|
404
404
|
|
405
405
|
def with_nats_publishing_disabled(&block)
|
406
|
+
original_skip = Thread.current[:skip_nats_wave_publishing]
|
407
|
+
original_processing = Thread.current[:nats_wave_processing]
|
408
|
+
|
406
409
|
Thread.current[:skip_nats_wave_publishing] = true
|
410
|
+
Thread.current[:nats_wave_processing] = true
|
411
|
+
|
407
412
|
yield
|
408
413
|
ensure
|
409
|
-
Thread.current[:skip_nats_wave_publishing] =
|
414
|
+
Thread.current[:skip_nats_wave_publishing] = original_skip
|
415
|
+
Thread.current[:nats_wave_processing] = original_processing
|
410
416
|
end
|
411
417
|
end
|
412
418
|
end
|
@@ -201,9 +201,11 @@ module NatsWave
|
|
201
201
|
def handle_auto_update(data, unique_fields, sync_strategy)
|
202
202
|
existing = find_by_unique_fields(data, unique_fields)
|
203
203
|
if existing
|
204
|
-
|
205
|
-
|
204
|
+
data_without_key = data.except('key', :key)
|
205
|
+
existing.update!(data_without_key)
|
206
|
+
NatsWave.logger.info "✅ Updated #{self.name}: #{existing.id} (preserved key: #{existing.key})"
|
206
207
|
elsif sync_strategy == :upsert
|
208
|
+
data['key'] = SecureRandom.uuid unless data['key'].present?
|
207
209
|
record = self.create!(data)
|
208
210
|
NatsWave.logger.info "✅ Created new #{self.name} during update: #{record.id}"
|
209
211
|
else
|
@@ -359,37 +361,50 @@ module NatsWave
|
|
359
361
|
|
360
362
|
# Auto-publishing callback methods (only called once per action)
|
361
363
|
def trigger_nats_wave_auto_publish_on_create
|
362
|
-
return if
|
363
|
-
@nats_wave_publishing_in_progress = true
|
364
|
+
return if should_skip_nats_publishing?
|
364
365
|
|
366
|
+
@nats_wave_publishing_in_progress = true
|
365
367
|
NatsWave.logger.debug "🚀 Auto-publishing on create"
|
366
368
|
nats_wave_publish('create')
|
367
|
-
|
369
|
+
ensure
|
368
370
|
@nats_wave_publishing_in_progress = false
|
369
371
|
end
|
370
372
|
|
371
373
|
def trigger_nats_wave_auto_publish_on_update
|
372
|
-
return if
|
373
|
-
@nats_wave_publishing_in_progress = true
|
374
|
+
return if should_skip_nats_publishing?
|
374
375
|
|
376
|
+
@nats_wave_publishing_in_progress = true
|
375
377
|
NatsWave.logger.debug "🚀 Auto-publishing on update"
|
376
378
|
nats_wave_publish('update')
|
377
|
-
|
379
|
+
ensure
|
378
380
|
@nats_wave_publishing_in_progress = false
|
379
381
|
end
|
380
382
|
|
381
383
|
def trigger_nats_wave_auto_publish_on_destroy
|
382
|
-
return if
|
383
|
-
@nats_wave_publishing_in_progress = true
|
384
|
+
return if should_skip_nats_publishing?
|
384
385
|
|
386
|
+
@nats_wave_publishing_in_progress = true
|
385
387
|
NatsWave.logger.debug "🚀 Auto-publishing on destroy"
|
386
388
|
nats_wave_publish('destroy')
|
387
|
-
|
389
|
+
ensure
|
388
390
|
@nats_wave_publishing_in_progress = false
|
389
391
|
end
|
390
392
|
|
391
393
|
private
|
392
394
|
|
395
|
+
def should_skip_nats_publishing?
|
396
|
+
# Skip if already publishing
|
397
|
+
return true if @nats_wave_publishing_in_progress
|
398
|
+
|
399
|
+
# Skip if processing a NATS message
|
400
|
+
return true if Thread.current[:nats_wave_processing]
|
401
|
+
|
402
|
+
# Skip if explicitly disabled
|
403
|
+
return true if Thread.current[:skip_nats_wave_publishing]
|
404
|
+
|
405
|
+
false
|
406
|
+
end
|
407
|
+
|
393
408
|
def determine_action_from_context
|
394
409
|
# Try to determine action from ActiveRecord context
|
395
410
|
if defined?(ActiveRecord) && is_a?(ActiveRecord::Base)
|
@@ -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
|
21
|
+
@version = ENV['NATS_SERVICE_VERSION'] || "1.2.1"
|
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
|
data/lib/nats_wave/subscriber.rb
CHANGED
@@ -1,394 +1,3 @@
|
|
1
|
-
# # frozen_string_literal: true
|
2
|
-
#
|
3
|
-
# module NatsWave
|
4
|
-
# class Subscriber
|
5
|
-
# attr_reader :config, :client
|
6
|
-
#
|
7
|
-
# def initialize(config, client, middleware_stack = [])
|
8
|
-
# @config = config
|
9
|
-
# @client = client
|
10
|
-
# @original_client_factory = nil # Store how to recreate the client
|
11
|
-
# @database_connector = DatabaseConnector.new(config)
|
12
|
-
# @model_mapper = ModelMapper.new(config)
|
13
|
-
# @message_transformer = MessageTransformer.new(config)
|
14
|
-
# @dead_letter_queue = DeadLetterQueue.new(config)
|
15
|
-
#
|
16
|
-
# @registry_subscriptions = ModelRegistry.subscriptions
|
17
|
-
# @nats_subscriptions = [] # NATS::Subscription objects
|
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
|
-
# }
|
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
|
31
|
-
# end
|
32
|
-
#
|
33
|
-
# def begin
|
34
|
-
# return if @running || @shutdown
|
35
|
-
# return unless @config.subscription_enabled
|
36
|
-
#
|
37
|
-
# @running = true
|
38
|
-
# @shutdown = false
|
39
|
-
#
|
40
|
-
# NatsWave.logger.info "Starting NATS subscriber for #{@config.service_name}"
|
41
|
-
#
|
42
|
-
# setup_subscriptions
|
43
|
-
#
|
44
|
-
# NatsWave.logger.info "Started #{@registry_subscriptions.size} subscriptions from Model Registry"
|
45
|
-
#
|
46
|
-
# # Keep the subscriber alive
|
47
|
-
# keep_alive
|
48
|
-
# end
|
49
|
-
#
|
50
|
-
# def listen(subjects:, model_mappings: {}, handler: nil)
|
51
|
-
# subjects = Array(subjects)
|
52
|
-
#
|
53
|
-
# subjects.each do |subject|
|
54
|
-
# subscribe_to_subject(subject, handler, model_mappings)
|
55
|
-
# NatsWave.logger.info "Subscribed to #{subject}"
|
56
|
-
# end
|
57
|
-
# end
|
58
|
-
#
|
59
|
-
# def reset
|
60
|
-
# @shutdown = true
|
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"
|
74
|
-
# end
|
75
|
-
#
|
76
|
-
# def database_connected?
|
77
|
-
# @database_connector.connected?
|
78
|
-
# end
|
79
|
-
#
|
80
|
-
# def disconnect
|
81
|
-
# reset
|
82
|
-
# end
|
83
|
-
#
|
84
|
-
# private
|
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
|
-
#
|
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})"
|
135
|
-
#
|
136
|
-
# rescue => e
|
137
|
-
# NatsWave.logger.error "Failed to subscribe to #{subject_pattern}: #{e.message}"
|
138
|
-
# raise
|
139
|
-
# end
|
140
|
-
# end
|
141
|
-
#
|
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
|
-
#
|
147
|
-
# message = parse_message(raw_message)
|
148
|
-
#
|
149
|
-
# if custom_handler
|
150
|
-
# custom_handler.call(message)
|
151
|
-
# else
|
152
|
-
# handle_model_sync(message, model_mappings)
|
153
|
-
# end
|
154
|
-
#
|
155
|
-
# NatsWave.logger.debug('✅ Successfully processed message')
|
156
|
-
# rescue StandardError => e
|
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
|
338
|
-
# end
|
339
|
-
#
|
340
|
-
# # ... rest of your existing methods (parse_message, handle_model_sync, handle_error)
|
341
|
-
# def parse_message(raw_message)
|
342
|
-
# @message_transformer.parse_message(raw_message)
|
343
|
-
# end
|
344
|
-
#
|
345
|
-
# def handle_model_sync(message, model_mappings)
|
346
|
-
# source_model = message['model']
|
347
|
-
# mapping = model_mappings[source_model] || @config.model_mappings[source_model]
|
348
|
-
#
|
349
|
-
# return unless mapping
|
350
|
-
#
|
351
|
-
# target_model = mapping[:target_model]
|
352
|
-
# field_mappings = mapping[:field_mappings] || {}
|
353
|
-
# transformations = mapping[:transformations] || {}
|
354
|
-
#
|
355
|
-
# # Transform the data
|
356
|
-
# transformed_data = @model_mapper.transform_data(
|
357
|
-
# message['data'],
|
358
|
-
# field_mappings,
|
359
|
-
# transformations
|
360
|
-
# )
|
361
|
-
#
|
362
|
-
# # Apply to database
|
363
|
-
# @database_connector.apply_change(
|
364
|
-
# model: target_model,
|
365
|
-
# action: message['action'],
|
366
|
-
# data: transformed_data,
|
367
|
-
# metadata: message['metadata']
|
368
|
-
# )
|
369
|
-
#
|
370
|
-
# NatsWave.logger.info "Synced #{source_model} -> #{target_model}: #{message['action']}"
|
371
|
-
# end
|
372
|
-
#
|
373
|
-
# def handle_error(error, raw_message, parsed_message = nil)
|
374
|
-
# event_id = parsed_message&.dig('event_id') || 'unknown'
|
375
|
-
# subject = parsed_message&.dig('subject') || 'unknown'
|
376
|
-
#
|
377
|
-
# NatsWave.logger.error("Error processing message #{event_id} from #{subject}: #{error.message}")
|
378
|
-
#
|
379
|
-
# # Send to dead letter queue
|
380
|
-
# @dead_letter_queue.store_failed_message(
|
381
|
-
# parsed_message || raw_message,
|
382
|
-
# error,
|
383
|
-
# 0 # retry count
|
384
|
-
# )
|
385
|
-
#
|
386
|
-
# # Continue processing - don't raise to avoid breaking subscription
|
387
|
-
# end
|
388
|
-
# end
|
389
|
-
# end
|
390
|
-
|
391
|
-
|
392
1
|
# frozen_string_literal: true
|
393
2
|
|
394
3
|
module NatsWave
|
@@ -398,14 +7,16 @@ module NatsWave
|
|
398
7
|
def initialize(config, client, middleware_stack = [])
|
399
8
|
@config = config
|
400
9
|
@client = client
|
401
|
-
@original_client_factory = nil
|
10
|
+
@original_client_factory = nil
|
402
11
|
@database_connector = DatabaseConnector.new(config)
|
403
12
|
@model_mapper = ModelMapper.new(config)
|
404
13
|
@message_transformer = MessageTransformer.new(config)
|
405
14
|
@dead_letter_queue = DeadLetterQueue.new(config)
|
406
|
-
|
15
|
+
@processed_messages = {}
|
16
|
+
@dedup_cleanup_interval = 300
|
17
|
+
@last_dedup_cleanup = Time.current
|
407
18
|
@registry_subscriptions = ModelRegistry.subscriptions
|
408
|
-
@nats_subscriptions = []
|
19
|
+
@nats_subscriptions = []
|
409
20
|
@running = false
|
410
21
|
@shutdown = false
|
411
22
|
@reconnecting = false
|
@@ -474,6 +85,16 @@ module NatsWave
|
|
474
85
|
|
475
86
|
private
|
476
87
|
|
88
|
+
def should_cleanup_dedup_cache?
|
89
|
+
Time.current - @last_dedup_cleanup > @dedup_cleanup_interval
|
90
|
+
end
|
91
|
+
|
92
|
+
def cleanup_old_message_ids
|
93
|
+
cutoff = Time.current - 300 # 5 minutes
|
94
|
+
@processed_messages.delete_if { |_, timestamp| timestamp < cutoff }
|
95
|
+
@last_dedup_cleanup = Time.current
|
96
|
+
end
|
97
|
+
|
477
98
|
def setup_subscriptions
|
478
99
|
return unless @client&.connected?
|
479
100
|
|
@@ -533,6 +154,8 @@ module NatsWave
|
|
533
154
|
def process_message(raw_message, custom_handler, model_mappings)
|
534
155
|
return unless should_process_message?(raw_message)
|
535
156
|
|
157
|
+
Thread.current[:nats_wave_processing] = true
|
158
|
+
|
536
159
|
NatsWave.logger.debug "🔄 Processing message: #{raw_message[0..200]}..."
|
537
160
|
|
538
161
|
message = parse_message(raw_message)
|
@@ -546,19 +169,38 @@ module NatsWave
|
|
546
169
|
NatsWave.logger.debug('✅ Successfully processed message')
|
547
170
|
rescue StandardError => e
|
548
171
|
handle_error(e, raw_message, message)
|
549
|
-
|
172
|
+
ensure
|
173
|
+
Thread.current[:nats_wave_processing] = false
|
550
174
|
end
|
551
175
|
|
552
176
|
def should_process_message?(raw_message_data)
|
553
177
|
message = JSON.parse(raw_message_data)
|
554
178
|
source = message['source'] || {}
|
555
179
|
|
556
|
-
# Skip messages from same service instance
|
557
180
|
if source['service'] == @config.service_name && source['instance_id'] == @config.instance_id
|
558
181
|
NatsWave.logger.debug "🔄 Skipping message from same service instance"
|
559
182
|
return false
|
560
183
|
end
|
561
184
|
|
185
|
+
if Thread.current[:nats_wave_processing]
|
186
|
+
NatsWave.logger.debug "🔄 Skipping message - already processing NATS update"
|
187
|
+
return false
|
188
|
+
end
|
189
|
+
|
190
|
+
message_id = message['event_id'] || message['message_id']
|
191
|
+
if message_id && @processed_messages[message_id]
|
192
|
+
if @processed_messages[message_id] > Time.current - 60 # 1 minute window
|
193
|
+
NatsWave.logger.debug "🔄 Skipping duplicate message: #{message_id}"
|
194
|
+
return false
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Track this message
|
199
|
+
if message_id
|
200
|
+
@processed_messages[message_id] = Time.current
|
201
|
+
cleanup_old_message_ids if should_cleanup_dedup_cache?
|
202
|
+
end
|
203
|
+
|
562
204
|
true
|
563
205
|
rescue JSON::ParserError => e
|
564
206
|
NatsWave.logger.error "Failed to parse message for filtering: #{e.message}"
|
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.2.1
|
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-
|
11
|
+
date: 2025-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nats-pure
|