lex-mesh 0.4.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edbe1f87a3c1b6cad5d87f2ee8c1b04551b9bbd36804c7d4f71ed18f9833f58e
4
- data.tar.gz: 50f43be7cffacaa7b23e316d2f69a1763a2b82a7437f7d1e854e4f5cd088bc3e
3
+ metadata.gz: b16d673e30a813f0fa38f61386eae0e9a72a35cc2b0eedc6b45ced88a4acadf7
4
+ data.tar.gz: dc7a12c1de2412117ac9ce56c7803626dba16604f9cbfba57ca41e72e55dc3b5
5
5
  SHA512:
6
- metadata.gz: d14a5e8b04963df9a79bdc216c27c669330801c2d0677299908a15cbf2abd125b2e841a245a678823c6c9efab9b83c13e7b0d35b230ae0ba13ddbc42c0e152fc
7
- data.tar.gz: fe99ad0868788b91826c3bfa25dbbaa131e63b471b409e9ae110c4296bb2af8f44a366939331fff63f9358b6d49e1f513bc268aaf3f2c6c00ac530791c4a4060
6
+ metadata.gz: 7abe441618ba1fca371412adf3a2a48f16f54adc4130d060369e1fcdc7222a777fcc48e165576e74e4cf1892fdd9a42f4562242523bf2d9e8239cdb8af771b4c
7
+ data.tar.gz: 61e2b0e8aba072966d10e820db3708ff2c76ecfcda9fc73e99bcd61eff051323c81ea5c1cef0e1c56ce126604369983205f32e732376eab53ad1fae4af1857f5
@@ -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
@@ -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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
3
5
  module Legion
4
6
  module Extensions
5
7
  module Mesh
@@ -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, **) # rubocop:disable Lint/UnusedMethodArgument
93
+ def merge_gossip(incoming_peers:, sender: nil, **)
82
94
  registry = mesh_registry
95
+ conflict_agents = []
83
96
  merged = 0
84
-
85
- incoming_peers.each do |peer|
86
- peer = peer.transform_keys(&:to_sym)
87
- next if peer[:node] == local_node_name
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,6 +145,53 @@ 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
@@ -147,6 +208,10 @@ module Legion
147
208
  def mesh_registry
148
209
  @mesh_registry ||= Helpers::Registry.new # rubocop:disable Legion/Singleton/UseInstance
149
210
  end
211
+
212
+ def peer_table
213
+ @peer_table ||= Helpers::PeerTable.new
214
+ end
150
215
  end
151
216
  end
152
217
  end
@@ -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
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Mesh
6
- VERSION = '0.4.2'
6
+ VERSION = '0.4.3'
7
7
  end
8
8
  end
9
9
  end
@@ -7,6 +7,7 @@ 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'
@@ -16,7 +17,10 @@ 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
@@ -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
@@ -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.2
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