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
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jetstream_bridge
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Attara
|
@@ -67,75 +67,61 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '6.0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: bundler-audit
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
75
|
+
version: 0.9.1
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
82
|
+
version: 0.9.1
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: rake
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
89
|
+
version: '13.0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: rubocop
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - "~>"
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '1.66'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - "~>"
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '1.66'
|
96
|
+
version: '13.0'
|
111
97
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
98
|
+
name: rspec
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
114
100
|
requirements:
|
115
|
-
- - "
|
101
|
+
- - ">="
|
116
102
|
- !ruby/object:Gem::Version
|
117
|
-
version: '
|
103
|
+
version: '3.12'
|
118
104
|
type: :development
|
119
105
|
prerelease: false
|
120
106
|
version_requirements: !ruby/object:Gem::Requirement
|
121
107
|
requirements:
|
122
|
-
- - "
|
108
|
+
- - ">="
|
123
109
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
110
|
+
version: '3.12'
|
125
111
|
- !ruby/object:Gem::Dependency
|
126
|
-
name: rubocop
|
112
|
+
name: rubocop
|
127
113
|
requirement: !ruby/object:Gem::Requirement
|
128
114
|
requirements:
|
129
115
|
- - "~>"
|
130
116
|
- !ruby/object:Gem::Version
|
131
|
-
version: '
|
117
|
+
version: '1.66'
|
132
118
|
type: :development
|
133
119
|
prerelease: false
|
134
120
|
version_requirements: !ruby/object:Gem::Requirement
|
135
121
|
requirements:
|
136
122
|
- - "~>"
|
137
123
|
- !ruby/object:Gem::Version
|
138
|
-
version: '
|
124
|
+
version: '1.66'
|
139
125
|
- !ruby/object:Gem::Dependency
|
140
126
|
name: rubocop-packaging
|
141
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -151,19 +137,19 @@ dependencies:
|
|
151
137
|
- !ruby/object:Gem::Version
|
152
138
|
version: '0.5'
|
153
139
|
- !ruby/object:Gem::Dependency
|
154
|
-
name:
|
140
|
+
name: rubocop-performance
|
155
141
|
requirement: !ruby/object:Gem::Requirement
|
156
142
|
requirements:
|
157
|
-
- - "
|
143
|
+
- - "~>"
|
158
144
|
- !ruby/object:Gem::Version
|
159
|
-
version:
|
145
|
+
version: '1.21'
|
160
146
|
type: :development
|
161
147
|
prerelease: false
|
162
148
|
version_requirements: !ruby/object:Gem::Requirement
|
163
149
|
requirements:
|
164
|
-
- - "
|
150
|
+
- - "~>"
|
165
151
|
- !ruby/object:Gem::Version
|
166
|
-
version:
|
152
|
+
version: '1.21'
|
167
153
|
description: |-
|
168
154
|
Publisher/Consumer utilities for NATS JetStream with environment-scoped subjects,
|
169
155
|
overlap guards, DLQ routing, retries/backoff, and optional Inbox/Outbox patterns.
|
@@ -182,27 +168,41 @@ files:
|
|
182
168
|
- ".idea/misc.xml"
|
183
169
|
- ".idea/modules.xml"
|
184
170
|
- ".idea/vcs.xml"
|
171
|
+
- ".rubocop.yml"
|
185
172
|
- Gemfile
|
186
173
|
- Gemfile.lock
|
187
174
|
- LICENSE
|
188
175
|
- README.md
|
189
176
|
- jetstream_bridge.gemspec
|
177
|
+
- lib/generators/jetstream_bridge/initializer/initializer_generator.rb
|
178
|
+
- lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb
|
179
|
+
- lib/generators/jetstream_bridge/install/install_generator.rb
|
180
|
+
- lib/generators/jetstream_bridge/migrations/migrations_generator.rb
|
181
|
+
- lib/generators/jetstream_bridge/migrations/templates/create_jetstream_inbox_events.rb.erb
|
182
|
+
- lib/generators/jetstream_bridge/migrations/templates/create_jetstream_outbox_events.rb.erb
|
190
183
|
- lib/jetstream_bridge.rb
|
191
|
-
- lib/jetstream_bridge/
|
192
|
-
- lib/jetstream_bridge/
|
193
|
-
- lib/jetstream_bridge/consumer.rb
|
194
|
-
- lib/jetstream_bridge/
|
195
|
-
- lib/jetstream_bridge/
|
196
|
-
- lib/jetstream_bridge/
|
197
|
-
- lib/jetstream_bridge/
|
198
|
-
- lib/jetstream_bridge/
|
199
|
-
- lib/jetstream_bridge/
|
200
|
-
- lib/jetstream_bridge/
|
201
|
-
- lib/jetstream_bridge/
|
202
|
-
- lib/jetstream_bridge/
|
203
|
-
- lib/jetstream_bridge/
|
204
|
-
- lib/jetstream_bridge/
|
205
|
-
- lib/jetstream_bridge/
|
184
|
+
- lib/jetstream_bridge/consumer/consumer.rb
|
185
|
+
- lib/jetstream_bridge/consumer/consumer_config.rb
|
186
|
+
- lib/jetstream_bridge/consumer/inbox/inbox_message.rb
|
187
|
+
- lib/jetstream_bridge/consumer/inbox/inbox_processor.rb
|
188
|
+
- lib/jetstream_bridge/consumer/inbox/inbox_repository.rb
|
189
|
+
- lib/jetstream_bridge/consumer/message_processor.rb
|
190
|
+
- lib/jetstream_bridge/consumer/subscription_manager.rb
|
191
|
+
- lib/jetstream_bridge/core/config.rb
|
192
|
+
- lib/jetstream_bridge/core/connection.rb
|
193
|
+
- lib/jetstream_bridge/core/duration.rb
|
194
|
+
- lib/jetstream_bridge/core/logging.rb
|
195
|
+
- lib/jetstream_bridge/core/model_utils.rb
|
196
|
+
- lib/jetstream_bridge/models/inbox_event.rb
|
197
|
+
- lib/jetstream_bridge/models/outbox_event.rb
|
198
|
+
- lib/jetstream_bridge/publisher/outbox_repository.rb
|
199
|
+
- lib/jetstream_bridge/publisher/publisher.rb
|
200
|
+
- lib/jetstream_bridge/railtie.rb
|
201
|
+
- lib/jetstream_bridge/tasks/install.rake
|
202
|
+
- lib/jetstream_bridge/topology/overlap_guard.rb
|
203
|
+
- lib/jetstream_bridge/topology/stream.rb
|
204
|
+
- lib/jetstream_bridge/topology/subject_matcher.rb
|
205
|
+
- lib/jetstream_bridge/topology/topology.rb
|
206
206
|
- lib/jetstream_bridge/version.rb
|
207
207
|
homepage: https://github.com/attaradev/jetstream_bridge
|
208
208
|
licenses:
|
@@ -1,136 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'json'
|
4
|
-
require 'securerandom'
|
5
|
-
require_relative 'connection'
|
6
|
-
require_relative 'duration'
|
7
|
-
require_relative 'logging'
|
8
|
-
require_relative 'consumer_config'
|
9
|
-
require_relative 'message_processor'
|
10
|
-
require_relative 'config'
|
11
|
-
|
12
|
-
module JetstreamBridge
|
13
|
-
# Subscribes to "{env}.data.sync.{dest}.{app}" and processes messages.
|
14
|
-
class Consumer
|
15
|
-
DEFAULT_BATCH_SIZE = 25
|
16
|
-
FETCH_TIMEOUT_SECS = 5
|
17
|
-
IDLE_SLEEP_SECS = 0.05
|
18
|
-
|
19
|
-
# @param durable_name [String] Consumer name
|
20
|
-
# @param batch_size [Integer] Max messages per fetch
|
21
|
-
# @yield [event, subject, deliveries] Message handler
|
22
|
-
def initialize(durable_name:, batch_size: DEFAULT_BATCH_SIZE, &block)
|
23
|
-
@handler = block
|
24
|
-
@batch_size = batch_size
|
25
|
-
@durable = durable_name
|
26
|
-
@jts = Connection.connect!
|
27
|
-
|
28
|
-
ensure_destination!
|
29
|
-
ensure_consumer!
|
30
|
-
subscribe!
|
31
|
-
@processor = MessageProcessor.new(@jts, @handler)
|
32
|
-
end
|
33
|
-
|
34
|
-
# Starts the consumer loop.
|
35
|
-
def run!
|
36
|
-
Logging.info("Consumer #{@durable} started…", tag: 'JetstreamBridge::Consumer')
|
37
|
-
loop do
|
38
|
-
processed = process_batch
|
39
|
-
sleep(IDLE_SLEEP_SECS) if processed.zero?
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def ensure_destination!
|
46
|
-
return unless JetstreamBridge.config.destination_app.to_s.empty?
|
47
|
-
raise ArgumentError, 'destination_app must be configured'
|
48
|
-
end
|
49
|
-
|
50
|
-
def stream_name
|
51
|
-
JetstreamBridge.config.stream_name
|
52
|
-
end
|
53
|
-
|
54
|
-
def filter_subject
|
55
|
-
JetstreamBridge.config.destination_subject
|
56
|
-
end
|
57
|
-
|
58
|
-
def desired_consumer_cfg
|
59
|
-
ConsumerConfig.consumer_config(@durable, filter_subject)
|
60
|
-
end
|
61
|
-
|
62
|
-
def ensure_consumer!
|
63
|
-
info = @jts.consumer_info(stream_name, @durable)
|
64
|
-
if consumer_mismatch?(info, desired_consumer_cfg)
|
65
|
-
Logging.warn(
|
66
|
-
"Consumer #{@durable} exists with mismatched config; recreating (filter=#{filter_subject})",
|
67
|
-
tag: 'JetstreamBridge::Consumer'
|
68
|
-
)
|
69
|
-
# Be tolerant if delete fails due to races
|
70
|
-
begin
|
71
|
-
@jts.delete_consumer(stream_name, @durable)
|
72
|
-
rescue NATS::JetStream::Error => e
|
73
|
-
Logging.warn("Delete consumer #{@durable} ignored: #{e.class} #{e.message}",
|
74
|
-
tag: 'JetstreamBridge::Consumer')
|
75
|
-
end
|
76
|
-
@jts.add_consumer(stream_name, **desired_consumer_cfg)
|
77
|
-
Logging.info("Created consumer #{@durable} (filter=#{filter_subject})",
|
78
|
-
tag: 'JetstreamBridge::Consumer')
|
79
|
-
else
|
80
|
-
Logging.info("Consumer #{@durable} exists with desired config.",
|
81
|
-
tag: 'JetstreamBridge::Consumer')
|
82
|
-
end
|
83
|
-
rescue NATS::JetStream::Error
|
84
|
-
# Not found -> create fresh
|
85
|
-
@jts.add_consumer(stream_name, **desired_consumer_cfg)
|
86
|
-
Logging.info("Created consumer #{@durable} (filter=#{filter_subject})",
|
87
|
-
tag: 'JetstreamBridge::Consumer')
|
88
|
-
end
|
89
|
-
|
90
|
-
def consumer_mismatch?(info, desired_cfg)
|
91
|
-
cfg = info.config
|
92
|
-
(cfg.respond_to?(:filter_subject) ? cfg.filter_subject.to_s : cfg[:filter_subject].to_s) !=
|
93
|
-
desired_cfg[:filter_subject].to_s
|
94
|
-
end
|
95
|
-
|
96
|
-
def subscribe!
|
97
|
-
@psub = @jts.pull_subscribe(
|
98
|
-
filter_subject,
|
99
|
-
@durable,
|
100
|
-
stream: stream_name,
|
101
|
-
config: ConsumerConfig.subscribe_config
|
102
|
-
)
|
103
|
-
Logging.info("Subscribed to #{filter_subject} (durable=#{@durable})",
|
104
|
-
tag: 'JetstreamBridge::Consumer')
|
105
|
-
end
|
106
|
-
|
107
|
-
# Returns number of messages processed; 0 on timeout/idle or after recovery.
|
108
|
-
def process_batch
|
109
|
-
msgs = @psub.fetch(@batch_size, timeout: FETCH_TIMEOUT_SECS)
|
110
|
-
msgs.each { |m| @processor.handle_message(m) }
|
111
|
-
msgs.size
|
112
|
-
rescue NATS::Timeout, NATS::IO::Timeout
|
113
|
-
0
|
114
|
-
rescue NATS::JetStream::Error => e
|
115
|
-
# Handle common recoverable states by re-ensuring consumer & subscription.
|
116
|
-
if recoverable_consumer_error?(e)
|
117
|
-
Logging.warn("Recovering subscription after error: #{e.class} #{e.message}",
|
118
|
-
tag: 'JetstreamBridge::Consumer')
|
119
|
-
ensure_consumer!
|
120
|
-
subscribe!
|
121
|
-
0
|
122
|
-
else
|
123
|
-
Logging.error("Fetch failed: #{e.class} #{e.message}",
|
124
|
-
tag: 'JetstreamBridge::Consumer')
|
125
|
-
0
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
def recoverable_consumer_error?(error)
|
130
|
-
msg = error.message.to_s
|
131
|
-
msg =~ /consumer.*(not\s+found|deleted)/i ||
|
132
|
-
msg =~ /no\s+responders/i ||
|
133
|
-
msg =~ /stream.*not\s+found/i
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
data/lib/jetstream_bridge/dlq.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'overlap_guard'
|
4
|
-
require_relative 'logging'
|
5
|
-
require_relative 'subject_matcher'
|
6
|
-
|
7
|
-
module JetstreamBridge
|
8
|
-
# Ensures the DLQ subject is added to the stream.
|
9
|
-
class DLQ
|
10
|
-
def self.ensure!(jts)
|
11
|
-
name = JetstreamBridge.config.stream_name
|
12
|
-
info = jts.stream_info(name)
|
13
|
-
subs = Array(info.config.subjects || [])
|
14
|
-
dlq = JetstreamBridge.config.dlq_subject
|
15
|
-
return if SubjectMatcher.covered?(subs, dlq)
|
16
|
-
|
17
|
-
desired = (subs + [dlq]).uniq
|
18
|
-
OverlapGuard.check!(jts, name, desired)
|
19
|
-
|
20
|
-
jts.update_stream(name: name, subjects: desired)
|
21
|
-
Logging.info("Added DLQ subject #{dlq} to stream #{name}", tag: 'JetstreamBridge::DLQ')
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
@@ -1,46 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# If ActiveRecord is not available, a shim class is defined that raises
|
4
|
-
# a helpful error when used.
|
5
|
-
begin
|
6
|
-
require 'active_record'
|
7
|
-
rescue LoadError
|
8
|
-
# Ignore; we handle the lack of AR below.
|
9
|
-
end
|
10
|
-
|
11
|
-
module JetstreamBridge
|
12
|
-
# InboxEvent is the default ActiveRecord model used by the gem when
|
13
|
-
# `use_inbox` is enabled.
|
14
|
-
# It records processed event IDs for idempotency.
|
15
|
-
if defined?(ActiveRecord::Base)
|
16
|
-
class InboxEvent < ActiveRecord::Base
|
17
|
-
self.table_name = 'jetstream_inbox_events'
|
18
|
-
|
19
|
-
validates :event_id, presence: true, uniqueness: true
|
20
|
-
validates :subject, presence: true
|
21
|
-
end
|
22
|
-
else
|
23
|
-
# Shim that fails loudly if the app misconfigures the gem without AR.
|
24
|
-
class InboxEvent
|
25
|
-
class << self
|
26
|
-
def method_missing(method_name, *_args, &_block)
|
27
|
-
raise_missing_ar!('Inbox', method_name)
|
28
|
-
end
|
29
|
-
|
30
|
-
def respond_to_missing?(_method_name, _include_private = false)
|
31
|
-
false
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def raise_missing_ar!(which, method_name)
|
37
|
-
raise(
|
38
|
-
"#{which} requires ActiveRecord (tried to call ##{method_name}). " \
|
39
|
-
'Enable `use_inbox` only in apps with ActiveRecord, or add ' \
|
40
|
-
'`gem "activerecord"` to your Gemfile.'
|
41
|
-
)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
@@ -1,60 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# If ActiveRecord is not available, a shim class is defined that raises
|
4
|
-
# a helpful error when used.
|
5
|
-
begin
|
6
|
-
require 'active_record'
|
7
|
-
rescue LoadError
|
8
|
-
# Ignore; we handle the lack of AR below.
|
9
|
-
end
|
10
|
-
|
11
|
-
module JetstreamBridge
|
12
|
-
# OutboxEvent is the default ActiveRecord model used by the gem when
|
13
|
-
# `use_outbox` is enabled.
|
14
|
-
# It stores pending events that will be flushed
|
15
|
-
# to NATS JetStream by your worker/cron.
|
16
|
-
if defined?(ActiveRecord::Base)
|
17
|
-
class OutboxEvent < ActiveRecord::Base
|
18
|
-
self.table_name = 'jetstream_outbox_events'
|
19
|
-
|
20
|
-
# Prefer native JSON/JSONB column.
|
21
|
-
# If the column is text, `serialize` with JSON keeps backward compatibility.
|
22
|
-
if respond_to?(:attribute_types) &&
|
23
|
-
attribute_types.key?('payload') &&
|
24
|
-
attribute_types['payload'].type.in?(%i[json jsonb])
|
25
|
-
# No-op: AR already gives JSON casting.
|
26
|
-
else
|
27
|
-
serialize :payload, coder: JSON
|
28
|
-
end
|
29
|
-
|
30
|
-
# Minimal validations that are generally useful; keep them short.
|
31
|
-
validates :resource_type, presence: true
|
32
|
-
validates :resource_id, presence: true
|
33
|
-
validates :event_type, presence: true
|
34
|
-
validates :payload, presence: true
|
35
|
-
end
|
36
|
-
else
|
37
|
-
# Shim that fails loudly if the app misconfigures the gem without AR.
|
38
|
-
class OutboxEvent
|
39
|
-
class << self
|
40
|
-
def method_missing(method_name, *_args, &_block)
|
41
|
-
raise_missing_ar!('Outbox', method_name)
|
42
|
-
end
|
43
|
-
|
44
|
-
def respond_to_missing?(_method_name, _include_private = false)
|
45
|
-
false
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
|
50
|
-
def raise_missing_ar!(which, method_name)
|
51
|
-
raise(
|
52
|
-
"#{which} requires ActiveRecord (tried to call ##{method_name}). " \
|
53
|
-
'Enable `use_outbox` only in apps with ActiveRecord, or add ' \
|
54
|
-
'`gem "activerecord"` to your Gemfile.'
|
55
|
-
)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
@@ -1,114 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'overlap_guard'
|
4
|
-
require_relative 'logging'
|
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
|
-
|
17
|
-
begin
|
18
|
-
info = jts.stream_info(name)
|
19
|
-
existing = normalize_subjects(info.config.subjects || [])
|
20
|
-
|
21
|
-
# Skip anything already COVERED by existing patterns (not just exact match)
|
22
|
-
to_add = desired.reject { |d| SubjectMatcher.covered?(existing, d) }
|
23
|
-
if to_add.empty?
|
24
|
-
Logging.info("Stream #{name} exists; subjects already covered.", tag: 'JetstreamBridge::Stream')
|
25
|
-
return
|
26
|
-
end
|
27
|
-
|
28
|
-
# Filter out subjects owned by other streams to prevent overlap BadRequest
|
29
|
-
allowed, blocked = OverlapGuard.partition_allowed(jts, name, to_add)
|
30
|
-
|
31
|
-
if allowed.empty?
|
32
|
-
if blocked.any?
|
33
|
-
Logging.warn(
|
34
|
-
"Stream #{name}: all missing subjects are owned by other streams; leaving unchanged. " \
|
35
|
-
"blocked=#{blocked.inspect}",
|
36
|
-
tag: 'JetstreamBridge::Stream'
|
37
|
-
)
|
38
|
-
else
|
39
|
-
Logging.info("Stream #{name} exists; nothing to add.", tag: 'JetstreamBridge::Stream')
|
40
|
-
end
|
41
|
-
return
|
42
|
-
end
|
43
|
-
|
44
|
-
target = (existing + allowed).uniq
|
45
|
-
|
46
|
-
# Validate and update (race may still occur; handled in rescue)
|
47
|
-
OverlapGuard.check!(jts, name, target)
|
48
|
-
jts.update_stream(name: name, subjects: target)
|
49
|
-
|
50
|
-
Logging.info(
|
51
|
-
"Updated stream #{name}; added subjects=#{allowed.inspect}" \
|
52
|
-
"#{blocked.any? ? " (skipped overlapped=#{blocked.inspect})" : ''}",
|
53
|
-
tag: 'JetstreamBridge::Stream'
|
54
|
-
)
|
55
|
-
rescue NATS::JetStream::Error => e
|
56
|
-
if stream_not_found?(e)
|
57
|
-
# Creating fresh: still filter to avoid BadRequest
|
58
|
-
allowed, blocked = OverlapGuard.partition_allowed(jts, name, desired)
|
59
|
-
if allowed.empty?
|
60
|
-
Logging.warn(
|
61
|
-
"Not creating stream #{name}: all desired subjects are owned by other streams. " \
|
62
|
-
"blocked=#{blocked.inspect}",
|
63
|
-
tag: 'JetstreamBridge::Stream'
|
64
|
-
)
|
65
|
-
return
|
66
|
-
end
|
67
|
-
|
68
|
-
jts.add_stream(
|
69
|
-
name: name,
|
70
|
-
subjects: allowed,
|
71
|
-
retention: 'interest',
|
72
|
-
storage: 'file'
|
73
|
-
)
|
74
|
-
Logging.info(
|
75
|
-
"Created stream #{name} subjects=#{allowed.inspect}" \
|
76
|
-
"#{blocked.any? ? " (skipped overlapped=#{blocked.inspect})" : ''}",
|
77
|
-
tag: 'JetstreamBridge::Stream'
|
78
|
-
)
|
79
|
-
elsif overlap_error?(e) && (attempts += 1) <= 1
|
80
|
-
# Late race: re-fetch and try once more
|
81
|
-
Logging.warn("Overlap race while ensuring #{name}; retrying once...", tag: 'JetstreamBridge::Stream')
|
82
|
-
sleep(0.05)
|
83
|
-
retry
|
84
|
-
elsif overlap_error?(e)
|
85
|
-
# Give up gracefully (don’t raise) — someone else now owns a conflicting subject
|
86
|
-
Logging.warn(
|
87
|
-
"Overlap persists ensuring #{name}; leaving unchanged. err=#{e.message.inspect}",
|
88
|
-
tag: 'JetstreamBridge::Stream'
|
89
|
-
)
|
90
|
-
return
|
91
|
-
else
|
92
|
-
raise
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
private
|
98
|
-
|
99
|
-
def normalize_subjects(list)
|
100
|
-
Array(list).flatten.compact.map!(&:to_s).reject(&:empty?).uniq
|
101
|
-
end
|
102
|
-
|
103
|
-
def stream_not_found?(error)
|
104
|
-
msg = error.message.to_s
|
105
|
-
msg =~ /stream\s+not\s+found/i || msg =~ /\b404\b/
|
106
|
-
end
|
107
|
-
|
108
|
-
def overlap_error?(error)
|
109
|
-
msg = error.message.to_s
|
110
|
-
msg =~ /subjects?\s+overlap/i || msg =~ /\berr_code=10065\b/ || msg =~ /\bstatus_code=400\b/
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|