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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.idea/dictionaries/project.xml +2 -0
  3. data/.idea/jetstream_bridge.iml +6 -1
  4. data/.rubocop.yml +102 -0
  5. data/Gemfile.lock +1 -5
  6. data/README.md +163 -78
  7. data/jetstream_bridge.gemspec +9 -10
  8. data/lib/generators/jetstream_bridge/initializer/initializer_generator.rb +16 -0
  9. data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +24 -0
  10. data/lib/generators/jetstream_bridge/install/install_generator.rb +19 -0
  11. data/lib/generators/jetstream_bridge/migrations/migrations_generator.rb +44 -0
  12. data/lib/generators/jetstream_bridge/migrations/templates/create_jetstream_inbox_events.rb.erb +24 -0
  13. data/lib/generators/jetstream_bridge/migrations/templates/create_jetstream_outbox_events.rb.erb +21 -0
  14. data/lib/jetstream_bridge/consumer/consumer.rb +103 -0
  15. data/lib/jetstream_bridge/{consumer_config.rb → consumer/consumer_config.rb} +3 -3
  16. data/lib/jetstream_bridge/consumer/inbox/inbox_message.rb +50 -0
  17. data/lib/jetstream_bridge/consumer/inbox/inbox_processor.rb +51 -0
  18. data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +102 -0
  19. data/lib/jetstream_bridge/{message_processor.rb → consumer/message_processor.rb} +1 -1
  20. data/lib/jetstream_bridge/consumer/subscription_manager.rb +91 -0
  21. data/lib/jetstream_bridge/{connection.rb → core/connection.rb} +1 -1
  22. data/lib/jetstream_bridge/core/model_utils.rb +51 -0
  23. data/lib/jetstream_bridge/models/inbox_event.rb +98 -0
  24. data/lib/jetstream_bridge/models/outbox_event.rb +114 -0
  25. data/lib/jetstream_bridge/publisher/outbox_repository.rb +70 -0
  26. data/lib/jetstream_bridge/{publisher.rb → publisher/publisher.rb} +41 -4
  27. data/lib/jetstream_bridge/railtie.rb +12 -0
  28. data/lib/jetstream_bridge/tasks/install.rake +10 -0
  29. data/lib/jetstream_bridge/{overlap_guard.rb → topology/overlap_guard.rb} +6 -4
  30. data/lib/jetstream_bridge/topology/stream.rb +129 -0
  31. data/lib/jetstream_bridge/{topology.rb → topology/topology.rb} +2 -2
  32. data/lib/jetstream_bridge/version.rb +1 -1
  33. data/lib/jetstream_bridge.rb +35 -23
  34. metadata +49 -49
  35. data/lib/jetstream_bridge/consumer.rb +0 -136
  36. data/lib/jetstream_bridge/dlq.rb +0 -24
  37. data/lib/jetstream_bridge/inbox_event.rb +0 -46
  38. data/lib/jetstream_bridge/outbox_event.rb +0 -60
  39. data/lib/jetstream_bridge/stream.rb +0 -114
  40. /data/lib/jetstream_bridge/{config.rb → core/config.rb} +0 -0
  41. /data/lib/jetstream_bridge/{duration.rb → core/duration.rb} +0 -0
  42. /data/lib/jetstream_bridge/{logging.rb → core/logging.rb} +0 -0
  43. /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
- with_retries { do_publish(subject, envelope) }
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module JetstreamBridge
6
+ # Railtie for JetstreamBridge.
7
+ class Railtie < ::Rails::Railtie
8
+ rake_tasks do
9
+ load File.expand_path('tasks/install.rake', __dir__)
10
+ end
11
+ end
12
+ end
@@ -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.map do |s|
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.compact
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']).map { |h| h['name'] }.compact
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
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../core/config'
4
+ require_relative '../core/logging'
3
5
  require_relative 'stream'
4
- require_relative 'config'
5
- require_relative 'logging'
6
6
 
7
7
  module JetstreamBridge
8
8
  class Topology
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '1.5.0'
7
+ VERSION = '1.7.0'
8
8
  end
@@ -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
- # JetstreamBridge
12
- #
13
- # Top-level module that exposes configuration and autoloads optional AR models.
14
- # Use `JetstreamBridge.configure` to set defaults for your environment.
15
- module JetstreamBridge
16
- autoload :OutboxEvent, 'jetstream_bridge/outbox_event'
17
- autoload :InboxEvent, 'jetstream_bridge/inbox_event'
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)