rswim 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 672b410f63b01447de5ba794f65d20f4da0fc0966381c39d3fa333cd21482717
4
- data.tar.gz: 369e88eeaf54294cda7b98ad65c9ee6f8c79c00b8ba362e9e3d844fb6f450ada
3
+ metadata.gz: b9a936a878b5bdbfd6503ac3693577d73a894d77d6d25a9b5794354595cb4b3b
4
+ data.tar.gz: 806de89b6fb5e6fdb4af90ba60d71c0af839a41a34dbfcd8f5ae40155a25856d
5
5
  SHA512:
6
- metadata.gz: 7236a1d7c5159b4113672f3eca7c931b9978ab54a353ea44198000e0894c4ef9266b9fcb2dcf6045476402057748682388200322a2b2eba3f9754c10c5091402
7
- data.tar.gz: 48f657cd9ff5d89cb93d9c684d546e876dba5b02a16183b50c0794b37e779f989bf9869e39de5aa2c91539ed39fa2380af091c2cf754ac50bdd37628d1477e06
6
+ metadata.gz: 439d9eac5fa4ce3d610190ae6f7d1e39fdebad50a699b37678d28d2c3f2c2d51917e9feedbea03f39ccd2e47f70a703fff36a2be425d79b211881025168fd8dc
7
+ data.tar.gz: 6736caa12593b49b06956580f1698ac009a8202d3f71a6473e9b68b35eb8cf7dee5071266f7fe19cd07396405509c4836a33257c1ec65984de02838605165bfa
data/.gitignore CHANGED
@@ -15,3 +15,4 @@
15
15
  # rspec failure tracking
16
16
  .rspec_status
17
17
  .byebug_history
18
+ .DS_Store
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.7.1
1
+ 3.0.2
data/CHANGELOG.md CHANGED
@@ -1 +1,2 @@
1
1
  # 1.0.0 Complete implementation for UDP plus simple, human readable serialisation of messages
2
+ # 2.0.0 Piggyback custom state on the liveness propagation mechanism using `RSwim::Node#append_custom_state`
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rswim (1.0.0)
4
+ rswim (2.0.0)
5
+ slop (~> 4.9)
5
6
  zeitwerk (~> 2.2)
6
7
 
7
8
  GEM
@@ -10,18 +11,18 @@ GEM
10
11
  byebug (11.1.3)
11
12
  coderay (1.1.3)
12
13
  diff-lcs (1.4.4)
13
- ffi (1.13.1)
14
- formatador (0.2.5)
15
- fuubar (2.5.0)
14
+ ffi (1.15.4)
15
+ formatador (0.3.0)
16
+ fuubar (2.5.1)
16
17
  rspec-core (~> 3.0)
17
18
  ruby-progressbar (~> 1.4)
18
- guard (2.16.2)
19
+ guard (2.18.0)
19
20
  formatador (>= 0.2.4)
20
21
  listen (>= 2.7, < 4.0)
21
22
  lumberjack (>= 1.0.12, < 2.0)
22
23
  nenv (~> 0.1)
23
24
  notiffany (~> 0.0)
24
- pry (>= 0.9.12)
25
+ pry (>= 0.13.0)
25
26
  shellany (~> 0.0)
26
27
  thor (>= 0.18.1)
27
28
  guard-compat (1.2.1)
@@ -29,7 +30,7 @@ GEM
29
30
  guard (~> 2.1)
30
31
  guard-compat (~> 1.1)
31
32
  rspec (>= 2.99.0, < 4.0)
32
- listen (3.2.1)
33
+ listen (3.7.0)
33
34
  rb-fsevent (~> 0.10, >= 0.10.3)
34
35
  rb-inotify (~> 0.9, >= 0.9.10)
35
36
  lumberjack (1.2.8)
@@ -38,36 +39,37 @@ GEM
38
39
  notiffany (0.1.3)
39
40
  nenv (~> 0.1)
40
41
  shellany (~> 0.0)
41
- pry (0.13.1)
42
+ pry (0.14.1)
42
43
  coderay (~> 1.1)
43
44
  method_source (~> 1.0)
44
45
  rake (12.3.3)
45
- rb-fsevent (0.10.4)
46
+ rb-fsevent (0.11.0)
46
47
  rb-inotify (0.10.1)
47
48
  ffi (~> 1.0)
48
- rspec (3.9.0)
49
- rspec-core (~> 3.9.0)
50
- rspec-expectations (~> 3.9.0)
51
- rspec-mocks (~> 3.9.0)
52
- rspec-core (3.9.3)
53
- rspec-support (~> 3.9.3)
54
- rspec-expectations (3.9.2)
49
+ rspec (3.10.0)
50
+ rspec-core (~> 3.10.0)
51
+ rspec-expectations (~> 3.10.0)
52
+ rspec-mocks (~> 3.10.0)
53
+ rspec-core (3.10.1)
54
+ rspec-support (~> 3.10.0)
55
+ rspec-expectations (3.10.1)
55
56
  diff-lcs (>= 1.2.0, < 2.0)
56
- rspec-support (~> 3.9.0)
57
- rspec-mocks (3.9.1)
57
+ rspec-support (~> 3.10.0)
58
+ rspec-mocks (3.10.2)
58
59
  diff-lcs (>= 1.2.0, < 2.0)
59
- rspec-support (~> 3.9.0)
60
- rspec-support (3.9.3)
61
- ruby-progressbar (1.10.1)
60
+ rspec-support (~> 3.10.0)
61
+ rspec-support (3.10.2)
62
+ ruby-progressbar (1.11.0)
62
63
  shellany (0.0.1)
63
- thor (1.0.1)
64
- zeitwerk (2.4.0)
64
+ slop (4.9.1)
65
+ thor (1.1.0)
66
+ zeitwerk (2.4.2)
65
67
 
66
68
  PLATFORMS
67
69
  ruby
68
70
 
69
71
  DEPENDENCIES
70
- bundler (~> 2.1.4)
72
+ bundler (>= 2.2.10)
71
73
  byebug
72
74
  fuubar (~> 2.5)
73
75
  guard-rspec (~> 4.7)
@@ -76,4 +78,4 @@ DEPENDENCIES
76
78
  rswim!
77
79
 
78
80
  BUNDLED WITH
79
- 2.1.4
81
+ 2.2.22
data/README.md CHANGED
@@ -4,7 +4,8 @@ RSwim is a Ruby implementation of the SWIM gossip protocol, a mechanism for disc
4
4
 
5
5
  It is an implementation inspired by the original [SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol](https://www.cs.cornell.edu/projects/Quicksilver/public_pdfs/SWIM.pdf) paper by Abhinandan Das, Indranil Gupta, Ashish Motivala.
6
6
 
7
- The implementation is kept intentionally simple and limited to the features described in the paper.
7
+ The implementation is kept intentionally simple and limited to the features described in the paper, except for the addition in version 2.0.0 of the ability to piggyback custom state on the liveness propagation mechanism, see `RSwim::Node#append_custom_state`
8
+
8
9
  No attempts have been made to address known security issues such as Byzantine attacks.
9
10
 
10
11
  Currently RSwim runs on UDP with a custom, human readable serialization format.
@@ -27,6 +28,7 @@ Or install it yourself as:
27
28
  $ gem install rswim
28
29
 
29
30
  ## Usage
31
+ To try out a small demo script, execute `bin/run_node --help` for more information.
30
32
 
31
33
  Example:
32
34
  ```ruby
@@ -43,9 +45,19 @@ Example:
43
45
  node = RSwim::Node.udp(nil, seed_hosts, port)
44
46
 
45
47
  # Subscribe to updates
46
- node.subscribe do |host, status|
47
- puts "Update: #{host} entered state #{status}"
48
+ node.subscribe do |host, status, custom_state|
49
+ puts "Update: #{host} entered liveness state #{status} with custom state #{custom_state}"
48
50
  end
51
+
52
+ # Periodically append new state for publishing
53
+ Thread.new do
54
+ uptime = 0
55
+ loop do
56
+ sleep(5)
57
+ uptime += 5
58
+ node.append_custom_state(:uptime_seconds, uptime)
59
+ end
60
+ end.abort_on_exception = true
49
61
 
50
62
  puts "Ready\n"
51
63
  begin
data/bin/run_node CHANGED
@@ -1,30 +1,59 @@
1
1
  #!/usr/bin/env ruby --jit
2
- require "bundler/setup"
2
+ require 'bundler/setup'
3
+ require 'slop'
3
4
  require 'rswim'
4
- puts "Ruby version: #{RUBY_VERSION}"
5
5
 
6
6
  PORT = 4545
7
7
 
8
- puts "Enter seed nodes"
9
- input = gets
10
- seed_hosts = input.split(" ")
8
+ opts = Slop.parse do |o|
9
+ o.array '-s', '--seeds', 'a comma separated list of seed nodes'
10
+ o.bool '-d', '--debug', 'turn on debug logging'
11
+ o.on '--help' do
12
+ puts o
13
+ exit
14
+ end
15
+ o.on '-v', '--version' do
16
+ puts RSwim::VERSION
17
+ exit
18
+ end
19
+ end
20
+
21
+ puts "Ruby version: #{RUBY_VERSION}"
22
+
23
+ RSwim::Logger.level = ::Logger::DEBUG if opts.debug?
24
+ seed_hosts = opts[:seeds]
11
25
  abort 'EOF' if seed_hosts.nil?
12
- puts "Operating with no seed nodes" if seed_hosts.empty?
13
26
 
14
- puts "Starting node"
27
+ if seed_hosts.empty?
28
+ puts 'Operating with no seed nodes'
29
+ else
30
+ seed_hosts.each { |h| puts "Seed node: #{h}" }
31
+ end
32
+
33
+ puts 'Starting node'
15
34
 
16
35
  # Instantiate node, setting my_host to nil to auto detect host IP.
17
- node = RSwim::Node.udp(nil, seed_hosts, PORT)
36
+ node = RSwim::Node.udp(nil, seed_hosts, PORT, 3_500, 1_000)
18
37
 
19
38
  # Subscribe to updates
20
- node.subscribe do |host, status|
21
- puts "Update: #{host} entered state #{status}"
39
+ node.subscribe do |host, status, custom_state|
40
+ puts "Update: #{host} entered liveness state #{status} with custom state #{custom_state}"
22
41
  end
23
42
 
43
+ Thread.new do
44
+ uptime = 0
45
+ loop do
46
+ sleep(5)
47
+ uptime += 5
48
+ node.append_custom_state(:uptime_seconds, uptime)
49
+ end
50
+ end.abort_on_exception = true
51
+
24
52
  puts "Ready\n"
25
53
  begin
26
54
  # Run node (blocking)
27
55
  node.start
28
56
  rescue Interrupt
57
+ puts "\nShutting down gracefully"
29
58
  end
30
59
  puts "\nDone"
data/lib/rswim/agent.rb CHANGED
@@ -5,7 +5,7 @@ module RSwim
5
5
  class Base
6
6
  def initialize(pipe, node_member_id, seed_member_ids, t_ms, r_ms)
7
7
  @pipe = pipe
8
- @state = ProtocolState.new(node_member_id, seed_member_ids, t_ms, r_ms)
8
+ @state = new_protocol_state(node_member_id, seed_member_ids, t_ms, r_ms)
9
9
  end
10
10
 
11
11
  def subscribe(&block)
@@ -20,8 +20,16 @@ module RSwim
20
20
  end
21
21
  end
22
22
 
23
+ def append_custom_state(key, value)
24
+ @state.append_custom_state(key, value)
25
+ end
26
+
23
27
  protected
24
28
 
29
+ def new_protocol_state(node_member_id, seed_member_ids, t_ms, r_ms)
30
+ ProtocolState.new(node_member_id, seed_member_ids, t_ms, r_ms)
31
+ end
32
+
25
33
  def pause
26
34
  raise 'implement this in a subclass'
27
35
  end
@@ -32,7 +40,7 @@ module RSwim
32
40
  end
33
41
 
34
42
  class SleepBased < Base
35
- def initialize(pipe, node_member_id, seed_member_ids, sleep_time_seconds = 0.1, t_ms = T_MS, r_ms = R_MS)
43
+ def initialize(pipe, node_member_id, seed_member_ids, t_ms, r_ms, sleep_time_seconds = 0.1)
36
44
  super(pipe, node_member_id, seed_member_ids, t_ms, r_ms)
37
45
  @sleep_time_seconds = sleep_time_seconds
38
46
  end
@@ -26,26 +26,32 @@ module RSwim
26
26
  protected
27
27
 
28
28
  def logger
29
- @_logger ||= begin
30
- RSwim::Logger.new(self.class, STDERR)
31
- end
29
+ @_logger ||= RSwim::Logger.new(self.class, $stderr)
32
30
  end
33
31
 
34
32
  private
35
33
 
36
34
  def parse_updates(lines)
37
35
  lines.map do |l|
38
- begin
39
- # host status incarnation_number
40
- host, status, incarnation_number = l.strip.split(' ')
41
- id = @directory.id(host)
42
- UpdateEntry.new(id, status.to_sym, incarnation_number.to_i)
43
- rescue StandardError => e
44
- logger.debug("Failed to parse line `#{l}`: #{e}")
45
- nil
46
- end
36
+ # host status incarnation_number
37
+ host, status, incarnation_number, *pairs = l.strip.split(' ')
38
+ id = @directory.id(host)
39
+ custom_state = parse_custom_state(pairs)
40
+
41
+ UpdateEntry.new(id, status.to_sym, incarnation_number.to_i, custom_state)
42
+ rescue StandardError => e
43
+ logger.debug("Failed to parse line `#{l}`: #{e}")
44
+ nil
47
45
  end.tap(&:compact!)
48
46
  end
47
+
48
+ def parse_custom_state(pairs)
49
+ pairs.each_slice(2).map do |(key, value)|
50
+ raise 'bad custom state' if !key.end_with?(':') || value.nil?
51
+
52
+ [key[0..-2].to_sym, value]
53
+ end.to_h
54
+ end
49
55
  end
50
56
  end
51
57
  end
@@ -10,9 +10,11 @@ module RSwim
10
10
  def serialize(message)
11
11
  l1 = message.type.to_s.gsub(/_/, '-')
12
12
  l1 << " #{@directory.host(message.payload[:target_id])}" if message.type == :ping_req
13
+
13
14
  message.payload[:updates].to_a.each do |update|
14
15
  # host status incarnation_number
15
16
  l1 << "\n#{@directory.host(update.member_id)} #{update.status} #{update.incarnation_number}"
17
+ l1 << " #{serialize_custom_state(update.custom_state)}" unless update.custom_state.empty?
16
18
  end
17
19
  l1
18
20
  end
@@ -20,9 +22,13 @@ module RSwim
20
22
  protected
21
23
 
22
24
  def logger
23
- @_logger ||= begin
24
- RSwim::Logger.new(self.class, STDERR)
25
- end
25
+ @_logger ||= RSwim::Logger.new(self.class, STDERR)
26
+ end
27
+
28
+ private
29
+
30
+ def serialize_custom_state(custom_state)
31
+ custom_state.map { |k, v| "#{k}: #{v}" }.join(' ')
26
32
  end
27
33
  end
28
34
  end
@@ -5,10 +5,11 @@ require 'socket'
5
5
  module RSwim
6
6
  module Integration
7
7
  module Udp
8
+ # Node implementation that sends and listens using UDP
8
9
  class Node < RSwim::Node
9
- def initialize(my_host, seed_hosts, port)
10
- my_host ||= Socket.ip_address_list.find { |x| x.ipv4_private? }.ip_address
11
- super(my_host, seed_hosts)
10
+ def initialize(my_host, seed_hosts, port, t_ms, r_ms)
11
+ my_host ||= Socket.ip_address_list.find(&:ipv4_private?).ip_address
12
+ super(my_host, seed_hosts, t_ms, r_ms)
12
13
  @port = port
13
14
  end
14
15
 
@@ -19,39 +20,35 @@ module RSwim
19
20
  @out_s = UDPSocket.new
20
21
  @in_s.bind(@my_host, @port)
21
22
  logger.info "node listening on UDP port #{@port}"
22
- Thread.new { receive }.tap { |t| t.abort_on_exception = true }
23
- Thread.new { send }.tap { |t| t.abort_on_exception = true }
23
+ Thread.new { receive }.abort_on_exception = true
24
+ Thread.new { send }.abort_on_exception = true
24
25
  end
25
26
 
26
27
  private
27
28
 
28
29
  def send
29
30
  loop do
30
- begin
31
- message = @pipe.q_out.pop
32
- wire_message = @serializer.serialize(message)
33
- host = @directory.host(message.to)
34
- logger.debug "about to send message to #{host}"
35
- @out_s.send(wire_message, 0, host, @port)
36
- rescue StandardError => e
37
- logger.debug("Error while sending: #{e}")
38
- end
31
+ message = @pipe.q_out.pop
32
+ wire_message = @serializer.serialize(message)
33
+ host = @directory.host(message.to)
34
+ logger.debug "about to send message to #{host}"
35
+ @out_s.send(wire_message, 0, host, @port)
36
+ rescue StandardError => e
37
+ logger.debug("Error while sending: #{e}")
39
38
  end
40
39
  logger.info 'node no longer receiving'
41
40
  end
42
41
 
43
42
  def receive
44
43
  loop do
45
- begin
46
- logger.debug 'about to recieve message'
47
- text, sender = @in_s.recvfrom(10_000)
48
-
49
- message = @deserializer.deserialize(sender[3], text)
50
- logger.debug "received #{message}"
51
- @pipe.q_in << message
52
- rescue StandardError => e
53
- logger.debug("Error while receiving: #{e}")
54
- end
44
+ logger.debug 'about to recieve message'
45
+ text, sender = @in_s.recvfrom(10_000)
46
+
47
+ message = @deserializer.deserialize(sender[3], text)
48
+ logger.debug "received #{message}"
49
+ @pipe.q_in << message
50
+ rescue StandardError => e
51
+ logger.debug("Error while receiving: #{e}")
55
52
  end
56
53
  logger.info 'node no longer receiving'
57
54
  end
@@ -5,6 +5,7 @@ module RSwim
5
5
  class Base
6
6
  def initialize(id)
7
7
  @id = id
8
+ @incarnation_number = 0
8
9
  end
9
10
  end
10
11
  end
@@ -4,20 +4,32 @@ module RSwim
4
4
  module Member
5
5
  module HealthState
6
6
  class Alive < Base
7
- def initialize(id, member_pool, update_entry = UpdateEntry.new(id, :alive, 0, 0))
7
+ def initialize(id, node_member_id, member_pool, must_propagate: false)
8
8
  super
9
9
  @failed_to_reply = false
10
10
  end
11
11
 
12
12
  def advance(_elapsed_seconds)
13
13
  if @failed_to_reply
14
- ue = UpdateEntry.new(@id, :suspected, @update_entry.incarnation_number, -1)
15
- Suspected.new(@id, @member_pool, ue, true)
14
+ Suspected.new(@id, @node_member_id, @member_pool, must_propagate: true, send_ping_request: true)
16
15
  else
17
16
  self
18
17
  end
19
18
  end
20
19
 
20
+ def update_suspicion(status, old_incarnation_number, gossip_incarnation_number)
21
+ case status
22
+ when :confirmed then Confirmed.new(@id, @node_member_id, @member_pool)
23
+ when :suspected
24
+ if gossip_incarnation_number >= old_incarnation_number
25
+ Suspected.new(@id, @node_member_id, @member_pool, send_ping_request: false)
26
+ else
27
+ self
28
+ end
29
+ when :alive then self
30
+ end
31
+ end
32
+
21
33
  def member_failed_to_reply
22
34
  @failed_to_reply = true
23
35
  end
@@ -25,6 +37,10 @@ module RSwim
25
37
  def can_be_pinged?
26
38
  true
27
39
  end
40
+
41
+ def status
42
+ :alive
43
+ end
28
44
  end
29
45
  end
30
46
  end
@@ -4,56 +4,26 @@ module RSwim
4
4
  module Member
5
5
  module HealthState
6
6
  class Base
7
- attr_reader :update_entry
7
+ attr_reader :propagation_count
8
8
 
9
- def initialize(id, member_pool, update_entry)
9
+ def initialize(id, node_member_id, member_pool, must_propagate:)
10
10
  @member_pool = member_pool
11
11
  @id = id
12
- @update_entry = update_entry
12
+ @node_member_id = node_member_id
13
+ @propagation_count = must_propagate ? -2 : 0
13
14
  logger.debug("Member with id #{id} entered new state: #{self.class}")
14
15
  end
15
16
 
16
- def advance(_elapsed_seconds)
17
- self
17
+ def increment_propagation_count
18
+ @propagation_count += 1
18
19
  end
19
20
 
20
- def update_suspicion(status, incarnation_number)
21
- incarnation_number ||= @update_entry.incarnation_number
22
- s0 = @update_entry.status
23
- i0 = @update_entry.incarnation_number
24
- case status
25
- when :confirmed
26
- if (s0 == :confirmed)
27
- self
28
- else
29
- ue = UpdateEntry.new(@id, status, incarnation_number, 0)
30
- Confirmed.new(@id, @member_pool, ue)
31
- end
32
- when :suspected
33
- if (s0 == :suspected && incarnation_number > i0) ||
34
- (s0 == :alive && incarnation_number >= i0)
35
- ue = UpdateEntry.new(@id, status, incarnation_number, 0)
36
- Suspected.new(@id, @member_pool, ue, false)
37
- else
38
- self
39
- end
40
- when :alive
41
- if (s0 == :suspected && incarnation_number > i0) ||
42
- (s0 == :alive && incarnation_number > i0)
43
- ue = UpdateEntry.new(@id, status, incarnation_number, 0)
44
- Alive.new(@id, @member_pool, ue)
45
- else
46
- self
47
- end
48
- end
21
+ def advance(_elapsed_seconds)
22
+ self
49
23
  end
50
24
 
51
25
  def member_failed_to_reply; end
52
26
 
53
- def increment_propagation_count
54
- @update_entry.increment_propagation_count
55
- end
56
-
57
27
  def can_be_pinged?
58
28
  false
59
29
  end
@@ -61,9 +31,7 @@ module RSwim
61
31
  protected
62
32
 
63
33
  def logger
64
- @_logger ||= begin
65
- RSwim::Logger.new("unknown node", STDERR)
66
- end
34
+ @_logger ||= RSwim::Logger.new("Node #{@node_member_id}", STDERR)
67
35
  end
68
36
  end
69
37
  end
@@ -4,7 +4,7 @@ module RSwim
4
4
  module Member
5
5
  module HealthState
6
6
  class Confirmed < Base
7
- def initialize(id, member_pool, update_entry)
7
+ def initialize(id, node_member_id, member_pool, must_propagate: false)
8
8
  super
9
9
  @member_halted = false
10
10
  @member_removed = false
@@ -17,7 +17,7 @@ module RSwim
17
17
  @member_pool.halt_member(@id)
18
18
  @member_halted = true
19
19
  end
20
-
20
+
21
21
  if !@member_removed && @life_time_seconds > 10
22
22
  @member_pool.remove_member(@id)
23
23
  @member_removed = true
@@ -25,6 +25,14 @@ module RSwim
25
25
 
26
26
  self
27
27
  end
28
+
29
+ def update_suspicion(_status, _old_incarnation_number, _gossip_incarnation_number)
30
+ self
31
+ end
32
+
33
+ def status
34
+ :confirmed
35
+ end
28
36
  end
29
37
  end
30
38
  end
@@ -4,8 +4,8 @@ module RSwim
4
4
  module Member
5
5
  module HealthState
6
6
  class Suspected < Base
7
- def initialize(id, member_pool, update_entry, send_ping_request)
8
- super(id, member_pool, update_entry)
7
+ def initialize(id, node_member_id, member_pool, send_ping_request:, must_propagate: false)
8
+ super(id, node_member_id, member_pool, must_propagate: must_propagate)
9
9
  @ping_request_sent = !send_ping_request
10
10
  @life_time_seconds = 0
11
11
  end
@@ -17,15 +17,33 @@ module RSwim
17
17
  @ping_request_sent = true
18
18
  end
19
19
  if @life_time_seconds > 60
20
- Confirmed.new(@id, @member_pool, UpdateEntry.new(@id, :confirmed, @update_entry.incarnation_number, -2))
20
+ # TODO: make sure to propagate this information with priority
21
+ Confirmed.new(@id, @node_member_id, @member_pool, must_propagate: true)
21
22
  else
22
23
  self
23
24
  end
24
25
  end
25
26
 
27
+ def update_suspicion(status, old_incarnation_number, gossip_incarnation_number)
28
+ case status
29
+ when :confirmed then Confirmed.new(@id, @node_member_id, @member_pool)
30
+ when :suspected then self
31
+ when :alive
32
+ if gossip_incarnation_number > old_incarnation_number
33
+ Alive.new(@id, @node_member_id, @member_pool)
34
+ else
35
+ self
36
+ end
37
+ end
38
+ end
39
+
26
40
  def can_be_pinged?
27
41
  true
28
42
  end
43
+
44
+ def status
45
+ :suspected
46
+ end
29
47
  end
30
48
  end
31
49
  end
@@ -2,12 +2,17 @@
2
2
 
3
3
  module RSwim
4
4
  module Member
5
+ # Member behaviour of local node ("me")
5
6
  class Me < Base
6
7
  def initialize(id)
7
8
  super
8
9
  @ack_responder = AckResponder.new(id)
9
- @incarnation_number = 0
10
10
  @propagation_count = 0
11
+ @custom_state = {}
12
+ end
13
+
14
+ def increment_propagation_count
15
+ @propagation_count += 1
11
16
  end
12
17
 
13
18
  def schedule_ack(member_id)
@@ -18,21 +23,20 @@ module RSwim
18
23
  @ack_responder.prepare_output
19
24
  end
20
25
 
21
- def update_suspicion(status, incarnation_number)
22
- if status != :alive && incarnation_number == @incarnation_number
23
- @incarnation_number += 1
24
-
25
- # making sure to get priority in being propagated
26
- @propagation_count = -10
27
- end
26
+ def append_custom_state(key, value)
27
+ @custom_state[key] = value
28
+ propagate_change
28
29
  end
29
30
 
30
- def increment_propagation_count
31
- @propagation_count += 1
31
+ def incorporate_gossip(update_entry)
32
+ if update_entry.incarnation_number >= @incarnation_number &&
33
+ (update_entry.status != :alive || update_entry.custom_state != @custom_state)
34
+ propagate_change
35
+ end
32
36
  end
33
37
 
34
38
  def prepare_update_entry
35
- UpdateEntry.new(@id, :alive, @incarnation_number, @propagation_count)
39
+ UpdateEntry.new(@id, :alive, @incarnation_number, @custom_state, @propagation_count)
36
40
  end
37
41
 
38
42
  def update(elapsed_seconds); end
@@ -40,6 +44,15 @@ module RSwim
40
44
  def can_be_pinged?
41
45
  false
42
46
  end
47
+
48
+ private
49
+
50
+ def propagate_change
51
+ @incarnation_number += 1
52
+
53
+ # making sure to get priority in being propagated
54
+ @propagation_count = -10
55
+ end
43
56
  end
44
57
  end
45
58
  end
@@ -6,9 +6,11 @@ module RSwim
6
6
  def initialize(id, node_member_id, member_pool)
7
7
  super(id)
8
8
  @member_pool = member_pool
9
+ @node_member_id = node_member_id
9
10
  @transmission_state = TransmissionState::Ready.new(id, node_member_id, member_pool)
10
- @health_state = HealthState::Alive.new(id, member_pool)
11
+ @health_state = HealthState::Alive.new(id, node_member_id, member_pool)
11
12
  @forwarding_state = ForwardingState::Ready.new(id, node_member_id)
13
+ @custom_state_holder = CustomStateHolder.new(id, node_member_id)
12
14
  end
13
15
 
14
16
  ## Messages
@@ -28,8 +30,7 @@ module RSwim
28
30
  @transmission_state.enqueue_ping_from(source_id)
29
31
  end
30
32
 
31
-
32
- ## Callbacks
33
+ # # Callbacks
33
34
 
34
35
  #  call this when you received ack from member
35
36
  def replied_with_ack
@@ -44,11 +45,10 @@ module RSwim
44
45
  @health_state.member_failed_to_reply
45
46
  end
46
47
 
47
-
48
48
  ## Commands
49
49
 
50
50
  def halt
51
- @transmission_state = TransmissionState::Off.new(@id)
51
+ @transmission_state = TransmissionState::Off.new(@id, @node_member_id)
52
52
  end
53
53
 
54
54
  def forward_ack
@@ -61,25 +61,68 @@ module RSwim
61
61
  @health_state = @health_state.advance(elapsed_seconds)
62
62
  end
63
63
 
64
+ def increment_propagation_count
65
+ @health_state.increment_propagation_count
66
+ @custom_state_holder.increment_propagation_count
67
+ end
68
+
64
69
  def prepare_output
65
70
  [@transmission_state, @forwarding_state].flat_map(&:prepare_output)
66
71
  end
67
72
 
68
73
  def prepare_update_entry
69
- @health_state.update_entry
70
- end
71
-
72
- def increment_propagation_count
73
- @health_state.increment_propagation_count
74
+ pc = [@health_state, @custom_state_holder].map!(&:propagation_count).min
75
+ UpdateEntry.new(@id, @health_state.status, @incarnation_number, @custom_state_holder.state, pc)
74
76
  end
75
77
 
76
- def update_suspicion(status, incarnation_number=nil)
77
- @health_state = @health_state.update_suspicion(status, incarnation_number)
78
+ def incorporate_gossip(update_entry)
79
+ update_custom_state(update_entry.custom_state, update_entry.incarnation_number)
80
+ update_suspicion(update_entry.status, update_entry.incarnation_number)
81
+ @incarnation_number = update_entry.incarnation_number if update_entry.incarnation_number > @incarnation_number
78
82
  end
79
83
 
80
84
  def can_be_pinged?
81
85
  @health_state.can_be_pinged?
82
86
  end
87
+
88
+ private
89
+
90
+ def update_custom_state(new_state, incarnation_number)
91
+ should_update = new_state != @custom_state_holder.state && incarnation_number > @incarnation_number
92
+ @custom_state_holder.state = new_state if should_update
93
+ end
94
+
95
+
96
+ def update_suspicion(status, incarnation_number = nil)
97
+ old_incarnation_number = @incarnation_number
98
+ incarnation_number ||= @incarnation_number
99
+ @health_state = @health_state.update_suspicion(status, old_incarnation_number, incarnation_number)
100
+ end
101
+
102
+ class CustomStateHolder
103
+ attr_reader :propagation_count, :state
104
+
105
+ def initialize(id, node_member_id)
106
+ @id = id
107
+ @node_member_id = node_member_id
108
+ @state = {}
109
+ @propagation_count = 0
110
+ end
111
+
112
+ def state=(arg)
113
+ @state = arg
114
+ @propagation_count = 0
115
+ logger.debug("Member with id #{@id} updated custom state: #{@state}")
116
+ end
117
+
118
+ def increment_propagation_count
119
+ @propagation_count += 1
120
+ end
121
+
122
+ def logger
123
+ @_logger ||= RSwim::Logger.new("Node #{@node_member_id}", $stderr)
124
+ end
125
+ end
83
126
  end
84
127
  end
85
128
  end
@@ -4,8 +4,8 @@ module RSwim
4
4
  module Member
5
5
  module TransmissionState
6
6
  class Off < Base
7
- def initialize(id)
8
- super(id, nil, nil, [], [])
7
+ def initialize(id, node_member_id)
8
+ super(id, node_member_id, nil, [], [])
9
9
  end
10
10
 
11
11
  def member_replied_with_ack
@@ -11,9 +11,13 @@ module RSwim
11
11
  @subscribers = []
12
12
  end
13
13
 
14
+ def append_custom_state(key, value)
15
+ @me.append_custom_state(key, value)
16
+ end
17
+
14
18
  def update_member(message)
15
19
  updates = message.payload[:updates]
16
- update_suspicions(updates) unless updates.nil?
20
+ incorporate_gossip(updates) unless updates.nil?
17
21
 
18
22
  sender = member(message.from) # NB: records member if not seen before
19
23
  case message.type
@@ -41,19 +45,19 @@ module RSwim
41
45
  @subscribers << block
42
46
  end
43
47
 
44
-
45
48
  def prepare_output
49
+ ms = @members.values.flat_map(&:prepare_output)
50
+ return ms if ms.empty?
51
+
46
52
  update_entries = @members.map { |_k, member| member.prepare_update_entry }
47
- # .select { |entry| entry.propagation_count < 5 }
48
- .sort_by { |entry| entry.propagation_count } # sort ascending!
53
+ .sort_by(&:propagation_count) # sort ascending!
49
54
  .take(15) # TODO: constant
50
55
 
51
56
  update_entries.each do |entry|
52
- publish(entry.member_id, entry.status) if entry.propagation_count.zero?
57
+ publish(entry) if entry.propagation_count.zero?
53
58
  member(entry.member_id).increment_propagation_count
54
59
  end
55
60
 
56
- ms = @members.values.flat_map(&:prepare_output)
57
61
  ms.each { |message| message.payload[:updates] = update_entries }
58
62
  ms
59
63
  end
@@ -62,8 +66,7 @@ module RSwim
62
66
  ms = @members.values.select(&:can_be_pinged?)
63
67
  return if ms.empty?
64
68
 
65
- index = ms.one? ? 0 : rand(ms.size)
66
- member = ms[index]
69
+ member = random_member(ms)
67
70
  member.ping!
68
71
  end
69
72
 
@@ -83,6 +86,7 @@ module RSwim
83
86
 
84
87
  def remove_member(member_id)
85
88
  raise 'boom' if member_id == @node_member_id
89
+
86
90
  @members.delete(member_id)
87
91
  end
88
92
 
@@ -94,20 +98,29 @@ module RSwim
94
98
  member(member_id).failed_to_reply
95
99
  end
96
100
 
101
+ protected
102
+
103
+ def random_member(members)
104
+ index = members.one? ? 0 : rand(members.size)
105
+ members[index]
106
+ end
107
+
97
108
  private
98
109
 
99
- def publish(member_id, status)
100
- @subscribers.each { |s| s.call(member_id, status) }
110
+ def publish(update_entry)
111
+ @subscribers.each { |s| s.call(update_entry) }
101
112
  end
102
113
 
103
- def update_suspicions(updates)
114
+ def incorporate_gossip(updates)
104
115
  updates.each do |u|
105
- member(u.member_id).update_suspicion(u.status, u.incarnation_number)
116
+ m = member(u.member_id)
117
+ m.incorporate_gossip(u)
106
118
  end
107
119
  end
108
120
 
109
121
  def member(id)
110
122
  raise 'boom' if id.nil?
123
+
111
124
  @members[id] ||= Member::Peer.new(id, @node_member_id, self)
112
125
  end
113
126
  end
data/lib/rswim/message.rb CHANGED
@@ -14,5 +14,9 @@ module RSwim
14
14
  def to_s
15
15
  "message of type #{type} from #{from} to #{to} with #{payload[:updates].to_a.size} updates"
16
16
  end
17
+
18
+ def ==(other)
19
+ %i[to from type payload].all? { |a| send(a) == other.send(a) }
20
+ end
17
21
  end
18
22
  end
data/lib/rswim/node.rb CHANGED
@@ -4,7 +4,7 @@ require 'socket'
4
4
 
5
5
  module RSwim
6
6
  class Node
7
- def initialize(my_host, seed_hosts)
7
+ def initialize(my_host, seed_hosts, t_ms, r_ms)
8
8
  @my_host = my_host
9
9
  @directory = Directory.new
10
10
  @my_id = @directory.id(@my_host)
@@ -12,27 +12,31 @@ module RSwim
12
12
  @serializer = Integration::Serializer.new(@directory)
13
13
  @pipe = RSwim::Pipe.simple
14
14
  seed_ids = seed_hosts.map { |host| @directory.id(host) }
15
- @agent = RSwim::Agent::SleepBased.new(@pipe, @my_id, seed_ids)
15
+ @agent = RSwim::Agent::SleepBased.new(@pipe, @my_id, seed_ids, t_ms, r_ms)
16
16
  end
17
17
 
18
- def self.udp(my_host, seed_hosts, port)
19
- Integration::Udp::Node.new(my_host, seed_hosts, port)
18
+ def self.udp(my_host, seed_hosts, port, t_ms = T_MS, r_ms = R_MS)
19
+ Integration::Udp::Node.new(my_host, seed_hosts, port, t_ms, r_ms)
20
20
  end
21
21
 
22
22
  def subscribe(&block)
23
- @agent.subscribe do |id, status|
24
- host = @directory.host(id)
25
- block.call(host, status)
23
+ @agent.subscribe do |update_entry|
24
+ host = @directory.host(update_entry.member_id)
25
+ block.call(host, update_entry.status, update_entry.custom_state)
26
26
  end
27
27
  end
28
28
 
29
+ def append_custom_state(key, value)
30
+ @agent.append_custom_state(key, value)
31
+ end
32
+
29
33
  # blocks until interrupted
30
34
  def start
31
35
  logger.info 'starting node'
32
36
  before_start
33
37
  @agent.run
34
38
  rescue StandardError => e
35
- logger.debug("Error: #{e}")
39
+ logger.error("Node failed: #{e}")
36
40
  end
37
41
 
38
42
  protected
@@ -42,9 +46,7 @@ module RSwim
42
46
  end
43
47
 
44
48
  def logger
45
- @_logger ||= begin
46
- RSwim::Logger.new(self.class, STDERR)
47
- end
49
+ @_logger ||= RSwim::Logger.new(self.class, $stderr)
48
50
  end
49
51
  end
50
52
  end
@@ -5,7 +5,7 @@ module RSwim
5
5
  def initialize(node_member_id, seed_member_ids, t_ms, r_ms)
6
6
  @t_ms = t_ms
7
7
  @r_ms = r_ms
8
- @member_pool = MemberPool.new(node_member_id, seed_member_ids)
8
+ @member_pool = new_member_pool(node_member_id, seed_member_ids)
9
9
  @node_member_id = node_member_id
10
10
  @t = @r = 1
11
11
  end
@@ -14,6 +14,10 @@ module RSwim
14
14
  @member_pool.subscribe(&block)
15
15
  end
16
16
 
17
+ def append_custom_state(key, value)
18
+ @member_pool.append_custom_state(key, value)
19
+ end
20
+
17
21
  def advance(input_messages, elapsed_seconds)
18
22
  @t += elapsed_seconds * 1000
19
23
  @t = 0 if @t >= @t_ms
@@ -50,6 +54,12 @@ module RSwim
50
54
  output_messages
51
55
  end
52
56
 
57
+ protected
58
+
59
+ def new_member_pool(node_member_id, seed_member_ids)
60
+ MemberPool.new(node_member_id, seed_member_ids)
61
+ end
62
+
53
63
  private
54
64
 
55
65
  def logger
@@ -1,17 +1,21 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module RSwim
3
4
  class UpdateEntry
4
- attr_reader :member_id, :status, :incarnation_number, :propagation_count
5
+ attr_reader :member_id, :status, :incarnation_number, :propagation_count, :custom_state
5
6
 
6
- def initialize(member_id, status, incarnation_number, propagation_count = 0)
7
+ def initialize(member_id, status, incarnation_number, custom_state, propagation_count = 0)
7
8
  @member_id = member_id
8
9
  @status = status
9
10
  @incarnation_number = incarnation_number
11
+ @custom_state = custom_state
10
12
  @propagation_count = propagation_count
11
13
  end
12
14
 
13
- def increment_propagation_count
14
- @propagation_count += 1
15
+ def ==(other)
16
+ %i[member_id status incarnation_number propagation_count custom_state].all? do |a|
17
+ send(a) == other.send(a)
18
+ end
15
19
  end
16
20
  end
17
21
  end
data/lib/rswim/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSwim
4
- VERSION = '1.0.0'
4
+ VERSION = '2.0.0'
5
5
  end
data/rswim.gemspec CHANGED
@@ -26,9 +26,12 @@ Gem::Specification.new do |spec|
26
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
27
  spec.require_paths = ["lib"]
28
28
 
29
+ spec.required_ruby_version = '>= 3.0.0'
30
+
29
31
  spec.add_dependency 'zeitwerk', '~> 2.2'
32
+ spec.add_dependency 'slop', '~> 4.9'
30
33
 
31
- spec.add_development_dependency "bundler", "~> 2.1.4"
34
+ spec.add_development_dependency "bundler", ">= 2.2.10"
32
35
  spec.add_development_dependency "rake", "~> 12.0"
33
36
  spec.add_development_dependency "rspec", "~> 3.0"
34
37
  spec.add_development_dependency 'guard-rspec', '~> 4.7'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rswim
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Madsen
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-06 00:00:00.000000000 Z
11
+ date: 2021-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -25,19 +25,33 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.2'
27
27
  - !ruby/object:Gem::Dependency
28
- name: bundler
28
+ name: slop
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 2.1.4
34
- type: :development
33
+ version: '4.9'
34
+ type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 2.1.4
40
+ version: '4.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.2.10
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.2.10
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -173,7 +187,7 @@ metadata:
173
187
  homepage_uri: https://github.com/beatmadsen/rswim
174
188
  source_code_uri: https://github.com/beatmadsen/rswim
175
189
  changelog_uri: https://github.com/beatmadsen/rswim/blob/master/CHANGELOG.md
176
- post_install_message:
190
+ post_install_message:
177
191
  rdoc_options: []
178
192
  require_paths:
179
193
  - lib
@@ -181,15 +195,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
181
195
  requirements:
182
196
  - - ">="
183
197
  - !ruby/object:Gem::Version
184
- version: '0'
198
+ version: 3.0.0
185
199
  required_rubygems_version: !ruby/object:Gem::Requirement
186
200
  requirements:
187
201
  - - ">="
188
202
  - !ruby/object:Gem::Version
189
203
  version: '0'
190
204
  requirements: []
191
- rubygems_version: 3.1.2
192
- signing_key:
205
+ rubygems_version: 3.2.22
206
+ signing_key:
193
207
  specification_version: 4
194
208
  summary: Ruby implementation of the SWIM gossip protocol
195
209
  test_files: []