legion-transport 1.4.25 → 1.4.26

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3257739440ff409750a6b555beeda06961106a34550640aba80d25047a89682
4
- data.tar.gz: 205dd2249629c4ded7831cd2e3c46b461deb26dc58ee23e6ca0b0d404c08e19b
3
+ metadata.gz: 3ef4fe3fdbefc12a9efd791c71b41422435c5ad5401e5ba0c51040649360f0b0
4
+ data.tar.gz: 8162f75c0d55ec521e8e1f3e2eaa1ff367b16bd0479a5c4f46471d9a6a92f8db
5
5
  SHA512:
6
- metadata.gz: 597ab35e69a59cf40b8d99f4d2cd17234f60104140cf83fa37594e2894002b30ae4f04a9beaa5873f56db4258d671ee4241e81dd4158455363f6b2f0189a4b8d
7
- data.tar.gz: f5552c15fe1463f572c960823bf9e02cec9605048909c1ba5c33c4dba384cd11369fbf3b3018dff2d8289e5a47a10ee3ba8564b921e2bd30992a04356ce5a297
6
+ metadata.gz: 577f95777048496f3155dc4e22a9fd865df45fe1869c076e7297dcd2390352d3929952b0fa849e5864974ad84b2595a28d3788f8bdfe50a9a3f55084f3fb535c
7
+ data.tar.gz: 96cda6bf47f5bfa71ef6e3684b1ea68ebe91573159d42e00adba7f6f664d9f1bc0a587a185abfd5108dbe60f77bbe6e598c61a7a4929b971aca24c7ccf128707
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.4.26] - 2026-05-16
6
+
7
+ ### Fixed
8
+ - Fixed channel leak caused by concurrent-ruby's IO-pool thread lifecycle: `TimerTask` (used by `Every` actors) dispatches onto `global_io_executor` threads that idle-timeout after 60s; thread-local Bunny channels were never closed when those threads died, eventually exhausting RabbitMQ's `channel_max` (1024). Added a `@channel_registry` that tracks publisher channels by owning thread and sweeps orphaned channels from dead threads before creating new ones.
9
+ - `Connection.shutdown` now explicitly closes all tracked publisher channels before tearing down the session.
10
+ - `Connection.force_reconnect` resets the channel registry to prevent stale references after reconnection.
11
+
5
12
  ## [1.4.25] - 2026-05-15
6
13
 
7
14
  ### Added
@@ -11,6 +11,7 @@ module Legion
11
11
  module Connection
12
12
  RECOVERY_WINDOW = 60
13
13
  MAX_RECOVERIES_PER_WINDOW = 5
14
+ MAX_PUBLISHER_CHANNELS = 128
14
15
 
15
16
  class << self
16
17
  include Legion::Logging::Helper
@@ -36,6 +37,7 @@ module Legion
36
37
  def reconnect(connection_name: 'Legion', **)
37
38
  @session = nil
38
39
  @channel_thread = Concurrent::ThreadLocalVar.new(nil)
40
+ @channel_registry = Concurrent::Hash.new
39
41
  setup(connection_name: connection_name)
40
42
  end
41
43
 
@@ -83,9 +85,18 @@ module Legion
83
85
  s = session
84
86
  raise IOError, 'transport session unavailable (recovery in progress)' unless s&.open?
85
87
 
88
+ sweep_dead_thread_channels
89
+
90
+ current_size = channel_registry_size
91
+ if current_size >= MAX_PUBLISHER_CHANNELS
92
+ log.warn "Channel registry at capacity (size=#{current_size}, max=#{MAX_PUBLISHER_CHANNELS}); " \
93
+ 'RabbitMQ channel_max exhaustion risk — investigate thread lifecycle'
94
+ end
95
+
86
96
  @channel_thread.value = s.create_channel(nil, settings[:channel][:default_worker_pool_size], false, 10)
87
97
  @channel_thread.value.prefetch(settings[:prefetch])
88
- log.debug "Channel created for thread #{Thread.current.object_id}"
98
+ track_channel(Thread.current, @channel_thread.value)
99
+ log.debug "Channel created for thread #{Thread.current.object_id} (tracked=#{channel_registry_size})"
89
100
  @channel_thread.value
90
101
  end
91
102
 
@@ -127,6 +138,7 @@ module Legion
127
138
  @shutting_down = true
128
139
  pre_mark_sessions_closing
129
140
  close_build_session
141
+ close_all_tracked_channels
130
142
 
131
143
  if @pool
132
144
  @pool.shutdown
@@ -150,6 +162,7 @@ module Legion
150
162
  ensure
151
163
  @log_channel = nil
152
164
  @session = nil
165
+ @channel_registry = Concurrent::Hash.new
153
166
  @shutting_down = false
154
167
  end
155
168
 
@@ -163,6 +176,7 @@ module Legion
163
176
  reset_pool if pool_mode
164
177
  @session = nil
165
178
  @channel_thread = Concurrent::ThreadLocalVar.new(nil)
179
+ @channel_registry = Concurrent::Hash.new
166
180
  @recovery_timestamps = []
167
181
 
168
182
  tear_down_session(old) if old && !pool_mode
@@ -272,8 +286,50 @@ module Legion
272
286
  sess
273
287
  end
274
288
 
289
+ def channel_registry_size
290
+ (@channel_registry ||= Concurrent::Hash.new).size
291
+ end
292
+
275
293
  private
276
294
 
295
+ def track_channel(thread, channel)
296
+ @channel_registry ||= Concurrent::Hash.new
297
+ @channel_registry[thread] = channel
298
+ end
299
+
300
+ def close_all_tracked_channels
301
+ @channel_registry ||= Concurrent::Hash.new
302
+ return if @channel_registry.empty?
303
+
304
+ @channel_registry.each_value do |channel|
305
+ channel.close if channel&.open?
306
+ rescue StandardError => e
307
+ handle_exception(e, level: :warn, handled: true, operation: 'transport.connection.close_tracked_channel')
308
+ end
309
+ @channel_registry.clear
310
+ end
311
+
312
+ def sweep_dead_thread_channels
313
+ @channel_registry ||= Concurrent::Hash.new
314
+ return if @channel_registry.empty?
315
+
316
+ swept = 0
317
+ @channel_registry.each do |thread, channel|
318
+ next if thread&.alive?
319
+
320
+ if channel&.open?
321
+ channel.close
322
+ swept += 1
323
+ end
324
+ @channel_registry.delete(thread)
325
+ rescue StandardError => e
326
+ @channel_registry.delete(thread)
327
+ handle_exception(e, level: :warn, handled: true, operation: 'transport.connection.sweep_channel')
328
+ end
329
+
330
+ log.info "Swept #{swept} orphaned channel(s) from dead threads (remaining=#{@channel_registry.size})" if swept.positive?
331
+ end
332
+
277
333
  def pre_mark_sessions_closing
278
334
  candidates = [
279
335
  session,
@@ -493,6 +549,7 @@ module Legion
493
549
  @log_channel = nil
494
550
  @session = Concurrent::AtomicReference.new(create_session_with_failover(connection_name: connection_name))
495
551
  @channel_thread = Concurrent::ThreadLocalVar.new(nil)
552
+ @channel_registry = Concurrent::Hash.new
496
553
  start_session(session)
497
554
  qos_channel = session.create_channel(nil, settings[:channel][:session_worker_pool_size])
498
555
  apply_qos_and_close(qos_channel)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Transport
5
- VERSION = '1.4.25'
5
+ VERSION = '1.4.26'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-transport
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.25
4
+ version: 1.4.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity