jetstream_bridge 1.5.0 → 1.7.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 +4 -4
- data/.idea/dictionaries/project.xml +2 -0
- data/.idea/jetstream_bridge.iml +6 -1
- data/.rubocop.yml +102 -0
- data/Gemfile.lock +1 -5
- data/README.md +163 -78
- data/jetstream_bridge.gemspec +9 -10
- data/lib/generators/jetstream_bridge/initializer/initializer_generator.rb +16 -0
- data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +24 -0
- data/lib/generators/jetstream_bridge/install/install_generator.rb +19 -0
- data/lib/generators/jetstream_bridge/migrations/migrations_generator.rb +44 -0
- data/lib/generators/jetstream_bridge/migrations/templates/create_jetstream_inbox_events.rb.erb +24 -0
- data/lib/generators/jetstream_bridge/migrations/templates/create_jetstream_outbox_events.rb.erb +21 -0
- data/lib/jetstream_bridge/consumer/consumer.rb +103 -0
- data/lib/jetstream_bridge/{consumer_config.rb → consumer/consumer_config.rb} +3 -3
- data/lib/jetstream_bridge/consumer/inbox/inbox_message.rb +50 -0
- data/lib/jetstream_bridge/consumer/inbox/inbox_processor.rb +51 -0
- data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +102 -0
- data/lib/jetstream_bridge/{message_processor.rb → consumer/message_processor.rb} +1 -1
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +91 -0
- data/lib/jetstream_bridge/{connection.rb → core/connection.rb} +1 -1
- data/lib/jetstream_bridge/core/model_utils.rb +51 -0
- data/lib/jetstream_bridge/models/inbox_event.rb +98 -0
- data/lib/jetstream_bridge/models/outbox_event.rb +114 -0
- data/lib/jetstream_bridge/publisher/outbox_repository.rb +70 -0
- data/lib/jetstream_bridge/{publisher.rb → publisher/publisher.rb} +41 -4
- data/lib/jetstream_bridge/railtie.rb +12 -0
- data/lib/jetstream_bridge/tasks/install.rake +10 -0
- data/lib/jetstream_bridge/{overlap_guard.rb → topology/overlap_guard.rb} +6 -4
- data/lib/jetstream_bridge/topology/stream.rb +129 -0
- data/lib/jetstream_bridge/{topology.rb → topology/topology.rb} +2 -2
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +35 -23
- metadata +49 -49
- data/lib/jetstream_bridge/consumer.rb +0 -136
- data/lib/jetstream_bridge/dlq.rb +0 -24
- data/lib/jetstream_bridge/inbox_event.rb +0 -46
- data/lib/jetstream_bridge/outbox_event.rb +0 -60
- data/lib/jetstream_bridge/stream.rb +0 -114
- /data/lib/jetstream_bridge/{config.rb → core/config.rb} +0 -0
- /data/lib/jetstream_bridge/{duration.rb → core/duration.rb} +0 -0
- /data/lib/jetstream_bridge/{logging.rb → core/logging.rb} +0 -0
- /data/lib/jetstream_bridge/{subject_matcher.rb → topology/subject_matcher.rb} +0 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'active_record'
|
5
|
+
rescue LoadError
|
6
|
+
# No-op; we provide a shim below if AR is missing.
|
7
|
+
end
|
8
|
+
|
9
|
+
module JetstreamBridge
|
10
|
+
# Default Outbox model when `use_outbox` is enabled.
|
11
|
+
# Works with event-centric columns and stays compatible with legacy resource_* fields.
|
12
|
+
if defined?(ActiveRecord::Base)
|
13
|
+
class OutboxEvent < ActiveRecord::Base
|
14
|
+
self.table_name = 'jetstream_outbox_events'
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def column?(name) = column_names.include?(name.to_s)
|
18
|
+
|
19
|
+
def attribute_json?(name)
|
20
|
+
return false unless respond_to?(:attribute_types) && attribute_types.key?(name.to_s)
|
21
|
+
attribute_types[name.to_s].to_s.downcase.include?('json')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# JSON casting fallback if column is text
|
26
|
+
if column?(:payload)
|
27
|
+
serialize :payload, coder: JSON unless attribute_json?(:payload)
|
28
|
+
end
|
29
|
+
if column?(:headers)
|
30
|
+
serialize :headers, coder: JSON unless attribute_json?(:headers)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Validations (guarded by column existence)
|
34
|
+
validates :payload, presence: true, if: -> { self.class.column?(:payload) }
|
35
|
+
|
36
|
+
if self.class.column?(:event_id)
|
37
|
+
validates :event_id, presence: true, uniqueness: true
|
38
|
+
else
|
39
|
+
validates :resource_type, presence: true, if: -> { self.class.column?(:resource_type) }
|
40
|
+
validates :resource_id, presence: true, if: -> { self.class.column?(:resource_id) }
|
41
|
+
validates :event_type, presence: true, if: -> { self.class.column?(:event_type) }
|
42
|
+
end
|
43
|
+
|
44
|
+
validates :subject, presence: true, if: -> { self.class.column?(:subject) }
|
45
|
+
|
46
|
+
if self.class.column?(:status)
|
47
|
+
STATUSES = %w[pending publishing sent failed].freeze
|
48
|
+
validates :status, inclusion: { in: STATUSES }
|
49
|
+
end
|
50
|
+
|
51
|
+
if self.class.column?(:attempts)
|
52
|
+
validates :attempts, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Scopes (optional)
|
56
|
+
scope :pending, -> { where(status: 'pending') }, if: -> { column?(:status) }
|
57
|
+
scope :publishing, -> { where(status: 'publishing') }, if: -> { column?(:status) }
|
58
|
+
scope :failed, -> { where(status: 'failed') }, if: -> { column?(:status) }
|
59
|
+
scope :sent, -> { where(status: 'sent') }, if: -> { column?(:status) }
|
60
|
+
scope :ready_to_send, -> { where(status: %w[pending failed]) }, if: -> { column?(:status) }
|
61
|
+
|
62
|
+
before_validation do
|
63
|
+
now = Time.now.utc
|
64
|
+
self.status ||= 'pending' if self.class.column?(:status) && status.blank?
|
65
|
+
self.enqueued_at ||= now if self.class.column?(:enqueued_at) && enqueued_at.blank?
|
66
|
+
self.attempts = 0 if self.class.column?(:attempts) && attempts.nil?
|
67
|
+
end
|
68
|
+
|
69
|
+
# Helpers (no-ops if columns missing)
|
70
|
+
def mark_sent!
|
71
|
+
now = Time.now.utc
|
72
|
+
self.status = 'sent' if self.class.column?(:status)
|
73
|
+
self.sent_at = now if self.class.column?(:sent_at)
|
74
|
+
save!
|
75
|
+
end
|
76
|
+
|
77
|
+
def mark_failed!(err_msg)
|
78
|
+
self.status = 'failed' if self.class.column?(:status)
|
79
|
+
self.last_error = err_msg if self.class.column?(:last_error)
|
80
|
+
save!
|
81
|
+
end
|
82
|
+
|
83
|
+
def payload_hash
|
84
|
+
v = self[:payload]
|
85
|
+
case v
|
86
|
+
when String then JSON.parse(v) rescue {}
|
87
|
+
when Hash then v
|
88
|
+
else v.respond_to?(:as_json) ? v.as_json : {}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
else
|
93
|
+
# Shim: friendly error if AR is not available.
|
94
|
+
class OutboxEvent
|
95
|
+
class << self
|
96
|
+
def method_missing(method_name, *_args, &_block)
|
97
|
+
raise_missing_ar!('Outbox', method_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
def respond_to_missing?(_name, _priv = false) = false
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def raise_missing_ar!(which, method_name)
|
105
|
+
raise(
|
106
|
+
"#{which} requires ActiveRecord (tried to call ##{method_name}). " \
|
107
|
+
'Enable `use_outbox` only in apps with ActiveRecord, or add ' \
|
108
|
+
'`gem "activerecord"` to your Gemfile.'
|
109
|
+
)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../core/model_utils'
|
4
|
+
require_relative '../core/logging'
|
5
|
+
|
6
|
+
module JetstreamBridge
|
7
|
+
# Encapsulates AR-backed outbox persistence operations.
|
8
|
+
class OutboxRepository
|
9
|
+
def initialize(klass)
|
10
|
+
@klass = klass
|
11
|
+
end
|
12
|
+
|
13
|
+
def find_or_build(event_id)
|
14
|
+
ModelUtils.find_or_init_by_best(
|
15
|
+
@klass,
|
16
|
+
{ event_id: event_id },
|
17
|
+
{ dedup_key: event_id } # fallback if app uses a different unique column
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def already_sent?(record)
|
22
|
+
record.respond_to?(:sent_at) && record.sent_at
|
23
|
+
end
|
24
|
+
|
25
|
+
def persist_pre(record, subject, envelope)
|
26
|
+
now = Time.now.utc
|
27
|
+
event_id = envelope['event_id'].to_s
|
28
|
+
|
29
|
+
attrs = {
|
30
|
+
event_id: event_id,
|
31
|
+
subject: subject,
|
32
|
+
payload: ModelUtils.json_dump(envelope),
|
33
|
+
headers: ModelUtils.json_dump({ 'Nats-Msg-Id' => event_id }),
|
34
|
+
status: 'publishing',
|
35
|
+
last_error: nil
|
36
|
+
}
|
37
|
+
attrs[:attempts] = 1 + (record.attempts || 0) if record.respond_to?(:attempts)
|
38
|
+
attrs[:enqueued_at]= (record.enqueued_at || now) if record.respond_to?(:enqueued_at)
|
39
|
+
attrs[:updated_at] = now if record.respond_to?(:updated_at)
|
40
|
+
|
41
|
+
ModelUtils.assign_known_attrs(record, attrs)
|
42
|
+
record.save!
|
43
|
+
end
|
44
|
+
|
45
|
+
def persist_success(record)
|
46
|
+
now = Time.now.utc
|
47
|
+
attrs = { status: 'sent' }
|
48
|
+
attrs[:sent_at] = now if record.respond_to?(:sent_at)
|
49
|
+
attrs[:updated_at]= now if record.respond_to?(:updated_at)
|
50
|
+
ModelUtils.assign_known_attrs(record, attrs)
|
51
|
+
record.save!
|
52
|
+
end
|
53
|
+
|
54
|
+
def persist_failure(record, message)
|
55
|
+
now = Time.now.utc
|
56
|
+
attrs = { status: 'failed', last_error: message }
|
57
|
+
attrs[:updated_at] = now if record.respond_to?(:updated_at)
|
58
|
+
ModelUtils.assign_known_attrs(record, attrs)
|
59
|
+
record.save!
|
60
|
+
end
|
61
|
+
|
62
|
+
def persist_exception(record, error)
|
63
|
+
return unless record
|
64
|
+
persist_failure(record, "#{error.class}: #{error.message}")
|
65
|
+
rescue => e2
|
66
|
+
Logging.warn("Failed to persist outbox failure: #{e2.class}: #{e2.message}",
|
67
|
+
tag: 'JetstreamBridge::Publisher')
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
require 'securerandom'
|
5
|
-
require_relative 'connection'
|
6
|
-
require_relative 'logging'
|
7
|
-
require_relative 'config'
|
5
|
+
require_relative '../core/connection'
|
6
|
+
require_relative '../core/logging'
|
7
|
+
require_relative '../core/config'
|
8
|
+
require_relative '../core/model_utils'
|
8
9
|
|
9
10
|
module JetstreamBridge
|
10
11
|
# Publishes to "{env}.data.sync.{app}.{dest}".
|
@@ -27,7 +28,12 @@ module JetstreamBridge
|
|
27
28
|
ensure_destination!
|
28
29
|
envelope = build_envelope(resource_type, event_type, payload, options)
|
29
30
|
subject = JetstreamBridge.config.source_subject
|
30
|
-
|
31
|
+
|
32
|
+
if JetstreamBridge.config.use_outbox
|
33
|
+
publish_via_outbox(subject, envelope)
|
34
|
+
else
|
35
|
+
with_retries { do_publish(subject, envelope) }
|
36
|
+
end
|
31
37
|
rescue StandardError => e
|
32
38
|
log_error(false, e)
|
33
39
|
end
|
@@ -47,6 +53,37 @@ module JetstreamBridge
|
|
47
53
|
true
|
48
54
|
end
|
49
55
|
|
56
|
+
# ---- Outbox path ----
|
57
|
+
def publish_via_outbox(subject, envelope)
|
58
|
+
klass = ModelUtils.constantize(JetstreamBridge.config.outbox_model)
|
59
|
+
|
60
|
+
unless ModelUtils.ar_class?(klass)
|
61
|
+
Logging.warn("Outbox model #{klass} is not an ActiveRecord model; publishing directly.",
|
62
|
+
tag: 'JetstreamBridge::Publisher')
|
63
|
+
return with_retries { do_publish(subject, envelope) }
|
64
|
+
end
|
65
|
+
|
66
|
+
repo = OutboxRepository.new(klass)
|
67
|
+
event_id = envelope['event_id'].to_s
|
68
|
+
record = repo.find_or_build(event_id)
|
69
|
+
|
70
|
+
if repo.already_sent?(record)
|
71
|
+
Logging.info("Outbox already sent event_id=#{event_id}; skipping publish.",
|
72
|
+
tag: 'JetstreamBridge::Publisher')
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
|
76
|
+
repo.persist_pre(record, subject, envelope)
|
77
|
+
|
78
|
+
ok = with_retries { do_publish(subject, envelope) }
|
79
|
+
ok ? repo.persist_success(record) : repo.persist_failure(record, 'Publish returned false')
|
80
|
+
ok
|
81
|
+
rescue => e
|
82
|
+
repo.persist_exception(record, e) if defined?(repo) && defined?(record)
|
83
|
+
log_error(false, e)
|
84
|
+
end
|
85
|
+
# ---- /Outbox path ----
|
86
|
+
|
50
87
|
# Retry only on transient NATS IO errors
|
51
88
|
def with_retries(retries = DEFAULT_RETRIES)
|
52
89
|
attempts = 0
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :jetstream_bridge do
|
4
|
+
desc 'Install JetstreamBridge (initializer + migrations)'
|
5
|
+
task install: :environment do
|
6
|
+
puts '[jetstream_bridge] Generating initializer and migrations...'
|
7
|
+
Rails::Generators.invoke('jetstream_bridge:install', [], behavior: :invoke, destination_root: Rails.root.to_s)
|
8
|
+
puts '[jetstream_bridge] Done.'
|
9
|
+
end
|
10
|
+
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
require_relative 'subject_matcher'
|
5
|
-
require_relative 'logging'
|
5
|
+
require_relative '../core/logging'
|
6
6
|
|
7
7
|
module JetstreamBridge
|
8
8
|
# Checks for overlapping subjects.
|
@@ -12,6 +12,7 @@ module JetstreamBridge
|
|
12
12
|
def check!(jts, target_name, new_subjects)
|
13
13
|
conflicts = overlaps(jts, target_name, new_subjects)
|
14
14
|
return if conflicts.empty?
|
15
|
+
|
15
16
|
raise conflict_message(target_name, conflicts)
|
16
17
|
end
|
17
18
|
|
@@ -22,13 +23,13 @@ module JetstreamBridge
|
|
22
23
|
streams = list_streams_with_subjects(jts)
|
23
24
|
others = streams.reject { |s| s[:name] == target_name }
|
24
25
|
|
25
|
-
others.
|
26
|
+
others.filter_map do |s|
|
26
27
|
pairs = desired.flat_map do |n|
|
27
28
|
Array(s[:subjects]).map(&:to_s).select { |e| SubjectMatcher.overlap?(n, e) }
|
28
29
|
.map { |e| [n, e] }
|
29
30
|
end
|
30
31
|
{ name: s[:name], pairs: pairs } unless pairs.empty?
|
31
|
-
end
|
32
|
+
end
|
32
33
|
end
|
33
34
|
|
34
35
|
# Returns [allowed, blocked] given desired subjects.
|
@@ -56,9 +57,10 @@ module JetstreamBridge
|
|
56
57
|
offset = 0
|
57
58
|
loop do
|
58
59
|
resp = js_api_request(jts, '$JS.API.STREAM.NAMES', { offset: offset })
|
59
|
-
batch = Array(resp['streams']).
|
60
|
+
batch = Array(resp['streams']).filter_map { |h| h['name'] }
|
60
61
|
names.concat(batch)
|
61
62
|
break if names.size >= resp['total'].to_i || batch.empty?
|
63
|
+
|
62
64
|
offset = names.size
|
63
65
|
end
|
64
66
|
names
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../core/logging'
|
4
|
+
require_relative 'overlap_guard'
|
5
|
+
require_relative 'subject_matcher'
|
6
|
+
|
7
|
+
module JetstreamBridge
|
8
|
+
# Ensures a stream exists and adds only subjects that are not already covered.
|
9
|
+
class Stream
|
10
|
+
class << self
|
11
|
+
def ensure!(jts, name, subjects)
|
12
|
+
desired = normalize_subjects(subjects)
|
13
|
+
raise ArgumentError, 'subjects must not be empty' if desired.empty?
|
14
|
+
|
15
|
+
attempts = 0
|
16
|
+
begin
|
17
|
+
info = safe_stream_info(jts, name)
|
18
|
+
info ? ensure_update(jts, name, info, desired) : ensure_create(jts, name, desired)
|
19
|
+
rescue NATS::JetStream::Error => e
|
20
|
+
if overlap_error?(e) && (attempts += 1) <= 1
|
21
|
+
Logging.warn("Overlap race while ensuring #{name}; retrying once...", tag: 'JetstreamBridge::Stream')
|
22
|
+
sleep(0.05)
|
23
|
+
retry
|
24
|
+
elsif overlap_error?(e)
|
25
|
+
Logging.warn(
|
26
|
+
"Overlap persists ensuring #{name}; leaving unchanged. err=#{e.message.inspect}",
|
27
|
+
tag: 'JetstreamBridge::Stream')
|
28
|
+
nil
|
29
|
+
else
|
30
|
+
raise
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# ---------- Update existing stream ----------
|
38
|
+
|
39
|
+
def ensure_update(jts, name, info, desired)
|
40
|
+
existing = normalize_subjects(info.config.subjects || [])
|
41
|
+
to_add = missing_subjects(existing, desired)
|
42
|
+
return log_already_covered(name) if to_add.empty?
|
43
|
+
|
44
|
+
allowed, blocked = OverlapGuard.partition_allowed(jts, name, to_add)
|
45
|
+
return log_all_blocked(name, blocked) if allowed.empty?
|
46
|
+
|
47
|
+
target = (existing + allowed).uniq
|
48
|
+
OverlapGuard.check!(jts, name, target)
|
49
|
+
jts.update_stream(name: name, subjects: target)
|
50
|
+
log_updated(name, allowed, blocked)
|
51
|
+
end
|
52
|
+
|
53
|
+
# ---------- Create new stream ----------
|
54
|
+
|
55
|
+
def ensure_create(jts, name, desired)
|
56
|
+
allowed, blocked = OverlapGuard.partition_allowed(jts, name, desired)
|
57
|
+
return log_not_created(name, blocked) if allowed.empty?
|
58
|
+
|
59
|
+
jts.add_stream(name: name, subjects: allowed, retention: 'interest', storage: 'file')
|
60
|
+
log_created(name, allowed, blocked)
|
61
|
+
end
|
62
|
+
|
63
|
+
# ---------- Helpers ----------
|
64
|
+
|
65
|
+
def safe_stream_info(jts, name)
|
66
|
+
jts.stream_info(name)
|
67
|
+
rescue NATS::JetStream::Error => e
|
68
|
+
return nil if stream_not_found?(e)
|
69
|
+
raise
|
70
|
+
end
|
71
|
+
|
72
|
+
def missing_subjects(existing, desired)
|
73
|
+
desired.reject { |d| SubjectMatcher.covered?(existing, d) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def normalize_subjects(list)
|
77
|
+
Array(list).flatten.compact.map!(&:to_s).reject(&:empty?).uniq
|
78
|
+
end
|
79
|
+
|
80
|
+
def stream_not_found?(error)
|
81
|
+
msg = error.message.to_s
|
82
|
+
msg =~ /stream\s+not\s+found/i || msg =~ /\b404\b/
|
83
|
+
end
|
84
|
+
|
85
|
+
def overlap_error?(error)
|
86
|
+
msg = error.message.to_s
|
87
|
+
msg =~ /subjects?\s+overlap/i || msg =~ /\berr_code=10065\b/ || msg =~ /\bstatus_code=400\b/
|
88
|
+
end
|
89
|
+
|
90
|
+
# ---------- Logging wrappers ----------
|
91
|
+
|
92
|
+
def log_already_covered(name)
|
93
|
+
Logging.info("Stream #{name} exists; subjects already covered.", tag: 'JetstreamBridge::Stream')
|
94
|
+
end
|
95
|
+
|
96
|
+
def log_all_blocked(name, blocked)
|
97
|
+
if blocked.any?
|
98
|
+
Logging.warn(
|
99
|
+
"Stream #{name}: all missing subjects are owned by other streams; leaving unchanged. " \
|
100
|
+
"blocked=#{blocked.inspect}",
|
101
|
+
tag: 'JetstreamBridge::Stream'
|
102
|
+
)
|
103
|
+
else
|
104
|
+
Logging.info("Stream #{name} exists; nothing to add.", tag: 'JetstreamBridge::Stream')
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def log_updated(name, added, blocked)
|
109
|
+
msg = "Updated stream #{name}; added subjects=#{added.inspect}"
|
110
|
+
msg += " (skipped overlapped=#{blocked.inspect})" if blocked.any?
|
111
|
+
Logging.info(msg, tag: 'JetstreamBridge::Stream')
|
112
|
+
end
|
113
|
+
|
114
|
+
def log_not_created(name, blocked)
|
115
|
+
Logging.warn(
|
116
|
+
"Not creating stream #{name}: all desired subjects are owned by other streams. " \
|
117
|
+
"blocked=#{blocked.inspect}",
|
118
|
+
tag: 'JetstreamBridge::Stream'
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
def log_created(name, allowed, blocked)
|
123
|
+
msg = "Created stream #{name} subjects=#{allowed.inspect}"
|
124
|
+
msg += " (skipped overlapped=#{blocked.inspect})" if blocked.any?
|
125
|
+
Logging.info(msg, tag: 'JetstreamBridge::Stream')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/jetstream_bridge.rb
CHANGED
@@ -1,48 +1,60 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'jetstream_bridge/version'
|
4
|
-
require_relative 'jetstream_bridge/config'
|
5
|
-
require_relative 'jetstream_bridge/duration'
|
6
|
-
require_relative 'jetstream_bridge/logging'
|
7
|
-
require_relative 'jetstream_bridge/connection'
|
8
|
-
require_relative 'jetstream_bridge/publisher'
|
9
|
-
require_relative 'jetstream_bridge/consumer'
|
10
|
-
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
4
|
+
require_relative 'jetstream_bridge/core/config'
|
5
|
+
require_relative 'jetstream_bridge/core/duration'
|
6
|
+
require_relative 'jetstream_bridge/core/logging'
|
7
|
+
require_relative 'jetstream_bridge/core/connection'
|
8
|
+
require_relative 'jetstream_bridge/publisher/publisher'
|
9
|
+
require_relative 'jetstream_bridge/consumer/consumer'
|
10
|
+
|
11
|
+
# If you have a Railtie for tasks/eager-loading
|
12
|
+
require_relative 'jetstream_bridge/railtie' if defined?(Rails::Railtie)
|
13
|
+
|
14
|
+
# Load gem-provided models from lib/
|
15
|
+
require_relative 'jetstream_bridge/models/outbox_event'
|
16
|
+
require_relative 'jetstream_bridge/models/inbox_event'
|
17
|
+
|
18
18
|
|
19
|
+
# JetstreamBridge main module.
|
20
|
+
module JetstreamBridge
|
19
21
|
class << self
|
20
|
-
# Access the global configuration.
|
21
|
-
# @return [JetstreamBridge::Config]
|
22
22
|
def config
|
23
23
|
@config ||= Config.new
|
24
24
|
end
|
25
25
|
|
26
|
-
# Configure via hash and/or block.
|
27
|
-
# @param overrides [Hash] optional config key/value pairs
|
28
|
-
# @yieldparam [JetstreamBridge::Config] config
|
29
|
-
# @return [JetstreamBridge::Config]
|
30
26
|
def configure(overrides = {})
|
31
27
|
cfg = config
|
32
|
-
overrides.each { |k, v| assign!(cfg, k, v) }
|
28
|
+
overrides.each { |k, v| assign!(cfg, k, v) } unless overrides.nil? || overrides.empty?
|
33
29
|
yield(cfg) if block_given?
|
34
30
|
cfg
|
35
31
|
end
|
36
32
|
|
37
|
-
# Reset memoized config (useful in tests).
|
38
33
|
def reset!
|
39
34
|
@config = nil
|
40
35
|
end
|
41
36
|
|
37
|
+
def use_outbox?
|
38
|
+
config.use_outbox
|
39
|
+
end
|
40
|
+
|
41
|
+
def use_inbox?
|
42
|
+
config.use_inbox
|
43
|
+
end
|
44
|
+
|
45
|
+
def use_dlq?
|
46
|
+
config.use_dlq
|
47
|
+
end
|
48
|
+
|
49
|
+
def ensure_topology!
|
50
|
+
Connection.connect!
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
42
54
|
private
|
43
55
|
|
44
56
|
def assign!(cfg, key, val)
|
45
|
-
setter = "#{key}="
|
57
|
+
setter = :"#{key}="
|
46
58
|
raise ArgumentError, "Unknown configuration option: #{key}" unless cfg.respond_to?(setter)
|
47
59
|
|
48
60
|
cfg.public_send(setter, val)
|