lex-mesh 0.4.1 → 0.4.3
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/Gemfile +1 -0
- data/lib/legion/extensions/mesh/actors/gossip.rb +1 -1
- data/lib/legion/extensions/mesh/actors/gossip_listener.rb +44 -0
- data/lib/legion/extensions/mesh/actors/heartbeat.rb +1 -1
- data/lib/legion/extensions/mesh/actors/pending_expiry.rb +1 -1
- data/lib/legion/extensions/mesh/actors/preference_listener.rb +3 -3
- data/lib/legion/extensions/mesh/actors/silence_watchdog.rb +1 -1
- data/lib/legion/extensions/mesh/client.rb +1 -1
- data/lib/legion/extensions/mesh/helpers/peer_table.rb +55 -0
- data/lib/legion/extensions/mesh/helpers/peer_verify.rb +6 -6
- data/lib/legion/extensions/mesh/helpers/pending_requests.rb +1 -1
- data/lib/legion/extensions/mesh/helpers/preference_profile.rb +12 -12
- data/lib/legion/extensions/mesh/helpers/registry.rb +1 -1
- data/lib/legion/extensions/mesh/runners/mesh.rb +91 -26
- data/lib/legion/extensions/mesh/runners/preferences.rb +7 -7
- data/lib/legion/extensions/mesh/runners/task_request.rb +3 -3
- data/lib/legion/extensions/mesh/transport/messages/mesh_conflict.rb +35 -0
- data/lib/legion/extensions/mesh/version.rb +1 -1
- data/lib/legion/extensions/mesh.rb +6 -2
- data/spec/legion/extensions/mesh/actors/gossip_listener_spec.rb +90 -0
- data/spec/legion/extensions/mesh/helpers/peer_table_spec.rb +103 -0
- data/spec/legion/extensions/mesh/runners/mesh_gossip_spec.rb +44 -1
- data/spec/legion/extensions/mesh/transport/messages/mesh_conflict_spec.rb +88 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b16d673e30a813f0fa38f61386eae0e9a72a35cc2b0eedc6b45ced88a4acadf7
|
|
4
|
+
data.tar.gz: dc7a12c1de2412117ac9ce56c7803626dba16604f9cbfba57ca41e72e55dc3b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7abe441618ba1fca371412adf3a2a48f16f54adc4130d060369e1fcdc7222a777fcc48e165576e74e4cf1892fdd9a42f4562242523bf2d9e8239cdb8af771b4c
|
|
7
|
+
data.tar.gz: 61e2b0e8aba072966d10e820db3708ff2c76ecfcda9fc73e99bcd61eff051323c81ea5c1cef0e1c56ce126604369983205f32e732376eab53ad1fae4af1857f5
|
data/Gemfile
CHANGED
|
@@ -6,7 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
module Mesh
|
|
8
8
|
module Actor
|
|
9
|
-
class Gossip < Legion::Extensions::Actors::Every
|
|
9
|
+
class Gossip < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
|
|
10
10
|
def runner_class
|
|
11
11
|
Legion::Extensions::Mesh::Runners::Mesh
|
|
12
12
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/subscription'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Mesh
|
|
8
|
+
module Actor
|
|
9
|
+
class GossipListener < Legion::Extensions::Actors::Subscription
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::Mesh::Runners::Mesh
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'dispatch_gossip_message'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def check_subtask?
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate_task?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def use_runner?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def queue
|
|
31
|
+
Legion::Extensions::Mesh::Transport::Queues::Gossip
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def enabled? # rubocop:disable Legion/Extension/ActorEnabledSideEffects
|
|
35
|
+
defined?(Legion::Extensions::Mesh::Runners::Mesh) &&
|
|
36
|
+
Legion.const_defined?(:Transport, false)
|
|
37
|
+
rescue StandardError => _e
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -6,7 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
module Mesh
|
|
8
8
|
module Actor
|
|
9
|
-
class Heartbeat < Legion::Extensions::Actors::Every
|
|
9
|
+
class Heartbeat < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
|
|
10
10
|
def runner_class
|
|
11
11
|
Legion::Extensions::Mesh::Runners::Mesh
|
|
12
12
|
end
|
|
@@ -6,7 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
module Mesh
|
|
8
8
|
module Actor
|
|
9
|
-
class PendingExpiry < Legion::Extensions::Actors::Every
|
|
9
|
+
class PendingExpiry < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
|
|
10
10
|
def runner_class
|
|
11
11
|
Legion::Extensions::Mesh::Runners::Preferences
|
|
12
12
|
end
|
|
@@ -31,10 +31,10 @@ module Legion
|
|
|
31
31
|
Legion::Extensions::Mesh::Transport::Queues::Preference
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def enabled?
|
|
34
|
+
def enabled? # rubocop:disable Legion/Extension/ActorEnabledSideEffects
|
|
35
35
|
defined?(Legion::Extensions::Mesh::Runners::Preferences) &&
|
|
36
|
-
|
|
37
|
-
rescue StandardError
|
|
36
|
+
Legion.const_defined?(:Transport, false)
|
|
37
|
+
rescue StandardError => _e
|
|
38
38
|
false
|
|
39
39
|
end
|
|
40
40
|
end
|
|
@@ -6,7 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
module Mesh
|
|
8
8
|
module Actor
|
|
9
|
-
class SilenceWatchdog < Legion::Extensions::Actors::Every
|
|
9
|
+
class SilenceWatchdog < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
|
|
10
10
|
def runner_class
|
|
11
11
|
Legion::Extensions::Mesh::Runners::Mesh
|
|
12
12
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Mesh
|
|
6
|
+
module Helpers
|
|
7
|
+
class PeerTable
|
|
8
|
+
DEFAULT_TTL = 60 # seconds
|
|
9
|
+
|
|
10
|
+
def initialize(ttl: DEFAULT_TTL)
|
|
11
|
+
@ttl = ttl
|
|
12
|
+
@peers = {}
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def upsert(agent_id, data = {})
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
@peers[agent_id] = data.merge(last_seen_at: Time.now.utc)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def get(agent_id)
|
|
23
|
+
@mutex.synchronize { @peers[agent_id] }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def all
|
|
27
|
+
@mutex.synchronize { @peers.dup }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def expire
|
|
31
|
+
cutoff = Time.now.utc - @ttl
|
|
32
|
+
expired = []
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
@peers.each do |id, entry|
|
|
35
|
+
next unless entry[:last_seen_at] < cutoff
|
|
36
|
+
|
|
37
|
+
expired << id
|
|
38
|
+
end
|
|
39
|
+
expired.each { |id| @peers.delete(id) }
|
|
40
|
+
end
|
|
41
|
+
expired
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def count
|
|
45
|
+
@mutex.synchronize { @peers.size }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def remove(agent_id)
|
|
49
|
+
@mutex.synchronize { @peers.delete(agent_id) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -21,21 +21,21 @@ module Legion
|
|
|
21
21
|
return { valid: false, org_id: org_id, reason: :unknown_peer } unless peer
|
|
22
22
|
|
|
23
23
|
require 'ed25519'
|
|
24
|
-
pub_key_b64 = peer[:public_key].
|
|
24
|
+
pub_key_b64 = peer[:public_key].delete_prefix('ed25519:')
|
|
25
25
|
verify_key = Ed25519::VerifyKey.new(Base64.strict_decode64(pub_key_b64))
|
|
26
26
|
signature = Base64.strict_decode64(signed_message[:signature])
|
|
27
27
|
message_bytes = signed_message[:signed_bytes] || json_dump(signed_message[:payload])
|
|
28
28
|
verify_key.verify(signature, message_bytes)
|
|
29
29
|
{ valid: true, org_id: org_id }
|
|
30
|
-
rescue Ed25519::VerifyError
|
|
30
|
+
rescue Ed25519::VerifyError => _e
|
|
31
31
|
{ valid: false, org_id: org_id, reason: :invalid_signature }
|
|
32
32
|
rescue StandardError => e
|
|
33
33
|
{ valid: false, org_id: org_id, reason: :error, message: e.message }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def check_rate_limit(org_id)
|
|
37
|
-
@counters ||= Hash.new { |h, k| h[k] = { count: 0, window_start: Time.now.utc } }
|
|
38
|
-
counter = @counters[org_id]
|
|
37
|
+
@counters ||= Hash.new { |h, k| h[k] = { count: 0, window_start: Time.now.utc } } # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
38
|
+
counter = @counters[org_id] # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
39
39
|
peer = find_peer(org_id)
|
|
40
40
|
limit = peer&.dig(:rate_limit) || 100
|
|
41
41
|
|
|
@@ -51,7 +51,7 @@ module Legion
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def reset_counters!
|
|
54
|
-
@counters = nil
|
|
54
|
+
@counters = nil # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
private
|
|
@@ -65,7 +65,7 @@ module Legion
|
|
|
65
65
|
|
|
66
66
|
def json_dump(data)
|
|
67
67
|
if defined?(Legion::JSON)
|
|
68
|
-
Legion::JSON.dump({ data: data })
|
|
68
|
+
Legion::JSON.dump({ data: data }) # rubocop:disable Legion/HelperMigration/DirectJson
|
|
69
69
|
else
|
|
70
70
|
require 'json'
|
|
71
71
|
::JSON.dump({ data: data })
|
|
@@ -124,8 +124,8 @@ module Legion
|
|
|
124
124
|
MESH_CACHE_TTL = 3600 # 1 hour default
|
|
125
125
|
|
|
126
126
|
def store_mesh_profile(agent_id:, profile:, source_agent_id:)
|
|
127
|
-
@mesh_cache ||= {}
|
|
128
|
-
@mesh_cache[agent_id.to_s] = {
|
|
127
|
+
@mesh_cache ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
128
|
+
@mesh_cache[agent_id.to_s] = { # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
129
129
|
profile: profile,
|
|
130
130
|
source_agent_id: source_agent_id,
|
|
131
131
|
origin: :mesh_transfer,
|
|
@@ -147,26 +147,26 @@ module Legion
|
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
def cached_mesh_profile(agent_id:, ttl: MESH_CACHE_TTL)
|
|
150
|
-
@mesh_cache ||= {}
|
|
151
|
-
entry = @mesh_cache[agent_id.to_s]
|
|
150
|
+
@mesh_cache ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
151
|
+
entry = @mesh_cache[agent_id.to_s] # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
152
152
|
return nil unless entry
|
|
153
153
|
|
|
154
154
|
if Time.now - entry[:cached_at] > ttl
|
|
155
|
-
@mesh_cache.delete(agent_id.to_s)
|
|
155
|
+
@mesh_cache.delete(agent_id.to_s) # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
156
156
|
return nil
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
entry[:profile]
|
|
160
|
-
rescue StandardError
|
|
160
|
+
rescue StandardError => _e
|
|
161
161
|
nil
|
|
162
162
|
end
|
|
163
163
|
|
|
164
164
|
def clear_mesh_cache(agent_id: nil)
|
|
165
|
-
@mesh_cache ||= {}
|
|
165
|
+
@mesh_cache ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
166
166
|
if agent_id
|
|
167
|
-
@mesh_cache.delete(agent_id.to_s)
|
|
167
|
+
@mesh_cache.delete(agent_id.to_s) # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
168
168
|
else
|
|
169
|
-
@mesh_cache.clear
|
|
169
|
+
@mesh_cache.clear # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
170
170
|
end
|
|
171
171
|
end
|
|
172
172
|
|
|
@@ -214,7 +214,7 @@ module Legion
|
|
|
214
214
|
traces.select { |t| t[:domain_tags]&.include?('preference') }.filter_map do |trace|
|
|
215
215
|
parse_preference_trace(trace)
|
|
216
216
|
end
|
|
217
|
-
rescue StandardError
|
|
217
|
+
rescue StandardError => _e
|
|
218
218
|
[]
|
|
219
219
|
end
|
|
220
220
|
|
|
@@ -226,7 +226,7 @@ module Legion
|
|
|
226
226
|
return nil unless match
|
|
227
227
|
|
|
228
228
|
{ domain: match[1], value: match[2], source: match[3], confidence: trace[:confidence] }
|
|
229
|
-
rescue StandardError
|
|
229
|
+
rescue StandardError => _e
|
|
230
230
|
nil
|
|
231
231
|
end
|
|
232
232
|
|
|
@@ -239,7 +239,7 @@ module Legion
|
|
|
239
239
|
)
|
|
240
240
|
result = personality_runner.personality_compatibility(other_profile: profile[:personality])
|
|
241
241
|
{ score: result[:compatibility], interpretation: result[:interpretation] }
|
|
242
|
-
rescue StandardError
|
|
242
|
+
rescue StandardError => _e
|
|
243
243
|
nil
|
|
244
244
|
end
|
|
245
245
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Extensions
|
|
5
7
|
module Mesh
|
|
6
8
|
module Runners
|
|
7
9
|
module Mesh
|
|
8
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
-
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
11
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
10
12
|
|
|
11
13
|
def register(agent_id:, capabilities: [], endpoint: nil, **)
|
|
12
14
|
mesh_registry.register_agent(agent_id, capabilities: capabilities, endpoint: endpoint)
|
|
@@ -65,7 +67,17 @@ module Legion
|
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
def publish_gossip(**)
|
|
70
|
+
expired = peer_table.expire
|
|
68
71
|
registry = mesh_registry
|
|
72
|
+
|
|
73
|
+
Array(expired).each do |agent_id|
|
|
74
|
+
result = registry.unregister_agent(agent_id)
|
|
75
|
+
if result
|
|
76
|
+
log.info "[mesh] ttl-expired agent unregistered from registry: agent=#{agent_id}"
|
|
77
|
+
else
|
|
78
|
+
log.debug "[mesh] ttl-expired agent not found in registry: agent=#{agent_id}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
69
81
|
peers = registry.all_agents.first(gossip_max_peers).map do |agent|
|
|
70
82
|
agent.slice(:agent_id, :capabilities, :node, :source, :status, :generation,
|
|
71
83
|
:last_seen, :registered_at).transform_values { |v| v.is_a?(Time) ? v.to_s : v }
|
|
@@ -78,36 +90,38 @@ module Legion
|
|
|
78
90
|
{ success: false, reason: :error, message: e.message }
|
|
79
91
|
end
|
|
80
92
|
|
|
81
|
-
def merge_gossip(incoming_peers:, sender: nil, **)
|
|
93
|
+
def merge_gossip(incoming_peers:, sender: nil, **)
|
|
82
94
|
registry = mesh_registry
|
|
95
|
+
conflict_agents = []
|
|
83
96
|
merged = 0
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
peer
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
local = registry.agents[peer[:agent_id]]
|
|
90
|
-
if local.nil?
|
|
91
|
-
registry.register_agent(
|
|
92
|
-
peer[:agent_id],
|
|
93
|
-
capabilities: (peer[:capabilities] || []).map(&:to_sym),
|
|
94
|
-
source: (peer[:source] || :native).to_sym,
|
|
95
|
-
node: peer[:node]
|
|
96
|
-
)
|
|
97
|
-
registry.agents[peer[:agent_id]][:generation] = peer[:generation] || 1
|
|
98
|
-
merged += 1
|
|
99
|
-
elsif (peer[:generation] || 0) > (local[:generation] || 0)
|
|
100
|
-
local.merge!(peer.slice(:capabilities, :status, :generation, :last_seen))
|
|
101
|
-
local[:capabilities] = (local[:capabilities] || []).map(&:to_sym)
|
|
102
|
-
merged += 1
|
|
97
|
+
incoming_peers.each do |raw_peer|
|
|
98
|
+
peer = raw_peer.transform_keys(&:to_sym)
|
|
99
|
+
if peer[:node] == local_node_name
|
|
100
|
+
conflict_agents << peer[:agent_id] if sender && sender != local_node_name
|
|
101
|
+
next
|
|
103
102
|
end
|
|
103
|
+
merged += 1 if upsert_remote_peer(peer, registry)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if conflict_agents.any?
|
|
107
|
+
publish_mesh_conflict(local_node: local_node_name, conflicting_node: sender,
|
|
108
|
+
conflict_agents: conflict_agents)
|
|
104
109
|
end
|
|
105
110
|
|
|
106
|
-
{ success: true, merged: merged, total_peers: incoming_peers.size }
|
|
111
|
+
{ success: true, merged: merged, total_peers: incoming_peers.size, conflicts: conflict_agents.size }
|
|
107
112
|
rescue StandardError => e
|
|
108
113
|
{ success: false, reason: :error, message: e.message }
|
|
109
114
|
end
|
|
110
115
|
|
|
116
|
+
def dispatch_gossip_message(type: nil, sender: nil, peers: [], **)
|
|
117
|
+
case type
|
|
118
|
+
when 'mesh_gossip'
|
|
119
|
+
merge_gossip(incoming_peers: peers, sender: sender)
|
|
120
|
+
else
|
|
121
|
+
{ success: false, error: "unknown gossip message type: #{type}" }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
111
125
|
private
|
|
112
126
|
|
|
113
127
|
def publish_mesh_departure(agent_id:, capabilities:)
|
|
@@ -131,21 +145,72 @@ module Legion
|
|
|
131
145
|
).publish
|
|
132
146
|
end
|
|
133
147
|
|
|
148
|
+
def publish_mesh_conflict(local_node:, conflicting_node:, conflict_agents:)
|
|
149
|
+
return unless defined?(Legion::Extensions::Mesh::Transport::Messages::MeshConflict)
|
|
150
|
+
|
|
151
|
+
Legion::Extensions::Mesh::Transport::Messages::MeshConflict.new(
|
|
152
|
+
local_node: local_node,
|
|
153
|
+
conflicting_node: conflicting_node,
|
|
154
|
+
conflict_agents: conflict_agents
|
|
155
|
+
).publish
|
|
156
|
+
log.warn "[mesh] split-brain detected: node=#{conflicting_node} claims agents on #{local_node}: #{conflict_agents.join(',')}"
|
|
157
|
+
rescue StandardError => e
|
|
158
|
+
log.warn "[mesh] failed to publish conflict signal: #{e.message}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def upsert_remote_peer(peer, registry)
|
|
162
|
+
peer_table.upsert(peer[:agent_id], peer)
|
|
163
|
+
local = registry.agents[peer[:agent_id]]
|
|
164
|
+
if local.nil?
|
|
165
|
+
registry.register_agent(
|
|
166
|
+
peer[:agent_id],
|
|
167
|
+
capabilities: (peer[:capabilities] || []).map(&:to_sym),
|
|
168
|
+
source: (peer[:source] || :native).to_sym,
|
|
169
|
+
node: peer[:node]
|
|
170
|
+
)
|
|
171
|
+
entry = registry.agents[peer[:agent_id]]
|
|
172
|
+
entry[:generation] = peer[:generation] || 1
|
|
173
|
+
entry[:last_seen] = parse_last_seen(peer[:last_seen])
|
|
174
|
+
true
|
|
175
|
+
elsif (peer[:generation] || 0) > (local[:generation] || 0)
|
|
176
|
+
local.merge!(peer.slice(:capabilities, :status, :generation))
|
|
177
|
+
local[:capabilities] = (local[:capabilities] || []).map(&:to_sym)
|
|
178
|
+
local[:last_seen] = parse_last_seen(peer[:last_seen])
|
|
179
|
+
true
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def parse_last_seen(value)
|
|
184
|
+
if value.nil?
|
|
185
|
+
Time.now.utc
|
|
186
|
+
elsif value.is_a?(Time)
|
|
187
|
+
value
|
|
188
|
+
else
|
|
189
|
+
Time.parse(value.to_s)
|
|
190
|
+
end
|
|
191
|
+
rescue ArgumentError => _e
|
|
192
|
+
Time.now.utc
|
|
193
|
+
end
|
|
194
|
+
|
|
134
195
|
def gossip_max_peers
|
|
135
196
|
settings = Legion::Settings.dig(:mesh, :gossip)
|
|
136
197
|
(settings.is_a?(Hash) ? settings[:max_peers_per_message] : nil) || 100
|
|
137
|
-
rescue StandardError
|
|
198
|
+
rescue StandardError => _e
|
|
138
199
|
100
|
|
139
200
|
end
|
|
140
201
|
|
|
141
202
|
def local_node_name
|
|
142
203
|
Legion::Settings[:client][:name]
|
|
143
|
-
rescue StandardError
|
|
204
|
+
rescue StandardError => _e
|
|
144
205
|
'unknown'
|
|
145
206
|
end
|
|
146
207
|
|
|
147
208
|
def mesh_registry
|
|
148
|
-
@mesh_registry ||= Helpers::Registry.new
|
|
209
|
+
@mesh_registry ||= Helpers::Registry.new # rubocop:disable Legion/Singleton/UseInstance
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def peer_table
|
|
213
|
+
@peer_table ||= Helpers::PeerTable.new
|
|
149
214
|
end
|
|
150
215
|
end
|
|
151
216
|
end
|
|
@@ -7,8 +7,8 @@ module Legion
|
|
|
7
7
|
module Mesh
|
|
8
8
|
module Runners
|
|
9
9
|
module Preferences
|
|
10
|
-
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
11
|
-
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
11
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
12
12
|
|
|
13
13
|
def query_preferences(target_agent_id:, domains: nil, callback: nil, ttl: 5, **)
|
|
14
14
|
default_profile = Helpers::PreferenceProfile.resolve(owner_id: target_agent_id)
|
|
@@ -96,7 +96,7 @@ module Legion
|
|
|
96
96
|
else
|
|
97
97
|
'unknown'
|
|
98
98
|
end
|
|
99
|
-
rescue StandardError
|
|
99
|
+
rescue StandardError => _e
|
|
100
100
|
'unknown'
|
|
101
101
|
end
|
|
102
102
|
|
|
@@ -110,7 +110,7 @@ module Legion
|
|
|
110
110
|
correlation_id: msg[:correlation_id]
|
|
111
111
|
).publish
|
|
112
112
|
rescue StandardError => e
|
|
113
|
-
|
|
113
|
+
log.debug("[mesh] failed to publish preference response: #{e.message}")
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
def publish_preference_query(target_agent_id:, correlation_id:, domains:)
|
|
@@ -125,7 +125,7 @@ module Legion
|
|
|
125
125
|
|
|
126
126
|
def default_preference_callback(target_agent_id:)
|
|
127
127
|
lambda do |_profile|
|
|
128
|
-
|
|
128
|
+
log.debug("[mesh] received and cached preferences for #{target_agent_id}")
|
|
129
129
|
end
|
|
130
130
|
end
|
|
131
131
|
|
|
@@ -138,11 +138,11 @@ module Legion
|
|
|
138
138
|
evaluator = Object.new.extend(trust_mod)
|
|
139
139
|
result = evaluator.get_trust(agent_id: agent_id, domain: :general)
|
|
140
140
|
|
|
141
|
-
return :allowed unless result[:found]
|
|
141
|
+
return :allowed unless result[:found] # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
142
142
|
|
|
143
143
|
composite = result.dig(:trust, :composite) || 0.0
|
|
144
144
|
composite >= Helpers::Topology::TRUST_CONSIDER_THRESHOLD ? :allowed : :denied
|
|
145
|
-
rescue StandardError
|
|
145
|
+
rescue StandardError => _e
|
|
146
146
|
:allowed
|
|
147
147
|
end
|
|
148
148
|
|
|
@@ -10,7 +10,7 @@ module Legion
|
|
|
10
10
|
module Extensions
|
|
11
11
|
module Mesh
|
|
12
12
|
module Runners
|
|
13
|
-
module TaskRequest
|
|
13
|
+
module TaskRequest # rubocop:disable Legion/Extension/RunnerIncludeHelpers
|
|
14
14
|
include Runners::Mesh
|
|
15
15
|
|
|
16
16
|
DEFAULT_TIMEOUT = 30
|
|
@@ -65,10 +65,10 @@ module Legion
|
|
|
65
65
|
private
|
|
66
66
|
|
|
67
67
|
def resolve_target(to)
|
|
68
|
-
return to if mesh_registry.agents.key?(to)
|
|
68
|
+
return to if mesh_registry.agents.key?(to) # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
69
69
|
|
|
70
70
|
agents = mesh_registry.find_by_capability(to.to_sym)
|
|
71
|
-
return nil if agents.empty?
|
|
71
|
+
return nil if agents.empty? # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
72
72
|
|
|
73
73
|
online = agents.select { |a| a[:status] == :online }
|
|
74
74
|
(online.empty? ? agents : online).sample[:agent_id]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Mesh
|
|
6
|
+
module Transport
|
|
7
|
+
module Messages
|
|
8
|
+
class MeshConflict < Legion::Transport::Message
|
|
9
|
+
def exchange
|
|
10
|
+
Legion::Transport::Exchanges::Node
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def routing_key
|
|
14
|
+
'mesh.conflict'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def message
|
|
18
|
+
{
|
|
19
|
+
type: 'mesh_conflict',
|
|
20
|
+
local_node: @options[:local_node],
|
|
21
|
+
conflicting_node: @options[:conflicting_node],
|
|
22
|
+
conflict_agents: @options[:conflict_agents] || [],
|
|
23
|
+
detected_at: Time.now.to_s
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def type
|
|
28
|
+
'mesh_conflict'
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -7,22 +7,26 @@ require 'legion/extensions/mesh/helpers/preference_profile'
|
|
|
7
7
|
require 'legion/extensions/mesh/helpers/pending_requests'
|
|
8
8
|
require 'legion/extensions/mesh/helpers/delegation'
|
|
9
9
|
require 'legion/extensions/mesh/helpers/peer_verify'
|
|
10
|
+
require 'legion/extensions/mesh/helpers/peer_table'
|
|
10
11
|
require 'legion/extensions/mesh/runners/mesh'
|
|
11
12
|
require 'legion/extensions/mesh/runners/preferences'
|
|
12
13
|
require 'legion/extensions/mesh/runners/delegation'
|
|
13
14
|
require 'legion/extensions/mesh/runners/task_request'
|
|
14
15
|
|
|
15
|
-
if
|
|
16
|
+
if Legion.const_defined?(:Transport, false)
|
|
16
17
|
require 'legion/extensions/mesh/transport/messages/preference_query'
|
|
17
18
|
require 'legion/extensions/mesh/transport/messages/preference_response'
|
|
18
19
|
require 'legion/extensions/mesh/transport/messages/mesh_departure'
|
|
20
|
+
require 'legion/extensions/mesh/transport/messages/gossip'
|
|
21
|
+
require 'legion/extensions/mesh/transport/messages/mesh_conflict'
|
|
19
22
|
require 'legion/extensions/mesh/transport/queues/preference'
|
|
23
|
+
require 'legion/extensions/mesh/transport/queues/gossip'
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
module Legion
|
|
23
27
|
module Extensions
|
|
24
28
|
module Mesh
|
|
25
|
-
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
29
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core, false
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Stub the framework subscription base class
|
|
4
|
+
unless defined?(Legion::Extensions::Actors::Subscription)
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Actors
|
|
8
|
+
class Subscription # rubocop:disable Lint/EmptyClass
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
$LOADED_FEATURES << 'legion/extensions/actors/subscription'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Stub transport queue class if not loaded
|
|
18
|
+
unless defined?(Legion::Extensions::Mesh::Transport::Queues::Gossip)
|
|
19
|
+
module Legion
|
|
20
|
+
module Extensions
|
|
21
|
+
module Mesh
|
|
22
|
+
module Transport
|
|
23
|
+
module Queues
|
|
24
|
+
class Gossip # rubocop:disable Lint/EmptyClass
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
require 'legion/extensions/mesh/runners/mesh'
|
|
34
|
+
require 'legion/extensions/mesh/actors/gossip_listener'
|
|
35
|
+
|
|
36
|
+
RSpec.describe Legion::Extensions::Mesh::Actor::GossipListener do
|
|
37
|
+
subject(:actor) { described_class.new }
|
|
38
|
+
|
|
39
|
+
describe '#runner_class' do
|
|
40
|
+
it 'returns the Mesh runner module' do
|
|
41
|
+
expect(actor.runner_class).to eq(Legion::Extensions::Mesh::Runners::Mesh)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#runner_function' do
|
|
46
|
+
it 'returns dispatch_gossip_message' do
|
|
47
|
+
expect(actor.runner_function).to eq('dispatch_gossip_message')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe '#use_runner?' do
|
|
52
|
+
it 'returns false (actor handles dispatch directly without runner pipeline)' do
|
|
53
|
+
expect(actor.use_runner?).to be false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe '#check_subtask?' do
|
|
58
|
+
it 'returns false' do
|
|
59
|
+
expect(actor.check_subtask?).to be false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#generate_task?' do
|
|
64
|
+
it 'returns false' do
|
|
65
|
+
expect(actor.generate_task?).to be false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe '#queue' do
|
|
70
|
+
it 'returns the Gossip queue class' do
|
|
71
|
+
expect(actor.queue).to eq(Legion::Extensions::Mesh::Transport::Queues::Gossip)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe '#enabled?' do
|
|
76
|
+
context 'when Mesh runner and Transport are both available' do
|
|
77
|
+
it 'returns truthy' do
|
|
78
|
+
stub_const('Legion::Transport', Module.new)
|
|
79
|
+
expect(actor.enabled?).to be_truthy
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
context 'when Transport is not available' do
|
|
84
|
+
it 'returns falsey' do
|
|
85
|
+
hide_const('Legion::Transport')
|
|
86
|
+
expect(actor.enabled?).to be_falsey
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/mesh/helpers/peer_table'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Mesh::Helpers::PeerTable do
|
|
7
|
+
subject(:table) { described_class.new(ttl: 60) }
|
|
8
|
+
|
|
9
|
+
describe '#upsert / #get' do
|
|
10
|
+
it 'stores and retrieves a peer entry' do
|
|
11
|
+
table.upsert('agent-1', { node: 'node-01' })
|
|
12
|
+
entry = table.get('agent-1')
|
|
13
|
+
expect(entry[:node]).to eq('node-01')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'sets last_seen_at on upsert' do
|
|
17
|
+
table.upsert('agent-1', {})
|
|
18
|
+
expect(table.get('agent-1')[:last_seen_at]).to be_a(Time)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'overwrites an existing entry on re-upsert' do
|
|
22
|
+
table.upsert('agent-1', { node: 'node-01' })
|
|
23
|
+
table.upsert('agent-1', { node: 'node-02' })
|
|
24
|
+
expect(table.get('agent-1')[:node]).to eq('node-02')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'returns nil for unknown agent' do
|
|
28
|
+
expect(table.get('missing')).to be_nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#expire' do
|
|
33
|
+
let(:short_ttl_table) { described_class.new(ttl: 1) }
|
|
34
|
+
|
|
35
|
+
it 'removes peers whose last_seen_at is older than TTL' do
|
|
36
|
+
short_ttl_table.upsert('old-agent', {})
|
|
37
|
+
entry = short_ttl_table.instance_variable_get(:@peers)['old-agent']
|
|
38
|
+
entry[:last_seen_at] = Time.now.utc - 120
|
|
39
|
+
expired = short_ttl_table.expire
|
|
40
|
+
expect(expired).to include('old-agent')
|
|
41
|
+
expect(short_ttl_table.get('old-agent')).to be_nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'keeps peers within TTL' do
|
|
45
|
+
short_ttl_table.upsert('fresh-agent', {})
|
|
46
|
+
short_ttl_table.expire
|
|
47
|
+
expect(short_ttl_table.get('fresh-agent')).not_to be_nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'returns an array of expired agent IDs' do
|
|
51
|
+
short_ttl_table.upsert('e1', {})
|
|
52
|
+
short_ttl_table.upsert('e2', {})
|
|
53
|
+
short_ttl_table.instance_variable_get(:@peers).each_value { |e| e[:last_seen_at] = Time.now.utc - 120 }
|
|
54
|
+
expired = short_ttl_table.expire
|
|
55
|
+
expect(expired).to contain_exactly('e1', 'e2')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'returns empty array when nothing is expired' do
|
|
59
|
+
table.upsert('fresh', {})
|
|
60
|
+
expect(table.expire).to be_empty
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe 'thread safety' do
|
|
65
|
+
it 'handles concurrent upserts without data corruption' do
|
|
66
|
+
threads = Array.new(10) do |i|
|
|
67
|
+
Thread.new { table.upsert("agent-#{i}", { node: "node-#{i}" }) }
|
|
68
|
+
end
|
|
69
|
+
threads.each(&:join)
|
|
70
|
+
expect(table.count).to eq(10)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '#count' do
|
|
75
|
+
it 'reflects the number of stored peers' do
|
|
76
|
+
table.upsert('a1', {})
|
|
77
|
+
table.upsert('a2', {})
|
|
78
|
+
expect(table.count).to eq(2)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe '#remove' do
|
|
83
|
+
it 'removes a specific peer' do
|
|
84
|
+
table.upsert('agent-1', {})
|
|
85
|
+
table.remove('agent-1')
|
|
86
|
+
expect(table.get('agent-1')).to be_nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'does nothing for unknown agent' do
|
|
90
|
+
expect { table.remove('missing') }.not_to raise_error
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe '#all' do
|
|
95
|
+
it 'returns a copy of all peers' do
|
|
96
|
+
table.upsert('a1', { node: 'n1' })
|
|
97
|
+
all = table.all
|
|
98
|
+
expect(all.keys).to include('a1')
|
|
99
|
+
all.delete('a1')
|
|
100
|
+
expect(table.get('a1')).not_to be_nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -7,7 +7,7 @@ RSpec.describe 'Gossip runner methods' do
|
|
|
7
7
|
|
|
8
8
|
before do
|
|
9
9
|
allow(subject).to receive(:mesh_registry).and_return(
|
|
10
|
-
Legion::Extensions::Mesh::Helpers::Registry.new
|
|
10
|
+
Legion::Extensions::Mesh::Helpers::Registry.new # rubocop:disable Legion/Singleton/UseInstance
|
|
11
11
|
)
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -60,6 +60,49 @@ RSpec.describe 'Gossip runner methods' do
|
|
|
60
60
|
result = subject.merge_gossip(incoming_peers: incoming, sender: 'node-02')
|
|
61
61
|
expect(result[:merged]).to eq(0)
|
|
62
62
|
end
|
|
63
|
+
|
|
64
|
+
context 'split-brain detection' do
|
|
65
|
+
before do
|
|
66
|
+
allow(subject).to receive(:local_node_name).and_return('local-node')
|
|
67
|
+
allow(subject).to receive(:publish_mesh_conflict)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'detects a conflict when incoming peers claim the local node from a different sender' do
|
|
71
|
+
incoming = [
|
|
72
|
+
{ agent_id: 'local-agent', node: 'local-node', generation: 1 }
|
|
73
|
+
]
|
|
74
|
+
result = subject.merge_gossip(incoming_peers: incoming, sender: 'other-node')
|
|
75
|
+
expect(result[:conflicts]).to eq(1)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'publishes MeshConflict when split-brain is detected' do
|
|
79
|
+
incoming = [
|
|
80
|
+
{ agent_id: 'local-agent', node: 'local-node', generation: 1 }
|
|
81
|
+
]
|
|
82
|
+
subject.merge_gossip(incoming_peers: incoming, sender: 'other-node')
|
|
83
|
+
expect(subject).to have_received(:publish_mesh_conflict).with(
|
|
84
|
+
local_node: 'local-node',
|
|
85
|
+
conflicting_node: 'other-node',
|
|
86
|
+
conflict_agents: ['local-agent']
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'does not publish MeshConflict when sender is the local node' do
|
|
91
|
+
incoming = [
|
|
92
|
+
{ agent_id: 'local-agent', node: 'local-node', generation: 1 }
|
|
93
|
+
]
|
|
94
|
+
subject.merge_gossip(incoming_peers: incoming, sender: 'local-node')
|
|
95
|
+
expect(subject).not_to have_received(:publish_mesh_conflict)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'reports zero conflicts when no peers claim the local node' do
|
|
99
|
+
incoming = [
|
|
100
|
+
{ agent_id: 'remote-agent', node: 'remote-node', generation: 1 }
|
|
101
|
+
]
|
|
102
|
+
result = subject.merge_gossip(incoming_peers: incoming, sender: 'remote-node')
|
|
103
|
+
expect(result[:conflicts]).to eq(0)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
63
106
|
end
|
|
64
107
|
|
|
65
108
|
describe '#publish_gossip' do
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
unless defined?(Legion::Transport::Message)
|
|
6
|
+
module Legion
|
|
7
|
+
module Transport
|
|
8
|
+
class Message
|
|
9
|
+
def initialize(**options)
|
|
10
|
+
@options = options
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless defined?(Legion::Transport::Exchanges::Node)
|
|
18
|
+
module Legion
|
|
19
|
+
module Transport
|
|
20
|
+
module Exchanges
|
|
21
|
+
class Node # rubocop:disable Lint/EmptyClass
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
require 'legion/extensions/mesh/transport/messages/mesh_conflict'
|
|
29
|
+
|
|
30
|
+
RSpec.describe Legion::Extensions::Mesh::Transport::Messages::MeshConflict do
|
|
31
|
+
subject(:msg) do
|
|
32
|
+
described_class.new(
|
|
33
|
+
local_node: 'node-01',
|
|
34
|
+
conflicting_node: 'node-02',
|
|
35
|
+
conflict_agents: %w[agent-a agent-b]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe '#exchange' do
|
|
40
|
+
it 'returns the Node exchange' do
|
|
41
|
+
expect(msg.exchange).to eq(Legion::Transport::Exchanges::Node)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#routing_key' do
|
|
46
|
+
it 'uses mesh.conflict' do
|
|
47
|
+
expect(msg.routing_key).to eq('mesh.conflict')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe '#message' do
|
|
52
|
+
it 'includes type mesh_conflict' do
|
|
53
|
+
expect(msg.message[:type]).to eq('mesh_conflict')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'includes local_node' do
|
|
57
|
+
expect(msg.message[:local_node]).to eq('node-01')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'includes conflicting_node' do
|
|
61
|
+
expect(msg.message[:conflicting_node]).to eq('node-02')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'includes conflict_agents list' do
|
|
65
|
+
expect(msg.message[:conflict_agents]).to eq(%w[agent-a agent-b])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'includes detected_at timestamp string' do
|
|
69
|
+
expect(msg.message[:detected_at]).to be_a(String)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '#type' do
|
|
74
|
+
it 'returns mesh_conflict' do
|
|
75
|
+
expect(msg.type).to eq('mesh_conflict')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe 'conflict_agents default' do
|
|
80
|
+
subject(:msg_no_agents) do
|
|
81
|
+
described_class.new(local_node: 'node-01', conflicting_node: 'node-02')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'defaults conflict_agents to empty array' do
|
|
85
|
+
expect(msg_no_agents.message[:conflict_agents]).to eq([])
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-mesh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -147,12 +147,14 @@ files:
|
|
|
147
147
|
- lex-mesh.gemspec
|
|
148
148
|
- lib/legion/extensions/mesh.rb
|
|
149
149
|
- lib/legion/extensions/mesh/actors/gossip.rb
|
|
150
|
+
- lib/legion/extensions/mesh/actors/gossip_listener.rb
|
|
150
151
|
- lib/legion/extensions/mesh/actors/heartbeat.rb
|
|
151
152
|
- lib/legion/extensions/mesh/actors/pending_expiry.rb
|
|
152
153
|
- lib/legion/extensions/mesh/actors/preference_listener.rb
|
|
153
154
|
- lib/legion/extensions/mesh/actors/silence_watchdog.rb
|
|
154
155
|
- lib/legion/extensions/mesh/client.rb
|
|
155
156
|
- lib/legion/extensions/mesh/helpers/delegation.rb
|
|
157
|
+
- lib/legion/extensions/mesh/helpers/peer_table.rb
|
|
156
158
|
- lib/legion/extensions/mesh/helpers/peer_verify.rb
|
|
157
159
|
- lib/legion/extensions/mesh/helpers/pending_requests.rb
|
|
158
160
|
- lib/legion/extensions/mesh/helpers/preference_profile.rb
|
|
@@ -163,12 +165,14 @@ files:
|
|
|
163
165
|
- lib/legion/extensions/mesh/runners/preferences.rb
|
|
164
166
|
- lib/legion/extensions/mesh/runners/task_request.rb
|
|
165
167
|
- lib/legion/extensions/mesh/transport/messages/gossip.rb
|
|
168
|
+
- lib/legion/extensions/mesh/transport/messages/mesh_conflict.rb
|
|
166
169
|
- lib/legion/extensions/mesh/transport/messages/mesh_departure.rb
|
|
167
170
|
- lib/legion/extensions/mesh/transport/messages/preference_query.rb
|
|
168
171
|
- lib/legion/extensions/mesh/transport/messages/preference_response.rb
|
|
169
172
|
- lib/legion/extensions/mesh/transport/queues/gossip.rb
|
|
170
173
|
- lib/legion/extensions/mesh/transport/queues/preference.rb
|
|
171
174
|
- lib/legion/extensions/mesh/version.rb
|
|
175
|
+
- spec/legion/extensions/mesh/actors/gossip_listener_spec.rb
|
|
172
176
|
- spec/legion/extensions/mesh/actors/gossip_spec.rb
|
|
173
177
|
- spec/legion/extensions/mesh/actors/heartbeat_spec.rb
|
|
174
178
|
- spec/legion/extensions/mesh/actors/pending_expiry_spec.rb
|
|
@@ -176,6 +180,7 @@ files:
|
|
|
176
180
|
- spec/legion/extensions/mesh/actors/silence_watchdog_spec.rb
|
|
177
181
|
- spec/legion/extensions/mesh/client_spec.rb
|
|
178
182
|
- spec/legion/extensions/mesh/helpers/delegation_spec.rb
|
|
183
|
+
- spec/legion/extensions/mesh/helpers/peer_table_spec.rb
|
|
179
184
|
- spec/legion/extensions/mesh/helpers/peer_verify_spec.rb
|
|
180
185
|
- spec/legion/extensions/mesh/helpers/pending_requests_spec.rb
|
|
181
186
|
- spec/legion/extensions/mesh/helpers/preference_profile_spec.rb
|
|
@@ -187,6 +192,7 @@ files:
|
|
|
187
192
|
- spec/legion/extensions/mesh/runners/preferences_spec.rb
|
|
188
193
|
- spec/legion/extensions/mesh/runners/task_request_spec.rb
|
|
189
194
|
- spec/legion/extensions/mesh/transport/messages/gossip_spec.rb
|
|
195
|
+
- spec/legion/extensions/mesh/transport/messages/mesh_conflict_spec.rb
|
|
190
196
|
- spec/legion/extensions/mesh/transport/messages/mesh_departure_spec.rb
|
|
191
197
|
- spec/legion/extensions/mesh/transport/messages/preference_query_spec.rb
|
|
192
198
|
- spec/legion/extensions/mesh/transport/messages/preference_response_spec.rb
|