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
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'datadog/statsd'
4
+
5
+ module NatsWave
6
+ module Adapters
7
+ class DatadogMetrics
8
+ def initialize(config = {})
9
+ @statsd = Datadog::Statsd.new(
10
+ config[:host] || ENV['DD_AGENT_HOST'] || 'localhost',
11
+ config[:port] || ENV['DD_AGENT_PORT'] || 8125,
12
+ tags: default_tags.merge(config[:tags] || {}),
13
+ namespace: config[:namespace] || 'nats_wave'
14
+ )
15
+ @service_name = config[:service_name] || 'unknown'
16
+ end
17
+
18
+ def increment(metric_name, tags: [], value: 1)
19
+ formatted_tags = format_tags(tags)
20
+
21
+ case metric_name
22
+ when 'nats_wave.messages.published'
23
+ @statsd.increment('messages.published', value, tags: formatted_tags)
24
+ when 'nats_wave.messages.received'
25
+ @statsd.increment('messages.received', value, tags: formatted_tags)
26
+ when 'nats_wave.messages.failed'
27
+ @statsd.increment('messages.failed', value, tags: formatted_tags)
28
+ when 'nats_wave.errors'
29
+ @statsd.increment('errors', value, tags: formatted_tags)
30
+ when 'nats_wave.retries'
31
+ @statsd.increment('retries', value, tags: formatted_tags)
32
+ end
33
+ end
34
+
35
+ def histogram(metric_name, value, tags: [])
36
+ formatted_tags = format_tags(tags)
37
+
38
+ case metric_name
39
+ when 'nats_wave.processing_time'
40
+ @statsd.histogram('processing_time', value, tags: formatted_tags)
41
+ when 'nats_wave.message_size'
42
+ @statsd.histogram('message_size', value, tags: formatted_tags)
43
+ when 'nats_wave.queue_depth'
44
+ @statsd.histogram('queue_depth', value, tags: formatted_tags)
45
+ end
46
+ end
47
+
48
+ def gauge(metric_name, value, tags: [])
49
+ formatted_tags = format_tags(tags)
50
+
51
+ case metric_name
52
+ when 'nats_wave.connection_status'
53
+ @statsd.gauge('connection_status', value, tags: formatted_tags)
54
+ when 'nats_wave.subscriber_count'
55
+ @statsd.gauge('subscriber_count', value, tags: formatted_tags)
56
+ when 'nats_wave.failed_messages_count'
57
+ @statsd.gauge('failed_messages_count', value, tags: formatted_tags)
58
+ end
59
+ end
60
+
61
+ def timing(metric_name, &block)
62
+ formatted_tags = format_tags([])
63
+
64
+ case metric_name
65
+ when 'nats_wave.operation_time'
66
+ @statsd.time('operation_time', tags: formatted_tags, &block)
67
+ end
68
+ end
69
+
70
+ def close
71
+ @statsd.close if @statsd.respond_to?(:close)
72
+ end
73
+
74
+ private
75
+
76
+ def default_tags
77
+ {
78
+ service: @service_name,
79
+ environment: ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'unknown',
80
+ version: ENV['APP_VERSION'] || '1.1.0'
81
+ }
82
+ end
83
+
84
+ def format_tags(tags)
85
+ # Convert ["subject:user.create", "status:success"] to ["subject:user.create", "status:success"]
86
+ # Datadog expects this format
87
+ Array(tags).map do |tag|
88
+ tag.is_a?(String) ? tag : "#{tag[:key]}:#{tag[:value]}"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NatsWave
4
+ class AutoRegistration
5
+ class << self
6
+ def register_all_models!
7
+ return unless defined?(Rails)
8
+
9
+ Rails.logger.info "🔍 Auto-registering NatsWave models..."
10
+
11
+ # Load all models
12
+ load_all_models
13
+
14
+ # Find models with NatsWave mappings
15
+ mappable_models = find_mappable_models
16
+
17
+ Rails.logger.info "📋 Found #{mappable_models.size} models with NatsWave mappings"
18
+
19
+ # Register their subscriptions with the client
20
+ register_model_subscriptions(mappable_models)
21
+
22
+ # Log registration summary
23
+ log_registration_summary
24
+ end
25
+
26
+ private
27
+
28
+ def load_all_models
29
+ # Ensure all models are loaded
30
+ Rails.application.eager_load! if Rails.env.development?
31
+
32
+ # Also try to load models manually
33
+ Dir[Rails.root.join('app/models/**/*.rb')].each do |file|
34
+ require_dependency file
35
+ rescue => e
36
+ Rails.logger.warn "Could not load model file #{file}: #{e.message}"
37
+ end
38
+ end
39
+
40
+ def find_mappable_models
41
+ models = []
42
+
43
+ ActiveRecord::Base.descendants.each do |model_class|
44
+ next unless model_class.respond_to?(:nats_wave_subscribed_subjects)
45
+ next if model_class.nats_wave_subscribed_subjects.empty?
46
+
47
+ models << model_class
48
+ end
49
+
50
+ models
51
+ end
52
+
53
+ def register_model_subscriptions(models)
54
+ models.each do |model_class|
55
+ begin
56
+ register_model_subscription(model_class)
57
+ rescue => e
58
+ Rails.logger.error "Failed to register subscriptions for #{model_class.name}: #{e.message}"
59
+ end
60
+ end
61
+ end
62
+
63
+ def register_model_subscription(model_class)
64
+ subjects = model_class.nats_wave_subscribed_subjects
65
+ return if subjects.empty?
66
+
67
+ Rails.logger.debug "📡 Registering #{model_class.name} subscriptions: #{subjects.join(', ')}"
68
+
69
+ # Register with NATS client
70
+ NatsWave.client.subscribe(
71
+ subjects: subjects,
72
+ model_mappings: build_model_mappings_for(model_class)
73
+ )
74
+ end
75
+
76
+ def build_model_mappings_for(model_class)
77
+ mappings = {}
78
+
79
+ model_class.nats_wave_external_models.each do |external_model|
80
+ mapping_config = model_class.nats_wave_mapping_for(external_model)
81
+
82
+ mappings[external_model] = {
83
+ target_model: model_class.name,
84
+ field_mappings: mapping_config[:field_mappings] || {},
85
+ transformations: mapping_config[:transformations] || {},
86
+ conditions: mapping_config[:conditions] || {},
87
+ sync_strategy: mapping_config[:sync_strategy] || :upsert,
88
+ unique_fields: mapping_config[:unique_fields] || [:id]
89
+ }
90
+ end
91
+
92
+ mappings
93
+ end
94
+
95
+ def log_registration_summary
96
+ stats = ModelRegistry.subscription_stats
97
+
98
+ Rails.logger.info "📊 NatsWave Registration Summary:"
99
+ Rails.logger.info " Total Subscriptions: #{stats[:total_subscriptions]}"
100
+ Rails.logger.info " Unique Subjects: #{stats[:unique_subjects]}"
101
+ Rails.logger.info " Models with Subscriptions: #{stats[:models_with_subscriptions]}"
102
+
103
+ stats[:subscription_breakdown].each do |model, count|
104
+ Rails.logger.info " #{model}: #{count} subscription(s)"
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NatsWave
4
+ class Client
5
+ attr_reader :config, :publisher, :subscriber, :nats_client
6
+
7
+ def initialize(options = {})
8
+ @config = Configuration.new(options)
9
+ @nats_client = nil
10
+ @publisher = nil
11
+ @subscriber = nil
12
+ @middleware_stack = []
13
+
14
+ setup_connection_pool
15
+ setup_middleware
16
+ establish_connections
17
+ end
18
+
19
+ def publish(subject:, model:, action:, data:, metadata: {})
20
+ ensure_connected!
21
+ @publisher.publish(
22
+ subject: subject,
23
+ model: model,
24
+ action: action,
25
+ data: data,
26
+ metadata: metadata
27
+ )
28
+ end
29
+
30
+ def publish_batch(events)
31
+ ensure_connected!
32
+ @publisher.publish_batch(events)
33
+ end
34
+
35
+ def subscribe(subjects:, model_mappings: {}, &block)
36
+ ensure_connected!
37
+ @subscriber.subscribe(
38
+ subjects: subjects,
39
+ model_mappings: model_mappings,
40
+ handler: block
41
+ )
42
+ end
43
+
44
+ def start_subscriber
45
+ ensure_connected!
46
+ @subscriber.start if @subscriber
47
+ end
48
+
49
+ def health_check
50
+ {
51
+ nats_connected: connected?,
52
+ database_connected: database_connected?,
53
+ nats_url: @config.nats_url,
54
+ nats_server_url: @config.nats_server_url,
55
+ service_name: @config.service_name,
56
+ version: @config.version,
57
+ instance_id: @config.instance_id,
58
+ published_subjects: @config.subject_patterns,
59
+ timestamp: Time.current.iso8601,
60
+ }
61
+ end
62
+
63
+ def connected?
64
+ @nats_client && @nats_client.connected?
65
+ end
66
+
67
+ def disconnect!
68
+ @subscriber&.unsubscribe_all
69
+ @nats_client&.close
70
+ @connection_pool&.shutdown
71
+ @connection_pool&.wait_for_termination(5)
72
+ end
73
+
74
+ private
75
+
76
+ def setup_connection_pool
77
+ return unless defined?(Concurrent)
78
+
79
+ @connection_pool = Concurrent::ThreadPoolExecutor.new(
80
+ min_threads: 2,
81
+ max_threads: @config.connection_pool_size
82
+ )
83
+ end
84
+
85
+ def setup_middleware
86
+ @middleware_stack = []
87
+
88
+ if @config.middleware_authentication_enabled
89
+ @middleware_stack << Middleware::Authentication.new(@config)
90
+ end
91
+
92
+ if @config.middleware_validation_enabled
93
+ @middleware_stack << Middleware::Validation.new(@config)
94
+ end
95
+
96
+ if @config.middleware_logging_enabled
97
+ @middleware_stack << Middleware::Logging.new(@config)
98
+ end
99
+ end
100
+
101
+ def establish_connections
102
+ return unless nats_available?
103
+
104
+ establish_nats_connection
105
+
106
+ if @config.publishing_enabled
107
+ @publisher = Publisher.new(@config, @nats_client, @middleware_stack)
108
+ end
109
+
110
+ if @config.subscription_enabled
111
+ @subscriber = Subscriber.new(@config, @nats_client, @middleware_stack)
112
+ end
113
+ end
114
+
115
+ def establish_nats_connection
116
+ @nats_client = NATS.connect(
117
+ @config.nats_url,
118
+ reconnect_time_wait: @config.retry_delay,
119
+ max_reconnect_attempts: @config.reconnect_attempts
120
+ )
121
+ NatsWave.logger.info("Connected to NATS at #{@config.nats_url}")
122
+ rescue => e
123
+ NatsWave.logger.error("Failed to connect to NATS: #{e.message}")
124
+ raise ConnectionError, "Failed to connect to NATS: #{e.message}"
125
+ end
126
+
127
+ def ensure_connected!
128
+ raise ConnectionError, "Not connected to NATS" unless connected?
129
+ end
130
+
131
+ def database_connected?
132
+ return false unless @subscriber
133
+ @subscriber.database_connected?
134
+ end
135
+
136
+ def nats_available?
137
+ defined?(NATS)
138
+ rescue LoadError
139
+ false
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NatsWave
4
+ module Concerns
5
+ module Mappable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :nats_wave_mapping_config, :nats_wave_subscription_config
10
+ self.nats_wave_mapping_config = {}
11
+ self.nats_wave_subscription_config = {}
12
+ end
13
+
14
+ class_methods do
15
+ # Configure how this model maps to external models
16
+ def nats_wave_maps_to(external_models)
17
+ self.nats_wave_mapping_config = external_models
18
+
19
+ # Register this mapping globally
20
+ NatsWave::ModelRegistry.register_mapping(self.name, external_models)
21
+ end
22
+
23
+ # Configure how external models map to this model AND what subjects to subscribe to
24
+ def nats_wave_maps_from(external_model, options = {})
25
+ mapping = {
26
+ field_mappings: options[:field_mappings] || {},
27
+ transformations: options[:transformations] || {},
28
+ conditions: options[:conditions] || {},
29
+ sync_strategy: options[:sync_strategy] || :upsert,
30
+ unique_fields: options[:unique_fields] || [:id],
31
+ skip_fields: options[:skip_fields] || [],
32
+ # NEW: Subscription configuration
33
+ subjects: options[:subjects] || [],
34
+ handler: options[:handler],
35
+ queue_group: options[:queue_group]
36
+ }
37
+
38
+ self.nats_wave_mapping_config[external_model] = mapping
39
+
40
+ # Register this mapping globally
41
+ NatsWave::ModelRegistry.register_reverse_mapping(external_model, self.name, mapping)
42
+
43
+ # Register subscription if subjects are provided
44
+ if mapping[:subjects].any?
45
+ NatsWave::ModelRegistry.register_subscription(
46
+ subjects: mapping[:subjects],
47
+ model: self.name,
48
+ external_model: external_model,
49
+ handler: mapping[:handler],
50
+ queue_group: mapping[:queue_group]
51
+ )
52
+ end
53
+ end
54
+
55
+ # Configure subscriptions without model mapping (for custom handlers)
56
+ def nats_wave_subscribes_to(*subjects, handler: nil, queue_group: nil, &block)
57
+ handler ||= block
58
+
59
+ subscription_config = {
60
+ subjects: subjects.flatten,
61
+ handler: handler,
62
+ queue_group: queue_group,
63
+ model: self.name
64
+ }
65
+
66
+ self.nats_wave_subscription_config[:custom] = subscription_config
67
+
68
+ # Register subscription
69
+ NatsWave::ModelRegistry.register_subscription(
70
+ subjects: subjects.flatten,
71
+ model: self.name,
72
+ handler: handler,
73
+ queue_group: queue_group
74
+ )
75
+ end
76
+
77
+ # Auto-generate subjects based on external model pattern
78
+ def nats_wave_auto_subjects_for(external_model, service_prefix: nil, actions: [:create, :update, :destroy])
79
+ model_name = external_model.underscore
80
+
81
+ if service_prefix
82
+ subjects = actions.map { |action| "#{service_prefix}.#{model_name}.#{action}" }
83
+ subjects << "#{service_prefix}.#{model_name}.*" # Wildcard for all actions
84
+ else
85
+ # Try to infer from common patterns
86
+ subjects = [
87
+ "#{model_name}.*", # Simple pattern
88
+ "events.#{model_name}.*", # Events pattern
89
+ "*.#{model_name}.*", # Service.model pattern
90
+ "*.events.#{model_name}.*" # Service.events.model pattern
91
+ ]
92
+ end
93
+
94
+ subjects
95
+ end
96
+
97
+ # Get mapping configuration for an external model
98
+ def nats_wave_mapping_for(external_model)
99
+ nats_wave_mapping_config[external_model]
100
+ end
101
+
102
+ # Check if this model can sync from an external model
103
+ def nats_wave_can_sync_from?(external_model)
104
+ nats_wave_mapping_config.key?(external_model)
105
+ end
106
+
107
+ # Get all external models this model can sync from
108
+ def nats_wave_external_models
109
+ nats_wave_mapping_config.keys
110
+ end
111
+
112
+ # Get all subjects this model subscribes to
113
+ def nats_wave_subscribed_subjects
114
+ subjects = []
115
+
116
+ # From model mappings
117
+ nats_wave_mapping_config.each do |_, config|
118
+ subjects.concat(config[:subjects] || [])
119
+ end
120
+
121
+ # From custom subscriptions
122
+ if nats_wave_subscription_config[:custom]
123
+ subjects.concat(nats_wave_subscription_config[:custom][:subjects] || [])
124
+ end
125
+
126
+ subjects.uniq
127
+ end
128
+ end
129
+
130
+ # Instance methods remain the same...
131
+ def nats_wave_mapped_attributes_for(target_model)
132
+ mapping = self.class.nats_wave_mapping_config[target_model]
133
+ return attributes unless mapping
134
+
135
+ mapped_attrs = {}
136
+ field_mappings = mapping[:field_mappings] || {}
137
+ skip_fields = mapping[:skip_fields] || []
138
+
139
+ attributes.each do |key, value|
140
+ next if skip_fields.include?(key.to_s) || skip_fields.include?(key.to_sym)
141
+
142
+ mapped_key = field_mappings[key] || field_mappings[key.to_sym] || key
143
+ mapped_attrs[mapped_key] = transform_value(value, mapped_key, mapping[:transformations] || {})
144
+ end
145
+
146
+ mapped_attrs
147
+ end
148
+
149
+ def nats_wave_unique_identifier_for(external_model)
150
+ mapping = self.class.nats_wave_mapping_config[external_model]
151
+ unique_fields = mapping&.dig(:unique_fields) || [:id]
152
+
153
+ unique_fields.map { |field| [field, send(field)] }.to_h
154
+ end
155
+
156
+ private
157
+
158
+ def transform_value(value, field, transformations)
159
+ transformation = transformations[field] || transformations[field.to_sym]
160
+
161
+ case transformation
162
+ when Proc
163
+ transformation.call(value)
164
+ when Symbol
165
+ send(transformation, value) if respond_to?(transformation, true)
166
+ else
167
+ value
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end