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 +4 -4
- data/CHANGELOG.md +12 -1
- data/CLAUDE.md +27 -3
- data/README.md +4 -7
- data/legion-transport.gemspec +1 -0
- data/lib/legion/transport/message.rb +25 -0
- data/lib/legion/transport/spool.rb +141 -0
- data/lib/legion/transport/version.rb +1 -1
- data/lib/legion/transport.rb +1 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5df8a97c0a9116b6e6882820661a69e91708c578511526752fa69aca081c688e
|
|
4
|
+
data.tar.gz: 3407435e75ef5ddae5a9dd5c68d8b3b755541b98345909edfb13dae0cfa6112f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
## [
|
|
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.
|
|
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:
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
Copyright 2021 Esity
|
|
190
|
+
Apache-2.0
|
data/legion-transport.gemspec
CHANGED
|
@@ -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
|
data/lib/legion/transport.rb
CHANGED
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.
|
|
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
|