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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/.gitignore +8 -0
  3. data/.idea/misc.xml +4 -0
  4. data/.idea/modules.xml +8 -0
  5. data/.idea/nats_wave.iml +169 -0
  6. data/.idea/vcs.xml +6 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +16 -0
  9. data/CHANGELOG.md +5 -0
  10. data/CODE_OF_CONDUCT.md +136 -0
  11. data/Gemfile +15 -0
  12. data/Gemfile.lock +332 -0
  13. data/LICENSE.txt +21 -0
  14. data/README.md +985 -0
  15. data/Rakefile +12 -0
  16. data/config/nats_wave.yml +65 -0
  17. data/examples/catalog_model.rb +36 -0
  18. data/examples/configuration_examples.rb +68 -0
  19. data/examples/user_model.rb +58 -0
  20. data/lib/generators/nats_wave/install_generator.rb +40 -0
  21. data/lib/generators/nats_wave/templates/README +31 -0
  22. data/lib/generators/nats_wave/templates/create_nats_wave_failed_messages.rb +20 -0
  23. data/lib/generators/nats_wave/templates/create_nats_wave_failed_subscriptions.rb +20 -0
  24. data/lib/generators/nats_wave/templates/initializer.rb +64 -0
  25. data/lib/generators/nats_wave/templates/nats_wave.yml +65 -0
  26. data/lib/nats_wave/adapters/active_record.rb +206 -0
  27. data/lib/nats_wave/adapters/datadog_metrics.rb +93 -0
  28. data/lib/nats_wave/auto_registration.rb +109 -0
  29. data/lib/nats_wave/client.rb +142 -0
  30. data/lib/nats_wave/concerns/mappable.rb +172 -0
  31. data/lib/nats_wave/concerns/publishable.rb +216 -0
  32. data/lib/nats_wave/configuration.rb +105 -0
  33. data/lib/nats_wave/database_connector.rb +50 -0
  34. data/lib/nats_wave/dead_letter_queue.rb +146 -0
  35. data/lib/nats_wave/errors.rb +27 -0
  36. data/lib/nats_wave/message_transformer.rb +95 -0
  37. data/lib/nats_wave/metrics.rb +220 -0
  38. data/lib/nats_wave/middleware/authentication.rb +65 -0
  39. data/lib/nats_wave/middleware/base.rb +19 -0
  40. data/lib/nats_wave/middleware/logging.rb +58 -0
  41. data/lib/nats_wave/middleware/validation.rb +74 -0
  42. data/lib/nats_wave/model_mapper.rb +125 -0
  43. data/lib/nats_wave/model_registry.rb +150 -0
  44. data/lib/nats_wave/publisher.rb +151 -0
  45. data/lib/nats_wave/railtie.rb +111 -0
  46. data/lib/nats_wave/schema_registry.rb +77 -0
  47. data/lib/nats_wave/subscriber.rb +161 -0
  48. data/lib/nats_wave/version.rb +5 -0
  49. data/lib/nats_wave.rb +97 -0
  50. data/lib/tasks/nats_wave.rake +360 -0
  51. data/sig/nats_wave.rbs +5 -0
  52. 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
data/sig/nats_wave.rbs ADDED
@@ -0,0 +1,5 @@
1
+ module NatsWave
2
+ VERSION: String
3
+
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end