jetstream_bridge 2.9.0 → 2.10.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/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +4 -1
- data/lib/generators/jetstream_bridge/install/install_generator.rb +4 -2
- data/lib/generators/jetstream_bridge/migrations/migrations_generator.rb +1 -0
- data/lib/jetstream_bridge/consumer/consumer.rb +7 -9
- data/lib/jetstream_bridge/consumer/inbox/inbox_message.rb +5 -1
- data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +26 -56
- data/lib/jetstream_bridge/consumer/message_processor.rb +40 -2
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +13 -2
- data/lib/jetstream_bridge/core/config.rb +2 -1
- data/lib/jetstream_bridge/core/logging.rb +20 -7
- data/lib/jetstream_bridge/core/model_utils.rb +4 -3
- data/lib/jetstream_bridge/inbox_event.rb +4 -4
- data/lib/jetstream_bridge/outbox_event.rb +3 -1
- data/lib/jetstream_bridge/topology/stream.rb +1 -3
- data/lib/jetstream_bridge/topology/subject_matcher.rb +17 -7
- data/lib/jetstream_bridge/topology/topology.rb +1 -1
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +0 -8
- metadata +2 -5
- data/lib/jetstream_bridge/consumer/backoff_strategy.rb +0 -24
- data/lib/jetstream_bridge/consumer/consumer_config.rb +0 -26
- data/lib/jetstream_bridge/consumer/message_context.rb +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15fe547a615edbbd3bbeafb5bf5193bfdc4f1bd9911d7497803cf5d331ced36b
|
4
|
+
data.tar.gz: 5c0b98f2cbbff19762e98abcb39fa7c5f006cbfcd951295020adbcc08da80d49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4135f2c9287c5ddc0716f9eb7f3bcfeab104072c0ed59bdf41ff7a3df07dd5254240147e6a176d80f3a93198240ee633ff24ed742caf6142c859d6f259110c56
|
7
|
+
data.tar.gz: d341432852e94c1983128f5d0b0094e812c867f7133c609f609fb4606a4945ee2e5129cc20a24ef166e9e36dbd73a06e6bcb3b3a6d8c19752906a26756883345
|
@@ -6,7 +6,7 @@ JetstreamBridge.configure do |config|
|
|
6
6
|
config.nats_urls = ENV.fetch('NATS_URLS', 'nats://localhost:4222')
|
7
7
|
config.env = ENV.fetch('NATS_ENV', Rails.env)
|
8
8
|
config.app_name = ENV.fetch('APP_NAME', Rails.application.class.module_parent_name.underscore)
|
9
|
-
config.destination_app = ENV
|
9
|
+
config.destination_app = ENV.fetch('DESTINATION_APP', nil) # required for cross-app data sync
|
10
10
|
|
11
11
|
# Consumer Tuning
|
12
12
|
config.max_deliver = 5
|
@@ -21,4 +21,7 @@ JetstreamBridge.configure do |config|
|
|
21
21
|
# Models (override if you keep custom AR classes)
|
22
22
|
config.outbox_model = 'JetstreamBridge::OutboxEvent'
|
23
23
|
config.inbox_model = 'JetstreamBridge::InboxEvent'
|
24
|
+
|
25
|
+
# Logging
|
26
|
+
# config.logger = Rails.logger
|
24
27
|
end
|
@@ -8,11 +8,13 @@ module JetstreamBridge
|
|
8
8
|
class InstallGenerator < Rails::Generators::Base
|
9
9
|
desc 'Creates JetstreamBridge initializer and migrations'
|
10
10
|
def create_initializer
|
11
|
-
Rails::Generators.invoke('jetstream_bridge:initializer', [], behavior: behavior,
|
11
|
+
Rails::Generators.invoke('jetstream_bridge:initializer', [], behavior: behavior,
|
12
|
+
destination_root: destination_root)
|
12
13
|
end
|
13
14
|
|
14
15
|
def create_migrations
|
15
|
-
Rails::Generators.invoke('jetstream_bridge:migrations', [], behavior: behavior,
|
16
|
+
Rails::Generators.invoke('jetstream_bridge:migrations', [], behavior: behavior,
|
17
|
+
destination_root: destination_root)
|
16
18
|
end
|
17
19
|
end
|
18
20
|
end
|
@@ -7,7 +7,6 @@ require_relative '../core/duration'
|
|
7
7
|
require_relative '../core/logging'
|
8
8
|
require_relative '../core/config'
|
9
9
|
require_relative '../core/model_utils'
|
10
|
-
require_relative 'consumer_config'
|
11
10
|
require_relative 'message_processor'
|
12
11
|
require_relative 'subscription_manager'
|
13
12
|
require_relative 'inbox/inbox_processor'
|
@@ -20,13 +19,12 @@ module JetstreamBridge
|
|
20
19
|
IDLE_SLEEP_SECS = 0.05
|
21
20
|
MAX_IDLE_BACKOFF_SECS = 1.0
|
22
21
|
|
23
|
-
def initialize(durable_name:
|
24
|
-
batch_size: DEFAULT_BATCH_SIZE, &block)
|
22
|
+
def initialize(durable_name: nil, batch_size: nil, &block)
|
25
23
|
raise ArgumentError, 'handler block required' unless block_given?
|
26
24
|
|
27
25
|
@handler = block
|
28
|
-
@batch_size = Integer(batch_size)
|
29
|
-
@durable = durable_name
|
26
|
+
@batch_size = Integer(batch_size || DEFAULT_BATCH_SIZE)
|
27
|
+
@durable = durable_name || JetstreamBridge.config.durable_name
|
30
28
|
@idle_backoff = IDLE_SLEEP_SECS
|
31
29
|
@running = true
|
32
30
|
@jts = Connection.connect!
|
@@ -103,15 +101,15 @@ module JetstreamBridge
|
|
103
101
|
0
|
104
102
|
end
|
105
103
|
|
106
|
-
def handle_js_error(
|
107
|
-
if recoverable_consumer_error?(
|
104
|
+
def handle_js_error(error)
|
105
|
+
if recoverable_consumer_error?(error)
|
108
106
|
Logging.warn(
|
109
|
-
"Recovering subscription after error: #{
|
107
|
+
"Recovering subscription after error: #{error.class} #{error.message}",
|
110
108
|
tag: 'JetstreamBridge::Consumer'
|
111
109
|
)
|
112
110
|
ensure_subscription!
|
113
111
|
else
|
114
|
-
Logging.error("Fetch failed (non-recoverable): #{
|
112
|
+
Logging.error("Fetch failed (non-recoverable): #{error.class} #{error.message}", tag: 'JetstreamBridge::Consumer')
|
115
113
|
end
|
116
114
|
0
|
117
115
|
end
|
@@ -7,6 +7,7 @@ module JetstreamBridge
|
|
7
7
|
class InboxMessage
|
8
8
|
attr_reader :msg, :seq, :deliveries, :stream, :subject, :headers, :body, :raw, :event_id, :now
|
9
9
|
|
10
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
10
11
|
def self.from_nats(msg)
|
11
12
|
meta = (msg.respond_to?(:metadata) && msg.metadata) || nil
|
12
13
|
seq = meta.respond_to?(:stream_sequence) ? meta.stream_sequence : nil
|
@@ -30,7 +31,9 @@ module JetstreamBridge
|
|
30
31
|
|
31
32
|
new(msg, seq, deliveries, stream, subject, headers, body, raw, id, Time.now.utc, consumer)
|
32
33
|
end
|
34
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
33
35
|
|
36
|
+
# rubocop:disable Metrics/ParameterLists
|
34
37
|
def initialize(msg, seq, deliveries, stream, subject, headers, body, raw, event_id, now, consumer = nil)
|
35
38
|
@msg = msg
|
36
39
|
@seq = seq
|
@@ -44,6 +47,7 @@ module JetstreamBridge
|
|
44
47
|
@now = now
|
45
48
|
@consumer = consumer
|
46
49
|
end
|
50
|
+
# rubocop:enable Metrics/ParameterLists
|
47
51
|
|
48
52
|
def body_for_store
|
49
53
|
body.empty? ? raw : body
|
@@ -59,7 +63,7 @@ module JetstreamBridge
|
|
59
63
|
|
60
64
|
def metadata
|
61
65
|
@metadata ||= Struct.new(:num_delivered, :sequence, :consumer, :stream)
|
62
|
-
|
66
|
+
.new(deliveries, seq, @consumer, stream)
|
63
67
|
end
|
64
68
|
|
65
69
|
def ack(*args, **kwargs)
|
@@ -25,53 +25,31 @@ module JetstreamBridge
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def persist_pre(record, msg)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
msg.seq
|
43
|
-
end),
|
44
|
-
deliveries: (if ModelUtils.has_columns?(@klass,
|
45
|
-
:deliveries)
|
46
|
-
msg.deliveries
|
47
|
-
end),
|
48
|
-
status: 'processing',
|
49
|
-
last_error: nil,
|
50
|
-
received_at: (if ModelUtils.has_columns?(@klass,
|
51
|
-
:received_at)
|
52
|
-
record.received_at || msg.now
|
53
|
-
end),
|
54
|
-
updated_at: (if ModelUtils.has_columns?(@klass,
|
55
|
-
:updated_at)
|
56
|
-
msg.now
|
57
|
-
end)
|
58
|
-
})
|
28
|
+
attrs = {
|
29
|
+
event_id: msg.event_id,
|
30
|
+
subject: msg.subject,
|
31
|
+
payload: ModelUtils.json_dump(msg.body_for_store),
|
32
|
+
headers: ModelUtils.json_dump(msg.headers),
|
33
|
+
stream: msg.stream,
|
34
|
+
stream_seq: msg.seq,
|
35
|
+
deliveries: msg.deliveries,
|
36
|
+
status: 'processing',
|
37
|
+
last_error: nil,
|
38
|
+
received_at: record.respond_to?(:received_at) ? (record.received_at || msg.now) : nil,
|
39
|
+
updated_at: record.respond_to?(:updated_at) ? msg.now : nil
|
40
|
+
}
|
41
|
+
ModelUtils.assign_known_attrs(record, attrs)
|
59
42
|
record.save!
|
60
43
|
end
|
61
44
|
|
62
45
|
def persist_post(record)
|
63
46
|
now = Time.now.utc
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
updated_at: (if ModelUtils.has_columns?(@klass,
|
71
|
-
:updated_at)
|
72
|
-
now
|
73
|
-
end)
|
74
|
-
})
|
47
|
+
attrs = {
|
48
|
+
status: 'processed',
|
49
|
+
processed_at: record.respond_to?(:processed_at) ? now : nil,
|
50
|
+
updated_at: record.respond_to?(:updated_at) ? now : nil
|
51
|
+
}
|
52
|
+
ModelUtils.assign_known_attrs(record, attrs)
|
75
53
|
record.save!
|
76
54
|
end
|
77
55
|
|
@@ -79,20 +57,12 @@ module JetstreamBridge
|
|
79
57
|
return unless record
|
80
58
|
|
81
59
|
now = Time.now.utc
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
:last_error)
|
89
|
-
"#{error.class}: #{error.message}"
|
90
|
-
end),
|
91
|
-
updated_at: (if ModelUtils.has_columns?(@klass,
|
92
|
-
:updated_at)
|
93
|
-
now
|
94
|
-
end)
|
95
|
-
})
|
60
|
+
attrs = {
|
61
|
+
status: 'failed',
|
62
|
+
last_error: "#{error.class}: #{error.message}",
|
63
|
+
updated_at: record.respond_to?(:updated_at) ? now : nil
|
64
|
+
}
|
65
|
+
ModelUtils.assign_known_attrs(record, attrs)
|
96
66
|
record.save!
|
97
67
|
rescue StandardError => e
|
98
68
|
Logging.warn("Failed to persist inbox failure: #{e.class}: #{e.message}",
|
@@ -1,12 +1,50 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'oj'
|
4
|
+
require 'securerandom'
|
4
5
|
require_relative '../core/logging'
|
5
|
-
require_relative 'message_context'
|
6
6
|
require_relative 'dlq_publisher'
|
7
|
-
require_relative 'backoff_strategy'
|
8
7
|
|
9
8
|
module JetstreamBridge
|
9
|
+
# Immutable per-message metadata.
|
10
|
+
MessageContext = Struct.new(
|
11
|
+
:event_id, :deliveries, :subject, :seq, :consumer, :stream,
|
12
|
+
keyword_init: true
|
13
|
+
) do
|
14
|
+
def self.build(msg)
|
15
|
+
new(
|
16
|
+
event_id: msg.header&.[]('nats-msg-id') || SecureRandom.uuid,
|
17
|
+
deliveries: msg.metadata&.num_delivered.to_i,
|
18
|
+
subject: msg.subject,
|
19
|
+
seq: msg.metadata&.sequence,
|
20
|
+
consumer: msg.metadata&.consumer,
|
21
|
+
stream: msg.metadata&.stream
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Simple exponential backoff strategy for transient failures.
|
27
|
+
class BackoffStrategy
|
28
|
+
TRANSIENT_ERRORS = [Timeout::Error, IOError].freeze
|
29
|
+
MAX_EXPONENT = 6
|
30
|
+
MAX_DELAY = 60
|
31
|
+
MIN_DELAY = 1
|
32
|
+
|
33
|
+
# Returns a bounded delay in seconds
|
34
|
+
def delay(deliveries, error)
|
35
|
+
base = transient?(error) ? 0.5 : 2.0
|
36
|
+
power = [deliveries - 1, MAX_EXPONENT].min
|
37
|
+
raw = (base * (2**power)).to_i
|
38
|
+
raw.clamp(MIN_DELAY, MAX_DELAY)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def transient?(error)
|
44
|
+
TRANSIENT_ERRORS.any? { |k| error.is_a?(k) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
10
48
|
# Orchestrates parse → handler → ack/nak → DLQ
|
11
49
|
class MessageProcessor
|
12
50
|
UNRECOVERABLE_ERRORS = [ArgumentError, TypeError].freeze
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require_relative '../core/logging'
|
4
4
|
require_relative '../core/duration'
|
5
|
-
require_relative '../consumer/consumer_config'
|
6
5
|
|
7
6
|
module JetstreamBridge
|
8
7
|
# Encapsulates durable ensure + subscribe for a pull consumer.
|
@@ -11,7 +10,7 @@ module JetstreamBridge
|
|
11
10
|
@jts = jts
|
12
11
|
@durable = durable
|
13
12
|
@cfg = cfg
|
14
|
-
@desired_cfg =
|
13
|
+
@desired_cfg = build_consumer_config(@durable, filter_subject)
|
15
14
|
@desired_cfg_norm = normalize_consumer_config(@desired_cfg)
|
16
15
|
end
|
17
16
|
|
@@ -74,6 +73,18 @@ module JetstreamBridge
|
|
74
73
|
)
|
75
74
|
end
|
76
75
|
|
76
|
+
def build_consumer_config(durable, filter_subject)
|
77
|
+
{
|
78
|
+
durable_name: durable,
|
79
|
+
filter_subject: filter_subject,
|
80
|
+
ack_policy: 'explicit',
|
81
|
+
deliver_policy: 'all',
|
82
|
+
max_deliver: JetstreamBridge.config.max_deliver,
|
83
|
+
ack_wait: Duration.to_millis(JetstreamBridge.config.ack_wait),
|
84
|
+
backoff: Array(JetstreamBridge.config.backoff).map { |d| Duration.to_millis(d) }
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
77
88
|
# Normalize both server-returned config objects and our desired hash
|
78
89
|
# into a common hash with consistent units/types for accurate comparison.
|
79
90
|
def normalize_consumer_config(cfg)
|
@@ -5,7 +5,7 @@ module JetstreamBridge
|
|
5
5
|
attr_accessor :destination_app, :nats_urls, :env, :app_name,
|
6
6
|
:max_deliver, :ack_wait, :backoff,
|
7
7
|
:use_outbox, :use_inbox, :inbox_model, :outbox_model,
|
8
|
-
:use_dlq
|
8
|
+
:use_dlq, :logger
|
9
9
|
|
10
10
|
def initialize
|
11
11
|
@nats_urls = ENV['NATS_URLS'] || ENV['NATS_URL'] || 'nats://localhost:4222'
|
@@ -22,6 +22,7 @@ module JetstreamBridge
|
|
22
22
|
@use_dlq = true
|
23
23
|
@outbox_model = 'JetstreamBridge::OutboxEvent'
|
24
24
|
@inbox_model = 'JetstreamBridge::InboxEvent'
|
25
|
+
@logger = nil
|
25
26
|
end
|
26
27
|
|
27
28
|
# Single stream name per env
|
@@ -1,20 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'uri'
|
4
|
+
require 'logger'
|
4
5
|
|
5
6
|
module JetstreamBridge
|
6
|
-
# Logging helpers that route to
|
7
|
-
# falling back to STDOUT.
|
7
|
+
# Logging helpers that route to the configured logger when available,
|
8
|
+
# falling back to Rails.logger or STDOUT.
|
8
9
|
module Logging
|
9
10
|
module_function
|
10
11
|
|
12
|
+
def logger
|
13
|
+
JetstreamBridge.config.logger ||
|
14
|
+
(defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
|
15
|
+
default_logger
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_logger
|
19
|
+
@default_logger ||= Logger.new($stdout)
|
20
|
+
end
|
21
|
+
|
11
22
|
def log(level, msg, tag: nil)
|
12
23
|
message = tag ? "[#{tag}] #{msg}" : msg
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
24
|
+
logger.public_send(level, message)
|
25
|
+
end
|
26
|
+
|
27
|
+
def debug(msg, tag: nil)
|
28
|
+
log(:debug, msg, tag: tag)
|
18
29
|
end
|
19
30
|
|
20
31
|
def info(msg, tag: nil)
|
@@ -29,6 +40,7 @@ module JetstreamBridge
|
|
29
40
|
log(:error, msg, tag: tag)
|
30
41
|
end
|
31
42
|
|
43
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
32
44
|
def sanitize_url(url)
|
33
45
|
uri = URI.parse(url)
|
34
46
|
return url unless uri.user || uri.password
|
@@ -55,5 +67,6 @@ module JetstreamBridge
|
|
55
67
|
"#{scheme}://#{masked}@"
|
56
68
|
end
|
57
69
|
end
|
70
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
58
71
|
end
|
59
72
|
end
|
@@ -14,10 +14,13 @@ module JetstreamBridge
|
|
14
14
|
defined?(ActiveRecord::Base) && klass <= ActiveRecord::Base
|
15
15
|
end
|
16
16
|
|
17
|
+
# rubocop:disable Naming/PredicatePrefix
|
17
18
|
def has_columns?(klass, *cols)
|
18
19
|
return false unless ar_class?(klass)
|
20
|
+
|
19
21
|
cols.flatten.all? { |c| klass.column_names.include?(c.to_s) }
|
20
22
|
end
|
23
|
+
# rubocop:enable Naming/PredicatePrefix
|
21
24
|
|
22
25
|
def assign_known_attrs(record, attrs)
|
23
26
|
attrs.each do |k, v|
|
@@ -30,9 +33,7 @@ module JetstreamBridge
|
|
30
33
|
def find_or_init_by_best(klass, *keysets)
|
31
34
|
keysets.each do |keys|
|
32
35
|
next if keys.nil? || keys.empty?
|
33
|
-
if has_columns?(klass, keys.keys)
|
34
|
-
return klass.find_or_initialize_by(keys)
|
35
|
-
end
|
36
|
+
return klass.find_or_initialize_by(keys) if has_columns?(klass, keys.keys)
|
36
37
|
end
|
37
38
|
klass.new
|
38
39
|
end
|
@@ -15,6 +15,7 @@ module JetstreamBridge
|
|
15
15
|
|
16
16
|
class << self
|
17
17
|
# Safe column presence check that never boots a connection during class load.
|
18
|
+
# rubocop:disable Naming/PredicatePrefix
|
18
19
|
def has_column?(name)
|
19
20
|
return false unless ar_connected?
|
20
21
|
|
@@ -22,6 +23,7 @@ module JetstreamBridge
|
|
22
23
|
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
|
23
24
|
false
|
24
25
|
end
|
26
|
+
# rubocop:enable Naming/PredicatePrefix
|
25
27
|
|
26
28
|
def ar_connected?
|
27
29
|
ActiveRecord::Base.connected? && connection_pool.active_connection?
|
@@ -66,9 +68,7 @@ module JetstreamBridge
|
|
66
68
|
# ---- Defaults that do not require schema at load time ----
|
67
69
|
before_validation do
|
68
70
|
self.status ||= 'received' if self.class.has_column?(:status) && status.blank?
|
69
|
-
if self.class.has_column?(:received_at) && received_at.blank?
|
70
|
-
self.received_at ||= Time.now.utc
|
71
|
-
end
|
71
|
+
self.received_at ||= Time.now.utc if self.class.has_column?(:received_at) && received_at.blank?
|
72
72
|
end
|
73
73
|
|
74
74
|
# ---- Helpers ----
|
@@ -103,7 +103,7 @@ module JetstreamBridge
|
|
103
103
|
raise_missing_ar!('Inbox', method_name)
|
104
104
|
end
|
105
105
|
|
106
|
-
def respond_to_missing?(
|
106
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
107
107
|
false
|
108
108
|
end
|
109
109
|
|
@@ -15,6 +15,7 @@ module JetstreamBridge
|
|
15
15
|
|
16
16
|
class << self
|
17
17
|
# Safe column presence check that never boots a connection during class load.
|
18
|
+
# rubocop:disable Naming/PredicatePrefix
|
18
19
|
def has_column?(name)
|
19
20
|
return false unless ar_connected?
|
20
21
|
|
@@ -22,6 +23,7 @@ module JetstreamBridge
|
|
22
23
|
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
|
23
24
|
false
|
24
25
|
end
|
26
|
+
# rubocop:enable Naming/PredicatePrefix
|
25
27
|
|
26
28
|
def ar_connected?
|
27
29
|
# Avoid creating a connection; rescue if pool isn't set yet.
|
@@ -111,7 +113,7 @@ module JetstreamBridge
|
|
111
113
|
raise_missing_ar!('Outbox', method_name)
|
112
114
|
end
|
113
115
|
|
114
|
-
def respond_to_missing?(
|
116
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
115
117
|
false
|
116
118
|
end
|
117
119
|
|
@@ -123,9 +123,7 @@ module JetstreamBridge
|
|
123
123
|
|
124
124
|
# Retention is immutable; warn if different and do not include on update.
|
125
125
|
have_ret = info.config.retention.to_s.downcase
|
126
|
-
if have_ret != RETENTION
|
127
|
-
StreamSupport.log_retention_mismatch(name, have: have_ret, want: RETENTION)
|
128
|
-
end
|
126
|
+
StreamSupport.log_retention_mismatch(name, have: have_ret, want: RETENTION) if have_ret != RETENTION
|
129
127
|
|
130
128
|
# Storage can be updated; do it without passing retention.
|
131
129
|
have_storage = info.config.storage.to_s.downcase
|
@@ -48,20 +48,30 @@ module JetstreamBridge
|
|
48
48
|
while ai < a_parts.length && bi < b_parts.length
|
49
49
|
at = a_parts[ai]
|
50
50
|
bt = b_parts[bi]
|
51
|
-
return true if at
|
52
|
-
return false unless at
|
51
|
+
return true if tail?(at, bt)
|
52
|
+
return false unless token_match?(at, bt)
|
53
53
|
|
54
54
|
ai += 1
|
55
55
|
bi += 1
|
56
56
|
end
|
57
57
|
|
58
|
-
|
59
|
-
|
60
|
-
|
58
|
+
tail_overlap?(a_parts[ai..], b_parts[bi..])
|
59
|
+
end
|
60
|
+
|
61
|
+
def tail?(a_token, b_token)
|
62
|
+
a_token == '>' || b_token == '>'
|
63
|
+
end
|
64
|
+
|
65
|
+
def token_match?(a_token, b_token)
|
66
|
+
a_token == b_token || a_token == '*' || b_token == '*'
|
67
|
+
end
|
68
|
+
|
69
|
+
def tail_overlap?(a_tail, b_tail)
|
70
|
+
a_tail ||= []
|
71
|
+
b_tail ||= []
|
61
72
|
return true if a_tail.include?('>') || b_tail.include?('>')
|
62
73
|
|
63
|
-
|
64
|
-
ai == a_parts.length && bi == b_parts.length
|
74
|
+
a_tail.empty? && b_tail.empty?
|
65
75
|
end
|
66
76
|
end
|
67
77
|
end
|
@@ -14,7 +14,7 @@ module JetstreamBridge
|
|
14
14
|
|
15
15
|
Logging.info(
|
16
16
|
"Subjects ready: producer=#{cfg.source_subject}, consumer=#{cfg.destination_subject}. " \
|
17
|
-
|
17
|
+
"Counterpart publishes on #{cfg.destination_subject} and subscribes on #{cfg.source_subject}.",
|
18
18
|
tag: 'JetstreamBridge::Topology'
|
19
19
|
)
|
20
20
|
end
|
data/lib/jetstream_bridge.rb
CHANGED
@@ -15,7 +15,6 @@ require_relative 'jetstream_bridge/railtie' if defined?(Rails::Railtie)
|
|
15
15
|
require_relative 'jetstream_bridge/inbox_event'
|
16
16
|
require_relative 'jetstream_bridge/outbox_event'
|
17
17
|
|
18
|
-
|
19
18
|
# JetstreamBridge main module.
|
20
19
|
module JetstreamBridge
|
21
20
|
class << self
|
@@ -54,13 +53,6 @@ module JetstreamBridge
|
|
54
53
|
Connection.jetstream
|
55
54
|
end
|
56
55
|
|
57
|
-
# @deprecated Use {ensure_topology!} instead. This method will be removed
|
58
|
-
# in a future version.
|
59
|
-
def ensure_topology?
|
60
|
-
Logging.warn('ensure_topology? is deprecated; use ensure_topology! instead', tag: 'JetstreamBridge')
|
61
|
-
!!ensure_topology!
|
62
|
-
end
|
63
|
-
|
64
56
|
private
|
65
57
|
|
66
58
|
def assign!(cfg, key, val)
|
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: 2.
|
4
|
+
version: 2.10.0
|
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-08-
|
11
|
+
date: 2025-08-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -83,14 +83,11 @@ files:
|
|
83
83
|
- lib/generators/jetstream_bridge/migrations/templates/create_jetstream_inbox_events.rb.erb
|
84
84
|
- lib/generators/jetstream_bridge/migrations/templates/create_jetstream_outbox_events.rb.erb
|
85
85
|
- lib/jetstream_bridge.rb
|
86
|
-
- lib/jetstream_bridge/consumer/backoff_strategy.rb
|
87
86
|
- lib/jetstream_bridge/consumer/consumer.rb
|
88
|
-
- lib/jetstream_bridge/consumer/consumer_config.rb
|
89
87
|
- lib/jetstream_bridge/consumer/dlq_publisher.rb
|
90
88
|
- lib/jetstream_bridge/consumer/inbox/inbox_message.rb
|
91
89
|
- lib/jetstream_bridge/consumer/inbox/inbox_processor.rb
|
92
90
|
- lib/jetstream_bridge/consumer/inbox/inbox_repository.rb
|
93
|
-
- lib/jetstream_bridge/consumer/message_context.rb
|
94
91
|
- lib/jetstream_bridge/consumer/message_processor.rb
|
95
92
|
- lib/jetstream_bridge/consumer/subscription_manager.rb
|
96
93
|
- lib/jetstream_bridge/core/config.rb
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module JetstreamBridge
|
4
|
-
class BackoffStrategy
|
5
|
-
TRANSIENT_ERRORS = [Timeout::Error, IOError].freeze
|
6
|
-
MAX_EXPONENT = 6
|
7
|
-
MAX_DELAY = 60
|
8
|
-
MIN_DELAY = 1
|
9
|
-
|
10
|
-
# Returns a bounded delay in seconds
|
11
|
-
def delay(deliveries, error)
|
12
|
-
base = transient?(error) ? 0.5 : 2.0
|
13
|
-
power = [deliveries - 1, MAX_EXPONENT].min
|
14
|
-
raw = (base * (2**power)).to_i
|
15
|
-
raw.clamp(MIN_DELAY, MAX_DELAY)
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def transient?(error)
|
21
|
-
TRANSIENT_ERRORS.any? { |k| error.is_a?(k) }
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../core/duration'
|
4
|
-
require_relative '../core/config'
|
5
|
-
require_relative '../core/logging'
|
6
|
-
|
7
|
-
module JetstreamBridge
|
8
|
-
# Consumer configuration helpers.
|
9
|
-
module ConsumerConfig
|
10
|
-
module_function
|
11
|
-
|
12
|
-
# Complete consumer config (pre-provisioned durable, pull mode).
|
13
|
-
def consumer_config(durable, filter_subject)
|
14
|
-
{
|
15
|
-
durable_name: durable,
|
16
|
-
filter_subject: filter_subject,
|
17
|
-
ack_policy: 'explicit',
|
18
|
-
deliver_policy: 'all',
|
19
|
-
max_deliver: JetstreamBridge.config.max_deliver,
|
20
|
-
ack_wait: Duration.to_millis(JetstreamBridge.config.ack_wait),
|
21
|
-
backoff: Array(JetstreamBridge.config.backoff)
|
22
|
-
.map { |d| Duration.to_millis(d) }
|
23
|
-
}
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'securerandom'
|
4
|
-
|
5
|
-
module JetstreamBridge
|
6
|
-
# Immutable per-message metadata
|
7
|
-
MessageContext = Struct.new(
|
8
|
-
:event_id, :deliveries, :subject, :seq, :consumer, :stream,
|
9
|
-
keyword_init: true
|
10
|
-
) do
|
11
|
-
def self.build(msg)
|
12
|
-
new(
|
13
|
-
event_id: msg.header&.[]('nats-msg-id') || SecureRandom.uuid,
|
14
|
-
deliveries: msg.metadata&.num_delivered.to_i,
|
15
|
-
subject: msg.subject,
|
16
|
-
seq: msg.metadata&.sequence,
|
17
|
-
consumer: msg.metadata&.consumer,
|
18
|
-
stream: msg.metadata&.stream
|
19
|
-
)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|