legion-transport 1.2.2 → 1.2.4

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: b44d9bbca572702bcfe3d8f37f5ff0330b9e2fd5626d3a094bf32219470fb457
4
- data.tar.gz: 36521e18cfe03a26403538bc1001d5f6b0203219c50b466447eae5f00bfaeec4
3
+ metadata.gz: 5df8a97c0a9116b6e6882820661a69e91708c578511526752fa69aca081c688e
4
+ data.tar.gz: 3407435e75ef5ddae5a9dd5c68d8b3b755541b98345909edfb13dae0cfa6112f
5
5
  SHA512:
6
- metadata.gz: 131abad2f93409e13ed874eedf04f0014f61b89fa0d953449ad8747076ae48841e9a8a30a37174086e4cfc37a08fd039410d88d9b74c16b4eb81b7dca865798d
7
- data.tar.gz: cbead836289a7df9f8fdc44d9e157fc86874b63795b587f2c27ecf4dd2cdeab8b8b3820fb6905086ca9e80af5dded515df51b77eb7e675c5bc9b0c8779b12718
6
+ metadata.gz: a9869f1963d7c939cc2ecfa27a49b15f60c4fee20dce78818040e9813f4b81a31352704e99e152b390d8578e09ac462a0dd84903fba45599c3386e1707c2d7ff
7
+ data.tar.gz: 950098bfa689097417cad4ee2fb4395b0e2b6793127d3ac04a654254038f7ff9afc3cc91663d01b19f206ea29d6c751ea65843b35b7416bda881047378c40333
data/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Legion::Transport ChangeLog
2
2
 
3
- ## [Unreleased]
3
+ ## [1.2.4] - 2026-03-20
4
+
5
+ ### Fixed
6
+ - Add `logger` gem as runtime dependency for Ruby 4.0 compatibility (extracted from stdlib)
7
+ - Disable DNS bootstrap in test environment to prevent `NameError` from legion-settings 1.3.5
8
+
9
+ ## [1.2.3] - 2026-03-19
10
+
11
+ ### Added
12
+ - `Legion::Transport::Spool` JSONL disk buffer for offline message persistence when AMQP is unavailable
13
+ - Automatic spool intercept on `Message#publish` Bunny connection errors with configurable limits
14
+ - Spool drain reads oldest-first with file rotation and stale eviction (72hr TTL, 10MB/file, 500MB total, 100 files max)
4
15
 
5
16
  ## [1.2.2] - 2026-03-17
6
17
 
data/CLAUDE.md CHANGED
@@ -8,6 +8,7 @@
8
8
  Ruby gem that manages the connection between LegionIO and its FIFO queue system (RabbitMQ over AMQP 0.9.1). Provides abstractions for exchanges, queues, messages, and consumers with thread-safe connection management.
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/legion-transport
11
+ **Version**: 1.2.3
11
12
  **License**: Apache-2.0
12
13
 
13
14
  ## Architecture
@@ -20,13 +21,15 @@ Legion::Transport
20
21
  ├── Exchange # Base exchange class (extends Bunny::Exchange)
21
22
  │ └── Exchanges/
22
23
  │ ├── Task # Task routing exchange
23
- │ ├── Node # Node communication exchange
24
+ │ ├── Node # Node communication exchange (infrastructure: swarms, services, heartbeats)
25
+ │ ├── Agent # Agent communication exchange (identity-bound: GAIA frames, preferences, proactive)
24
26
  │ ├── Crypt # Encryption exchange
25
27
  │ ├── Extensions # Extension exchange
26
28
  │ └── Lex # LEX exchange (inherits Extensions)
27
29
  ├── Queue # Base queue class (extends Bunny::Queue)
28
30
  │ └── Queues/
29
31
  │ ├── Node # Node queue
32
+ │ ├── Agent # Per-agent queue (auto-delete, routing key: agent.<agent_id>)
30
33
  │ ├── NodeCrypt # Node encryption queue
31
34
  │ ├── NodeStatus # Node status queue
32
35
  │ ├── TaskLog # Task logging queue
@@ -44,8 +47,9 @@ Legion::Transport
44
47
  ├── Consumer # AMQP consumer with auto-generated tags
45
48
  ├── Common # Shared utilities (channel mgmt, options merging, consumer tags)
46
49
  ├── Local # In-memory pub/sub for local development mode (no RabbitMQ)
50
+ ├── Spool # Disk-backed message buffer: persist messages when RabbitMQ unavailable, replay on reconnect
47
51
  ├── Settings # Default configuration with env var overrides
48
- └── Version # 1.2.1
52
+ └── Version # 1.2.3
49
53
  ```
50
54
 
51
55
  ## Key Design Patterns
@@ -106,13 +110,33 @@ Vault integration: If `Legion::Settings[:crypt][:vault][:connected]` is true, cr
106
110
  | `lib/legion/transport/connection/vault.rb` | Vault PKI integration (stub) |
107
111
  | `lib/legion/transport/common.rb` | Shared module (channel access, deep_merge, consumer tags) |
108
112
  | `lib/legion/transport/exchange.rb` | Base Exchange class |
113
+ | `lib/legion/transport/exchanges/agent.rb` | Agent exchange for identity-bound communication |
109
114
  | `lib/legion/transport/queue.rb` | Base Queue class |
115
+ | `lib/legion/transport/queues/agent.rb` | Per-agent queue (auto-delete, keyed by agent_id) |
110
116
  | `lib/legion/transport/message.rb` | Base Message class with publish/encode/encrypt |
111
117
  | `lib/legion/transport/consumer.rb` | AMQP consumer wrapper |
118
+ | `lib/legion/transport/spool.rb` | Disk-backed message buffer (~/.legionio/spool, 10MB/file, 500MB total, 3-day TTL) |
112
119
  | `lib/legion/transport/settings.rb` | Default config, env var loading, Vault cred fetch |
113
120
  | `lib/legion/transport/version.rb` | Version constant |
114
121
  | `spec/` | RSpec test suite |
115
122
 
123
+ ## Node vs Agent Exchange
124
+
125
+ Two identity-scoped exchanges separate infrastructure traffic from agent-bound traffic:
126
+
127
+ | Exchange | Routing Key Pattern | Use Case |
128
+ |----------|-------------------|----------|
129
+ | `node` | `node.<fqdn/nodename>` | Infrastructure: swarm coordination, service heartbeats, non-identity traffic |
130
+ | `agent` | `agent.<agent_id>` | Identity-bound: GAIA cognitive frames, preference queries, proactive messages |
131
+
132
+ **Agent queue defaults** differ from standard queues:
133
+ - `durable: false` — agent queues are ephemeral (recreated on connect)
134
+ - `auto_delete: true` — cleaned up when the agent disconnects
135
+ - Dead letter exchange: `agent.dlx`
136
+ - Agent ID defaults to `Legion::Settings['client']['name']` if not provided
137
+
138
+ The `agent` exchange is used by `legion-gaia` for inbound cognitive frames (replacing the former `gaia` exchange routing) and by `lex-mesh` for async preference queries via `reply_to` + `correlation_id` RPC.
139
+
116
140
  ## Queue Defaults
117
141
 
118
142
  All queues are created with:
@@ -137,7 +161,7 @@ bundle exec rspec
137
161
  bundle exec rubocop
138
162
  ```
139
163
 
140
- Spec count: 144 examples
164
+ Spec count: 166 examples
141
165
 
142
166
  ---
143
167
 
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Legion::Transport is the Ruby gem responsible for connecting LegionIO to its FIFO queue system (RabbitMQ over AMQP 0.9.1). It provides thread-safe connection management, exchange/queue abstractions, message publishing with optional encryption, and consumer wrappers.
4
4
 
5
+ **Version**: 1.2.3
6
+
5
7
  ## Features
6
8
 
7
9
  - Thread-safe connection management using `concurrent-ruby`
@@ -11,6 +13,7 @@ Legion::Transport is the Ruby gem responsible for connecting LegionIO to its FIF
11
13
  - Dynamic credential retrieval from HashiCorp Vault
12
14
  - Auto-recovery on connection loss
13
15
  - Dead letter exchange support
16
+ - Spool buffer for disk-backed message persistence when RabbitMQ is unavailable
14
17
 
15
18
  ## Supported Ruby Versions
16
19
 
@@ -182,12 +185,6 @@ bundle exec rspec
182
185
  bundle exec rubocop
183
186
  ```
184
187
 
185
- ## Authors
186
-
187
- - [Matthew Iverson](https://github.com/Esity) - current maintainer
188
-
189
188
  ## License
190
189
 
191
- This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
192
-
193
- Copyright 2021 Esity
190
+ Apache-2.0
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency 'concurrent-ruby', '>= 1.2'
31
31
  spec.add_dependency 'legion-json'
32
32
  spec.add_dependency 'legion-settings'
33
+ spec.add_dependency 'logger'
33
34
  end
@@ -27,6 +27,9 @@ module Legion
27
27
  correlation_id: correlation_id,
28
28
  app_id: app_id,
29
29
  timestamp: timestamp)
30
+ rescue Bunny::ConnectionClosedError, Bunny::ChannelAlreadyClosed, Bunny::ChannelError,
31
+ Bunny::NetworkErrorWrapper, IOError => e
32
+ spool_message(e)
30
33
  end
31
34
 
32
35
  def app_id
@@ -165,6 +168,28 @@ module Legion
165
168
  def channel
166
169
  Legion::Transport::Connection.channel
167
170
  end
171
+
172
+ private
173
+
174
+ def spool_message(error)
175
+ return unless defined?(Legion::Transport::Spool)
176
+
177
+ Legion::Transport::Spool.write(
178
+ exchange: exchange_name_for_spool,
179
+ routing_key: routing_key || '',
180
+ payload: message
181
+ )
182
+ Legion::Logging.debug { "Message spooled due to: #{error.message}" } if defined?(Legion::Logging)
183
+ rescue StandardError => e
184
+ Legion::Logging.warn { "Spool write failed: #{e.message}" } if defined?(Legion::Logging)
185
+ end
186
+
187
+ def exchange_name_for_spool
188
+ ex = exchange
189
+ ex.respond_to?(:name) ? ex.name : ex.to_s
190
+ rescue StandardError
191
+ self.class.name
192
+ end
168
193
  end
169
194
  end
170
195
  end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'securerandom'
5
+
6
+ module Legion
7
+ module Transport
8
+ module Spool
9
+ class << self
10
+ def setup(directory: nil, max_file_bytes: 10_485_760, max_total_bytes: 524_288_000,
11
+ max_files: 100, max_age_seconds: 259_200)
12
+ @directory = directory || File.expand_path('~/.legionio/spool')
13
+ @max_file_bytes = max_file_bytes
14
+ @max_total_bytes = max_total_bytes
15
+ @max_files = max_files
16
+ @max_age_seconds = max_age_seconds
17
+ @current_file = nil
18
+ @mutex = Mutex.new
19
+
20
+ FileUtils.mkdir_p(@directory)
21
+ end
22
+
23
+ def write(exchange:, routing_key:, payload:)
24
+ setup unless @directory
25
+
26
+ @mutex.synchronize do
27
+ evict_oldest if over_limits?
28
+
29
+ line = Legion::JSON.dump({
30
+ exchange: exchange,
31
+ routing_key: routing_key,
32
+ payload: payload,
33
+ spooled_at: Time.now.iso8601
34
+ })
35
+
36
+ file = current_file
37
+ File.open(file, 'a') { |f| f.puts(line) }
38
+
39
+ rotate_if_needed
40
+ end
41
+ rescue StandardError => e
42
+ Legion::Logging.warn { "Spool write failed: #{e.message}" } if defined?(Legion::Logging)
43
+ end
44
+
45
+ def drain
46
+ setup unless @directory
47
+
48
+ sorted_files.each do |file|
49
+ lines = File.readlines(file).map(&:strip).reject(&:empty?)
50
+ lines.each do |line|
51
+ msg = Legion::JSON.load(line)
52
+ yield(msg)
53
+ end
54
+ File.delete(file)
55
+ rescue StandardError => e
56
+ Legion::Logging.warn { "Spool drain error on #{file}: #{e.message}" } if defined?(Legion::Logging)
57
+ break
58
+ end
59
+ end
60
+
61
+ def count
62
+ setup unless @directory
63
+
64
+ sorted_files.sum do |file|
65
+ File.readlines(file).count { |l| !l.strip.empty? }
66
+ rescue StandardError
67
+ 0
68
+ end
69
+ end
70
+
71
+ def evict_stale
72
+ setup unless @directory
73
+
74
+ cutoff = Time.now - @max_age_seconds
75
+ sorted_files.each do |file|
76
+ File.delete(file) if File.mtime(file) < cutoff
77
+ rescue StandardError
78
+ nil
79
+ end
80
+ end
81
+
82
+ def reset!
83
+ @directory = nil
84
+ @current_file = nil
85
+ @mutex = nil
86
+ @max_file_bytes = nil
87
+ @max_total_bytes = nil
88
+ @max_files = nil
89
+ @max_age_seconds = nil
90
+ end
91
+
92
+ attr_reader :max_file_bytes
93
+
94
+ private
95
+
96
+ def sorted_files
97
+ Dir.glob(File.join(@directory, '*.jsonl'))
98
+ end
99
+
100
+ def current_file
101
+ @current_file ||= new_file_path
102
+ @current_file = new_file_path unless File.exist?(@current_file)
103
+ @current_file
104
+ end
105
+
106
+ def new_file_path
107
+ File.join(@directory, "spool-#{Time.now.strftime('%Y%m%d%H%M%S')}-#{SecureRandom.hex(4)}.jsonl")
108
+ end
109
+
110
+ def rotate_if_needed
111
+ return unless File.exist?(@current_file) && File.size(@current_file) >= @max_file_bytes
112
+
113
+ @current_file = nil
114
+ end
115
+
116
+ def over_limits?
117
+ files = sorted_files
118
+ return true if files.size >= @max_files
119
+
120
+ total = files.sum do |f|
121
+ File.size(f)
122
+ rescue StandardError
123
+ 0
124
+ end
125
+ total >= @max_total_bytes
126
+ end
127
+
128
+ def evict_oldest
129
+ files = sorted_files
130
+ while files.size >= @max_files
131
+ begin
132
+ File.delete(files.shift)
133
+ rescue StandardError
134
+ break
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Transport
5
- VERSION = '1.2.2'
5
+ VERSION = '1.2.4'
6
6
  end
7
7
  end
@@ -43,4 +43,5 @@ module Legion
43
43
  require_relative 'transport/queue'
44
44
  require_relative 'transport/exchange'
45
45
  require_relative 'transport/message'
46
+ require_relative 'transport/spool'
46
47
  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.2.2
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: logger
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
68
82
  description: The Gem to connect LegionIO and it's extensions to the transport tier
69
83
  email:
70
84
  - matthewdiverson@gmail.com
@@ -115,6 +129,7 @@ files:
115
129
  - lib/legion/transport/queues/task_log.rb
116
130
  - lib/legion/transport/queues/task_update.rb
117
131
  - lib/legion/transport/settings.rb
132
+ - lib/legion/transport/spool.rb
118
133
  - lib/legion/transport/version.rb
119
134
  - sonar-project.properties
120
135
  homepage: https://github.com/LegionIO/legion-transport