jetstream_bridge 2.9.0 → 3.0.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/CHANGELOG.md +164 -0
- data/LICENSE +21 -0
- data/README.md +379 -0
- data/lib/generators/jetstream_bridge/health_check/health_check_generator.rb +65 -0
- data/lib/generators/jetstream_bridge/health_check/templates/health_controller.rb +38 -0
- data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +61 -13
- 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 +50 -9
- data/lib/jetstream_bridge/consumer/dlq_publisher.rb +4 -1
- data/lib/jetstream_bridge/consumer/inbox/inbox_message.rb +8 -2
- data/lib/jetstream_bridge/consumer/inbox/inbox_repository.rb +37 -61
- data/lib/jetstream_bridge/consumer/message_processor.rb +105 -33
- data/lib/jetstream_bridge/consumer/subscription_manager.rb +13 -2
- data/lib/jetstream_bridge/core/config.rb +37 -1
- data/lib/jetstream_bridge/core/connection.rb +80 -3
- data/lib/jetstream_bridge/core/connection_factory.rb +102 -0
- data/lib/jetstream_bridge/core/debug_helper.rb +107 -0
- data/lib/jetstream_bridge/core/duration.rb +8 -1
- data/lib/jetstream_bridge/core/logging.rb +20 -7
- data/lib/jetstream_bridge/core/model_utils.rb +4 -3
- data/lib/jetstream_bridge/core/retry_strategy.rb +135 -0
- data/lib/jetstream_bridge/errors.rb +39 -0
- data/lib/jetstream_bridge/inbox_event.rb +4 -4
- data/lib/jetstream_bridge/models/event_envelope.rb +133 -0
- data/lib/jetstream_bridge/models/subject.rb +94 -0
- data/lib/jetstream_bridge/outbox_event.rb +3 -1
- data/lib/jetstream_bridge/publisher/outbox_repository.rb +47 -28
- data/lib/jetstream_bridge/publisher/publisher.rb +12 -35
- data/lib/jetstream_bridge/railtie.rb +35 -1
- data/lib/jetstream_bridge/tasks/install.rake +99 -0
- data/lib/jetstream_bridge/topology/overlap_guard.rb +15 -1
- data/lib/jetstream_bridge/topology/stream.rb +16 -8
- 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 +63 -6
- metadata +51 -10
- 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: d47bc85ddad1450a71960670d94969ec5f6416dc98c07c8409e4d7243ec92f3e
|
|
4
|
+
data.tar.gz: b9130b48f7129d6c61c9f2cddc61a2e292c906c282925a3a550f79c24cc7eff4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 430fa83af6f8b1d9eb22fdf8edfbd2787c4a0609c6abf6626239a05132f1732bf9ecd8f0e6ad92cab7e53ffd234d0c3df5e626f06c332680a99b84849bdf7bc0
|
|
7
|
+
data.tar.gz: b17ccb8f7bf525aa5f21917c87932115a11ccb4058d79c81af741ee985203c97d326b3c7cd8960819c4a93f5d088a8d0b39e1f9bbef108e2619e7114e621f74a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [3.0.0] - 2025-11-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
#### Production-Ready Features
|
|
13
|
+
|
|
14
|
+
- **Health checks** - Comprehensive health check API via `JetstreamBridge.health_check`
|
|
15
|
+
- NATS connection status monitoring
|
|
16
|
+
- Stream existence and subject verification
|
|
17
|
+
- Configuration validation
|
|
18
|
+
- Returns structured health data for monitoring systems
|
|
19
|
+
- **Health check generator** - Rails generator for creating health check endpoints
|
|
20
|
+
- `rails g jetstream_bridge:health_check` creates controller and route
|
|
21
|
+
- Automatic route injection into `config/routes.rb`
|
|
22
|
+
- Returns appropriate HTTP status codes (200/503)
|
|
23
|
+
- **Auto-reconnection** - Automatic recovery from connection failures
|
|
24
|
+
- Exponential backoff retry strategy
|
|
25
|
+
- Configurable retry limits and delays
|
|
26
|
+
- Connection state tracking with timestamps
|
|
27
|
+
- **Connection factory** - Centralized connection management
|
|
28
|
+
- Singleton pattern for connection handling
|
|
29
|
+
- Thread-safe connection access
|
|
30
|
+
- Public `connected?` and `connected_at` accessors
|
|
31
|
+
|
|
32
|
+
#### Error Handling
|
|
33
|
+
|
|
34
|
+
- **Comprehensive error hierarchy** - Well-organized exception classes
|
|
35
|
+
- `ConfigurationError` - Base for configuration issues
|
|
36
|
+
- `ConnectionError` - Base for connection problems
|
|
37
|
+
- `PublishError` - Base for publishing failures
|
|
38
|
+
- `ConsumerError` - Base for consumption issues
|
|
39
|
+
- `TopologyError` - Base for stream/subject topology errors
|
|
40
|
+
- `DlqError` - Base for dead-letter queue operations
|
|
41
|
+
- All inherit from `JetstreamBridge::Error`
|
|
42
|
+
|
|
43
|
+
#### Developer Experience
|
|
44
|
+
|
|
45
|
+
- **Debug helper** - Comprehensive debugging utility via `JetstreamBridge::DebugHelper`
|
|
46
|
+
- Configuration dump
|
|
47
|
+
- Connection status
|
|
48
|
+
- Stream information
|
|
49
|
+
- Subject validation
|
|
50
|
+
- Model availability checks
|
|
51
|
+
- **Rake tasks** - CLI tools for operations
|
|
52
|
+
- `rake jetstream_bridge:health` - Check health and connection status
|
|
53
|
+
- `rake jetstream_bridge:validate` - Validate configuration
|
|
54
|
+
- `rake jetstream_bridge:test_connection` - Test NATS connection
|
|
55
|
+
- `rake jetstream_bridge:debug` - Show comprehensive debug information
|
|
56
|
+
- **Value objects** - Type-safe domain models
|
|
57
|
+
- `EventEnvelope` - Structured event representation
|
|
58
|
+
- `Subject` - Subject pattern validation and parsing
|
|
59
|
+
|
|
60
|
+
#### Testing & Quality
|
|
61
|
+
|
|
62
|
+
- **Comprehensive test suite** - 248 RSpec tests covering all core functionality
|
|
63
|
+
- Configuration validation specs
|
|
64
|
+
- Connection factory specs with edge cases (48 new tests added)
|
|
65
|
+
- Server URL normalization (single, comma-separated, arrays, whitespace)
|
|
66
|
+
- Authentication options (user, pass, token)
|
|
67
|
+
- NATS URL validation (nil, empty, whitespace-only)
|
|
68
|
+
- JetStream context creation and nc accessor
|
|
69
|
+
- Event envelope specs with full coverage (18 new tests added)
|
|
70
|
+
- Initialization edge cases (trace ID, Time objects, resource ID extraction)
|
|
71
|
+
- Hash serialization and deserialization
|
|
72
|
+
- Deep immutability validation
|
|
73
|
+
- Equality and hashing behavior
|
|
74
|
+
- Error handling for invalid timestamps
|
|
75
|
+
- Retry strategy specs
|
|
76
|
+
- Model specs
|
|
77
|
+
- Error hierarchy specs
|
|
78
|
+
- **Subject validation** - Prevents NATS wildcards in configuration
|
|
79
|
+
- Validates `env`, `app_name`, and `destination_app` don't contain `.`, `*`, or `>`
|
|
80
|
+
- Clear error messages for invalid subjects
|
|
81
|
+
- **Bug fixes in implementation** - Fixed `EventEnvelope.from_h` to properly handle deserialization
|
|
82
|
+
- Removed invalid `schema_version` parameter
|
|
83
|
+
- Added missing `resource_id` parameter
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
#### Architecture Improvements
|
|
88
|
+
|
|
89
|
+
- **Consolidated configuration** - Streamlined `Config` class
|
|
90
|
+
- Removed redundant configuration classes
|
|
91
|
+
- Centralized validation logic
|
|
92
|
+
- Better default values
|
|
93
|
+
- **Enhanced generator templates** - Improved initializer template
|
|
94
|
+
- Better documentation and comments
|
|
95
|
+
- Organized sections (Connection, Consumer, Reliability, Models, Logging)
|
|
96
|
+
- Clear indication of required vs optional settings
|
|
97
|
+
- **Simplified consumer initialization** - Cleaner API
|
|
98
|
+
- Sensible defaults for `durable_name` and `batch_size`
|
|
99
|
+
- Removed unnecessary configuration classes
|
|
100
|
+
- **Better logging** - Configurable logger with sensible defaults
|
|
101
|
+
- Respects `config.logger` setting
|
|
102
|
+
- Falls back to `Rails.logger` in Rails apps
|
|
103
|
+
- Uses `STDOUT` logger otherwise
|
|
104
|
+
|
|
105
|
+
#### Bug Fixes
|
|
106
|
+
|
|
107
|
+
- **Fixed duration parsing** - Correct integer conversion for duration strings
|
|
108
|
+
- Handles edge cases properly
|
|
109
|
+
- Added comprehensive tests
|
|
110
|
+
- **Fixed subject format** - Corrected DLQ subject pattern
|
|
111
|
+
- Changed from `{env}.data.sync.dlq` to `{env}.sync.dlq`
|
|
112
|
+
- Updated all documentation
|
|
113
|
+
- **Repository improvements**
|
|
114
|
+
- Transaction safety for all database operations
|
|
115
|
+
- Pessimistic locking for outbox operations
|
|
116
|
+
- Race condition protection
|
|
117
|
+
- Better error handling and logging
|
|
118
|
+
|
|
119
|
+
### Refactored
|
|
120
|
+
|
|
121
|
+
- **Consumer architecture** - Consolidated consumer support classes
|
|
122
|
+
- Merged related functionality
|
|
123
|
+
- Reduced file count
|
|
124
|
+
- Clearer separation of concerns
|
|
125
|
+
- **Model handling** - Streamlined inbox/outbox models
|
|
126
|
+
- Graceful column detection without connection boot
|
|
127
|
+
- Better validation guards
|
|
128
|
+
- Simplified attribute handling
|
|
129
|
+
- **JSON handling** - Switched to Oj for performance
|
|
130
|
+
- Faster JSON parsing/serialization
|
|
131
|
+
- Consistent JSON handling across the gem
|
|
132
|
+
- **Topology management** - Enhanced stream and subject handling
|
|
133
|
+
- Improved overlap detection
|
|
134
|
+
- Better error messages
|
|
135
|
+
- More robust subject matching
|
|
136
|
+
|
|
137
|
+
### Documentation
|
|
138
|
+
|
|
139
|
+
- **Updated README** - Comprehensive documentation updates
|
|
140
|
+
- Added section for Rails generators and rake tasks
|
|
141
|
+
- Documented all new health check features
|
|
142
|
+
- Fixed incorrect subject patterns
|
|
143
|
+
- Added operations guide
|
|
144
|
+
- Improved getting started section
|
|
145
|
+
- **Enhanced code comments** - Better inline documentation
|
|
146
|
+
- YARD-style documentation for public APIs
|
|
147
|
+
- Clear explanations of complex logic
|
|
148
|
+
- Usage examples in comments
|
|
149
|
+
- **Generator improvements** - Better user guidance
|
|
150
|
+
- Clear success messages
|
|
151
|
+
- Usage instructions after generation
|
|
152
|
+
- Example configurations
|
|
153
|
+
|
|
154
|
+
### Internal
|
|
155
|
+
|
|
156
|
+
- **Removed deprecated code**
|
|
157
|
+
- Cleaned up unused classes and methods
|
|
158
|
+
- Removed `BackoffStrategy` (consolidated into retry strategy)
|
|
159
|
+
- Removed `ConsumerConfig` (merged into main config)
|
|
160
|
+
- Removed `MessageContext` (functionality integrated)
|
|
161
|
+
|
|
162
|
+
## [2.10.0] and earlier
|
|
163
|
+
|
|
164
|
+
See git history for changes in earlier versions.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mike Attara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.svg" alt="JetStream Bridge Logo" width="200"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">JetStream Bridge</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Production-safe realtime data bridge</strong> between systems using <strong>NATS JetStream</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
Includes durable consumers, backpressure, retries, <strong>DLQ</strong>, optional <strong>Inbox/Outbox</strong>, and <strong>overlap-safe stream provisioning</strong>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="#-features">Features</a> •
|
|
17
|
+
<a href="#-install">Install</a> •
|
|
18
|
+
<a href="#-getting-started">Getting Started</a> •
|
|
19
|
+
<a href="#-operations-guide">Operations</a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✨ Features
|
|
25
|
+
|
|
26
|
+
### Core Capabilities
|
|
27
|
+
|
|
28
|
+
* 🔌 Simple **Publisher** and **Consumer** interfaces
|
|
29
|
+
* 🛡 **Outbox** (reliable send) & **Inbox** (idempotent receive), opt-in
|
|
30
|
+
* 🧨 **DLQ** for poison messages
|
|
31
|
+
* ⚙️ Durable `pull_subscribe` with backoff & `max_deliver`
|
|
32
|
+
* 🎯 Clear **source/destination** subject conventions
|
|
33
|
+
* 🧱 **Overlap-safe stream ensure** (prevents "subjects overlap" BadRequest)
|
|
34
|
+
* 🚂 **Rails generators** for initializer & migrations, plus an install **rake task**
|
|
35
|
+
* ⚡️ **Eager-loaded models** via Railtie (production)
|
|
36
|
+
* 📊 Configurable logging with sensible defaults
|
|
37
|
+
|
|
38
|
+
### Production-Ready Features (v2.10+)
|
|
39
|
+
|
|
40
|
+
* 🏥 **Health checks** - Monitor NATS connection and stream status
|
|
41
|
+
* 🔄 **Auto-reconnection** - Automatic recovery from connection failures
|
|
42
|
+
* 🔒 **Race condition protection** - Pessimistic locking for outbox operations
|
|
43
|
+
* 🛡️ **Transaction safety** - All database operations wrapped in transactions
|
|
44
|
+
* 🎯 **Subject validation** - Prevents NATS wildcards in configuration
|
|
45
|
+
* 🚦 **Graceful shutdown** - Signal handlers and message draining
|
|
46
|
+
* 📈 **Retry strategies** - Pluggable exponential/linear backoff algorithms
|
|
47
|
+
* 🎨 **Value objects** - Type-safe domain models for events and subjects
|
|
48
|
+
* 🏗️ **SOLID architecture** - Clean separation of concerns and dependency injection
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 📦 Install
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# Gemfile
|
|
56
|
+
gem "jetstream_bridge", "~> 2.10"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bundle install
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 🧰 Rails Generators & Rake Tasks
|
|
66
|
+
|
|
67
|
+
### Installation
|
|
68
|
+
|
|
69
|
+
From your Rails app:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Create initializer + migrations
|
|
73
|
+
bin/rails g jetstream_bridge:install
|
|
74
|
+
|
|
75
|
+
# Or run them separately:
|
|
76
|
+
bin/rails g jetstream_bridge:initializer
|
|
77
|
+
bin/rails g jetstream_bridge:migrations
|
|
78
|
+
|
|
79
|
+
# Create health check endpoint
|
|
80
|
+
bin/rails g jetstream_bridge:health_check
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
bin/rails db:migrate
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
> The generators create:
|
|
90
|
+
>
|
|
91
|
+
> * `config/initializers/jetstream_bridge.rb`
|
|
92
|
+
> * `db/migrate/*_create_jetstream_outbox_events.rb`
|
|
93
|
+
> * `db/migrate/*_create_jetstream_inbox_events.rb`
|
|
94
|
+
> * `app/controllers/jetstream_health_controller.rb` (if health_check generator used)
|
|
95
|
+
|
|
96
|
+
### Rake Tasks
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Check health and connection status
|
|
100
|
+
bin/rake jetstream_bridge:health
|
|
101
|
+
|
|
102
|
+
# Validate configuration
|
|
103
|
+
bin/rake jetstream_bridge:validate
|
|
104
|
+
|
|
105
|
+
# Test NATS connection
|
|
106
|
+
bin/rake jetstream_bridge:test_connection
|
|
107
|
+
|
|
108
|
+
# Show comprehensive debug information
|
|
109
|
+
bin/rake jetstream_bridge:debug
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 🔧 Configure (Rails)
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# config/initializers/jetstream_bridge.rb
|
|
118
|
+
JetstreamBridge.configure do |config|
|
|
119
|
+
# NATS connection
|
|
120
|
+
config.nats_urls = ENV.fetch("NATS_URLS", "nats://localhost:4222")
|
|
121
|
+
config.env = ENV.fetch("NATS_ENV", "development")
|
|
122
|
+
config.app_name = ENV.fetch("APP_NAME", "app")
|
|
123
|
+
config.destination_app = ENV["DESTINATION_APP"] # required
|
|
124
|
+
|
|
125
|
+
# Consumer tuning
|
|
126
|
+
config.max_deliver = 5
|
|
127
|
+
config.ack_wait = "30s"
|
|
128
|
+
config.backoff = %w[1s 5s 15s 30s 60s]
|
|
129
|
+
|
|
130
|
+
# Reliability features (opt-in)
|
|
131
|
+
config.use_outbox = true
|
|
132
|
+
config.use_inbox = true
|
|
133
|
+
config.use_dlq = true
|
|
134
|
+
|
|
135
|
+
# Models (override if you use custom AR classes/table names)
|
|
136
|
+
config.outbox_model = "JetstreamBridge::OutboxEvent"
|
|
137
|
+
config.inbox_model = "JetstreamBridge::InboxEvent"
|
|
138
|
+
|
|
139
|
+
# Logging
|
|
140
|
+
# config.logger = Rails.logger
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
> **Defaults:**
|
|
145
|
+
>
|
|
146
|
+
> * `stream_name` → `#{env}-jetstream-bridge-stream`
|
|
147
|
+
> * `dlq_subject` → `#{env}.sync.dlq`
|
|
148
|
+
|
|
149
|
+
### Logging
|
|
150
|
+
|
|
151
|
+
JetstreamBridge logs through `config.logger` when set, falling back to `Rails.logger` or STDOUT. Provide any `Logger`-compatible instance in the initializer to integrate with your application's logging setup.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 📡 Subject Conventions
|
|
156
|
+
|
|
157
|
+
| Direction | Subject Pattern |
|
|
158
|
+
|---------------|---------------------------|
|
|
159
|
+
| **Publish** | `{env}.{app}.sync.{dest}` |
|
|
160
|
+
| **Subscribe** | `{env}.{dest}.sync.{app}` |
|
|
161
|
+
| **DLQ** | `{env}.sync.dlq` |
|
|
162
|
+
|
|
163
|
+
* `{app}`: `app_name`
|
|
164
|
+
* `{dest}`: `destination_app`
|
|
165
|
+
* `{env}`: `env`
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 🧱 Stream Topology (auto-ensure and overlap-safe)
|
|
170
|
+
|
|
171
|
+
On first connection, Jetstream Bridge **ensures** a single stream exists for your `env` and that it covers:
|
|
172
|
+
|
|
173
|
+
* `source_subject` (`{env}.{app}.sync.{dest}`)
|
|
174
|
+
* `destination_subject` (`{env}.{dest}.sync.{app}`)
|
|
175
|
+
* `dlq_subject` (if enabled)
|
|
176
|
+
|
|
177
|
+
It’s **overlap-safe**:
|
|
178
|
+
|
|
179
|
+
* Skips adding subjects already covered by existing wildcards
|
|
180
|
+
* Pre-filters subjects owned by other streams to avoid `BadRequest: subjects overlap with an existing stream`
|
|
181
|
+
* Retries once on concurrent races, then logs and continues safely
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 🗃 Database Setup (Inbox / Outbox)
|
|
186
|
+
|
|
187
|
+
Inbox/Outbox are **optional**. The library detects columns at runtime and only sets what exists, so you can start minimal and evolve later.
|
|
188
|
+
|
|
189
|
+
### Generator-created tables (recommended)
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# jetstream_outbox_events
|
|
193
|
+
create_table :jetstream_outbox_events do |t|
|
|
194
|
+
t.string :event_id, null: false
|
|
195
|
+
t.string :subject, null: false
|
|
196
|
+
t.jsonb :payload, null: false, default: {}
|
|
197
|
+
t.jsonb :headers, null: false, default: {}
|
|
198
|
+
t.string :status, null: false, default: "pending" # pending|publishing|sent|failed
|
|
199
|
+
t.integer :attempts, null: false, default: 0
|
|
200
|
+
t.text :last_error
|
|
201
|
+
t.datetime :enqueued_at
|
|
202
|
+
t.datetime :sent_at
|
|
203
|
+
t.timestamps
|
|
204
|
+
end
|
|
205
|
+
add_index :jetstream_outbox_events, :event_id, unique: true
|
|
206
|
+
add_index :jetstream_outbox_events, :status
|
|
207
|
+
|
|
208
|
+
# jetstream_inbox_events
|
|
209
|
+
create_table :jetstream_inbox_events do |t|
|
|
210
|
+
t.string :event_id # preferred dedupe key
|
|
211
|
+
t.string :subject, null: false
|
|
212
|
+
t.jsonb :payload, null: false, default: {}
|
|
213
|
+
t.jsonb :headers, null: false, default: {}
|
|
214
|
+
t.string :stream
|
|
215
|
+
t.bigint :stream_seq
|
|
216
|
+
t.integer :deliveries
|
|
217
|
+
t.string :status, null: false, default: "received" # received|processing|processed|failed
|
|
218
|
+
t.text :last_error
|
|
219
|
+
t.datetime :received_at
|
|
220
|
+
t.datetime :processed_at
|
|
221
|
+
t.timestamps
|
|
222
|
+
end
|
|
223
|
+
add_index :jetstream_inbox_events, :event_id, unique: true, where: 'event_id IS NOT NULL'
|
|
224
|
+
add_index :jetstream_inbox_events, [:stream, :stream_seq], unique: true, where: 'stream IS NOT NULL AND stream_seq IS NOT NULL'
|
|
225
|
+
add_index :jetstream_inbox_events, :status
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
> Already have different table names? Point the config to your AR classes via `config.outbox_model` / `config.inbox_model`.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 📤 Publish Events
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
publisher = JetstreamBridge::Publisher.new
|
|
236
|
+
publisher.publish(
|
|
237
|
+
resource_type: "user",
|
|
238
|
+
event_type: "created",
|
|
239
|
+
payload: { id: "01H...", name: "Ada" }, # resource_id inferred from payload[:id] / payload["id"]
|
|
240
|
+
# optional:
|
|
241
|
+
# event_id: "uuid-or-ulid",
|
|
242
|
+
# trace_id: "hex",
|
|
243
|
+
# occurred_at: Time.now.utc
|
|
244
|
+
)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
If **Outbox** is enabled, the publish call:
|
|
248
|
+
|
|
249
|
+
* Upserts an outbox row by `event_id`
|
|
250
|
+
* Publishes with `nats-msg-id` (idempotent)
|
|
251
|
+
* Marks status `sent` or records `failed` with `last_error`
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 📥 Consume Events
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
JetstreamBridge::Consumer.new do |event, subject, deliveries|
|
|
259
|
+
# Your idempotent domain logic here
|
|
260
|
+
# `event` is the parsed envelope hash
|
|
261
|
+
UserCreatedHandler.call(event["payload"])
|
|
262
|
+
end.run!
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`durable_name` and `batch_size` default to the configured values and can be
|
|
266
|
+
overridden if needed:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
JetstreamBridge::Consumer.new(durable_name: 'my-durable', batch_size: 10) do |event, subject, deliveries|
|
|
270
|
+
# ...
|
|
271
|
+
end.run!
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
If **Inbox** is enabled, the consumer:
|
|
275
|
+
|
|
276
|
+
* Dedupes by `event_id` (falls back to stream sequence if needed)
|
|
277
|
+
* Records processing state, errors, and timestamps
|
|
278
|
+
* Skips already-processed messages (acks immediately)
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 📬 Envelope Format
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{
|
|
286
|
+
"event_id": "01H1234567890ABCDEF",
|
|
287
|
+
"schema_version": 1,
|
|
288
|
+
"event_type": "created",
|
|
289
|
+
"producer": "myapp",
|
|
290
|
+
"resource_type": "user",
|
|
291
|
+
"resource_id": "01H1234567890ABCDEF",
|
|
292
|
+
"occurred_at": "2025-08-13T21:00:00Z",
|
|
293
|
+
"trace_id": "abc123",
|
|
294
|
+
"payload": { "id": "01H...", "name": "Ada" }
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
* `resource_id` is inferred from `payload.id` when publishing.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## 🧨 Dead-Letter Queue (DLQ)
|
|
303
|
+
|
|
304
|
+
When enabled, the topology ensures the DLQ subject exists:
|
|
305
|
+
**`{env}.sync.dlq`**
|
|
306
|
+
|
|
307
|
+
You may run a separate process to subscribe and triage messages that exceed `max_deliver` or are NAK'ed to the DLQ.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## 🛠 Operations Guide
|
|
312
|
+
|
|
313
|
+
### Monitoring
|
|
314
|
+
|
|
315
|
+
* **Consumer lag**: `nats consumer info <stream> <durable>`
|
|
316
|
+
* **DLQ volume**: subscribe/metrics on `{env}.sync.dlq`
|
|
317
|
+
* **Outbox backlog**: alert on `jetstream_outbox_events` with `status != 'sent'` and growing count
|
|
318
|
+
|
|
319
|
+
### Scaling
|
|
320
|
+
|
|
321
|
+
* Run consumers in **separate processes/containers**
|
|
322
|
+
* Scale consumers independently of web
|
|
323
|
+
* Tune `batch_size`, `ack_wait`, `max_deliver`, and `backoff`
|
|
324
|
+
|
|
325
|
+
### Health Checks
|
|
326
|
+
|
|
327
|
+
The gem provides built-in health check functionality for monitoring:
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
# Get comprehensive health status
|
|
331
|
+
health = JetstreamBridge.health_check
|
|
332
|
+
# => {
|
|
333
|
+
# healthy: true,
|
|
334
|
+
# nats_connected: true,
|
|
335
|
+
# connected_at: "2025-11-22T20:00:00Z",
|
|
336
|
+
# stream: { exists: true, name: "...", ... },
|
|
337
|
+
# config: { env: "production", ... },
|
|
338
|
+
# version: "2.10.0"
|
|
339
|
+
# }
|
|
340
|
+
|
|
341
|
+
# Force-connect & ensure topology at boot or in a check
|
|
342
|
+
JetstreamBridge.ensure_topology!
|
|
343
|
+
|
|
344
|
+
# Debug helper for troubleshooting
|
|
345
|
+
JetstreamBridge::DebugHelper.debug_info
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### When to Use
|
|
349
|
+
|
|
350
|
+
* **Inbox**: you need idempotent processing and replay safety
|
|
351
|
+
* **Outbox**: you want “DB commit ⇒ event published (or recorded for retry)” guarantees
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 🧩 Troubleshooting
|
|
356
|
+
|
|
357
|
+
* **`subjects overlap with an existing stream`**
|
|
358
|
+
The library pre-filters overlapping subjects and retries once. If another team owns a broad wildcard (e.g., `env.data.sync.>`), coordinate subject boundaries.
|
|
359
|
+
|
|
360
|
+
* **Consumer exists with mismatched filter**
|
|
361
|
+
The library detects and recreates the durable with the desired filter subject.
|
|
362
|
+
|
|
363
|
+
* **Repeated redeliveries**
|
|
364
|
+
Increase `ack_wait`, review handler acks/NACKs, or move poison messages to DLQ.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## 🚀 Getting Started
|
|
369
|
+
|
|
370
|
+
1. Add the gem & run `bundle install`
|
|
371
|
+
2. `bin/rails g jetstream_bridge:install`
|
|
372
|
+
3. `bin/rails db:migrate`
|
|
373
|
+
4. Start publishing/consuming!
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## 📄 License
|
|
378
|
+
|
|
379
|
+
[MIT License](LICENSE)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module JetstreamBridge
|
|
6
|
+
module Generators
|
|
7
|
+
class HealthCheckGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
|
9
|
+
desc 'Creates a health check endpoint for JetStream Bridge monitoring'
|
|
10
|
+
|
|
11
|
+
def create_controller
|
|
12
|
+
template 'health_controller.rb', 'app/controllers/jetstream_health_controller.rb'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_route
|
|
16
|
+
route_content = " # JetStream Bridge health check endpoint\n" \
|
|
17
|
+
" get '/health/jetstream', to: 'jetstream_health#show'"
|
|
18
|
+
|
|
19
|
+
if File.exist?('config/routes.rb')
|
|
20
|
+
inject_into_file 'config/routes.rb', after: /Rails\.application\.routes\.draw do\n/ do
|
|
21
|
+
"#{route_content}\n"
|
|
22
|
+
end
|
|
23
|
+
say 'Added health check route to config/routes.rb', :green
|
|
24
|
+
else
|
|
25
|
+
say 'Could not find config/routes.rb - please add route manually:', :yellow
|
|
26
|
+
say route_content, :yellow
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def show_usage
|
|
31
|
+
say "\n" + '=' * 70, :green
|
|
32
|
+
say 'Health Check Endpoint Created!', :green
|
|
33
|
+
say '=' * 70, :green
|
|
34
|
+
say "\nThe health check endpoint is now available at:"
|
|
35
|
+
say " GET /health/jetstream", :cyan
|
|
36
|
+
say "\nExample response:"
|
|
37
|
+
say <<~EXAMPLE, :white
|
|
38
|
+
{
|
|
39
|
+
"healthy": true,
|
|
40
|
+
"nats_connected": true,
|
|
41
|
+
"connected_at": "2025-11-22T20:00:00Z",
|
|
42
|
+
"stream": {
|
|
43
|
+
"exists": true,
|
|
44
|
+
"name": "development-jetstream-bridge-stream",
|
|
45
|
+
"subjects": ["dev.app1.sync.app2"],
|
|
46
|
+
"messages": 42
|
|
47
|
+
},
|
|
48
|
+
"config": {
|
|
49
|
+
"env": "development",
|
|
50
|
+
"app_name": "my_app",
|
|
51
|
+
"destination_app": "other_app"
|
|
52
|
+
},
|
|
53
|
+
"version": "2.10.0"
|
|
54
|
+
}
|
|
55
|
+
EXAMPLE
|
|
56
|
+
say "\nUse this endpoint for:"
|
|
57
|
+
say " • Kubernetes liveness/readiness probes", :white
|
|
58
|
+
say " • Docker health checks", :white
|
|
59
|
+
say " • Monitoring and alerting", :white
|
|
60
|
+
say " • Load balancer health checks", :white
|
|
61
|
+
say '=' * 70, :green
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Health check controller for JetStream Bridge monitoring
|
|
4
|
+
#
|
|
5
|
+
# This controller provides a health check endpoint for monitoring the JetStream
|
|
6
|
+
# connection status. Use it for Kubernetes liveness/readiness probes, Docker
|
|
7
|
+
# health checks, or load balancer health checks.
|
|
8
|
+
#
|
|
9
|
+
# Example usage:
|
|
10
|
+
# GET /health/jetstream
|
|
11
|
+
#
|
|
12
|
+
# Returns:
|
|
13
|
+
# 200 OK - when healthy
|
|
14
|
+
# 503 Service Unavailable - when unhealthy
|
|
15
|
+
class JetstreamHealthController < ActionController::API
|
|
16
|
+
# GET /health/jetstream
|
|
17
|
+
#
|
|
18
|
+
# Returns comprehensive health status including:
|
|
19
|
+
# - NATS connection status
|
|
20
|
+
# - JetStream stream information
|
|
21
|
+
# - Configuration details
|
|
22
|
+
# - Gem version
|
|
23
|
+
def show
|
|
24
|
+
health = JetstreamBridge.health_check
|
|
25
|
+
|
|
26
|
+
if health[:healthy]
|
|
27
|
+
render json: health, status: :ok
|
|
28
|
+
else
|
|
29
|
+
render json: health, status: :service_unavailable
|
|
30
|
+
end
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
# Ensure we always return a valid JSON response
|
|
33
|
+
render json: {
|
|
34
|
+
healthy: false,
|
|
35
|
+
error: "#{e.class}: #{e.message}"
|
|
36
|
+
}, status: :service_unavailable
|
|
37
|
+
end
|
|
38
|
+
end
|