jetstream_bridge 4.5.0 → 4.5.2
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 +525 -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 +59 -12
- 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,525 @@
|
|
|
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: **one shared stream per app pair**, with one durable consumer per app (each filters the opposite direction). Pre-provision 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
|
+
### Connectivity check with restricted creds
|
|
55
|
+
|
|
56
|
+
After setting `config.auto_provision = false`, you can still confirm basic connectivity without touching `$JS.API.*` by running:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bundle exec rake jetstream_bridge:test_connection
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
In this mode the task performs a NATS ping and JetStream client setup only; it assumes the stream + consumer are already provisioned with admin credentials. If you see a permissions violation for `$JS.API.*`, re-run with a privileged user to provision or set `auto_provision=false` and pre-create the stream/consumer first.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Option A: Provision with the built-in task (creates stream + consumer)
|
|
67
|
+
|
|
68
|
+
Run this from CI/deploy with admin NATS credentials:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Uses your configured stream/app/destination to create the stream + durable consumer
|
|
72
|
+
NATS_URLS="nats://admin:pass@nats.example.com:4222" \
|
|
73
|
+
bundle exec rake jetstream_bridge:provision
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This is the easiest way to keep `auto_provision=false` in runtime while still reusing the bridge’s topology logic (subjects, DLQ, overlap guard).
|
|
77
|
+
|
|
78
|
+
## Option B: Pre-create the Consumer using NATS CLI
|
|
79
|
+
|
|
80
|
+
### Install NATS CLI
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Download from https://github.com/nats-io/natscli/releases
|
|
84
|
+
curl -sf https://binaries.nats.dev/nats-io/natscli/nats@latest | sh
|
|
85
|
+
|
|
86
|
+
# Or using Homebrew (macOS)
|
|
87
|
+
brew install nats-io/nats-tools/nats
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Create the Consumer
|
|
91
|
+
|
|
92
|
+
You need to create a durable pull consumer with the exact configuration your app expects. Both apps share a single stream; create one consumer per app (each filters the opposite direction).
|
|
93
|
+
|
|
94
|
+
**Required values from your JetStream Bridge config:**
|
|
95
|
+
|
|
96
|
+
- **Stream name**: `JETSTREAM_STREAM_NAME` (e.g., `pwas-heavyworth-sync`)
|
|
97
|
+
- **Consumer name**: `{app_name}-workers` (e.g., `pwas-workers`)
|
|
98
|
+
- **Filter subject**: `{destination_app}.sync.{app_name}` (e.g., `heavyworth.sync.pwas`)
|
|
99
|
+
|
|
100
|
+
**Create consumer command:**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Connect using a privileged NATS account
|
|
104
|
+
nats context save admin \
|
|
105
|
+
--server=nats://admin-user:admin-pass@nats.example.com:4222 \
|
|
106
|
+
--description="Admin account for consumer creation"
|
|
107
|
+
|
|
108
|
+
# Select the context
|
|
109
|
+
nats context select admin
|
|
110
|
+
|
|
111
|
+
# Create the consumer (replace stream/app/destination with yours)
|
|
112
|
+
nats consumer add pwas-heavyworth-sync pwas-workers \
|
|
113
|
+
--filter "heavyworth.sync.pwas" \
|
|
114
|
+
--ack explicit \
|
|
115
|
+
--pull \
|
|
116
|
+
--deliver all \
|
|
117
|
+
--max-deliver 5 \
|
|
118
|
+
--ack-wait 30s \
|
|
119
|
+
--replay instant \
|
|
120
|
+
--max-pending 25000
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**With backoff (recommended for production):**
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
nats consumer add pwas-heavyworth-sync pwas-workers \
|
|
127
|
+
--filter "heavyworth.sync.pwas" \
|
|
128
|
+
--ack explicit \
|
|
129
|
+
--pull \
|
|
130
|
+
--deliver all \
|
|
131
|
+
--max-deliver 5 \
|
|
132
|
+
--ack-wait 30s \
|
|
133
|
+
--backoff 1s,5s,15s,30s,60s \
|
|
134
|
+
--replay instant \
|
|
135
|
+
--max-pending 25000
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Example using your observed stream (`pwas-heavyworth-sync`) and defaults (shared stream, two consumers):**
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Admin context
|
|
142
|
+
nats context save admin \
|
|
143
|
+
--server=nats://admin-user:admin-pass@nats.example.com:4222 \
|
|
144
|
+
--description="Admin account for jetstream_bridge provisioning"
|
|
145
|
+
nats context select admin
|
|
146
|
+
|
|
147
|
+
# Create stream with bridge subjects + DLQ
|
|
148
|
+
nats stream add pwas-heavyworth-sync \
|
|
149
|
+
--subjects "pwas.sync.heavyworth" "heavyworth.sync.pwas" "pwas.sync.dlq" \
|
|
150
|
+
--retention workqueue \
|
|
151
|
+
--storage file
|
|
152
|
+
|
|
153
|
+
# Create durable pull consumer (must match JetstreamBridge config)
|
|
154
|
+
nats consumer add pwas-heavyworth-sync pwas-workers \
|
|
155
|
+
--filter "heavyworth.sync.pwas" \
|
|
156
|
+
--ack explicit \
|
|
157
|
+
--pull \
|
|
158
|
+
--deliver all \
|
|
159
|
+
--max-deliver 5 \
|
|
160
|
+
--ack-wait 30s \
|
|
161
|
+
--backoff 1s,5s,15s,30s,60s \
|
|
162
|
+
--replay instant \
|
|
163
|
+
--max-pending 25000
|
|
164
|
+
|
|
165
|
+
# Consumer for heavyworth (receives pwas -> heavyworth)
|
|
166
|
+
nats consumer add pwas-heavyworth-sync heavyworth-workers \
|
|
167
|
+
--filter "pwas.sync.heavyworth" \
|
|
168
|
+
--ack explicit \
|
|
169
|
+
--pull \
|
|
170
|
+
--deliver all \
|
|
171
|
+
--max-deliver 5 \
|
|
172
|
+
--ack-wait 30s \
|
|
173
|
+
--backoff 1s,5s,15s,30s,60s \
|
|
174
|
+
--replay instant \
|
|
175
|
+
--max-pending 25000
|
|
176
|
+
|
|
177
|
+
# Verify
|
|
178
|
+
nats stream info pwas-heavyworth-sync
|
|
179
|
+
nats consumer info pwas-heavyworth-sync heavyworth-workers
|
|
180
|
+
nats consumer info pwas-heavyworth-sync pwas-workers
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Provision both apps (shared stream + two consumers)
|
|
184
|
+
|
|
185
|
+
If both apps share one stream, create it once and add a durable consumer for each side:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
STREAM="appA-appB-sync"
|
|
189
|
+
APP_A="appA" # first app name
|
|
190
|
+
APP_B="appB" # second app name
|
|
191
|
+
|
|
192
|
+
# Admin context (update server/creds as needed)
|
|
193
|
+
nats context save admin \
|
|
194
|
+
--server=nats://admin-user:admin-pass@nats.example.com:4222 \
|
|
195
|
+
--description="Admin account for jetstream_bridge provisioning"
|
|
196
|
+
nats context select admin
|
|
197
|
+
|
|
198
|
+
# Stream covers both publish directions + both DLQs
|
|
199
|
+
nats stream add "$STREAM" \
|
|
200
|
+
--subjects "$APP_A.sync.$APP_B" "$APP_B.sync.$APP_A" "$APP_A.sync.dlq" "$APP_B.sync.dlq" \
|
|
201
|
+
--retention workqueue \
|
|
202
|
+
--storage file
|
|
203
|
+
|
|
204
|
+
# Consumer for APP_A (receives messages destined to APP_A)
|
|
205
|
+
nats consumer add "$STREAM" "$APP_A-workers" \
|
|
206
|
+
--filter "$APP_B.sync.$APP_A" \
|
|
207
|
+
--ack explicit \
|
|
208
|
+
--pull \
|
|
209
|
+
--deliver all \
|
|
210
|
+
--max-deliver 5 \
|
|
211
|
+
--ack-wait 30s \
|
|
212
|
+
--backoff 1s,5s,15s,30s,60s \
|
|
213
|
+
--replay instant \
|
|
214
|
+
--max-pending 25000
|
|
215
|
+
|
|
216
|
+
# Consumer for APP_B (receives messages destined to APP_B)
|
|
217
|
+
nats consumer add "$STREAM" "$APP_B-workers" \
|
|
218
|
+
--filter "$APP_A.sync.$APP_B" \
|
|
219
|
+
--ack explicit \
|
|
220
|
+
--pull \
|
|
221
|
+
--deliver all \
|
|
222
|
+
--max-deliver 5 \
|
|
223
|
+
--ack-wait 30s \
|
|
224
|
+
--backoff 1s,5s,15s,30s,60s \
|
|
225
|
+
--replay instant \
|
|
226
|
+
--max-pending 25000
|
|
227
|
+
|
|
228
|
+
# Verify both
|
|
229
|
+
nats consumer info "$STREAM" "$APP_A-workers"
|
|
230
|
+
nats consumer info "$STREAM" "$APP_B-workers"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Verify Consumer Creation
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
nats consumer info pwas-heavyworth-sync pwas-workers
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Expected output:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
Information for Consumer pwas-heavyworth-sync > pwas-workers
|
|
243
|
+
|
|
244
|
+
Configuration:
|
|
245
|
+
|
|
246
|
+
Durable Name: pwas-workers
|
|
247
|
+
Filter Subject: heavyworth.sync.pwas
|
|
248
|
+
Ack Policy: explicit
|
|
249
|
+
Ack Wait: 30s
|
|
250
|
+
Replay Policy: instant
|
|
251
|
+
Maximum Deliveries: 5
|
|
252
|
+
Backoff: [1s 5s 15s 30s 60s]
|
|
253
|
+
|
|
254
|
+
State:
|
|
255
|
+
|
|
256
|
+
Last Delivered Message: Consumer sequence: 0 Stream sequence: 0
|
|
257
|
+
Acknowledgment Floor: Consumer sequence: 0 Stream sequence: 0
|
|
258
|
+
Pending Messages: 0
|
|
259
|
+
Redelivered Messages: 0
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Step 2: Configure JetStream Bridge
|
|
265
|
+
|
|
266
|
+
Configure JetStream Bridge to avoid JetStream management APIs at runtime (pre-provisioning handles them instead):
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
# config/initializers/jetstream_bridge.rb
|
|
270
|
+
JetstreamBridge.configure do |config|
|
|
271
|
+
config.nats_urls = ENV.fetch("NATS_URLS")
|
|
272
|
+
config.stream_name = "pwas-heavyworth-sync"
|
|
273
|
+
config.app_name = "pwas"
|
|
274
|
+
config.destination_app = "heavyworth"
|
|
275
|
+
config.auto_provision = false
|
|
276
|
+
|
|
277
|
+
config.use_outbox = true
|
|
278
|
+
config.use_inbox = true
|
|
279
|
+
config.use_dlq = true
|
|
280
|
+
|
|
281
|
+
# These settings MUST match the pre-created consumer
|
|
282
|
+
config.max_deliver = 5
|
|
283
|
+
config.ack_wait = "30s"
|
|
284
|
+
config.backoff = %w[1s 5s 15s 30s 60s]
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**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.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Step 3: Deploy and Verify
|
|
293
|
+
|
|
294
|
+
### Deploy the Updated Gem
|
|
295
|
+
|
|
296
|
+
If you're working on the jetstream_bridge gem itself:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# Build the gem
|
|
300
|
+
gem build jetstream_bridge.gemspec
|
|
301
|
+
|
|
302
|
+
# Install locally for testing
|
|
303
|
+
gem install ./jetstream_bridge-4.5.0.gem
|
|
304
|
+
|
|
305
|
+
# Or update in your application's Gemfile.lock
|
|
306
|
+
bundle update jetstream_bridge
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
If this is a local modification, you can point your Gemfile to the local path:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
# Gemfile (temporary for testing)
|
|
313
|
+
gem 'jetstream_bridge', path: '/path/to/local/jetstream_bridge'
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Restart the Service
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
sudo systemctl restart pwas_production_sync
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Monitor the Logs
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
sudo journalctl -u pwas_production_sync -f
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Expected success output:**
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
INFO -- : [JetstreamBridge::ConnectionManager] Connected to NATS (1 server): nats://pwas:***@10.199.12.34:4222
|
|
332
|
+
INFO -- : [DataSync] Consumer starting (durable=pwas-workers, batch=25, dest="heavyworth")
|
|
333
|
+
INFO -- : [DataSync] run! started successfully
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Health checks will report connectivity only when `auto_provision=false` (stream info is skipped to avoid `$JS.API.STREAM.INFO`).
|
|
337
|
+
|
|
338
|
+
**If you still see errors:**
|
|
339
|
+
|
|
340
|
+
1. **Timeout during subscribe** - The pre-created consumer name/filter might not match. Verify:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
nats consumer ls pwas-heavyworth-sync
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
2. **Permission violations on fetch** - The NATS user also needs permission to fetch messages. Minimum required:
|
|
347
|
+
|
|
348
|
+
```conf
|
|
349
|
+
subscribe: {
|
|
350
|
+
allow: ["production.>", "_INBOX.>"]
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Maintenance
|
|
355
|
+
|
|
356
|
+
### Updating Consumer Configuration
|
|
357
|
+
|
|
358
|
+
If you need to change consumer settings (e.g., increase `max_deliver`):
|
|
359
|
+
|
|
360
|
+
1. **Stop your application** to prevent message processing
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
sudo systemctl stop pwas_production_sync
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
2. **Delete the old consumer** (using privileged account)
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
nats consumer rm pwas-heavyworth-sync pwas-workers -f
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
3. **Create the new consumer** with updated settings
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
nats consumer add pwas-heavyworth-sync pwas-workers \
|
|
376
|
+
--filter "heavyworth.sync.pwas" \
|
|
377
|
+
--ack explicit \
|
|
378
|
+
--pull \
|
|
379
|
+
--deliver all \
|
|
380
|
+
--max-deliver 10 \
|
|
381
|
+
--ack-wait 60s \
|
|
382
|
+
--backoff 2s,10s,30s,60s,120s \
|
|
383
|
+
--replay instant \
|
|
384
|
+
--max-pending 25000
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
4. **Update your application config** to match
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
config.max_deliver = 10
|
|
391
|
+
config.ack_wait = "60s"
|
|
392
|
+
config.backoff = %w[2s 10s 30s 60s 120s]
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
5. **Restart your application**
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
sudo systemctl start pwas_production_sync
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Monitoring
|
|
402
|
+
|
|
403
|
+
Add monitoring to detect configuration drift:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
# Check consumer exists and is healthy
|
|
407
|
+
health = JetstreamBridge.health_check
|
|
408
|
+
unless health[:healthy]
|
|
409
|
+
alert("JetStream consumer unhealthy: #{health}")
|
|
410
|
+
end
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Troubleshooting
|
|
416
|
+
|
|
417
|
+
### Consumer doesn't receive messages
|
|
418
|
+
|
|
419
|
+
**Check stream has messages:**
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
nats stream info pwas-heavyworth-sync
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Verify filter subject matches your topology:**
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
# App publishes to: {app_name}.sync.{destination_app}
|
|
429
|
+
# Consumer filters on: {destination_app}.sync.{app_name}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Service keeps restarting
|
|
433
|
+
|
|
434
|
+
Check if the issue is:
|
|
435
|
+
|
|
436
|
+
1. **Consumer doesn't exist** - Pre-create it with NATS CLI
|
|
437
|
+
2. **Filter subject mismatch** - Verify with `nats consumer info`
|
|
438
|
+
3. **Permissions still insufficient** - User needs subscribe permissions on the filtered subject
|
|
439
|
+
4. **Wrong stream name** - Ensure stream name matches your configured `config.stream_name`
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Security Considerations
|
|
444
|
+
|
|
445
|
+
1. **Consumer pre-creation requires privileged access** - Keep admin credentials secure
|
|
446
|
+
2. **Configuration drift risk** - If app config doesn't match consumer config, message delivery may fail silently
|
|
447
|
+
3. **No automatic recovery** - If consumer is deleted, it won't be recreated automatically
|
|
448
|
+
4. **Consider automation** - Use infrastructure-as-code (Terraform, Ansible) to manage consumer creation
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Example: Production Setup for pwas-api
|
|
453
|
+
|
|
454
|
+
Based on your logs, here's the exact setup:
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
# 1. Provision stream and both consumers (as admin)
|
|
458
|
+
nats stream add pwas-heavyworth-sync \
|
|
459
|
+
--subjects "pwas.sync.heavyworth" "heavyworth.sync.pwas" "pwas.sync.dlq" "heavyworth.sync.dlq" \
|
|
460
|
+
--retention workqueue \
|
|
461
|
+
--storage file
|
|
462
|
+
|
|
463
|
+
# Consumer for pwas (receives heavyworth -> pwas)
|
|
464
|
+
nats consumer add pwas-heavyworth-sync pwas-workers \
|
|
465
|
+
--filter "heavyworth.sync.pwas" \
|
|
466
|
+
--ack explicit \
|
|
467
|
+
--pull \
|
|
468
|
+
--deliver all \
|
|
469
|
+
--max-deliver 5 \
|
|
470
|
+
--ack-wait 30s \
|
|
471
|
+
--backoff 1s,5s,15s,30s,60s \
|
|
472
|
+
--replay instant \
|
|
473
|
+
--max-pending 25000
|
|
474
|
+
|
|
475
|
+
# Consumer for heavyworth (receives pwas -> heavyworth)
|
|
476
|
+
nats consumer add pwas-heavyworth-sync heavyworth-workers \
|
|
477
|
+
--filter "pwas.sync.heavyworth" \
|
|
478
|
+
--ack explicit \
|
|
479
|
+
--pull \
|
|
480
|
+
--deliver all \
|
|
481
|
+
--max-deliver 5 \
|
|
482
|
+
--ack-wait 30s \
|
|
483
|
+
--backoff 1s,5s,15s,30s,60s \
|
|
484
|
+
--replay instant \
|
|
485
|
+
--max-pending 25000
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
```ruby
|
|
489
|
+
# 2. Update config/initializers/jetstream_bridge.rb
|
|
490
|
+
JetstreamBridge.configure do |config|
|
|
491
|
+
config.nats_urls = ENV.fetch("NATS_URLS") # e.g., nats://pwas:***@10.199.12.34:4222
|
|
492
|
+
config.stream_name = "pwas-heavyworth-sync"
|
|
493
|
+
config.app_name = "pwas"
|
|
494
|
+
config.destination_app = "heavyworth"
|
|
495
|
+
config.max_deliver = 5
|
|
496
|
+
config.ack_wait = "30s"
|
|
497
|
+
config.backoff = %w[1s 5s 15s 30s 60s]
|
|
498
|
+
end
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**Note:** Verify your exact stream name and consumer durable (`app_name-workers`) match what was provisioned.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Alternative: Request Minimal Permissions
|
|
506
|
+
|
|
507
|
+
If you have any influence over NATS permissions, request only these minimal subjects:
|
|
508
|
+
|
|
509
|
+
```conf
|
|
510
|
+
# Minimal permissions needed for JetStream Bridge consumer
|
|
511
|
+
publish: {
|
|
512
|
+
allow: [
|
|
513
|
+
">", # Your app subjects (narrow if desired)
|
|
514
|
+
"$JS.API.CONSUMER.MSG.NEXT.{stream_name}.{app_name}-workers", # Fetch messages (required)
|
|
515
|
+
]
|
|
516
|
+
}
|
|
517
|
+
subscribe: {
|
|
518
|
+
allow: [
|
|
519
|
+
">", # Your app subjects (narrow if desired)
|
|
520
|
+
"_INBOX.>", # Request-reply responses (required)
|
|
521
|
+
]
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
These are read-only operations and don't allow creating/modifying streams or consumers.
|