jetstream_bridge 2.10.0 → 3.0.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/CHANGELOG.md +164 -0
- data/LICENSE +21 -0
- data/README.md +397 -0
- data/lib/generators/jetstream_bridge/health_check/health_check_generator.rb +65 -0
- data/lib/generators/jetstream_bridge/health_check/templates/health_controller.rb +38 -0
- data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +58 -13
- data/lib/generators/jetstream_bridge/migrations/templates/create_jetstream_inbox_events.rb.erb +1 -1
- data/lib/generators/jetstream_bridge/migrations/templates/create_jetstream_outbox_events.rb.erb +1 -1
- data/lib/jetstream_bridge/consumer/consumer.rb +42 -1
- data/lib/jetstream_bridge/consumer/dlq_publisher.rb +4 -1
- data/lib/jetstream_bridge/consumer/inbox/inbox_message.rb +3 -1
- data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +37 -31
- data/lib/jetstream_bridge/consumer/message_processor.rb +65 -31
- data/lib/jetstream_bridge/core/config.rb +35 -0
- data/lib/jetstream_bridge/core/connection.rb +78 -3
- data/lib/jetstream_bridge/core/connection_factory.rb +100 -0
- data/lib/jetstream_bridge/core/debug_helper.rb +119 -0
- data/lib/jetstream_bridge/core/duration.rb +8 -1
- data/lib/jetstream_bridge/core/retry_strategy.rb +136 -0
- data/lib/jetstream_bridge/errors.rb +39 -0
- data/lib/jetstream_bridge/models/event_envelope.rb +136 -0
- data/lib/jetstream_bridge/models/subject.rb +94 -0
- data/lib/jetstream_bridge/publisher/outbox_repository.rb +47 -28
- data/lib/jetstream_bridge/publisher/publisher.rb +12 -35
- data/lib/jetstream_bridge/railtie.rb +33 -1
- data/lib/jetstream_bridge/tasks/install.rake +99 -0
- data/lib/jetstream_bridge/topology/overlap_guard.rb +15 -1
- data/lib/jetstream_bridge/topology/stream.rb +15 -5
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +65 -0
- metadata +55 -11
|
@@ -11,52 +11,71 @@ module JetstreamBridge
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def find_or_build(event_id)
|
|
14
|
-
ModelUtils.find_or_init_by_best(
|
|
14
|
+
record = ModelUtils.find_or_init_by_best(
|
|
15
15
|
@klass,
|
|
16
16
|
{ event_id: event_id },
|
|
17
17
|
{ dedup_key: event_id } # fallback if app uses a different unique column
|
|
18
18
|
)
|
|
19
|
+
|
|
20
|
+
# Lock the row to prevent concurrent processing
|
|
21
|
+
if record.persisted? && !record.new_record? && record.respond_to?(:lock!)
|
|
22
|
+
begin
|
|
23
|
+
record.lock!
|
|
24
|
+
rescue ActiveRecord::RecordNotFound
|
|
25
|
+
# Record was deleted between find and lock, create new
|
|
26
|
+
record = @klass.new
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
record
|
|
19
31
|
end
|
|
20
32
|
|
|
21
33
|
def already_sent?(record)
|
|
22
|
-
record.respond_to?(:sent_at) && record.sent_at
|
|
34
|
+
(record.respond_to?(:sent_at) && record.sent_at) ||
|
|
35
|
+
(record.respond_to?(:status) && record.status == 'sent')
|
|
23
36
|
end
|
|
24
37
|
|
|
25
38
|
def persist_pre(record, subject, envelope)
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
ActiveRecord::Base.transaction do
|
|
40
|
+
now = Time.now.utc
|
|
41
|
+
event_id = envelope['event_id'].to_s
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
attrs = {
|
|
44
|
+
event_id: event_id,
|
|
45
|
+
subject: subject,
|
|
46
|
+
payload: ModelUtils.json_dump(envelope),
|
|
47
|
+
headers: ModelUtils.json_dump({ 'nats-msg-id' => event_id }),
|
|
48
|
+
status: 'publishing',
|
|
49
|
+
last_error: nil
|
|
50
|
+
}
|
|
51
|
+
attrs[:attempts] = 1 + (record.attempts || 0) if record.respond_to?(:attempts)
|
|
52
|
+
attrs[:enqueued_at] = (record.enqueued_at || now) if record.respond_to?(:enqueued_at)
|
|
53
|
+
attrs[:updated_at] = now if record.respond_to?(:updated_at)
|
|
40
54
|
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
ModelUtils.assign_known_attrs(record, attrs)
|
|
56
|
+
record.save!
|
|
57
|
+
end
|
|
43
58
|
end
|
|
44
59
|
|
|
45
60
|
def persist_success(record)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
ActiveRecord::Base.transaction do
|
|
62
|
+
now = Time.now.utc
|
|
63
|
+
attrs = { status: 'sent' }
|
|
64
|
+
attrs[:sent_at] = now if record.respond_to?(:sent_at)
|
|
65
|
+
attrs[:updated_at] = now if record.respond_to?(:updated_at)
|
|
66
|
+
ModelUtils.assign_known_attrs(record, attrs)
|
|
67
|
+
record.save!
|
|
68
|
+
end
|
|
52
69
|
end
|
|
53
70
|
|
|
54
71
|
def persist_failure(record, message)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
ActiveRecord::Base.transaction do
|
|
73
|
+
now = Time.now.utc
|
|
74
|
+
attrs = { status: 'failed', last_error: message }
|
|
75
|
+
attrs[:updated_at] = now if record.respond_to?(:updated_at)
|
|
76
|
+
ModelUtils.assign_known_attrs(record, attrs)
|
|
77
|
+
record.save!
|
|
78
|
+
end
|
|
60
79
|
end
|
|
61
80
|
|
|
62
81
|
def persist_exception(record, error)
|
|
@@ -6,22 +6,15 @@ require_relative '../core/connection'
|
|
|
6
6
|
require_relative '../core/logging'
|
|
7
7
|
require_relative '../core/config'
|
|
8
8
|
require_relative '../core/model_utils'
|
|
9
|
+
require_relative '../core/retry_strategy'
|
|
9
10
|
require_relative 'outbox_repository'
|
|
10
11
|
|
|
11
12
|
module JetstreamBridge
|
|
12
13
|
# Publishes to "{env}.{app}.sync.{dest}".
|
|
13
14
|
class Publisher
|
|
14
|
-
|
|
15
|
-
RETRY_BACKOFFS = [0.25, 1.0].freeze
|
|
16
|
-
|
|
17
|
-
TRANSIENT_ERRORS = begin
|
|
18
|
-
errs = [NATS::IO::Timeout, NATS::IO::Error]
|
|
19
|
-
errs << NATS::IO::SocketTimeoutError if defined?(NATS::IO::SocketTimeoutError)
|
|
20
|
-
errs.freeze
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def initialize
|
|
15
|
+
def initialize(retry_strategy: nil)
|
|
24
16
|
@jts = Connection.connect!
|
|
17
|
+
@retry_strategy = retry_strategy || PublisherRetryStrategy.new
|
|
25
18
|
end
|
|
26
19
|
|
|
27
20
|
# @return [Boolean]
|
|
@@ -33,7 +26,7 @@ module JetstreamBridge
|
|
|
33
26
|
if JetstreamBridge.config.use_outbox
|
|
34
27
|
publish_via_outbox(subject, envelope)
|
|
35
28
|
else
|
|
36
|
-
with_retries {
|
|
29
|
+
with_retries { publish_to_nats(subject, envelope) }
|
|
37
30
|
end
|
|
38
31
|
rescue StandardError => e
|
|
39
32
|
log_error(false, e)
|
|
@@ -47,7 +40,7 @@ module JetstreamBridge
|
|
|
47
40
|
raise ArgumentError, 'destination_app must be configured'
|
|
48
41
|
end
|
|
49
42
|
|
|
50
|
-
def
|
|
43
|
+
def publish_to_nats(subject, envelope)
|
|
51
44
|
headers = { 'nats-msg-id' => envelope['event_id'] }
|
|
52
45
|
|
|
53
46
|
ack = @jts.publish(subject, Oj.dump(envelope, mode: :compat), header: headers)
|
|
@@ -76,7 +69,7 @@ module JetstreamBridge
|
|
|
76
69
|
"Outbox model #{klass} is not an ActiveRecord model; publishing directly.",
|
|
77
70
|
tag: 'JetstreamBridge::Publisher'
|
|
78
71
|
)
|
|
79
|
-
return with_retries {
|
|
72
|
+
return with_retries { publish_to_nats(subject, envelope) }
|
|
80
73
|
end
|
|
81
74
|
|
|
82
75
|
repo = OutboxRepository.new(klass)
|
|
@@ -93,7 +86,7 @@ module JetstreamBridge
|
|
|
93
86
|
|
|
94
87
|
repo.persist_pre(record, subject, envelope)
|
|
95
88
|
|
|
96
|
-
ok = with_retries {
|
|
89
|
+
ok = with_retries { publish_to_nats(subject, envelope) }
|
|
97
90
|
ok ? repo.persist_success(record) : repo.persist_failure(record, 'Publish returned false')
|
|
98
91
|
ok
|
|
99
92
|
rescue StandardError => e
|
|
@@ -102,27 +95,11 @@ module JetstreamBridge
|
|
|
102
95
|
end
|
|
103
96
|
# ---- /Outbox path ----
|
|
104
97
|
|
|
105
|
-
# Retry
|
|
106
|
-
def with_retries(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
rescue *TRANSIENT_ERRORS => e
|
|
111
|
-
attempts += 1
|
|
112
|
-
return log_error(false, e) if attempts > retries
|
|
113
|
-
|
|
114
|
-
backoff(attempts, e)
|
|
115
|
-
retry
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def backoff(attempts, error)
|
|
120
|
-
delay = RETRY_BACKOFFS[attempts - 1] || RETRY_BACKOFFS.last
|
|
121
|
-
Logging.warn(
|
|
122
|
-
"Publish retry #{attempts} after #{error.class}: #{error.message}",
|
|
123
|
-
tag: 'JetstreamBridge::Publisher'
|
|
124
|
-
)
|
|
125
|
-
sleep delay
|
|
98
|
+
# Retry using strategy pattern
|
|
99
|
+
def with_retries(&block)
|
|
100
|
+
@retry_strategy.execute(context: 'Publisher', &block)
|
|
101
|
+
rescue RetryStrategy::RetryExhausted => e
|
|
102
|
+
log_error(false, e)
|
|
126
103
|
end
|
|
127
104
|
|
|
128
105
|
def log_error(val, exc)
|
|
@@ -1,17 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'core/model_codec_setup'
|
|
4
|
+
require_relative 'core/logging'
|
|
4
5
|
|
|
5
6
|
module JetstreamBridge
|
|
6
7
|
class Railtie < ::Rails::Railtie
|
|
7
|
-
|
|
8
|
+
# Set up logger to use Rails.logger by default
|
|
9
|
+
initializer 'jetstream_bridge.logger', before: :initialize_logger do
|
|
10
|
+
JetstreamBridge.configure do |config|
|
|
11
|
+
config.logger ||= Rails.logger if defined?(Rails.logger)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Load ActiveRecord model tweaks after ActiveRecord is loaded
|
|
16
|
+
initializer 'jetstream_bridge.active_record', after: 'active_record.initialize_database' do
|
|
8
17
|
ActiveSupport.on_load(:active_record) do
|
|
9
18
|
ActiveSupport::Reloader.to_prepare { JetstreamBridge::ModelCodecSetup.apply! }
|
|
10
19
|
end
|
|
11
20
|
end
|
|
12
21
|
|
|
22
|
+
# Validate configuration in development/test
|
|
23
|
+
initializer 'jetstream_bridge.validate_config', after: :load_config_initializers do |app|
|
|
24
|
+
if Rails.env.development? || Rails.env.test?
|
|
25
|
+
app.config.after_initialize do
|
|
26
|
+
JetstreamBridge.config.validate! if JetstreamBridge.config.destination_app
|
|
27
|
+
rescue JetstreamBridge::ConfigurationError => e
|
|
28
|
+
Rails.logger.warn "[JetStream Bridge] Configuration warning: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Add console helper methods
|
|
34
|
+
console do
|
|
35
|
+
Rails.logger.info "[JetStream Bridge] Loaded v#{JetstreamBridge::VERSION}"
|
|
36
|
+
Rails.logger.info '[JetStream Bridge] Use JetstreamBridge.health_check to check status'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Load rake tasks
|
|
13
40
|
rake_tasks do
|
|
14
41
|
load File.expand_path('tasks/install.rake', __dir__)
|
|
15
42
|
end
|
|
43
|
+
|
|
44
|
+
# Add generators
|
|
45
|
+
generators do
|
|
46
|
+
require 'generators/jetstream_bridge/health_check/health_check_generator'
|
|
47
|
+
end
|
|
16
48
|
end
|
|
17
49
|
end
|
|
@@ -7,4 +7,103 @@ namespace :jetstream_bridge do
|
|
|
7
7
|
Rails::Generators.invoke('jetstream_bridge:install', [], behavior: :invoke, destination_root: Rails.root.to_s)
|
|
8
8
|
puts '[jetstream_bridge] Done.'
|
|
9
9
|
end
|
|
10
|
+
|
|
11
|
+
desc 'Check health and connection status'
|
|
12
|
+
task health: :environment do
|
|
13
|
+
require 'json'
|
|
14
|
+
|
|
15
|
+
puts '=' * 70
|
|
16
|
+
puts 'JetStream Bridge Health Check'
|
|
17
|
+
puts '=' * 70
|
|
18
|
+
|
|
19
|
+
health = JetstreamBridge.health_check
|
|
20
|
+
|
|
21
|
+
puts "\nStatus: #{health[:healthy] ? '✓ HEALTHY' : '✗ UNHEALTHY'}"
|
|
22
|
+
puts "NATS Connected: #{health[:nats_connected] ? 'Yes' : 'No'}"
|
|
23
|
+
puts "Connected At: #{health[:connected_at] || 'N/A'}"
|
|
24
|
+
puts "Version: #{health[:version]}"
|
|
25
|
+
|
|
26
|
+
if health[:stream]
|
|
27
|
+
puts "\nStream:"
|
|
28
|
+
puts " Name: #{health[:stream][:name]}"
|
|
29
|
+
puts " Exists: #{health[:stream][:exists] ? 'Yes' : 'No'}"
|
|
30
|
+
if health[:stream][:subjects]
|
|
31
|
+
puts " Subjects: #{health[:stream][:subjects].join(', ')}"
|
|
32
|
+
puts " Messages: #{health[:stream][:messages]}"
|
|
33
|
+
end
|
|
34
|
+
puts " Error: #{health[:stream][:error]}" if health[:stream][:error]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if health[:config]
|
|
38
|
+
puts "\nConfiguration:"
|
|
39
|
+
puts " Environment: #{health[:config][:env]}"
|
|
40
|
+
puts " App Name: #{health[:config][:app_name]}"
|
|
41
|
+
puts " Destination: #{health[:config][:destination_app] || 'NOT SET'}"
|
|
42
|
+
puts " Outbox: #{health[:config][:use_outbox] ? 'Enabled' : 'Disabled'}"
|
|
43
|
+
puts " Inbox: #{health[:config][:use_inbox] ? 'Enabled' : 'Disabled'}"
|
|
44
|
+
puts " DLQ: #{health[:config][:use_dlq] ? 'Enabled' : 'Disabled'}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if health[:error]
|
|
48
|
+
puts "\n#{' ERROR '.center(70, '=')}"
|
|
49
|
+
puts health[:error]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
puts '=' * 70
|
|
53
|
+
|
|
54
|
+
exit(health[:healthy] ? 0 : 1)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc 'Validate configuration'
|
|
58
|
+
task validate: :environment do
|
|
59
|
+
puts '[jetstream_bridge] Validating configuration...'
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
JetstreamBridge.config.validate!
|
|
63
|
+
puts '✓ Configuration is valid'
|
|
64
|
+
puts "\nCurrent settings:"
|
|
65
|
+
puts " Environment: #{JetstreamBridge.config.env}"
|
|
66
|
+
puts " App Name: #{JetstreamBridge.config.app_name}"
|
|
67
|
+
puts " Destination: #{JetstreamBridge.config.destination_app}"
|
|
68
|
+
puts " Stream: #{JetstreamBridge.config.stream_name}"
|
|
69
|
+
puts " Source Subject: #{JetstreamBridge.config.source_subject}"
|
|
70
|
+
puts " Destination Subject: #{JetstreamBridge.config.destination_subject}"
|
|
71
|
+
exit 0
|
|
72
|
+
rescue JetstreamBridge::ConfigurationError => e
|
|
73
|
+
puts "✗ Configuration error: #{e.message}"
|
|
74
|
+
exit 1
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
desc 'Show debug information'
|
|
79
|
+
task debug: :environment do
|
|
80
|
+
JetstreamBridge::DebugHelper.debug_info
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
desc 'Test connection to NATS'
|
|
84
|
+
task test_connection: :environment do
|
|
85
|
+
puts '[jetstream_bridge] Testing NATS connection...'
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
jts = JetstreamBridge.ensure_topology!
|
|
89
|
+
puts '✓ Successfully connected to NATS'
|
|
90
|
+
puts '✓ JetStream is available'
|
|
91
|
+
puts '✓ Stream topology ensured'
|
|
92
|
+
|
|
93
|
+
# Check if we can get account info
|
|
94
|
+
info = jts.account_info
|
|
95
|
+
puts "\nAccount Info:"
|
|
96
|
+
puts " Memory: #{info.memory}"
|
|
97
|
+
puts " Storage: #{info.storage}"
|
|
98
|
+
puts " Streams: #{info.streams}"
|
|
99
|
+
puts " Consumers: #{info.consumers}"
|
|
100
|
+
|
|
101
|
+
exit 0
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
puts "✗ Connection failed: #{e.message}"
|
|
104
|
+
puts "\nBacktrace:" if ENV['VERBOSE']
|
|
105
|
+
puts e.backtrace.first(10).map { |line| " #{line}" }.join("\n") if ENV['VERBOSE']
|
|
106
|
+
exit 1
|
|
107
|
+
end
|
|
108
|
+
end
|
|
10
109
|
end
|
|
@@ -55,11 +55,25 @@ module JetstreamBridge
|
|
|
55
55
|
def list_stream_names(jts)
|
|
56
56
|
names = []
|
|
57
57
|
offset = 0
|
|
58
|
+
max_iterations = 100 # Safety limit to prevent infinite loops
|
|
59
|
+
iterations = 0
|
|
60
|
+
|
|
58
61
|
loop do
|
|
62
|
+
iterations += 1
|
|
63
|
+
if iterations > max_iterations
|
|
64
|
+
Logging.warn(
|
|
65
|
+
"Stream listing exceeded max iterations (#{max_iterations}), returning #{names.size} streams",
|
|
66
|
+
tag: 'JetstreamBridge::OverlapGuard'
|
|
67
|
+
)
|
|
68
|
+
break
|
|
69
|
+
end
|
|
70
|
+
|
|
59
71
|
resp = js_api_request(jts, '$JS.API.STREAM.NAMES', { offset: offset })
|
|
60
72
|
batch = Array(resp['streams']).filter_map { |h| h['name'] }
|
|
61
73
|
names.concat(batch)
|
|
62
|
-
|
|
74
|
+
total = resp['total'].to_i
|
|
75
|
+
|
|
76
|
+
break if names.size >= total || batch.empty?
|
|
63
77
|
|
|
64
78
|
offset = names.size
|
|
65
79
|
end
|
|
@@ -96,17 +96,27 @@ module JetstreamBridge
|
|
|
96
96
|
raise ArgumentError, 'subjects must not be empty' if desired.empty?
|
|
97
97
|
|
|
98
98
|
attempts = 0
|
|
99
|
+
max_attempts = 3
|
|
100
|
+
backoffs = [0.05, 0.2, 0.5]
|
|
101
|
+
|
|
99
102
|
begin
|
|
100
103
|
info = safe_stream_info(jts, name)
|
|
101
104
|
info ? ensure_update(jts, name, info, desired) : ensure_create(jts, name, desired)
|
|
102
105
|
rescue NATS::JetStream::Error => e
|
|
103
|
-
if StreamSupport.overlap_error?(e) && (attempts += 1) <=
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
if StreamSupport.overlap_error?(e) && (attempts += 1) <= max_attempts
|
|
107
|
+
backoff = backoffs[attempts - 1] || backoffs.last
|
|
108
|
+
Logging.warn(
|
|
109
|
+
"Overlap race while ensuring #{name}; retry #{attempts}/#{max_attempts} after #{backoff}s...",
|
|
110
|
+
tag: 'JetstreamBridge::Stream'
|
|
111
|
+
)
|
|
112
|
+
sleep(backoff)
|
|
106
113
|
retry
|
|
107
114
|
elsif StreamSupport.overlap_error?(e)
|
|
108
|
-
Logging.warn(
|
|
109
|
-
|
|
115
|
+
Logging.warn(
|
|
116
|
+
"Overlap persists ensuring #{name} after #{attempts} attempts; leaving unchanged. " \
|
|
117
|
+
"err=#{e.message.inspect}",
|
|
118
|
+
tag: 'JetstreamBridge::Stream'
|
|
119
|
+
)
|
|
110
120
|
nil
|
|
111
121
|
else
|
|
112
122
|
raise
|
data/lib/jetstream_bridge.rb
CHANGED
|
@@ -53,8 +53,73 @@ module JetstreamBridge
|
|
|
53
53
|
Connection.jetstream
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# Health check for monitoring and readiness probes
|
|
57
|
+
#
|
|
58
|
+
# @return [Hash] Health status including NATS connection, stream, and version
|
|
59
|
+
def health_check
|
|
60
|
+
conn_instance = Connection.instance
|
|
61
|
+
connected = conn_instance.connected?
|
|
62
|
+
connected_at = conn_instance.connected_at
|
|
63
|
+
|
|
64
|
+
stream_info = fetch_stream_info if connected
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
healthy: connected && stream_info[:exists],
|
|
68
|
+
nats_connected: connected,
|
|
69
|
+
connected_at: connected_at&.iso8601,
|
|
70
|
+
stream: stream_info,
|
|
71
|
+
config: {
|
|
72
|
+
env: config.env,
|
|
73
|
+
app_name: config.app_name,
|
|
74
|
+
destination_app: config.destination_app,
|
|
75
|
+
use_outbox: config.use_outbox,
|
|
76
|
+
use_inbox: config.use_inbox,
|
|
77
|
+
use_dlq: config.use_dlq
|
|
78
|
+
},
|
|
79
|
+
version: JetstreamBridge::VERSION
|
|
80
|
+
}
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
{
|
|
83
|
+
healthy: false,
|
|
84
|
+
error: "#{e.class}: #{e.message}"
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if connected to NATS
|
|
89
|
+
#
|
|
90
|
+
# @return [Boolean] true if connected and healthy
|
|
91
|
+
def connected?
|
|
92
|
+
Connection.instance.connected?
|
|
93
|
+
rescue StandardError
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get stream information for the configured stream
|
|
98
|
+
#
|
|
99
|
+
# @return [Hash] Stream information including subjects and message count
|
|
100
|
+
def stream_info
|
|
101
|
+
fetch_stream_info
|
|
102
|
+
end
|
|
103
|
+
|
|
56
104
|
private
|
|
57
105
|
|
|
106
|
+
def fetch_stream_info
|
|
107
|
+
jts = Connection.jetstream
|
|
108
|
+
info = jts.stream_info(config.stream_name)
|
|
109
|
+
{
|
|
110
|
+
exists: true,
|
|
111
|
+
name: config.stream_name,
|
|
112
|
+
subjects: info.config.subjects,
|
|
113
|
+
messages: info.state.messages
|
|
114
|
+
}
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
{
|
|
117
|
+
exists: false,
|
|
118
|
+
name: config.stream_name,
|
|
119
|
+
error: "#{e.class}: #{e.message}"
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
58
123
|
def assign!(cfg, key, val)
|
|
59
124
|
setter = :"#{key}="
|
|
60
125
|
raise ArgumentError, "Unknown configuration option: #{key}" unless cfg.respond_to?(setter)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jetstream_bridge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Attara
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-11-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -16,28 +16,54 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
19
|
+
version: 7.1.5.2
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '8.0'
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
27
|
- - ">="
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
29
|
+
version: 7.1.5.2
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.0'
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
34
|
name: activesupport
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
|
30
36
|
requirements:
|
|
31
37
|
- - ">="
|
|
32
38
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
39
|
+
version: 7.1.5.2
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '8.0'
|
|
34
43
|
type: :runtime
|
|
35
44
|
prerelease: false
|
|
36
45
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
46
|
requirements:
|
|
38
47
|
- - ">="
|
|
39
48
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
49
|
+
version: 7.1.5.2
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '8.0'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: mutex_m
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
type: :runtime
|
|
61
|
+
prerelease: false
|
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
41
67
|
- !ruby/object:Gem::Dependency
|
|
42
68
|
name: nats-pure
|
|
43
69
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -59,6 +85,9 @@ dependencies:
|
|
|
59
85
|
- - ">="
|
|
60
86
|
- !ruby/object:Gem::Version
|
|
61
87
|
version: '3.16'
|
|
88
|
+
- - "<"
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '4.0'
|
|
62
91
|
type: :runtime
|
|
63
92
|
prerelease: false
|
|
64
93
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -66,16 +95,25 @@ dependencies:
|
|
|
66
95
|
- - ">="
|
|
67
96
|
- !ruby/object:Gem::Version
|
|
68
97
|
version: '3.16'
|
|
98
|
+
- - "<"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '4.0'
|
|
69
101
|
description: |-
|
|
70
102
|
Publisher/Consumer utilities for NATS JetStream with environment-scoped subjects,
|
|
71
103
|
overlap guards, DLQ routing, retries/backoff, and optional Inbox/Outbox patterns.
|
|
72
|
-
Includes
|
|
104
|
+
Includes health checks, auto-reconnection, graceful shutdown, and topology setup
|
|
105
|
+
helpers for production-safe operation.
|
|
73
106
|
email:
|
|
74
107
|
- mpyebattara@gmail.com
|
|
75
108
|
executables: []
|
|
76
109
|
extensions: []
|
|
77
110
|
extra_rdoc_files: []
|
|
78
111
|
files:
|
|
112
|
+
- CHANGELOG.md
|
|
113
|
+
- LICENSE
|
|
114
|
+
- README.md
|
|
115
|
+
- lib/generators/jetstream_bridge/health_check/health_check_generator.rb
|
|
116
|
+
- lib/generators/jetstream_bridge/health_check/templates/health_controller.rb
|
|
79
117
|
- lib/generators/jetstream_bridge/initializer/initializer_generator.rb
|
|
80
118
|
- lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb
|
|
81
119
|
- lib/generators/jetstream_bridge/install/install_generator.rb
|
|
@@ -92,11 +130,17 @@ files:
|
|
|
92
130
|
- lib/jetstream_bridge/consumer/subscription_manager.rb
|
|
93
131
|
- lib/jetstream_bridge/core/config.rb
|
|
94
132
|
- lib/jetstream_bridge/core/connection.rb
|
|
133
|
+
- lib/jetstream_bridge/core/connection_factory.rb
|
|
134
|
+
- lib/jetstream_bridge/core/debug_helper.rb
|
|
95
135
|
- lib/jetstream_bridge/core/duration.rb
|
|
96
136
|
- lib/jetstream_bridge/core/logging.rb
|
|
97
137
|
- lib/jetstream_bridge/core/model_codec_setup.rb
|
|
98
138
|
- lib/jetstream_bridge/core/model_utils.rb
|
|
139
|
+
- lib/jetstream_bridge/core/retry_strategy.rb
|
|
140
|
+
- lib/jetstream_bridge/errors.rb
|
|
99
141
|
- lib/jetstream_bridge/inbox_event.rb
|
|
142
|
+
- lib/jetstream_bridge/models/event_envelope.rb
|
|
143
|
+
- lib/jetstream_bridge/models/subject.rb
|
|
100
144
|
- lib/jetstream_bridge/outbox_event.rb
|
|
101
145
|
- lib/jetstream_bridge/publisher/outbox_repository.rb
|
|
102
146
|
- lib/jetstream_bridge/publisher/publisher.rb
|
|
@@ -116,8 +160,8 @@ metadata:
|
|
|
116
160
|
changelog_uri: https://github.com/attaradev/jetstream_bridge/blob/main/CHANGELOG.md
|
|
117
161
|
documentation_uri: https://github.com/attaradev/jetstream_bridge#readme
|
|
118
162
|
bug_tracker_uri: https://github.com/attaradev/jetstream_bridge/issues
|
|
119
|
-
github_repo: ssh://github.com/attaradev/jetstream_bridge
|
|
120
163
|
rubygems_mfa_required: 'true'
|
|
164
|
+
allowed_push_host: https://rubygems.org
|
|
121
165
|
post_install_message:
|
|
122
166
|
rdoc_options: []
|
|
123
167
|
require_paths:
|
|
@@ -126,15 +170,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
126
170
|
requirements:
|
|
127
171
|
- - ">="
|
|
128
172
|
- !ruby/object:Gem::Version
|
|
129
|
-
version: 2.
|
|
173
|
+
version: 3.2.0
|
|
130
174
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
175
|
requirements:
|
|
132
176
|
- - ">="
|
|
133
177
|
- !ruby/object:Gem::Version
|
|
134
|
-
version:
|
|
178
|
+
version: '0'
|
|
135
179
|
requirements: []
|
|
136
180
|
rubygems_version: 3.4.19
|
|
137
181
|
signing_key:
|
|
138
182
|
specification_version: 4
|
|
139
|
-
summary:
|
|
183
|
+
summary: Production-safe realtime data bridge using NATS JetStream
|
|
140
184
|
test_files: []
|