nats_wave 1.1.4 → 1.1.7
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 +103 -54
- data/lib/nats_wave/adapters/datadog_metrics.rb +1 -1
- data/lib/nats_wave/client.rb +638 -29
- data/lib/nats_wave/concerns/mappable.rb +9 -3
- data/lib/nats_wave/configuration.rb +5 -4
- 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 +56 -61
- data/lib/nats_wave/railtie.rb +4 -3
- data/lib/nats_wave/subscriber.rb +127 -47
- data/lib/nats_wave/version.rb +1 -1
- data/lib/nats_wave.rb +2 -1
- metadata +2 -5
- data/examples/catalog_model.rb +0 -36
- data/examples/configuration_examples.rb +0 -68
- data/examples/user_model.rb +0 -58
data/lib/nats_wave/subscriber.rb
CHANGED
@@ -2,48 +2,74 @@
|
|
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
|
-
@
|
10
|
-
@middleware_stack = middleware_stack
|
9
|
+
@client = client
|
11
10
|
@database_connector = DatabaseConnector.new(config)
|
12
11
|
@model_mapper = ModelMapper.new(config)
|
13
12
|
@message_transformer = MessageTransformer.new(config)
|
14
13
|
@dead_letter_queue = DeadLetterQueue.new(config)
|
15
|
-
|
14
|
+
|
15
|
+
@registry_subscriptions = ModelRegistry.subscriptions
|
16
|
+
@nats_subscriptions = [] # NATS::Subscription objects
|
16
17
|
@running = false
|
18
|
+
@shutdown = false
|
17
19
|
end
|
18
20
|
|
19
|
-
def
|
20
|
-
return if @running
|
21
|
+
def begin
|
22
|
+
return if @running || @shutdown
|
21
23
|
return unless @config.subscription_enabled
|
22
24
|
|
23
25
|
@running = true
|
26
|
+
@shutdown = false
|
27
|
+
|
24
28
|
NatsWave.logger.info "Starting NATS subscriber for #{@config.service_name}"
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
|
30
|
+
# Use ModelRegistry subscriptions
|
31
|
+
@registry_subscriptions.each do |subscription|
|
32
|
+
subscription[:subjects].each do |subject|
|
33
|
+
subscribe_to_subject(subject, subscription[:handler])
|
34
|
+
end
|
29
35
|
end
|
30
36
|
|
31
|
-
NatsWave.logger.info "
|
37
|
+
NatsWave.logger.info "Started #{@registry_subscriptions.size} subscriptions from Model Registry"
|
38
|
+
|
39
|
+
# Keep the subscriber alive
|
40
|
+
keep_alive
|
32
41
|
end
|
33
42
|
|
34
|
-
def
|
43
|
+
def listen(subjects:, model_mappings: {}, handler: nil)
|
35
44
|
subjects = Array(subjects)
|
36
45
|
|
37
46
|
subjects.each do |subject|
|
38
|
-
subscribe_to_subject(subject, handler,
|
39
|
-
|
47
|
+
subscribe_to_subject(subject, handler, model_mappings)
|
48
|
+
NatsWave.logger.info "Subscribed to #{subject}"
|
40
49
|
end
|
41
50
|
end
|
42
51
|
|
43
|
-
def
|
44
|
-
@
|
45
|
-
@subscriptions.clear
|
52
|
+
def reset
|
53
|
+
@shutdown = true
|
46
54
|
@running = false
|
55
|
+
|
56
|
+
# Stop keep alive thread
|
57
|
+
if @keep_alive_thread&.alive?
|
58
|
+
@keep_alive_thread.kill
|
59
|
+
@keep_alive_thread = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
# Unsubscribe from all subscriptions
|
63
|
+
@nats_subscriptions.each do |subscription|
|
64
|
+
begin
|
65
|
+
subscription.unsubscribe if subscription.respond_to?(:unsubscribe)
|
66
|
+
rescue => e
|
67
|
+
NatsWave.logger.error "Error unsubscribing: #{e.message}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
@nats_subscriptions.clear
|
71
|
+
|
72
|
+
NatsWave.logger.info "🛑 Subscriber shutdown complete"
|
47
73
|
end
|
48
74
|
|
49
75
|
def database_connected?
|
@@ -51,67 +77,121 @@ module NatsWave
|
|
51
77
|
end
|
52
78
|
|
53
79
|
def disconnect
|
54
|
-
|
80
|
+
reset
|
55
81
|
end
|
56
82
|
|
57
83
|
private
|
58
84
|
|
59
85
|
def subscribe_to_subject(subject_pattern, custom_handler = nil, model_mappings = {})
|
60
|
-
NatsWave.logger.info "
|
86
|
+
NatsWave.logger.info "🔍 Attempting to subscribe to: #{subject_pattern}"
|
61
87
|
|
62
|
-
|
88
|
+
# Create the NATS subscription
|
89
|
+
nats_subscription = @client.subscribe(
|
63
90
|
subject_pattern,
|
64
91
|
queue: @config.queue_group
|
65
92
|
) do |msg|
|
66
|
-
|
67
|
-
|
93
|
+
begin
|
94
|
+
NatsWave.logger.debug "📨 Received message on #{msg.subject}"
|
95
|
+
process_message(msg.data, custom_handler, model_mappings)
|
96
|
+
rescue => e
|
97
|
+
NatsWave.logger.error "Error in subscription handler: #{e.message}"
|
98
|
+
NatsWave.logger.error e.backtrace.join("\n")
|
99
|
+
# Don't re-raise - this would kill the subscription
|
100
|
+
end
|
68
101
|
end
|
69
102
|
|
70
|
-
|
103
|
+
# Add to NATS subscriptions array
|
104
|
+
@nats_subscriptions << nats_subscription
|
105
|
+
NatsWave.logger.info "✅ Successfully subscribed to #{subject_pattern} (total: #{@nats_subscriptions.size})"
|
71
106
|
end
|
72
107
|
|
73
108
|
def process_message(raw_message, custom_handler, model_mappings)
|
74
109
|
return unless should_process_message?(raw_message)
|
75
110
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
processed_message, model_mappings
|
85
|
-
)
|
86
|
-
end
|
111
|
+
NatsWave.logger.debug "🔄 Processing message: #{raw_message[0..200]}..."
|
112
|
+
|
113
|
+
message = parse_message(raw_message)
|
114
|
+
|
115
|
+
if custom_handler
|
116
|
+
custom_handler.call(message)
|
117
|
+
else
|
118
|
+
handle_model_sync(message, model_mappings)
|
87
119
|
end
|
88
120
|
|
89
|
-
NatsWave.logger.debug('Successfully processed message')
|
121
|
+
NatsWave.logger.debug('✅ Successfully processed message')
|
90
122
|
rescue StandardError => e
|
91
123
|
handle_error(e, raw_message, message)
|
124
|
+
# Don't re-raise - this would kill the subscription
|
92
125
|
end
|
93
126
|
|
94
|
-
def should_process_message?(
|
95
|
-
|
96
|
-
message = JSON.parse(raw_message)
|
127
|
+
def should_process_message?(raw_message_data)
|
128
|
+
message = JSON.parse(raw_message_data)
|
97
129
|
source = message['source'] || {}
|
98
130
|
|
99
|
-
|
100
|
-
|
131
|
+
# Skip messages from same service instance
|
132
|
+
if source['service'] == @config.service_name && source['instance_id'] == @config.instance_id
|
133
|
+
NatsWave.logger.debug "🔄 Skipping message from same service instance"
|
134
|
+
return false
|
135
|
+
end
|
101
136
|
|
102
137
|
true
|
103
|
-
rescue JSON::ParserError
|
138
|
+
rescue JSON::ParserError => e
|
139
|
+
NatsWave.logger.error "Failed to parse message for filtering: #{e.message}"
|
104
140
|
false
|
105
141
|
end
|
106
142
|
|
107
|
-
def
|
108
|
-
|
143
|
+
def keep_alive
|
144
|
+
Rails.logger.info "🔄 Starting keep-alive thread for persistent JetStream connection"
|
145
|
+
|
146
|
+
@keep_alive_thread = Thread.new do
|
147
|
+
while @running && !@shutdown
|
148
|
+
begin
|
149
|
+
sleep 30 # Check every 30 seconds
|
150
|
+
|
151
|
+
if @client&.connected?
|
152
|
+
Rails.logger.debug "💓 Subscriber connection healthy - #{@nats_subscriptions.size} active subscriptions"
|
153
|
+
else
|
154
|
+
Rails.logger.error "❌ Subscriber connection lost! Attempting reconnection..."
|
155
|
+
attempt_reconnection
|
156
|
+
end
|
157
|
+
|
158
|
+
rescue => e
|
159
|
+
Rails.logger.error "Error in keep_alive thread: #{e.message}"
|
160
|
+
sleep 10
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
Rails.logger.info "🛑 Keep-alive thread shutting down"
|
165
|
+
end
|
109
166
|
end
|
110
167
|
|
111
|
-
def
|
112
|
-
|
113
|
-
|
168
|
+
def attempt_reconnection
|
169
|
+
return if @shutdown
|
170
|
+
|
171
|
+
NatsWave.logger.info "🔄 Attempting to reconnect to NATS..."
|
172
|
+
|
173
|
+
# Reset subscriptions
|
174
|
+
@nats_subscriptions.clear
|
175
|
+
|
176
|
+
# Try to reestablish subscriptions
|
177
|
+
sleep 5 # Wait before reconnecting
|
178
|
+
|
179
|
+
if @client&.connected?
|
180
|
+
NatsWave.logger.info "✅ NATS reconnected, reestablishing subscriptions"
|
181
|
+
|
182
|
+
@registry_subscriptions.each do |subscription|
|
183
|
+
subscription[:subjects].each do |subject|
|
184
|
+
subscribe_to_subject(subject, subscription[:handler])
|
185
|
+
end
|
186
|
+
end
|
114
187
|
end
|
188
|
+
rescue => e
|
189
|
+
NatsWave.logger.error "Failed to reconnect: #{e.message}"
|
190
|
+
end
|
191
|
+
|
192
|
+
# ... rest of your existing methods (parse_message, handle_model_sync, handle_error)
|
193
|
+
def parse_message(raw_message)
|
194
|
+
@message_transformer.parse_message(raw_message)
|
115
195
|
end
|
116
196
|
|
117
197
|
def handle_model_sync(message, model_mappings)
|
@@ -158,4 +238,4 @@ module NatsWave
|
|
158
238
|
# Continue processing - don't raise to avoid breaking subscription
|
159
239
|
end
|
160
240
|
end
|
161
|
-
end
|
241
|
+
end
|
data/lib/nats_wave/version.rb
CHANGED
data/lib/nats_wave.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.7
|
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
|
@@ -318,9 +318,6 @@ files:
|
|
318
318
|
- README.md
|
319
319
|
- Rakefile
|
320
320
|
- config/nats_wave.yml
|
321
|
-
- examples/catalog_model.rb
|
322
|
-
- examples/configuration_examples.rb
|
323
|
-
- examples/user_model.rb
|
324
321
|
- lib/generators/nats_wave/install_generator.rb
|
325
322
|
- lib/generators/nats_wave/templates/README
|
326
323
|
- lib/generators/nats_wave/templates/create_nats_wave_failed_messages.rb
|
data/examples/catalog_model.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Catalog < ApplicationRecord
|
4
|
-
include NatsWave::NatsPublishable
|
5
|
-
|
6
|
-
nats_publishable(
|
7
|
-
actions: %i[create update], # Don't publish deletes
|
8
|
-
skip_attributes: %i[view_count search_vector internal_notes],
|
9
|
-
include_associations: %i[category variants]
|
10
|
-
)
|
11
|
-
|
12
|
-
# Custom serializer support
|
13
|
-
def self.nats_wave_attribute_transformations
|
14
|
-
{
|
15
|
-
hammer_estimate: ->(value, _action) { value.to_f.round(2) },
|
16
|
-
name: :sanitize_name,
|
17
|
-
description: lambda { |value, _action|
|
18
|
-
ActionView::Base.full_sanitizer.sanitize(value)
|
19
|
-
}
|
20
|
-
}
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def sanitize_name(name, _action)
|
26
|
-
name&.strip&.titleize
|
27
|
-
end
|
28
|
-
|
29
|
-
def nats_wave_metadata
|
30
|
-
{
|
31
|
-
organization: organization&.name,
|
32
|
-
make:,
|
33
|
-
catalog_tracked: track_catalog?
|
34
|
-
}
|
35
|
-
end
|
36
|
-
end
|
@@ -1,68 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# Basic configuration
|
4
|
-
NatsWave.configure do |config|
|
5
|
-
config.nats_url = 'nats://localhost:4222'
|
6
|
-
config.service_name = 'ecommerce_api'
|
7
|
-
config.default_subject_prefix = 'ecommerce.events'
|
8
|
-
end
|
9
|
-
|
10
|
-
# Advanced configuration with model mappings
|
11
|
-
NatsWave.configure do |config|
|
12
|
-
config.nats_url = ENV.fetch('NATS_URL', 'nats://localhost:4222')
|
13
|
-
config.service_name = 'catalog_service'
|
14
|
-
config.environment = Rails.env
|
15
|
-
|
16
|
-
# Subscribe to other team's events
|
17
|
-
config.add_subscription('warehouse.assets.*')
|
18
|
-
config.add_subscription('external.catalogs.*')
|
19
|
-
config.add_subscription('payments.transactions.*')
|
20
|
-
|
21
|
-
# Map external models to local models
|
22
|
-
config.add_model_mapping('WarehouseAsset', 'Catalog', {
|
23
|
-
'asset_name' => 'name',
|
24
|
-
'asset_description' => 'description',
|
25
|
-
'asset_value' => 'hammer_estimate',
|
26
|
-
'asset_id' => 'external_icn',
|
27
|
-
'model' => 'category'
|
28
|
-
})
|
29
|
-
|
30
|
-
config.add_model_mapping('ExternalCatalog', 'Catalog', {
|
31
|
-
'package_name' => 'name',
|
32
|
-
'package_cost' => 'hammer_estimate',
|
33
|
-
'external_vin' => 'vin'
|
34
|
-
})
|
35
|
-
|
36
|
-
# Custom transformation rules
|
37
|
-
config.transformation_rules = {
|
38
|
-
'hammer_estimate' => ->(value) { value.to_f.round(2) },
|
39
|
-
'name' => ->(value) { value&.titleize },
|
40
|
-
'email' => ->(value) { value&.downcase }
|
41
|
-
}
|
42
|
-
|
43
|
-
# Custom subscription with handler
|
44
|
-
config.add_subscription('payments.failed.*') do |message|
|
45
|
-
PaymentFailureHandler.process(message)
|
46
|
-
end
|
47
|
-
|
48
|
-
# Middleware configuration
|
49
|
-
config.middleware_authentication_enabled = Rails.env.production?
|
50
|
-
config.auth_secret_key = ENV['NATS_AUTH_SECRET']
|
51
|
-
config.middleware_validation_enabled = true
|
52
|
-
config.middleware_logging_enabled = true
|
53
|
-
config.log_level = Rails.env.production? ? 'info' : 'debug'
|
54
|
-
end
|
55
|
-
|
56
|
-
# Catalogion configuration
|
57
|
-
if Rails.env.production?
|
58
|
-
NatsWave.configure do |config|
|
59
|
-
config.nats_url = ENV.fetch('NATS_CLUSTER_URL')
|
60
|
-
config.connection_pool_size = 20
|
61
|
-
config.async_publishing = true
|
62
|
-
config.middleware_authentication_enabled = true
|
63
|
-
config.auth_secret_key = ENV.fetch('NATS_AUTH_SECRET')
|
64
|
-
config.schema_registry_url = ENV['SCHEMA_REGISTRY_URL']
|
65
|
-
config.max_retries = 5
|
66
|
-
config.retry_delay = 10
|
67
|
-
end
|
68
|
-
end
|
data/examples/user_model.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class User < ApplicationRecord
|
4
|
-
include NatsWave::NatsPublishable
|
5
|
-
|
6
|
-
# Configure NATS publishing
|
7
|
-
nats_publishable(
|
8
|
-
actions: %i[create update destroy],
|
9
|
-
skip_attributes: %i[password_digest remember_token password_reset_token],
|
10
|
-
include_associations: %i[profile organization],
|
11
|
-
subject_prefix: 'users',
|
12
|
-
metadata: { tenant_id: 'main' },
|
13
|
-
if: -> { active? },
|
14
|
-
unless: -> { system_user? }
|
15
|
-
)
|
16
|
-
|
17
|
-
# Define unique attributes for syncing
|
18
|
-
def self.nats_wave_unique_attributes
|
19
|
-
%i[email external_icn username]
|
20
|
-
end
|
21
|
-
|
22
|
-
# Define attributes that shouldn't be synced from other instances
|
23
|
-
def self.nats_wave_skip_sync_attributes
|
24
|
-
%i[last_login_at login_count password_digest]
|
25
|
-
end
|
26
|
-
|
27
|
-
# Custom attribute transformations
|
28
|
-
def self.nats_wave_attribute_transformations
|
29
|
-
{
|
30
|
-
email: ->(value, _action) { value&.downcase&.strip },
|
31
|
-
phone: :format_phone_number
|
32
|
-
}
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
# Custom metadata for NATS messages
|
38
|
-
def nats_wave_metadata
|
39
|
-
{
|
40
|
-
tenant_id: organization&.tenant_id,
|
41
|
-
user_type: account_type,
|
42
|
-
subscription_level: subscription&.level
|
43
|
-
}
|
44
|
-
end
|
45
|
-
|
46
|
-
def format_phone_number(phone, _action)
|
47
|
-
# Custom transformation logic
|
48
|
-
phone&.gsub(/\D/, '')
|
49
|
-
end
|
50
|
-
|
51
|
-
def active?
|
52
|
-
status == 'active'
|
53
|
-
end
|
54
|
-
|
55
|
-
def system_user?
|
56
|
-
email&.ends_with?('@system.internal')
|
57
|
-
end
|
58
|
-
end
|