nats_wave 1.1.0
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 +7 -0
- data/.idea/.gitignore +8 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/nats_wave.iml +169 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +136 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +332 -0
- data/LICENSE.txt +21 -0
- data/README.md +985 -0
- data/Rakefile +12 -0
- data/config/nats_wave.yml +65 -0
- data/examples/catalog_model.rb +36 -0
- data/examples/configuration_examples.rb +68 -0
- data/examples/user_model.rb +58 -0
- data/lib/generators/nats_wave/install_generator.rb +40 -0
- data/lib/generators/nats_wave/templates/README +31 -0
- data/lib/generators/nats_wave/templates/create_nats_wave_failed_messages.rb +20 -0
- data/lib/generators/nats_wave/templates/create_nats_wave_failed_subscriptions.rb +20 -0
- data/lib/generators/nats_wave/templates/initializer.rb +64 -0
- data/lib/generators/nats_wave/templates/nats_wave.yml +65 -0
- data/lib/nats_wave/adapters/active_record.rb +206 -0
- data/lib/nats_wave/adapters/datadog_metrics.rb +93 -0
- data/lib/nats_wave/auto_registration.rb +109 -0
- data/lib/nats_wave/client.rb +142 -0
- data/lib/nats_wave/concerns/mappable.rb +172 -0
- data/lib/nats_wave/concerns/publishable.rb +216 -0
- data/lib/nats_wave/configuration.rb +105 -0
- data/lib/nats_wave/database_connector.rb +50 -0
- data/lib/nats_wave/dead_letter_queue.rb +146 -0
- data/lib/nats_wave/errors.rb +27 -0
- data/lib/nats_wave/message_transformer.rb +95 -0
- data/lib/nats_wave/metrics.rb +220 -0
- data/lib/nats_wave/middleware/authentication.rb +65 -0
- data/lib/nats_wave/middleware/base.rb +19 -0
- data/lib/nats_wave/middleware/logging.rb +58 -0
- data/lib/nats_wave/middleware/validation.rb +74 -0
- data/lib/nats_wave/model_mapper.rb +125 -0
- data/lib/nats_wave/model_registry.rb +150 -0
- data/lib/nats_wave/publisher.rb +151 -0
- data/lib/nats_wave/railtie.rb +111 -0
- data/lib/nats_wave/schema_registry.rb +77 -0
- data/lib/nats_wave/subscriber.rb +161 -0
- data/lib/nats_wave/version.rb +5 -0
- data/lib/nats_wave.rb +97 -0
- data/lib/tasks/nats_wave.rake +360 -0
- data/sig/nats_wave.rbs +5 -0
- metadata +385 -0
data/lib/nats_wave.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/all'
|
4
|
+
require 'logger'
|
5
|
+
require 'socket'
|
6
|
+
require 'json'
|
7
|
+
require 'securerandom'
|
8
|
+
|
9
|
+
require_relative "nats_wave/version"
|
10
|
+
require_relative "nats_wave/errors"
|
11
|
+
require_relative "nats_wave/configuration"
|
12
|
+
require_relative "nats_wave/client"
|
13
|
+
require_relative "nats_wave/publisher"
|
14
|
+
require_relative "nats_wave/subscriber"
|
15
|
+
require_relative "nats_wave/message_transformer"
|
16
|
+
require_relative "nats_wave/model_mapper"
|
17
|
+
require_relative "nats_wave/model_registry"
|
18
|
+
require_relative "nats_wave/concerns/mappable"
|
19
|
+
require_relative "nats_wave/adapters/datadog_metrics"
|
20
|
+
require_relative "nats_wave/database_connector"
|
21
|
+
require_relative "nats_wave/schema_registry"
|
22
|
+
require_relative "nats_wave/dead_letter_queue"
|
23
|
+
require_relative "nats_wave/metrics"
|
24
|
+
|
25
|
+
# Middleware
|
26
|
+
require_relative "nats_wave/middleware/base"
|
27
|
+
require_relative "nats_wave/middleware/authentication"
|
28
|
+
require_relative "nats_wave/middleware/validation"
|
29
|
+
require_relative "nats_wave/middleware/logging"
|
30
|
+
|
31
|
+
# Adapters
|
32
|
+
require_relative "nats_wave/adapters/active_record"
|
33
|
+
|
34
|
+
# Rails integration
|
35
|
+
require_relative "nats_wave/railtie" if defined?(Rails)
|
36
|
+
|
37
|
+
module NatsWave
|
38
|
+
class << self
|
39
|
+
attr_accessor :configuration
|
40
|
+
|
41
|
+
def client
|
42
|
+
@client ||= Client.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def configure
|
46
|
+
self.configuration ||= Configuration.new
|
47
|
+
yield(configuration) if block_given?
|
48
|
+
reset_client!
|
49
|
+
configuration
|
50
|
+
end
|
51
|
+
|
52
|
+
def reset_client!
|
53
|
+
@client = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def logger
|
57
|
+
@logger ||= begin
|
58
|
+
if defined?(Rails) && Rails.logger
|
59
|
+
Rails.logger
|
60
|
+
else
|
61
|
+
Logger.new($stdout).tap { |l| l.level = Logger::INFO }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def logger=(logger)
|
67
|
+
@logger = logger
|
68
|
+
end
|
69
|
+
|
70
|
+
# Convenience methods
|
71
|
+
def publish(subject:, model:, action:, data:, metadata: {})
|
72
|
+
client.publish(
|
73
|
+
subject: subject,
|
74
|
+
model: model,
|
75
|
+
action: action,
|
76
|
+
data: data,
|
77
|
+
metadata: metadata
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def subscribe(subjects:, model_mappings: {}, &block)
|
82
|
+
client.subscribe(
|
83
|
+
subjects: subjects,
|
84
|
+
model_mappings: model_mappings,
|
85
|
+
handler: block
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def start_subscriber
|
90
|
+
client.start_subscriber
|
91
|
+
end
|
92
|
+
|
93
|
+
def health_check
|
94
|
+
client.health_check
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,360 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :nats_wave do
|
4
|
+
desc 'Check NATS Wave health'
|
5
|
+
task health: :environment do
|
6
|
+
health = NatsWave.health_check
|
7
|
+
puts 'NATS Wave Health Check:'
|
8
|
+
puts '================================'
|
9
|
+
puts "NATS Connected: #{health[:nats_connected]}"
|
10
|
+
puts "Database Connected: #{health[:database_connected]}"
|
11
|
+
puts "Service: #{health[:service_name]}"
|
12
|
+
puts "Version: #{health[:version]}"
|
13
|
+
puts "Instance ID: #{health[:instance_id]}"
|
14
|
+
puts "NATS Server URL: #{health[:nats_server_url]}"
|
15
|
+
puts "Published Subjects: #{health[:published_subjects].join(', ')}"
|
16
|
+
puts "Timestamp: #{health[:timestamp]}"
|
17
|
+
|
18
|
+
exit(1) unless health[:nats_connected] && health[:database_connected]
|
19
|
+
rescue StandardError => e
|
20
|
+
puts "Health check failed: #{e.message}"
|
21
|
+
exit(1)
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Show NATS Wave configuration'
|
25
|
+
task config: :environment do
|
26
|
+
config = NatsWave.configuration
|
27
|
+
puts 'NatsWave Configuration:'
|
28
|
+
puts '======================'
|
29
|
+
puts "Service Name: #{config.service_name}"
|
30
|
+
puts "NATS URL: #{config.nats_url}"
|
31
|
+
puts "Subject Patterns: #{config.subject_patterns.join(', ')}"
|
32
|
+
puts "NATS Server URL for others: #{config.nats_server_url}"
|
33
|
+
puts "Publishing Enabled: #{config.publishing_enabled}"
|
34
|
+
puts "Subscription Enabled: #{config.subscription_enabled}"
|
35
|
+
puts "Queue Group: #{config.queue_group}"
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'Test NATS connectivity'
|
39
|
+
task test: :environment do
|
40
|
+
test_subject = "#{NatsWave.configuration.default_subject_prefix}.test.#{SecureRandom.hex(4)}"
|
41
|
+
test_data = {
|
42
|
+
message: 'Hello NATS!',
|
43
|
+
timestamp: Time.current.iso8601,
|
44
|
+
test_id: SecureRandom.uuid
|
45
|
+
}
|
46
|
+
|
47
|
+
NatsWave.publish(
|
48
|
+
subject: test_subject,
|
49
|
+
model: 'TestModel',
|
50
|
+
action: 'test',
|
51
|
+
data: test_data,
|
52
|
+
metadata: { source: 'rake_task' }
|
53
|
+
)
|
54
|
+
|
55
|
+
puts '✓ NATS connectivity test passed'
|
56
|
+
puts " Published to: #{test_subject}"
|
57
|
+
puts " Test data: #{test_data}"
|
58
|
+
rescue StandardError => e
|
59
|
+
puts "✗ NATS connectivity test failed: #{e.message}"
|
60
|
+
exit(1)
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'Publish a test message'
|
64
|
+
task :publish_test, %i[subject model action] => :environment do |_t, args|
|
65
|
+
subject = args[:subject] || 'test.message'
|
66
|
+
model = args[:model] || 'TestModel'
|
67
|
+
action = args[:action] || 'create'
|
68
|
+
|
69
|
+
data = {
|
70
|
+
id: 1,
|
71
|
+
name: 'Test Message',
|
72
|
+
timestamp: Time.current.iso8601
|
73
|
+
}
|
74
|
+
|
75
|
+
NatsWave.client.publish(
|
76
|
+
subject: subject,
|
77
|
+
model: model,
|
78
|
+
action: action,
|
79
|
+
data: data,
|
80
|
+
metadata: { source: 'rake_task' }
|
81
|
+
)
|
82
|
+
|
83
|
+
puts "Published test message to #{subject}"
|
84
|
+
end
|
85
|
+
|
86
|
+
desc 'Subscribe to test messages'
|
87
|
+
task :subscribe_test, [:subject] => :environment do |_t, args|
|
88
|
+
subject = args[:subject] || 'test.*'
|
89
|
+
|
90
|
+
puts "Subscribing to #{subject}..."
|
91
|
+
puts 'Press Ctrl+C to stop'
|
92
|
+
|
93
|
+
NatsWave.client.subscribe(
|
94
|
+
subjects: [subject]
|
95
|
+
) do |message|
|
96
|
+
puts 'Received message:'
|
97
|
+
puts "Subject: #{message['subject']}"
|
98
|
+
puts "Model: #{message['model']}"
|
99
|
+
puts "Action: #{message['action']}"
|
100
|
+
puts "Data: #{message['data']}"
|
101
|
+
puts "Metadata: #{message['metadata']}"
|
102
|
+
puts '---'
|
103
|
+
end
|
104
|
+
|
105
|
+
# Keep the task running
|
106
|
+
sleep
|
107
|
+
end
|
108
|
+
|
109
|
+
desc 'Start NATS Wave subscriber'
|
110
|
+
task start: :environment do
|
111
|
+
puts 'Starting NatsWave subscriber...'
|
112
|
+
|
113
|
+
begin
|
114
|
+
NatsWave.start_subscriber
|
115
|
+
puts 'NatsWave subscriber started successfully'
|
116
|
+
puts 'Press Ctrl+C to stop'
|
117
|
+
|
118
|
+
# Keep the process alive
|
119
|
+
trap('INT') do
|
120
|
+
puts "\nShutting down NatsWave subscriber..."
|
121
|
+
NatsWave.client.disconnect!
|
122
|
+
exit(0)
|
123
|
+
end
|
124
|
+
|
125
|
+
sleep
|
126
|
+
rescue StandardError => e
|
127
|
+
puts "Failed to start NatsWave subscriber: #{e.message}"
|
128
|
+
exit(1)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
desc 'Retry failed messages in dead letter queue'
|
133
|
+
task retry_failed: :environment do
|
134
|
+
dlq = NatsWave::DeadLetterQueue.new(NatsWave.configuration)
|
135
|
+
|
136
|
+
puts 'Retrying failed NatsWave messages...'
|
137
|
+
before_count = dlq.pending_retries_count
|
138
|
+
|
139
|
+
dlq.retry_failed_messages
|
140
|
+
|
141
|
+
after_count = dlq.pending_retries_count
|
142
|
+
processed = before_count - after_count
|
143
|
+
|
144
|
+
puts "Processed #{processed} failed messages"
|
145
|
+
puts "Remaining pending retries: #{after_count}"
|
146
|
+
end
|
147
|
+
|
148
|
+
desc 'Show failed messages in dead letter queue'
|
149
|
+
task show_failed: :environment do
|
150
|
+
dlq = NatsWave::DeadLetterQueue.new(NatsWave.configuration)
|
151
|
+
messages = dlq.get_failed_messages
|
152
|
+
|
153
|
+
if messages.empty?
|
154
|
+
puts 'No failed messages in queue'
|
155
|
+
else
|
156
|
+
puts 'Failed Messages:'
|
157
|
+
puts '================'
|
158
|
+
messages.each do |msg|
|
159
|
+
puts "ID: #{msg[:id]}"
|
160
|
+
puts "Subject: #{
|
161
|
+
begin
|
162
|
+
msg[:subject]
|
163
|
+
rescue StandardError
|
164
|
+
'Unknown'
|
165
|
+
end}"
|
166
|
+
puts "Error: #{msg[:error]}"
|
167
|
+
puts "Error Class: #{msg[:error_class]}"
|
168
|
+
puts "Retry Count: #{msg[:retry_count]}"
|
169
|
+
puts "Next Retry: #{msg[:next_retry]}"
|
170
|
+
puts "Created: #{msg[:timestamp]}"
|
171
|
+
puts '---'
|
172
|
+
end
|
173
|
+
|
174
|
+
puts "\nSummary:"
|
175
|
+
puts "Total failed messages: #{messages.size}"
|
176
|
+
puts "Pending retries: #{dlq.pending_retries_count}"
|
177
|
+
puts "Permanent failures: #{dlq.permanent_failures_count}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
desc 'Show NATS Wave statistics'
|
182
|
+
task stats: :environment do
|
183
|
+
puts "\n=== NATS Wave Statistics ==="
|
184
|
+
|
185
|
+
if defined?(NatsWaveFailedMessage)
|
186
|
+
puts "Failed Publications: #{NatsWaveFailedMessage.count}"
|
187
|
+
puts "Pending Publication Retries: #{NatsWaveFailedMessage.where('retry_count < ? AND next_retry_at <= ?', 3,
|
188
|
+
Time.current).count}"
|
189
|
+
end
|
190
|
+
|
191
|
+
if defined?(NatsWaveFailedSubscription)
|
192
|
+
puts "Failed Subscriptions: #{NatsWaveFailedSubscription.count}"
|
193
|
+
puts "Pending Subscription Retries: #{NatsWaveFailedSubscription.where('retry_count < ? AND next_retry_at <= ?',
|
194
|
+
3, Time.current).count}"
|
195
|
+
end
|
196
|
+
|
197
|
+
begin
|
198
|
+
health = NatsWave.health_check
|
199
|
+
puts "NATS Connected: #{health[:nats_connected]}"
|
200
|
+
puts "Database Connected: #{health[:database_connected]}"
|
201
|
+
rescue StandardError => e
|
202
|
+
puts "Health check failed: #{e.message}"
|
203
|
+
end
|
204
|
+
|
205
|
+
puts "============================\n"
|
206
|
+
end
|
207
|
+
|
208
|
+
desc 'Clear all failed messages'
|
209
|
+
task clear_failed: :environment do
|
210
|
+
print 'Are you sure you want to clear all failed messages? [y/N]: '
|
211
|
+
response = $stdin.gets.chomp.downcase
|
212
|
+
|
213
|
+
if %w[y yes].include?(response)
|
214
|
+
dlq = NatsWave::DeadLetterQueue.new(NatsWave.configuration)
|
215
|
+
|
216
|
+
# Clear in-memory storage
|
217
|
+
cleared_memory = dlq.get_failed_messages.size
|
218
|
+
dlq.get_failed_messages.each do |msg|
|
219
|
+
dlq.remove_failed_message(msg[:id])
|
220
|
+
end
|
221
|
+
|
222
|
+
# Clear database storage if tables exist
|
223
|
+
cleared_db = 0
|
224
|
+
if defined?(NatsWaveFailedMessage)
|
225
|
+
cleared_db += NatsWaveFailedMessage.count
|
226
|
+
NatsWaveFailedMessage.delete_all
|
227
|
+
end
|
228
|
+
|
229
|
+
if defined?(NatsWaveFailedSubscription)
|
230
|
+
cleared_db += NatsWaveFailedSubscription.count
|
231
|
+
NatsWaveFailedSubscription.delete_all
|
232
|
+
end
|
233
|
+
|
234
|
+
puts "Cleared #{cleared_memory} messages from memory storage"
|
235
|
+
puts "Cleared #{cleared_db} messages from database storage"
|
236
|
+
else
|
237
|
+
puts 'Operation cancelled'
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
desc 'Monitor NATS Wave activity'
|
242
|
+
task monitor: :environment do
|
243
|
+
puts 'Monitoring NatsWave activity...'
|
244
|
+
puts 'Press Ctrl+C to stop'
|
245
|
+
|
246
|
+
# Subscribe to all messages for monitoring
|
247
|
+
NatsWave.client.subscribe(
|
248
|
+
subjects: ['*', '*.>', 'rails.*', 'external.*']
|
249
|
+
) do |message|
|
250
|
+
timestamp = Time.current.strftime('%H:%M:%S')
|
251
|
+
source_service = message.dig('source', 'service') || 'unknown'
|
252
|
+
|
253
|
+
puts "[#{timestamp}] #{message['subject']} | #{message['model']}##{message['action']} | from: #{source_service}"
|
254
|
+
|
255
|
+
if ENV['VERBOSE']
|
256
|
+
puts " Data: #{message['data']}"
|
257
|
+
puts " Metadata: #{message['metadata']}"
|
258
|
+
puts ' ---'
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
sleep
|
263
|
+
end
|
264
|
+
|
265
|
+
desc "Show all model subscriptions"
|
266
|
+
task model_subscriptions: :environment do
|
267
|
+
puts "\n=== NatsWave Model Subscriptions ==="
|
268
|
+
|
269
|
+
stats = NatsWave::ModelRegistry.subscription_stats
|
270
|
+
|
271
|
+
puts "\nOverview:"
|
272
|
+
puts " Total Subscriptions: #{stats[:total_subscriptions]}"
|
273
|
+
puts " Unique Subjects: #{stats[:unique_subjects]}"
|
274
|
+
puts " Models with Subscriptions: #{stats[:models_with_subscriptions]}"
|
275
|
+
|
276
|
+
puts "\nModel Breakdown:"
|
277
|
+
stats[:subscription_breakdown].each do |model, count|
|
278
|
+
puts " #{model}: #{count} subscription(s)"
|
279
|
+
|
280
|
+
# Show details for each model
|
281
|
+
subscriptions = NatsWave::ModelRegistry.subscriptions_for_model(model)
|
282
|
+
subscriptions.each do |sub|
|
283
|
+
puts " Subjects: #{sub[:subjects].join(', ')}"
|
284
|
+
puts " External Model: #{sub[:external_model] || 'Custom Handler'}"
|
285
|
+
puts " Handler: #{sub[:handler] ? 'Custom' : 'Auto-Sync'}"
|
286
|
+
puts " ---"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
puts "\nAll Subscription Subjects:"
|
291
|
+
NatsWave::ModelRegistry.all_subscription_subjects.sort.each do |subject|
|
292
|
+
puts " #{subject}"
|
293
|
+
end
|
294
|
+
|
295
|
+
puts "================================\n"
|
296
|
+
end
|
297
|
+
|
298
|
+
desc "Test model auto-registration"
|
299
|
+
task test_registration: :environment do
|
300
|
+
puts "Testing NatsWave model auto-registration..."
|
301
|
+
|
302
|
+
begin
|
303
|
+
NatsWave::AutoRegistration.register_all_models!
|
304
|
+
puts "✅ Auto-registration completed successfully"
|
305
|
+
|
306
|
+
# Show what was registered
|
307
|
+
Rake::Task['nats_wave:model_subscriptions'].invoke
|
308
|
+
rescue => e
|
309
|
+
puts "❌ Auto-registration failed: #{e.message}"
|
310
|
+
puts e.backtrace.first(5)
|
311
|
+
exit(1)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
desc "Validate model configurations"
|
316
|
+
task validate_models: :environment do
|
317
|
+
puts "Validating NatsWave model configurations..."
|
318
|
+
|
319
|
+
errors = []
|
320
|
+
|
321
|
+
ActiveRecord::Base.descendants.each do |model_class|
|
322
|
+
next unless model_class.respond_to?(:nats_wave_external_models)
|
323
|
+
|
324
|
+
begin
|
325
|
+
# Check if model has valid mappings
|
326
|
+
model_class.nats_wave_external_models.each do |external_model|
|
327
|
+
mapping = model_class.nats_wave_mapping_for(external_model)
|
328
|
+
|
329
|
+
# Validate field mappings reference real fields
|
330
|
+
if mapping[:field_mappings]
|
331
|
+
mapping[:field_mappings].values.each do |local_field|
|
332
|
+
unless model_class.column_names.include?(local_field.to_s)
|
333
|
+
errors << "#{model_class.name}: Field '#{local_field}' not found in #{external_model} mapping"
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# Validate unique fields exist
|
339
|
+
if mapping[:unique_fields]
|
340
|
+
mapping[:unique_fields].each do |field|
|
341
|
+
unless model_class.column_names.include?(field.to_s)
|
342
|
+
errors << "#{model_class.name}: Unique field '#{field}' not found in #{external_model} mapping"
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
rescue => e
|
348
|
+
errors << "#{model_class.name}: Configuration error - #{e.message}"
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
if errors.empty?
|
353
|
+
puts "✅ All model configurations are valid"
|
354
|
+
else
|
355
|
+
puts "❌ Found #{errors.size} configuration errors:"
|
356
|
+
errors.each { |error| puts " - #{error}" }
|
357
|
+
exit(1)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|