jetstream_bridge 4.4.1 → 4.5.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -337
  3. data/README.md +1 -5
  4. data/docs/GETTING_STARTED.md +11 -7
  5. data/docs/PRODUCTION.md +51 -11
  6. data/docs/TESTING.md +24 -35
  7. data/lib/generators/jetstream_bridge/health_check/health_check_generator.rb +1 -1
  8. data/lib/generators/jetstream_bridge/initializer/initializer_generator.rb +1 -1
  9. data/lib/generators/jetstream_bridge/initializer/templates/jetstream_bridge.rb +0 -4
  10. data/lib/generators/jetstream_bridge/install/install_generator.rb +5 -5
  11. data/lib/generators/jetstream_bridge/migrations/migrations_generator.rb +2 -2
  12. data/lib/jetstream_bridge/consumer/consumer.rb +34 -96
  13. data/lib/jetstream_bridge/consumer/health_monitor.rb +107 -0
  14. data/lib/jetstream_bridge/consumer/message_processor.rb +1 -1
  15. data/lib/jetstream_bridge/consumer/subscription_manager.rb +51 -34
  16. data/lib/jetstream_bridge/core/config.rb +153 -46
  17. data/lib/jetstream_bridge/core/connection_manager.rb +513 -0
  18. data/lib/jetstream_bridge/core/debug_helper.rb +9 -3
  19. data/lib/jetstream_bridge/core/health_checker.rb +184 -0
  20. data/lib/jetstream_bridge/core.rb +0 -2
  21. data/lib/jetstream_bridge/facade.rb +212 -0
  22. data/lib/jetstream_bridge/publisher/event_envelope_builder.rb +110 -0
  23. data/lib/jetstream_bridge/publisher/publisher.rb +87 -117
  24. data/lib/jetstream_bridge/rails/integration.rb +8 -5
  25. data/lib/jetstream_bridge/rails/railtie.rb +4 -3
  26. data/lib/jetstream_bridge/tasks/install.rake +0 -1
  27. data/lib/jetstream_bridge/topology/topology.rb +6 -1
  28. data/lib/jetstream_bridge/version.rb +1 -1
  29. data/lib/jetstream_bridge.rb +206 -297
  30. metadata +7 -5
  31. data/lib/jetstream_bridge/core/bridge_helpers.rb +0 -109
  32. data/lib/jetstream_bridge/core/connection.rb +0 -464
  33. data/lib/jetstream_bridge/core/connection_factory.rb +0 -100
data/docs/PRODUCTION.md CHANGED
@@ -32,22 +32,26 @@ production:
32
32
  ### Sizing Guidelines
33
33
 
34
34
  **Publishers (Web/API processes):**
35
+
35
36
  - 1-2 connections per process (uses existing AR pool)
36
37
  - Example: 4 Puma workers × 5 threads = 20 connections minimum
37
38
 
38
39
  **Consumers:**
40
+
39
41
  - Dedicated connections per consumer process
40
42
  - Recommended: 2-5 connections per consumer
41
43
  - Example: 3 consumer processes = 6-15 connections
42
44
 
43
45
  **Total Formula:**
44
- ```
46
+
47
+ ```markdown
45
48
  Total Connections = (Web Workers × Threads) + (Consumers × 3) + 10 buffer
46
49
  ```
47
50
 
48
51
  ### Example Calculation
49
52
 
50
53
  For a typical production setup:
54
+
51
55
  - 4 Puma workers × 5 threads = 20 connections
52
56
  - 3 consumer processes × 3 connections = 9 connections
53
57
  - 10 connection buffer = 10 connections
@@ -74,9 +78,9 @@ JetstreamBridge.configure do |config|
74
78
  config.connect_retry_delay = 3 # Default: 2 seconds
75
79
 
76
80
  # Required configuration
77
- config.env = ENV.fetch("RAILS_ENV", "production")
78
81
  config.app_name = ENV.fetch("APP_NAME", "myapp")
79
82
  config.destination_app = ENV.fetch("DESTINATION_APP")
83
+ config.stream_name = ENV.fetch("STREAM_NAME", "myapp-stream")
80
84
 
81
85
  # Enable reliability features
82
86
  config.use_outbox = true
@@ -96,6 +100,31 @@ JetstreamBridge.configure do |config|
96
100
  end
97
101
  ```
98
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
+
99
128
  ---
100
129
 
101
130
  ## Consumer Tuning
@@ -135,9 +164,10 @@ end
135
164
  ### Memory Management
136
165
 
137
166
  Long-running consumers automatically:
167
+
138
168
  - Log health checks every 10 minutes (iterations, memory, uptime)
139
169
  - Warn when memory exceeds 1GB
140
- - Suggest garbage collection when heap grows large
170
+ - Warn once when heap object counts grow large so you can profile/trigger GC in the host app
141
171
 
142
172
  Monitor these logs to detect memory leaks early.
143
173
 
@@ -148,7 +178,7 @@ Monitor these logs to detect memory leaks early.
148
178
  ### Key Metrics to Track
149
179
 
150
180
  | Metric | Description | Alert Threshold |
151
- |--------|-------------|-----------------|
181
+ | -------- | ------------- | ----------------- |
152
182
  | Consumer Lag | Pending messages in stream | > 1000 messages |
153
183
  | DLQ Size | Messages in dead letter queue | > 100 messages |
154
184
  | Connection Status | Health check failures | 2 consecutive failures |
@@ -164,7 +194,7 @@ Use the built-in health check for monitoring:
164
194
  # config/routes.rb
165
195
  Rails.application.routes.draw do
166
196
  get '/health/jetstream', to: proc { |env|
167
- health = JetstreamBridge.health_check
197
+ health = JetstreamBridge.health
168
198
  status = health[:healthy] ? 200 : 503
169
199
  [status, { 'Content-Type' => 'application/json' }, [health.to_json]]
170
200
  }
@@ -197,9 +227,10 @@ end
197
227
  "destination_app": "worker",
198
228
  "use_outbox": true,
199
229
  "use_inbox": true,
200
- "use_dlq": true
230
+ "use_dlq": true,
231
+ "disable_js_api": true
201
232
  },
202
- "version": "4.0.3"
233
+ "version": "4.4.0"
203
234
  }
204
235
  ```
205
236
 
@@ -253,6 +284,7 @@ end
253
284
  ### Subject Validation
254
285
 
255
286
  JetStream Bridge validates subject components to prevent injection attacks. The following are automatically rejected:
287
+
256
288
  - NATS wildcards (`.`, `*`, `>`)
257
289
  - Spaces and control characters
258
290
  - Components exceeding 255 characters
@@ -270,6 +302,7 @@ config.nats_urls = ENV.fetch("NATS_URLS")
270
302
  ```
271
303
 
272
304
  Credentials in logs are automatically sanitized:
305
+
273
306
  - `nats://user:pass@host:4222` → `nats://user:***@host:4222`
274
307
  - `nats://token@host:4222` → `nats://***@host:4222`
275
308
 
@@ -345,6 +378,7 @@ spec:
345
378
  ### Health Probes
346
379
 
347
380
  **Liveness Probe:** Checks if the consumer process is running
381
+
348
382
  ```yaml
349
383
  livenessProbe:
350
384
  exec:
@@ -354,6 +388,7 @@ livenessProbe:
354
388
  ```
355
389
 
356
390
  **Readiness Probe:** Checks if NATS connection is healthy
391
+
357
392
  ```yaml
358
393
  readinessProbe:
359
394
  httpGet:
@@ -413,7 +448,7 @@ CREATE INDEX idx_inbox_stream_seq ON jetstream_inbox_events(stream, stream_seq);
413
448
  CREATE INDEX idx_inbox_status ON jetstream_inbox_events(status);
414
449
  ```
415
450
 
416
- 2. **Partition large tables** (for high-volume applications):
451
+ 1. **Partition large tables** (for high-volume applications):
417
452
 
418
453
  ```sql
419
454
  -- Partition outbox by month
@@ -426,7 +461,7 @@ CREATE TABLE jetstream_outbox_events_2025_11
426
461
  FOR VALUES FROM ('2025-11-01') TO ('2025-12-01');
427
462
  ```
428
463
 
429
- 3. **Archive old records** to prevent table bloat:
464
+ 1. **Archive old records** to prevent table bloat:
430
465
 
431
466
  ```ruby
432
467
  # lib/tasks/jetstream_maintenance.rake
@@ -462,24 +497,28 @@ end
462
497
  ### Common Issues
463
498
 
464
499
  **High Consumer Lag:**
500
+
465
501
  - Scale up consumer instances
466
502
  - Increase batch size
467
503
  - Optimize handler processing time
468
504
  - Check database connection pool
469
505
 
470
506
  **Memory Leaks:**
507
+
471
508
  - Monitor consumer health logs
472
509
  - Enable memory profiling
473
510
  - Check for circular references in handlers
474
511
  - Restart consumers periodically (Kubernetes handles this)
475
512
 
476
513
  **Connection Issues:**
514
+
477
515
  - Verify NATS server is accessible
478
516
  - Check firewall rules
479
517
  - Validate TLS certificates
480
518
  - Review connection retry settings
481
519
 
482
520
  **DLQ Growing:**
521
+
483
522
  - Investigate failed message patterns
484
523
  - Fix bugs in message handlers
485
524
  - Increase max_deliver for transient errors
@@ -499,5 +538,6 @@ end
499
538
  ## Support
500
539
 
501
540
  For issues or questions:
502
- - GitHub Issues: https://github.com/attaradev/jetstream_bridge/issues
503
- - Documentation: https://github.com/attaradev/jetstream_bridge
541
+
542
+ - GitHub Issues: <https://github.com/attaradev/jetstream_bridge/issues>
543
+ - Documentation: <https://github.com/attaradev/jetstream_bridge>
data/docs/TESTING.md CHANGED
@@ -28,31 +28,28 @@ RSpec.describe MyService do
28
28
  include JetstreamBridge::TestHelpers::Matchers
29
29
 
30
30
  before do
31
- # Reset singleton to ensure clean state
32
- JetstreamBridge::Connection.instance_variable_set(:@singleton__instance__, nil)
31
+ # Reset state
33
32
  JetstreamBridge.reset!
34
33
 
35
34
  # Enable test mode - automatically sets up mock NATS
36
35
  JetstreamBridge::TestHelpers.enable_test_mode!
37
36
 
38
37
  JetstreamBridge.configure do |config|
39
- config.env = 'test'
40
38
  config.app_name = 'my_app'
41
39
  config.destination_app = 'worker'
40
+ config.stream_name = 'test-stream'
42
41
  end
43
42
 
44
- # Setup mock stream and stub topology
43
+ # Setup mock stream
45
44
  mock_jts = JetstreamBridge::TestHelpers.mock_connection.jetstream
46
45
  mock_jts.add_stream(
47
- name: 'test-jetstream-bridge-stream',
48
- subjects: ['test.>']
46
+ name: 'test-stream',
47
+ subjects: ['my_app.sync.worker', 'worker.sync.my_app']
49
48
  )
50
- allow(JetstreamBridge::Topology).to receive(:ensure!)
51
49
  end
52
50
 
53
51
  after do
54
52
  JetstreamBridge::TestHelpers.reset_test_mode!
55
- JetstreamBridge::Connection.instance_variable_set(:@singleton__instance__, nil)
56
53
  end
57
54
 
58
55
  it 'publishes events through the full stack' do
@@ -266,27 +263,20 @@ end.to raise_error(NATS::JetStream::Error, 'consumer not found')
266
263
  ```ruby
267
264
  before do
268
265
  JetstreamBridge.reset!
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!)
266
+ JetstreamBridge::TestHelpers.enable_test_mode!
284
267
 
285
268
  JetstreamBridge.configure do |config|
286
- config.env = 'test'
287
269
  config.app_name = 'api'
288
270
  config.destination_app = 'worker'
271
+ config.stream_name = 'test-stream'
289
272
  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
+ )
290
280
  end
291
281
 
292
282
  it 'publishes through JetstreamBridge' do
@@ -296,9 +286,9 @@ it 'publishes through JetstreamBridge' do
296
286
  payload: { id: 1, name: 'Test' }
297
287
  )
298
288
 
299
- expect(result).to be_publish_success
289
+ expect(result.success?).to be true
300
290
  expect(result.event_id).to be_present
301
- expect(result.subject).to eq('test.api.sync.worker')
291
+ expect(result.subject).to eq('api.sync.worker')
302
292
 
303
293
  # Verify in storage
304
294
  storage = JetstreamBridge::TestHelpers.mock_storage
@@ -306,16 +296,15 @@ it 'publishes through JetstreamBridge' do
306
296
  end
307
297
  ```
308
298
 
309
- ### Full Consuming Flow
299
+ ### Full Consumer Flow
310
300
 
311
301
  ```ruby
312
- it 'consumes through JetstreamBridge' do
313
- mock_conn = JetstreamBridge::TestHelpers.mock_connection
314
- mock_jts = mock_conn.jetstream
302
+ it 'consumes messages through JetstreamBridge' do
303
+ mock_jts = JetstreamBridge::TestHelpers.mock_connection.jetstream
315
304
 
316
305
  # Publish message to destination subject
317
306
  mock_jts.publish(
318
- 'test.worker.sync.api',
307
+ 'worker.sync.api',
319
308
  Oj.dump({
320
309
  'event_id' => 'event-1',
321
310
  'schema_version' => 1,
@@ -338,9 +327,9 @@ it 'consumes through JetstreamBridge' do
338
327
 
339
328
  # Mock subscription
340
329
  subscription = mock_jts.pull_subscribe(
341
- 'test.worker.sync.api',
342
- 'test-consumer',
343
- stream: 'test-jetstream-bridge-stream'
330
+ 'worker.sync.api',
331
+ 'api-workers',
332
+ stream: 'test-stream'
344
333
  )
345
334
 
346
335
  allow_any_instance_of(JetstreamBridge::SubscriptionManager)
@@ -391,7 +380,7 @@ storage.reset!
391
380
  3. **Test both success and failure paths**: Use the mock to simulate errors
392
381
  4. **Verify message content**: Check that envelopes are correctly formatted
393
382
  5. **Test idempotency**: Verify duplicate detection and redelivery behavior
394
- 6. **Mock topology setup**: Remember to stub `JetstreamBridge::Topology.ensure!`
383
+ 6. **Set stream_name**: Always configure `stream_name` in your tests
395
384
 
396
385
  ## Examples
397
386
 
@@ -4,7 +4,7 @@ require 'rails/generators'
4
4
 
5
5
  module JetstreamBridge
6
6
  module Generators
7
- class HealthCheckGenerator < Rails::Generators::Base
7
+ class HealthCheckGenerator < ::Rails::Generators::Base
8
8
  source_root File.expand_path('templates', __dir__)
9
9
  desc 'Creates a health check endpoint for JetStream Bridge monitoring'
10
10
 
@@ -4,7 +4,7 @@ require 'rails/generators'
4
4
 
5
5
  module JetstreamBridge
6
6
  module Generators
7
- class InitializerGenerator < Rails::Generators::Base
7
+ class InitializerGenerator < ::Rails::Generators::Base
8
8
  source_root File.expand_path('templates', __dir__)
9
9
  desc 'Creates config/initializers/jetstream_bridge.rb'
10
10
 
@@ -13,10 +13,6 @@ 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
- # Environment identifier (e.g., 'development', 'production')
17
- # Used in stream names and subject routing
18
- config.env = ENV.fetch('NATS_ENV', Rails.env)
19
-
20
16
  # Application name (used in subject routing)
21
17
  config.app_name = ENV.fetch('APP_NAME', Rails.application.class.module_parent_name.underscore)
22
18
 
@@ -5,16 +5,16 @@ require 'rails/generators'
5
5
  module JetstreamBridge
6
6
  module Generators
7
7
  # Install generator.
8
- class InstallGenerator < Rails::Generators::Base
8
+ class InstallGenerator < ::Rails::Generators::Base
9
9
  desc 'Creates JetstreamBridge initializer and migrations'
10
10
  def create_initializer
11
- Rails::Generators.invoke('jetstream_bridge:initializer', [], behavior: behavior,
12
- destination_root: destination_root)
11
+ ::Rails::Generators.invoke('jetstream_bridge:initializer', [], behavior: behavior,
12
+ destination_root: destination_root)
13
13
  end
14
14
 
15
15
  def create_migrations
16
- Rails::Generators.invoke('jetstream_bridge:migrations', [], behavior: behavior,
17
- destination_root: destination_root)
16
+ ::Rails::Generators.invoke('jetstream_bridge:migrations', [], behavior: behavior,
17
+ destination_root: destination_root)
18
18
  end
19
19
  end
20
20
  end
@@ -6,8 +6,8 @@ require 'rails/generators/active_record'
6
6
  module JetstreamBridge
7
7
  module Generators
8
8
  # Migrations generator.
9
- class MigrationsGenerator < Rails::Generators::Base
10
- include Rails::Generators::Migration
9
+ class MigrationsGenerator < ::Rails::Generators::Base
10
+ include ::Rails::Generators::Migration
11
11
 
12
12
  source_root File.expand_path('templates', __dir__)
13
13
  desc 'Creates Inbox/Outbox migrations for JetstreamBridge'
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'oj'
4
4
  require 'securerandom'
5
- require_relative '../core/connection'
6
5
  require_relative '../core/duration'
7
6
  require_relative '../core/logging'
8
7
  require_relative '../core/config'
@@ -10,6 +9,7 @@ require_relative '../core/model_utils'
10
9
  require_relative 'message_processor'
11
10
  require_relative 'subscription_manager'
12
11
  require_relative 'inbox/inbox_processor'
12
+ require_relative 'health_monitor'
13
13
 
14
14
  module JetstreamBridge
15
15
  # Subscribes to destination subject and processes messages via a pull durable consumer.
@@ -48,6 +48,14 @@ module JetstreamBridge
48
48
  IDLE_SLEEP_SECS = 0.05
49
49
  # Maximum sleep duration during idle periods (seconds)
50
50
  MAX_IDLE_BACKOFF_SECS = 1.0
51
+ # Maximum number of batches to drain during shutdown
52
+ MAX_DRAIN_BATCHES = 5
53
+ # Drain timeout per batch in seconds
54
+ DRAIN_BATCH_TIMEOUT = 1
55
+ # Minimum reconnect backoff in seconds
56
+ MIN_RECONNECT_BACKOFF = 0.1
57
+ # Maximum reconnect backoff in seconds
58
+ MAX_RECONNECT_BACKOFF = 30.0
51
59
 
52
60
  # Alias middleware classes for easier access
53
61
  MiddlewareChain = ConsumerMiddleware::MiddlewareChain
@@ -64,60 +72,51 @@ module JetstreamBridge
64
72
  # @return [MiddlewareChain] Middleware chain for processing
65
73
  attr_reader :middleware_chain
66
74
 
67
- # Initialize a new Consumer instance.
75
+ # Initialize a new Consumer instance with dependency injection.
68
76
  #
69
77
  # @param handler [Proc, #call, nil] Message handler that processes events.
70
78
  # Must respond to #call(event) or #call(event, subject, deliveries).
79
+ # @param connection [NATS::JetStream::JS] JetStream connection
80
+ # @param config [Config] Configuration instance
71
81
  # @param durable_name [String, nil] Optional durable consumer name override.
72
- # Defaults to config.durable_name.
73
82
  # @param batch_size [Integer, nil] Number of messages to fetch per batch.
74
- # Defaults to DEFAULT_BATCH_SIZE (25).
75
83
  # @yield [event] Optional block as handler. Receives Models::Event object.
76
84
  #
77
- # @raise [ArgumentError] If neither handler nor block provided
78
- # @raise [ArgumentError] If destination_app not configured
79
- # @raise [ConnectionError] If unable to connect to NATS
85
+ # @raise [ArgumentError] If required dependencies are missing
80
86
  #
81
87
  # @example With proc handler
82
88
  # handler = ->(event) { puts "Received: #{event.type}" }
83
- # consumer = JetstreamBridge::Consumer.new(handler)
89
+ # consumer = JetstreamBridge::Consumer.new(handler, connection: jts, config: config)
84
90
  #
85
91
  # @example With block
86
- # consumer = JetstreamBridge::Consumer.new do |event|
92
+ # consumer = JetstreamBridge::Consumer.new(connection: jts, config: config) do |event|
87
93
  # UserEventHandler.process(event)
88
94
  # end
89
95
  #
90
- # @example With custom configuration
91
- # consumer = JetstreamBridge::Consumer.new(
92
- # handler,
93
- # durable_name: "my-consumer",
94
- # batch_size: 10
95
- # )
96
- #
97
- def initialize(handler = nil, durable_name: nil, batch_size: nil, &block)
96
+ def initialize(handler = nil, connection:, config:, durable_name: nil, batch_size: nil, &block)
98
97
  @handler = handler || block
99
98
  raise ArgumentError, 'handler or block required' unless @handler
99
+ raise ArgumentError, 'connection is required' unless connection
100
+ raise ArgumentError, 'config is required' unless config
101
+
102
+ @jts = connection
103
+ @config = config
100
104
 
101
105
  @batch_size = Integer(batch_size || DEFAULT_BATCH_SIZE)
102
- @durable = durable_name || JetstreamBridge.config.durable_name
106
+ @durable = durable_name || @config.durable_name
103
107
  @idle_backoff = IDLE_SLEEP_SECS
104
108
  @reconnect_attempts = 0
105
109
  @running = true
106
110
  @shutdown_requested = false
107
- @start_time = Time.now
108
- @iterations = 0
109
- @last_health_check = Time.now
110
- # Use existing connection (should already be established)
111
- @jts = Connection.jetstream
112
- raise ConnectionError, 'JetStream connection not available. Call JetstreamBridge.startup! first.' unless @jts
113
111
 
114
112
  @middleware_chain = MiddlewareChain.new
113
+ @health_monitor = ConsumerHealthMonitor.new(@durable)
115
114
 
116
115
  ensure_destination_app_configured!
117
116
 
118
- @sub_mgr = SubscriptionManager.new(@jts, @durable, JetstreamBridge.config)
117
+ @sub_mgr = SubscriptionManager.new(@jts, @durable, @config)
119
118
  @processor = MessageProcessor.new(@jts, @handler, middleware_chain: @middleware_chain)
120
- @inbox_proc = InboxProcessor.new(@processor) if JetstreamBridge.config.use_inbox
119
+ @inbox_proc = InboxProcessor.new(@processor) if @config.use_inbox
121
120
 
122
121
  ensure_subscription!
123
122
  setup_signal_handlers
@@ -193,17 +192,15 @@ module JetstreamBridge
193
192
  #
194
193
  def run!
195
194
  Logging.info(
196
- "Consumer #{@durable} started (batch=#{@batch_size}, dest=#{JetstreamBridge.config.destination_subject})…",
195
+ "Consumer #{@durable} started (batch=#{@batch_size}, dest=#{@config.destination_subject})…",
197
196
  tag: 'JetstreamBridge::Consumer'
198
197
  )
199
198
  while @running
200
199
  processed = process_batch
201
200
  idle_sleep(processed)
202
201
 
203
- @iterations += 1
204
-
205
- # Periodic health checks every 10 minutes (600 seconds)
206
- perform_health_check_if_due
202
+ @health_monitor.increment_iterations
203
+ @health_monitor.check_health_if_due
207
204
  end
208
205
 
209
206
  # Drain in-flight messages before exiting
@@ -245,13 +242,13 @@ module JetstreamBridge
245
242
  private
246
243
 
247
244
  def ensure_destination_app_configured!
248
- return unless JetstreamBridge.config.destination_app.to_s.empty?
245
+ return unless @config.destination_app.to_s.empty?
249
246
 
250
247
  raise ArgumentError, 'destination_app must be configured'
251
248
  end
252
249
 
253
250
  def ensure_subscription!
254
- @sub_mgr.ensure_consumer!
251
+ @sub_mgr.ensure_consumer! unless @config.disable_js_api
255
252
  @psub = @sub_mgr.subscribe!
256
253
  end
257
254
 
@@ -314,10 +311,8 @@ module JetstreamBridge
314
311
  end
315
312
 
316
313
  def calculate_reconnect_backoff(attempt)
317
- # Exponential backoff: 0.1s, 0.2s, 0.4s, 0.8s, 1.6s, ... up to 30s max
318
- base_delay = 0.1
319
- max_delay = 30.0
320
- [base_delay * (2**(attempt - 1)), max_delay].min
314
+ # Exponential backoff: 0.1s, 0.2s, 0.4s, 0.8s, 1.6s, ... up to max
315
+ [MIN_RECONNECT_BACKOFF * (2**(attempt - 1)), MAX_RECONNECT_BACKOFF].min
321
316
  end
322
317
 
323
318
  def recoverable_consumer_error?(error)
@@ -357,70 +352,13 @@ module JetstreamBridge
357
352
  Logging.debug("Could not set up signal handlers: #{e.message}", tag: 'JetstreamBridge::Consumer')
358
353
  end
359
354
 
360
- def perform_health_check_if_due
361
- now = Time.now
362
- time_since_check = now - @last_health_check
363
-
364
- return unless time_since_check >= 600 # 10 minutes
365
-
366
- @last_health_check = now
367
- uptime = now - @start_time
368
- memory_mb = memory_usage_mb
369
-
370
- Logging.info(
371
- "Consumer health: iterations=#{@iterations}, " \
372
- "memory=#{memory_mb}MB, uptime=#{uptime.round}s",
373
- tag: 'JetstreamBridge::Consumer'
374
- )
375
-
376
- # Warn if memory usage is high (over 1GB)
377
- if memory_mb > 1000
378
- Logging.warn(
379
- "High memory usage detected: #{memory_mb}MB",
380
- tag: 'JetstreamBridge::Consumer'
381
- )
382
- end
383
-
384
- # Suggest GC if heap is growing significantly
385
- suggest_gc_if_needed
386
- rescue StandardError => e
387
- Logging.debug(
388
- "Health check failed: #{e.class} #{e.message}",
389
- tag: 'JetstreamBridge::Consumer'
390
- )
391
- end
392
-
393
- def memory_usage_mb
394
- # Get memory usage from OS (works on Linux/macOS)
395
- rss_kb = `ps -o rss= -p #{Process.pid}`.to_i
396
- rss_kb / 1024.0
397
- rescue StandardError
398
- 0.0
399
- end
400
-
401
- def suggest_gc_if_needed
402
- # Suggest GC if heap has many live slots (Ruby-specific optimization)
403
- return unless defined?(GC) && GC.respond_to?(:stat)
404
-
405
- stats = GC.stat
406
- heap_live_slots = stats[:heap_live_slots] || stats['heap_live_slots'] || 0
407
-
408
- # Suggest GC if we have over 100k live objects
409
- GC.start if heap_live_slots > 100_000
410
- rescue StandardError => e
411
- Logging.debug(
412
- "GC check failed: #{e.class} #{e.message}",
413
- tag: 'JetstreamBridge::Consumer'
414
- )
415
- end
416
-
417
355
  def drain_inflight_messages
418
356
  return unless @psub
419
357
 
420
358
  Logging.info('Draining in-flight messages...', tag: 'JetstreamBridge::Consumer')
421
359
  # Process any pending messages with a short timeout
422
- 5.times do
423
- msgs = @psub.fetch(@batch_size, timeout: 1)
360
+ MAX_DRAIN_BATCHES.times do
361
+ msgs = @psub.fetch(@batch_size, timeout: DRAIN_BATCH_TIMEOUT)
424
362
  break if msgs.nil? || msgs.empty?
425
363
 
426
364
  msgs.each { |m| process_one(m) }