jetstream_bridge 4.5.0 → 4.5.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 +338 -87
- data/README.md +3 -13
- data/docs/GETTING_STARTED.md +8 -12
- data/docs/PRODUCTION.md +13 -35
- data/docs/RESTRICTED_PERMISSIONS.md +399 -0
- data/docs/TESTING.md +33 -22
- data/lib/generators/jetstream_bridge/health_check/health_check_generator.rb +3 -3
- data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +3 -0
- data/lib/jetstream_bridge/consumer/consumer.rb +100 -39
- data/lib/jetstream_bridge/consumer/message_processor.rb +1 -1
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +97 -121
- data/lib/jetstream_bridge/core/bridge_helpers.rb +127 -0
- data/lib/jetstream_bridge/core/config.rb +32 -161
- data/lib/jetstream_bridge/core/connection.rb +508 -0
- data/lib/jetstream_bridge/core/connection_factory.rb +95 -0
- data/lib/jetstream_bridge/core/debug_helper.rb +2 -9
- data/lib/jetstream_bridge/core.rb +2 -0
- data/lib/jetstream_bridge/models/subject.rb +15 -23
- data/lib/jetstream_bridge/provisioner.rb +67 -0
- data/lib/jetstream_bridge/publisher/publisher.rb +121 -92
- data/lib/jetstream_bridge/rails/integration.rb +5 -8
- data/lib/jetstream_bridge/rails/railtie.rb +3 -4
- data/lib/jetstream_bridge/tasks/install.rake +17 -1
- data/lib/jetstream_bridge/topology/topology.rb +1 -6
- data/lib/jetstream_bridge/version.rb +1 -1
- data/lib/jetstream_bridge.rb +345 -202
- metadata +8 -8
- data/lib/jetstream_bridge/consumer/health_monitor.rb +0 -107
- data/lib/jetstream_bridge/core/connection_manager.rb +0 -513
- data/lib/jetstream_bridge/core/health_checker.rb +0 -184
- data/lib/jetstream_bridge/facade.rb +0 -212
- data/lib/jetstream_bridge/publisher/event_envelope_builder.rb +0 -110
data/docs/PRODUCTION.md
CHANGED
|
@@ -78,9 +78,9 @@ JetstreamBridge.configure do |config|
|
|
|
78
78
|
config.connect_retry_delay = 3 # Default: 2 seconds
|
|
79
79
|
|
|
80
80
|
# Required configuration
|
|
81
|
+
config.stream_name = ENV.fetch("JETSTREAM_STREAM_NAME", "jetstream-bridge-stream")
|
|
81
82
|
config.app_name = ENV.fetch("APP_NAME", "myapp")
|
|
82
83
|
config.destination_app = ENV.fetch("DESTINATION_APP")
|
|
83
|
-
config.stream_name = ENV.fetch("STREAM_NAME", "myapp-stream")
|
|
84
84
|
|
|
85
85
|
# Enable reliability features
|
|
86
86
|
config.use_outbox = true
|
|
@@ -100,31 +100,6 @@ JetstreamBridge.configure do |config|
|
|
|
100
100
|
end
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
### Permissions and Inbox Prefix
|
|
104
|
-
|
|
105
|
-
If your NATS account restricts `_INBOX.>` subscriptions, set an allowed prefix:
|
|
106
|
-
|
|
107
|
-
```ruby
|
|
108
|
-
JetstreamBridge.configure do |config|
|
|
109
|
-
config.inbox_prefix = "$RPC"
|
|
110
|
-
end
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
For pre-provisioned streams and consumers:
|
|
114
|
-
|
|
115
|
-
```ruby
|
|
116
|
-
JetstreamBridge.configure do |config|
|
|
117
|
-
config.stream_name = "my-stream" # required
|
|
118
|
-
config.durable_name = "my-durable" # optional
|
|
119
|
-
config.disable_js_api = true # skip JetStream management APIs
|
|
120
|
-
end
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Minimum NATS permissions:
|
|
124
|
-
|
|
125
|
-
- **Publish**: `$JS.API.>`, `$JS.ACK.>`, source/destination subjects, DLQ subject
|
|
126
|
-
- **Subscribe**: `_INBOX.>` (or custom inbox_prefix), destination subject
|
|
127
|
-
|
|
128
103
|
---
|
|
129
104
|
|
|
130
105
|
## Consumer Tuning
|
|
@@ -167,7 +142,7 @@ Long-running consumers automatically:
|
|
|
167
142
|
|
|
168
143
|
- Log health checks every 10 minutes (iterations, memory, uptime)
|
|
169
144
|
- Warn when memory exceeds 1GB
|
|
170
|
-
-
|
|
145
|
+
- Suggest garbage collection when heap grows large
|
|
171
146
|
|
|
172
147
|
Monitor these logs to detect memory leaks early.
|
|
173
148
|
|
|
@@ -178,7 +153,7 @@ Monitor these logs to detect memory leaks early.
|
|
|
178
153
|
### Key Metrics to Track
|
|
179
154
|
|
|
180
155
|
| Metric | Description | Alert Threshold |
|
|
181
|
-
|
|
|
156
|
+
| --- | --- | --- |
|
|
182
157
|
| Consumer Lag | Pending messages in stream | > 1000 messages |
|
|
183
158
|
| DLQ Size | Messages in dead letter queue | > 100 messages |
|
|
184
159
|
| Connection Status | Health check failures | 2 consecutive failures |
|
|
@@ -194,7 +169,7 @@ Use the built-in health check for monitoring:
|
|
|
194
169
|
# config/routes.rb
|
|
195
170
|
Rails.application.routes.draw do
|
|
196
171
|
get '/health/jetstream', to: proc { |env|
|
|
197
|
-
health = JetstreamBridge.
|
|
172
|
+
health = JetstreamBridge.health_check
|
|
198
173
|
status = health[:healthy] ? 200 : 503
|
|
199
174
|
[status, { 'Content-Type' => 'application/json' }, [health.to_json]]
|
|
200
175
|
}
|
|
@@ -213,8 +188,8 @@ end
|
|
|
213
188
|
},
|
|
214
189
|
"stream": {
|
|
215
190
|
"exists": true,
|
|
216
|
-
"name": "
|
|
217
|
-
"subjects": ["
|
|
191
|
+
"name": "jetstream-bridge-stream",
|
|
192
|
+
"subjects": ["app.sync.worker"],
|
|
218
193
|
"messages": 1523
|
|
219
194
|
},
|
|
220
195
|
"performance": {
|
|
@@ -222,15 +197,14 @@ end
|
|
|
222
197
|
"health_check_duration_ms": 45.2
|
|
223
198
|
},
|
|
224
199
|
"config": {
|
|
225
|
-
"
|
|
200
|
+
"stream_name": "jetstream-bridge-stream",
|
|
226
201
|
"app_name": "app",
|
|
227
202
|
"destination_app": "worker",
|
|
228
203
|
"use_outbox": true,
|
|
229
204
|
"use_inbox": true,
|
|
230
|
-
"use_dlq": true
|
|
231
|
-
"disable_js_api": true
|
|
205
|
+
"use_dlq": true
|
|
232
206
|
},
|
|
233
|
-
"version": "4.
|
|
207
|
+
"version": "4.0.3"
|
|
234
208
|
}
|
|
235
209
|
```
|
|
236
210
|
|
|
@@ -267,6 +241,10 @@ jetstream_dlq = prometheus.gauge(
|
|
|
267
241
|
|
|
268
242
|
## Security Hardening
|
|
269
243
|
|
|
244
|
+
### Permissions & Topology
|
|
245
|
+
|
|
246
|
+
Keep runtime credentials least-privileged: set `config.auto_provision = false` and provision the stream/consumer during deploy (`bundle exec rake jetstream_bridge:provision` or NATS CLI). The exact durable/subject layout and minimal publish/subscribe permissions live in [RESTRICTED_PERMISSIONS.md](RESTRICTED_PERMISSIONS.md). Health checks skip stream info when `auto_provision=false`; rely on your provisioning pipeline for validation.
|
|
247
|
+
|
|
270
248
|
### Rate Limiting
|
|
271
249
|
|
|
272
250
|
The health check endpoint has built-in rate limiting (1 uncached request per 5 seconds). For HTTP endpoints, add additional protection:
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# Working with Restricted NATS Permissions
|
|
2
|
+
|
|
3
|
+
This guide explains how to use JetStream Bridge when your NATS user lacks JetStream API permissions (`$JS.API.*` subjects).
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
When the NATS user has restricted permissions and cannot access JetStream API subjects, you'll see errors like:
|
|
8
|
+
|
|
9
|
+
```markdown
|
|
10
|
+
ERROR -- : [JetstreamBridge::ConnectionManager] NATS error: 'Permissions Violation for Publish to "$JS.API.CONSUMER.INFO.{stream}.{consumer}"'
|
|
11
|
+
NATS::IO::Timeout: nats: timeout
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This happens because:
|
|
15
|
+
|
|
16
|
+
1. JetStream Bridge tries to verify consumer configuration using `$JS.API.CONSUMER.INFO.*`
|
|
17
|
+
2. The NATS user doesn't have permission to publish to these API subjects
|
|
18
|
+
3. The connection is terminated, causing timeout errors
|
|
19
|
+
|
|
20
|
+
## Solution Overview
|
|
21
|
+
|
|
22
|
+
When you cannot modify NATS server permissions, you need to:
|
|
23
|
+
|
|
24
|
+
0. **Turn off runtime provisioning** so the app never calls `$JS.API.*`:
|
|
25
|
+
- Set `config.auto_provision = false`
|
|
26
|
+
- Provision stream + consumer once with admin credentials (CLI below or `bundle exec rake jetstream_bridge:provision`)
|
|
27
|
+
1. **Pre-create the consumer** using a privileged NATS account
|
|
28
|
+
2. **Ensure the consumer configuration matches** what your app expects
|
|
29
|
+
|
|
30
|
+
> Tip: When `auto_provision=false`, the app still connects/publishes/consumes but skips JetStream management APIs (account_info, stream_info, consumer_info). Health checks will report basic connectivity only.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Runtime requirements (least privilege)
|
|
35
|
+
|
|
36
|
+
- Config: `config.auto_provision = false`, `config.stream_name` set explicitly.
|
|
37
|
+
- Topology: stream + durable consumer must be pre-provisioned (via `bundle exec rake jetstream_bridge:provision` or NATS CLI).
|
|
38
|
+
- NATS permissions for runtime creds:
|
|
39
|
+
- publish allow: `">"` (or narrowed to your business subjects) and `$JS.API.CONSUMER.MSG.NEXT.{stream_name}.{app_name}-workers`
|
|
40
|
+
- subscribe allow: `">"` (or narrowed) and `_INBOX.>` (responses for pull consumers)
|
|
41
|
+
- Health check will only report connectivity (stream info skipped).
|
|
42
|
+
|
|
43
|
+
### Topology required
|
|
44
|
+
|
|
45
|
+
- Stream: `config.stream_name` (retention: workqueue, storage: file).
|
|
46
|
+
- Subjects:
|
|
47
|
+
- Publish: `{app_name}.sync.{destination_app}`
|
|
48
|
+
- Consume: `{destination_app}.sync.{app_name}`
|
|
49
|
+
- DLQ (if enabled): `{app_name}.sync.dlq`
|
|
50
|
+
- Durable consumer: `{app_name}-workers` filtering on `{destination_app}.sync.{app_name}`.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Option A: Provision with the built-in task (creates stream + consumer)
|
|
55
|
+
|
|
56
|
+
Run this from CI/deploy with admin NATS credentials:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Uses your configured stream/app/destination to create the stream + durable consumer
|
|
60
|
+
NATS_URLS="nats://admin:pass@10.199.12.34:4222" \
|
|
61
|
+
bundle exec rake jetstream_bridge:provision
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is the easiest way to keep `auto_provision=false` in runtime while still reusing the bridge’s topology logic (subjects, DLQ, overlap guard).
|
|
65
|
+
|
|
66
|
+
## Option B: Pre-create the Consumer using NATS CLI
|
|
67
|
+
|
|
68
|
+
### Install NATS CLI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Download from https://github.com/nats-io/natscli/releases
|
|
72
|
+
curl -sf https://binaries.nats.dev/nats-io/natscli/nats@latest | sh
|
|
73
|
+
|
|
74
|
+
# Or using Homebrew (macOS)
|
|
75
|
+
brew install nats-io/nats-tools/nats
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Create the Consumer
|
|
79
|
+
|
|
80
|
+
You need to create a durable pull consumer with the exact configuration your app expects.
|
|
81
|
+
|
|
82
|
+
**Required values from your JetStream Bridge config:**
|
|
83
|
+
|
|
84
|
+
- **Stream name**: `JETSTREAM_STREAM_NAME` (e.g., `jetstream-bridge-stream`)
|
|
85
|
+
- **Consumer name**: `{app_name}-workers` (e.g., `pwas-workers`)
|
|
86
|
+
- **Filter subject**: `{app_name}.sync.{destination_app}` (e.g., `pwas-workers.sync.heavyworth`)
|
|
87
|
+
|
|
88
|
+
**Create consumer command:**
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Connect using a privileged NATS account
|
|
92
|
+
nats context save admin \
|
|
93
|
+
--server=nats://admin-user:admin-pass@10.199.12.34:4222 \
|
|
94
|
+
--description="Admin account for consumer creation"
|
|
95
|
+
|
|
96
|
+
# Select the context
|
|
97
|
+
nats context select admin
|
|
98
|
+
|
|
99
|
+
# Create the consumer
|
|
100
|
+
nats consumer add production-jetstream-bridge-stream production-pwas-workers \
|
|
101
|
+
--filter "production.pwas-workers.sync.heavyworth" \
|
|
102
|
+
--ack explicit \
|
|
103
|
+
--pull \
|
|
104
|
+
--deliver all \
|
|
105
|
+
--max-deliver 5 \
|
|
106
|
+
--ack-wait 30s \
|
|
107
|
+
--replay instant \
|
|
108
|
+
--max-pending 25000
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**With backoff (recommended for production):**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
nats consumer add production-jetstream-bridge-stream production-pwas-workers \
|
|
115
|
+
--filter "production.pwas-workers.sync.heavyworth" \
|
|
116
|
+
--ack explicit \
|
|
117
|
+
--pull \
|
|
118
|
+
--deliver all \
|
|
119
|
+
--max-deliver 5 \
|
|
120
|
+
--ack-wait 30s \
|
|
121
|
+
--backoff 1s,5s,15s,30s,60s \
|
|
122
|
+
--replay instant \
|
|
123
|
+
--max-pending 25000
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Verify Consumer Creation
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
nats consumer info production-jetstream-bridge-stream production-pwas-workers
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Expected output:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
Information for Consumer production-jetstream-bridge-stream > production-pwas-workers
|
|
136
|
+
|
|
137
|
+
Configuration:
|
|
138
|
+
|
|
139
|
+
Durable Name: production-pwas-workers
|
|
140
|
+
Filter Subject: production.pwas-workers.sync.heavyworth
|
|
141
|
+
Ack Policy: explicit
|
|
142
|
+
Ack Wait: 30s
|
|
143
|
+
Replay Policy: instant
|
|
144
|
+
Maximum Deliveries: 5
|
|
145
|
+
Backoff: [1s 5s 15s 30s 60s]
|
|
146
|
+
|
|
147
|
+
State:
|
|
148
|
+
|
|
149
|
+
Last Delivered Message: Consumer sequence: 0 Stream sequence: 0
|
|
150
|
+
Acknowledgment Floor: Consumer sequence: 0 Stream sequence: 0
|
|
151
|
+
Pending Messages: 0
|
|
152
|
+
Redelivered Messages: 0
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Step 2: Configure JetStream Bridge
|
|
158
|
+
|
|
159
|
+
Configure JetStream Bridge to avoid JetStream management APIs at runtime (pre-provisioning handles them instead):
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
# config/initializers/jetstream_bridge.rb
|
|
163
|
+
JetstreamBridge.configure do |config|
|
|
164
|
+
config.nats_urls = ENV.fetch("NATS_URLS")
|
|
165
|
+
config.stream_name = "jetstream-bridge-stream"
|
|
166
|
+
config.app_name = "pwas-workers"
|
|
167
|
+
config.destination_app = "heavyworth"
|
|
168
|
+
config.auto_provision = false
|
|
169
|
+
|
|
170
|
+
config.use_outbox = true
|
|
171
|
+
config.use_inbox = true
|
|
172
|
+
config.use_dlq = true
|
|
173
|
+
|
|
174
|
+
# These settings MUST match the pre-created consumer
|
|
175
|
+
config.max_deliver = 5
|
|
176
|
+
config.ack_wait = "30s"
|
|
177
|
+
config.backoff = %w[1s 5s 15s 30s 60s]
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Critical:** The `max_deliver`, `ack_wait`, and `backoff` values in your config **must exactly match** the pre-created consumer configuration. If they don't match, messages may be redelivered incorrectly.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Step 3: Deploy and Verify
|
|
186
|
+
|
|
187
|
+
### Deploy the Updated Gem
|
|
188
|
+
|
|
189
|
+
If you're working on the jetstream_bridge gem itself:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# Build the gem
|
|
193
|
+
gem build jetstream_bridge.gemspec
|
|
194
|
+
|
|
195
|
+
# Install locally for testing
|
|
196
|
+
gem install ./jetstream_bridge-4.5.0.gem
|
|
197
|
+
|
|
198
|
+
# Or update in your application's Gemfile.lock
|
|
199
|
+
bundle update jetstream_bridge
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
If this is a local modification, you can point your Gemfile to the local path:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
# Gemfile (temporary for testing)
|
|
206
|
+
gem 'jetstream_bridge', path: '/path/to/local/jetstream_bridge'
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Restart the Service
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
sudo systemctl restart pwas_production_sync
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Monitor the Logs
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
sudo journalctl -u pwas_production_sync -f
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Expected success output:**
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
INFO -- : [JetstreamBridge::ConnectionManager] Connected to NATS (1 server): nats://pwas:***@10.199.12.34:4222
|
|
225
|
+
INFO -- : [DataSync] Consumer starting (durable=production-pwas-workers, batch=25, dest="heavyworth")
|
|
226
|
+
INFO -- : [DataSync] run! started successfully
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Health checks will report connectivity only when `auto_provision=false` (stream info is skipped to avoid `$JS.API.STREAM.INFO`).
|
|
230
|
+
|
|
231
|
+
**If you still see errors:**
|
|
232
|
+
|
|
233
|
+
1. **Timeout during subscribe** - The pre-created consumer name/filter might not match. Verify:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
nats consumer ls production-jetstream-bridge-stream
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
2. **Permission violations on fetch** - The NATS user also needs permission to fetch messages. Minimum required:
|
|
240
|
+
|
|
241
|
+
```conf
|
|
242
|
+
subscribe: {
|
|
243
|
+
allow: ["production.>", "_INBOX.>"]
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Maintenance
|
|
248
|
+
|
|
249
|
+
### Updating Consumer Configuration
|
|
250
|
+
|
|
251
|
+
If you need to change consumer settings (e.g., increase `max_deliver`):
|
|
252
|
+
|
|
253
|
+
1. **Stop your application** to prevent message processing
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
sudo systemctl stop pwas_production_sync
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
2. **Delete the old consumer** (using privileged account)
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
nats consumer rm production-jetstream-bridge-stream production-pwas-workers -f
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
3. **Create the new consumer** with updated settings
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
nats consumer add production-jetstream-bridge-stream production-pwas-workers \
|
|
269
|
+
--filter "production.pwas-workers.sync.heavyworth" \
|
|
270
|
+
--ack explicit \
|
|
271
|
+
--pull \
|
|
272
|
+
--deliver all \
|
|
273
|
+
--max-deliver 10 \
|
|
274
|
+
--ack-wait 60s \
|
|
275
|
+
--backoff 2s,10s,30s,60s,120s \
|
|
276
|
+
--replay instant \
|
|
277
|
+
--max-pending 25000
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
4. **Update your application config** to match
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
config.max_deliver = 10
|
|
284
|
+
config.ack_wait = "60s"
|
|
285
|
+
config.backoff = %w[2s 10s 30s 60s 120s]
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
5. **Restart your application**
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
sudo systemctl start pwas_production_sync
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Monitoring
|
|
295
|
+
|
|
296
|
+
Add monitoring to detect configuration drift:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
# Check consumer exists and is healthy
|
|
300
|
+
health = JetstreamBridge.health_check
|
|
301
|
+
unless health[:healthy]
|
|
302
|
+
alert("JetStream consumer unhealthy: #{health}")
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Troubleshooting
|
|
309
|
+
|
|
310
|
+
### Consumer doesn't receive messages
|
|
311
|
+
|
|
312
|
+
**Check stream has messages:**
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
nats stream info production-jetstream-bridge-stream
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Verify filter subject matches your topology:**
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# App publishes to: {app_name}.sync.{destination_app}
|
|
322
|
+
# Consumer filters on: {destination_app}.sync.{app_name}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Service keeps restarting
|
|
326
|
+
|
|
327
|
+
Check if the issue is:
|
|
328
|
+
|
|
329
|
+
1. **Consumer doesn't exist** - Pre-create it with NATS CLI
|
|
330
|
+
2. **Filter subject mismatch** - Verify with `nats consumer info`
|
|
331
|
+
3. **Permissions still insufficient** - User needs subscribe permissions on the filtered subject
|
|
332
|
+
4. **Wrong stream name** - Ensure stream name matches your configured `config.stream_name`
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Security Considerations
|
|
337
|
+
|
|
338
|
+
1. **Consumer pre-creation requires privileged access** - Keep admin credentials secure
|
|
339
|
+
2. **Configuration drift risk** - If app config doesn't match consumer config, message delivery may fail silently
|
|
340
|
+
3. **No automatic recovery** - If consumer is deleted, it won't be recreated automatically
|
|
341
|
+
4. **Consider automation** - Use infrastructure-as-code (Terraform, Ansible) to manage consumer creation
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Example: Production Setup for pwas-api
|
|
346
|
+
|
|
347
|
+
Based on your logs, here's the exact setup:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
# 1. Pre-create the consumer (as admin)
|
|
351
|
+
nats consumer add pwas-heavyworth-sync production-pwas-workers \
|
|
352
|
+
--filter "production.pwas-workers.sync.heavyworth" \
|
|
353
|
+
--ack explicit \
|
|
354
|
+
--pull \
|
|
355
|
+
--deliver all \
|
|
356
|
+
--max-deliver 5 \
|
|
357
|
+
--ack-wait 30s \
|
|
358
|
+
--backoff 1s,5s,15s,30s,60s \
|
|
359
|
+
--replay instant
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# 2. Update config/initializers/jetstream_bridge.rb
|
|
364
|
+
JetstreamBridge.configure do |config|
|
|
365
|
+
config.nats_urls = "nats://pwas:***@10.199.12.34:4222"
|
|
366
|
+
config.stream_name = "jetstream-bridge-stream"
|
|
367
|
+
config.app_name = "pwas-workers"
|
|
368
|
+
config.destination_app = "heavyworth"
|
|
369
|
+
config.max_deliver = 5
|
|
370
|
+
config.ack_wait = "30s"
|
|
371
|
+
config.backoff = %w[1s 5s 15s 30s 60s]
|
|
372
|
+
end
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Note:** Verify your exact stream name and consumer durable (`app_name-workers`) match what was provisioned.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Alternative: Request Minimal Permissions
|
|
380
|
+
|
|
381
|
+
If you have any influence over NATS permissions, request only these minimal subjects:
|
|
382
|
+
|
|
383
|
+
```conf
|
|
384
|
+
# Minimal permissions needed for JetStream Bridge consumer
|
|
385
|
+
publish: {
|
|
386
|
+
allow: [
|
|
387
|
+
">", # Your app subjects (narrow if desired)
|
|
388
|
+
"$JS.API.CONSUMER.MSG.NEXT.{stream_name}.{app_name}-workers", # Fetch messages (required)
|
|
389
|
+
]
|
|
390
|
+
}
|
|
391
|
+
subscribe: {
|
|
392
|
+
allow: [
|
|
393
|
+
">", # Your app subjects (narrow if desired)
|
|
394
|
+
"_INBOX.>", # Request-reply responses (required)
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
These are read-only operations and don't allow creating/modifying streams or consumers.
|
data/docs/TESTING.md
CHANGED
|
@@ -28,28 +28,31 @@ RSpec.describe MyService do
|
|
|
28
28
|
include JetstreamBridge::TestHelpers::Matchers
|
|
29
29
|
|
|
30
30
|
before do
|
|
31
|
-
# Reset state
|
|
31
|
+
# Reset singleton to ensure clean state
|
|
32
|
+
JetstreamBridge::Connection.instance_variable_set(:@singleton__instance__, nil)
|
|
32
33
|
JetstreamBridge.reset!
|
|
33
34
|
|
|
34
35
|
# Enable test mode - automatically sets up mock NATS
|
|
35
36
|
JetstreamBridge::TestHelpers.enable_test_mode!
|
|
36
37
|
|
|
37
38
|
JetstreamBridge.configure do |config|
|
|
39
|
+
config.stream_name = 'jetstream-bridge-stream'
|
|
38
40
|
config.app_name = 'my_app'
|
|
39
41
|
config.destination_app = 'worker'
|
|
40
|
-
config.stream_name = 'test-stream'
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
# Setup mock stream
|
|
44
|
+
# Setup mock stream and stub topology
|
|
44
45
|
mock_jts = JetstreamBridge::TestHelpers.mock_connection.jetstream
|
|
45
46
|
mock_jts.add_stream(
|
|
46
|
-
name: 'test-stream',
|
|
47
|
-
subjects: ['
|
|
47
|
+
name: 'test-jetstream-bridge-stream',
|
|
48
|
+
subjects: ['test.>']
|
|
48
49
|
)
|
|
50
|
+
allow(JetstreamBridge::Topology).to receive(:ensure!)
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
after do
|
|
52
54
|
JetstreamBridge::TestHelpers.reset_test_mode!
|
|
55
|
+
JetstreamBridge::Connection.instance_variable_set(:@singleton__instance__, nil)
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
it 'publishes events through the full stack' do
|
|
@@ -263,20 +266,27 @@ end.to raise_error(NATS::JetStream::Error, 'consumer not found')
|
|
|
263
266
|
```ruby
|
|
264
267
|
before do
|
|
265
268
|
JetstreamBridge.reset!
|
|
266
|
-
|
|
269
|
+
|
|
270
|
+
mock_conn = JetstreamBridge::TestHelpers::MockNats.create_mock_connection
|
|
271
|
+
mock_jts = mock_conn.jetstream
|
|
272
|
+
|
|
273
|
+
allow(NATS::IO::Client).to receive(:new).and_return(mock_conn)
|
|
274
|
+
allow(JetstreamBridge::Connection).to receive(:connect!).and_call_original
|
|
275
|
+
|
|
276
|
+
# Setup stream
|
|
277
|
+
mock_jts.add_stream(
|
|
278
|
+
name: 'test-jetstream-bridge-stream',
|
|
279
|
+
subjects: ['test.>']
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Allow topology check to succeed
|
|
283
|
+
allow(JetstreamBridge::Topology).to receive(:ensure!)
|
|
267
284
|
|
|
268
285
|
JetstreamBridge.configure do |config|
|
|
286
|
+
config.stream_name = 'jetstream-bridge-stream'
|
|
269
287
|
config.app_name = 'api'
|
|
270
288
|
config.destination_app = 'worker'
|
|
271
|
-
config.stream_name = 'test-stream'
|
|
272
289
|
end
|
|
273
|
-
|
|
274
|
-
# Setup mock stream
|
|
275
|
-
mock_jts = JetstreamBridge::TestHelpers.mock_connection.jetstream
|
|
276
|
-
mock_jts.add_stream(
|
|
277
|
-
name: 'test-stream',
|
|
278
|
-
subjects: ['api.sync.worker', 'worker.sync.api']
|
|
279
|
-
)
|
|
280
290
|
end
|
|
281
291
|
|
|
282
292
|
it 'publishes through JetstreamBridge' do
|
|
@@ -286,9 +296,9 @@ it 'publishes through JetstreamBridge' do
|
|
|
286
296
|
payload: { id: 1, name: 'Test' }
|
|
287
297
|
)
|
|
288
298
|
|
|
289
|
-
expect(result
|
|
299
|
+
expect(result).to be_publish_success
|
|
290
300
|
expect(result.event_id).to be_present
|
|
291
|
-
|
|
301
|
+
expect(result.subject).to eq('api.sync.worker')
|
|
292
302
|
|
|
293
303
|
# Verify in storage
|
|
294
304
|
storage = JetstreamBridge::TestHelpers.mock_storage
|
|
@@ -296,11 +306,12 @@ it 'publishes through JetstreamBridge' do
|
|
|
296
306
|
end
|
|
297
307
|
```
|
|
298
308
|
|
|
299
|
-
### Full
|
|
309
|
+
### Full Consuming Flow
|
|
300
310
|
|
|
301
311
|
```ruby
|
|
302
|
-
it 'consumes
|
|
303
|
-
|
|
312
|
+
it 'consumes through JetstreamBridge' do
|
|
313
|
+
mock_conn = JetstreamBridge::TestHelpers.mock_connection
|
|
314
|
+
mock_jts = mock_conn.jetstream
|
|
304
315
|
|
|
305
316
|
# Publish message to destination subject
|
|
306
317
|
mock_jts.publish(
|
|
@@ -328,8 +339,8 @@ it 'consumes messages through JetstreamBridge' do
|
|
|
328
339
|
# Mock subscription
|
|
329
340
|
subscription = mock_jts.pull_subscribe(
|
|
330
341
|
'worker.sync.api',
|
|
331
|
-
'
|
|
332
|
-
stream: 'test-stream'
|
|
342
|
+
'test-consumer',
|
|
343
|
+
stream: 'test-jetstream-bridge-stream'
|
|
333
344
|
)
|
|
334
345
|
|
|
335
346
|
allow_any_instance_of(JetstreamBridge::SubscriptionManager)
|
|
@@ -380,7 +391,7 @@ storage.reset!
|
|
|
380
391
|
3. **Test both success and failure paths**: Use the mock to simulate errors
|
|
381
392
|
4. **Verify message content**: Check that envelopes are correctly formatted
|
|
382
393
|
5. **Test idempotency**: Verify duplicate detection and redelivery behavior
|
|
383
|
-
6. **
|
|
394
|
+
6. **Mock topology setup**: Remember to stub `JetstreamBridge::Topology.ensure!`
|
|
384
395
|
|
|
385
396
|
## Examples
|
|
386
397
|
|
|
@@ -41,12 +41,12 @@ module JetstreamBridge
|
|
|
41
41
|
"connected_at": "2025-11-22T20:00:00Z",
|
|
42
42
|
"stream": {
|
|
43
43
|
"exists": true,
|
|
44
|
-
"name": "
|
|
45
|
-
"subjects": ["
|
|
44
|
+
"name": "jetstream-bridge-stream",
|
|
45
|
+
"subjects": ["app1.sync.app2"],
|
|
46
46
|
"messages": 42
|
|
47
47
|
},
|
|
48
48
|
"config": {
|
|
49
|
-
"
|
|
49
|
+
"stream_name": "jetstream-bridge-stream",
|
|
50
50
|
"app_name": "my_app",
|
|
51
51
|
"destination_app": "other_app"
|
|
52
52
|
},
|
|
@@ -13,6 +13,9 @@ JetstreamBridge.configure do |config|
|
|
|
13
13
|
# NATS server URLs (comma-separated for cluster)
|
|
14
14
|
config.nats_urls = ENV.fetch('NATS_URLS', 'nats://localhost:4222')
|
|
15
15
|
|
|
16
|
+
# Stream name (required) - managed separately from runtime credentials
|
|
17
|
+
config.stream_name = ENV.fetch('JETSTREAM_STREAM_NAME', 'jetstream-bridge-stream')
|
|
18
|
+
|
|
16
19
|
# Application name (used in subject routing)
|
|
17
20
|
config.app_name = ENV.fetch('APP_NAME', Rails.application.class.module_parent_name.underscore)
|
|
18
21
|
|