lex-mesh 0.2.4 → 0.2.5

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: 25cafa977ed6aaf671c903e71895312253a412c32c141e8de9674b1a86747bff
4
- data.tar.gz: c7bbdde6f4c065f0fdaea58a5a5124bd53e49f0c48e0d275de0828bd93ec2439
3
+ metadata.gz: 650958df48e1ad4fa390b3fecb3ab2817dc535c279449dc4ce114e92f59cfff2
4
+ data.tar.gz: ff13ef5a0e18147daa15b72579de24f3691bec50f5f124627d2b503208cfba82
5
5
  SHA512:
6
- metadata.gz: 76357a6ca4c31089fd7f5317097f935b07df6349476b344bd2681dccfbd796d79b1061922b22d42ca45b92d045bdbf901ed8d6c502f525cff3a668099dfea56c
7
- data.tar.gz: 9e17a698c0b941fd2000ab9affffa87cd3bad983b37acacce794ee0fb4d975a8cdc04eee44480f976856e712fb6af8dfa80f976d07c2fd94ea302ba61f31b389
6
+ metadata.gz: fc982147fb2a22e4a5bb40fa8f7a767e87aed754f59474d4937059a831adcfeae6b4f4e4c9add47359c3b6a69200a0cdea21c1bafd1b00dfd74501c93415eb40
7
+ data.tar.gz: e34a1a0d9ae8481bb40949bda0e47d6e1647856777214d133acf10c121b8e70962bb85c10fda16120b9dd7bc958828fc20a73851ab259c89fa813d7df2236f26
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Mesh
8
+ module Actor
9
+ class Gossip < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::Mesh::Runners::Mesh
12
+ end
13
+
14
+ def runner_function
15
+ :publish_gossip
16
+ end
17
+
18
+ def time
19
+ 15
20
+ end
21
+
22
+ def use_runner?
23
+ true
24
+ end
25
+
26
+ def check_subtask?
27
+ false
28
+ end
29
+
30
+ def generate_task?
31
+ false
32
+ end
33
+
34
+ def args
35
+ {}
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -13,11 +13,14 @@ module Legion
13
13
  @messages = []
14
14
  end
15
15
 
16
- def register_agent(agent_id, capabilities: [], endpoint: nil)
16
+ def register_agent(agent_id, capabilities: [], endpoint: nil, source: :native, node: nil)
17
17
  @agents[agent_id] = {
18
18
  agent_id: agent_id,
19
19
  capabilities: capabilities,
20
20
  endpoint: endpoint,
21
+ source: source,
22
+ node: node || local_node_name,
23
+ generation: 1,
21
24
  registered_at: Time.now.utc,
22
25
  last_seen: Time.now.utc,
23
26
  status: :online
@@ -39,6 +42,7 @@ module Legion
39
42
 
40
43
  agent[:last_seen] = Time.now.utc
41
44
  agent[:status] = :online
45
+ agent[:generation] = (agent[:generation] || 0) + 1
42
46
  end
43
47
 
44
48
  def find_by_capability(capability)
@@ -81,9 +85,21 @@ module Legion
81
85
  @agents.values.select { |a| a[:status] == :online }
82
86
  end
83
87
 
88
+ def all_agents
89
+ @agents.values
90
+ end
91
+
84
92
  def count
85
93
  @agents.size
86
94
  end
95
+
96
+ private
97
+
98
+ def local_node_name
99
+ Legion::Settings[:client][:name]
100
+ rescue StandardError
101
+ 'unknown'
102
+ end
87
103
  end
88
104
  end
89
105
  end
@@ -64,6 +64,50 @@ module Legion
64
64
  { success: true, expired: expired, count: expired.size }
65
65
  end
66
66
 
67
+ def publish_gossip(**)
68
+ registry = mesh_registry
69
+ peers = registry.all_agents.first(gossip_max_peers).map do |agent|
70
+ agent.slice(:agent_id, :capabilities, :node, :source, :status, :generation,
71
+ :last_seen, :registered_at).transform_values { |v| v.is_a?(Time) ? v.to_s : v }
72
+ end
73
+
74
+ @gossip_round = (@gossip_round || 0) + 1
75
+ publish_gossip_message(peers)
76
+ { success: true, peers_broadcast: peers.size, gossip_round: @gossip_round }
77
+ rescue StandardError => e
78
+ { success: false, reason: :error, message: e.message }
79
+ end
80
+
81
+ def merge_gossip(incoming_peers:, sender: nil, **) # rubocop:disable Lint/UnusedMethodArgument
82
+ registry = mesh_registry
83
+ 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
103
+ end
104
+ end
105
+
106
+ { success: true, merged: merged, total_peers: incoming_peers.size }
107
+ rescue StandardError => e
108
+ { success: false, reason: :error, message: e.message }
109
+ end
110
+
67
111
  private
68
112
 
69
113
  def publish_mesh_departure(agent_id:, capabilities:)
@@ -77,6 +121,29 @@ module Legion
77
121
  Legion::Logging.warn "[mesh] failed to publish departure signal: #{e.message}"
78
122
  end
79
123
 
124
+ def publish_gossip_message(peers)
125
+ return unless defined?(Legion::Extensions::Mesh::Transport::Messages::Gossip)
126
+
127
+ Legion::Extensions::Mesh::Transport::Messages::Gossip.new(
128
+ sender: local_node_name,
129
+ gossip_round: @gossip_round,
130
+ peers: peers
131
+ ).publish
132
+ end
133
+
134
+ def gossip_max_peers
135
+ settings = Legion::Settings.dig(:mesh, :gossip)
136
+ (settings.is_a?(Hash) ? settings[:max_peers_per_message] : nil) || 100
137
+ rescue StandardError
138
+ 100
139
+ end
140
+
141
+ def local_node_name
142
+ Legion::Settings[:client][:name]
143
+ rescue StandardError
144
+ 'unknown'
145
+ end
146
+
80
147
  def mesh_registry
81
148
  @mesh_registry ||= Helpers::Registry.new
82
149
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mesh
6
+ module Transport
7
+ module Messages
8
+ class Gossip < Legion::Transport::Message
9
+ def exchange
10
+ Legion::Transport::Exchanges::Node
11
+ end
12
+
13
+ def routing_key
14
+ 'mesh.gossip'
15
+ end
16
+
17
+ def message
18
+ {
19
+ type: 'mesh_gossip',
20
+ sender: @options[:sender],
21
+ gossip_round: @options[:gossip_round] || 0,
22
+ peers: @options[:peers] || []
23
+ }
24
+ end
25
+
26
+ def type
27
+ 'mesh_gossip'
28
+ end
29
+
30
+ def encrypt?
31
+ false
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mesh
6
+ module Transport
7
+ module Queues
8
+ class Gossip < Legion::Transport::Queue
9
+ def queue_name
10
+ 'mesh.gossip'
11
+ end
12
+
13
+ def exchange
14
+ Legion::Transport::Exchanges::Node
15
+ end
16
+
17
+ def routing_key
18
+ 'mesh.gossip'
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Mesh
6
- VERSION = '0.2.4'
6
+ VERSION = '0.2.5'
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stub the framework actor base class since legionio gem is not available in test
4
+ unless defined?(Legion::Extensions::Actors::Every)
5
+ module Legion
6
+ module Extensions
7
+ module Actors
8
+ class Every # rubocop:disable Lint/EmptyClass
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ $LOADED_FEATURES << 'legion/extensions/actors/every' unless $LOADED_FEATURES.include?('legion/extensions/actors/every')
16
+
17
+ require 'legion/extensions/mesh/actors/gossip'
18
+
19
+ RSpec.describe Legion::Extensions::Mesh::Actor::Gossip do
20
+ it 'fires every 15 seconds' do
21
+ expect(described_class.new.time).to eq(15)
22
+ end
23
+
24
+ it 'calls publish_gossip runner function' do
25
+ instance = described_class.allocate
26
+ expect(instance.runner_function).to eq(:publish_gossip)
27
+ end
28
+ end
@@ -313,6 +313,32 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::Registry do
313
313
  end
314
314
  end
315
315
 
316
+ describe 'gossip fields' do
317
+ it 'stores source, node, and generation on registration' do
318
+ registry.register_agent('agent-1', capabilities: [:test], source: :native, node: 'node-01')
319
+ agent = registry.agents['agent-1']
320
+ expect(agent[:source]).to eq(:native)
321
+ expect(agent[:node]).to eq('node-01')
322
+ expect(agent[:generation]).to eq(1)
323
+ end
324
+
325
+ it 'increments generation on heartbeat' do
326
+ registry.register_agent('agent-1', capabilities: [:test])
327
+ registry.heartbeat('agent-1')
328
+ expect(registry.agents['agent-1'][:generation]).to eq(2)
329
+ end
330
+ end
331
+
332
+ describe '#all_agents' do
333
+ it 'returns all agent records as an array' do
334
+ registry.register_agent('a1', capabilities: [:search])
335
+ registry.register_agent('a2', capabilities: [:compute])
336
+ all = registry.all_agents
337
+ expect(all).to be_an(Array)
338
+ expect(all.size).to eq(2)
339
+ end
340
+ end
341
+
316
342
  describe '#count' do
317
343
  it 'returns 0 for an empty registry' do
318
344
  expect(registry.count).to eq(0)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Gossip runner methods' do
6
+ subject { Object.new.extend(Legion::Extensions::Mesh::Runners::Mesh) }
7
+
8
+ before do
9
+ allow(subject).to receive(:mesh_registry).and_return(
10
+ Legion::Extensions::Mesh::Helpers::Registry.new
11
+ )
12
+ end
13
+
14
+ describe '#merge_gossip' do
15
+ it 'adds unknown agents from incoming gossip' do
16
+ incoming = [
17
+ { agent_id: 'remote-1', capabilities: [:test], node: 'node-02',
18
+ source: :native, status: :online, generation: 5,
19
+ last_seen: Time.now.utc.to_s, registered_at: Time.now.utc.to_s }
20
+ ]
21
+ result = subject.merge_gossip(incoming_peers: incoming, sender: 'node-02')
22
+ expect(result[:success]).to be true
23
+ expect(result[:merged]).to eq(1)
24
+ end
25
+
26
+ it 'updates agents with higher generation' do
27
+ registry = subject.send(:mesh_registry)
28
+ registry.register_agent('agent-1', capabilities: [:old], node: 'node-01')
29
+
30
+ incoming = [
31
+ { agent_id: 'agent-1', capabilities: [:updated], node: 'node-01',
32
+ source: :native, status: :online, generation: 99,
33
+ last_seen: Time.now.utc.to_s, registered_at: Time.now.utc.to_s }
34
+ ]
35
+ result = subject.merge_gossip(incoming_peers: incoming, sender: 'node-01')
36
+ expect(result[:merged]).to eq(1)
37
+ expect(registry.agents['agent-1'][:generation]).to eq(99)
38
+ end
39
+
40
+ it 'skips agents from local node' do
41
+ local_name = 'local-node'
42
+ allow(subject).to receive(:local_node_name).and_return(local_name)
43
+
44
+ incoming = [
45
+ { agent_id: 'local-agent', node: local_name, generation: 1 }
46
+ ]
47
+ result = subject.merge_gossip(incoming_peers: incoming, sender: 'other-node')
48
+ expect(result[:merged]).to eq(0)
49
+ end
50
+
51
+ it 'ignores agents with lower or equal generation' do
52
+ registry = subject.send(:mesh_registry)
53
+ registry.register_agent('agent-1', capabilities: [:test], node: 'node-01')
54
+ # generation is 1 after register
55
+
56
+ incoming = [
57
+ { agent_id: 'agent-1', capabilities: [:test], node: 'node-01',
58
+ generation: 1, source: :native, status: :online }
59
+ ]
60
+ result = subject.merge_gossip(incoming_peers: incoming, sender: 'node-02')
61
+ expect(result[:merged]).to eq(0)
62
+ end
63
+ end
64
+
65
+ describe '#publish_gossip' do
66
+ it 'returns success with peer count' do
67
+ allow(subject).to receive(:publish_gossip_message)
68
+ result = subject.publish_gossip
69
+ expect(result[:success]).to be true
70
+ expect(result[:peers_broadcast]).to be_a(Integer)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,54 @@
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/gossip'
29
+
30
+ RSpec.describe Legion::Extensions::Mesh::Transport::Messages::Gossip do
31
+ subject do
32
+ described_class.new(
33
+ sender: 'node-01',
34
+ gossip_round: 5,
35
+ peers: [{ agent_id: 'w1', capabilities: [:test], node: 'node-01', generation: 3 }]
36
+ )
37
+ end
38
+
39
+ it 'uses the node exchange' do
40
+ expect(subject.exchange).to eq(Legion::Transport::Exchanges::Node)
41
+ end
42
+
43
+ it 'routes to mesh.gossip' do
44
+ expect(subject.routing_key).to eq('mesh.gossip')
45
+ end
46
+
47
+ it 'includes sender and peers in the message body' do
48
+ msg = subject.message
49
+ expect(msg[:type]).to eq('mesh_gossip')
50
+ expect(msg[:sender]).to eq('node-01')
51
+ expect(msg[:peers]).to be_an(Array)
52
+ expect(msg[:gossip_round]).to eq(5)
53
+ end
54
+ 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.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -48,6 +48,7 @@ files:
48
48
  - Gemfile
49
49
  - lex-mesh.gemspec
50
50
  - lib/legion/extensions/mesh.rb
51
+ - lib/legion/extensions/mesh/actors/gossip.rb
51
52
  - lib/legion/extensions/mesh/actors/heartbeat.rb
52
53
  - lib/legion/extensions/mesh/actors/pending_expiry.rb
53
54
  - lib/legion/extensions/mesh/actors/preference_listener.rb
@@ -62,11 +63,14 @@ files:
62
63
  - lib/legion/extensions/mesh/runners/delegation.rb
63
64
  - lib/legion/extensions/mesh/runners/mesh.rb
64
65
  - lib/legion/extensions/mesh/runners/preferences.rb
66
+ - lib/legion/extensions/mesh/transport/messages/gossip.rb
65
67
  - lib/legion/extensions/mesh/transport/messages/mesh_departure.rb
66
68
  - lib/legion/extensions/mesh/transport/messages/preference_query.rb
67
69
  - lib/legion/extensions/mesh/transport/messages/preference_response.rb
70
+ - lib/legion/extensions/mesh/transport/queues/gossip.rb
68
71
  - lib/legion/extensions/mesh/transport/queues/preference.rb
69
72
  - lib/legion/extensions/mesh/version.rb
73
+ - spec/legion/extensions/mesh/actors/gossip_spec.rb
70
74
  - spec/legion/extensions/mesh/actors/heartbeat_spec.rb
71
75
  - spec/legion/extensions/mesh/actors/pending_expiry_spec.rb
72
76
  - spec/legion/extensions/mesh/actors/preference_listener_spec.rb
@@ -79,8 +83,10 @@ files:
79
83
  - spec/legion/extensions/mesh/helpers/registry_spec.rb
80
84
  - spec/legion/extensions/mesh/helpers/topology_spec.rb
81
85
  - spec/legion/extensions/mesh/runners/delegation_spec.rb
86
+ - spec/legion/extensions/mesh/runners/mesh_gossip_spec.rb
82
87
  - spec/legion/extensions/mesh/runners/mesh_spec.rb
83
88
  - spec/legion/extensions/mesh/runners/preferences_spec.rb
89
+ - spec/legion/extensions/mesh/transport/messages/gossip_spec.rb
84
90
  - spec/legion/extensions/mesh/transport/messages/mesh_departure_spec.rb
85
91
  - spec/legion/extensions/mesh/transport/messages/preference_query_spec.rb
86
92
  - spec/legion/extensions/mesh/transport/messages/preference_response_spec.rb