evinrude 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +23 -0
- data/.gitignore +6 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTING.md +10 -0
- data/LICENCE +674 -0
- data/README.md +410 -0
- data/evinrude.gemspec +42 -0
- data/lib/evinrude.rb +1233 -0
- data/lib/evinrude/backoff.rb +19 -0
- data/lib/evinrude/cluster_configuration.rb +162 -0
- data/lib/evinrude/config_change_queue_entry.rb +19 -0
- data/lib/evinrude/config_change_queue_entry/add_node.rb +13 -0
- data/lib/evinrude/config_change_queue_entry/remove_node.rb +14 -0
- data/lib/evinrude/freedom_patches/range.rb +5 -0
- data/lib/evinrude/log.rb +102 -0
- data/lib/evinrude/log_entries.rb +3 -0
- data/lib/evinrude/log_entry.rb +13 -0
- data/lib/evinrude/log_entry/cluster_configuration.rb +15 -0
- data/lib/evinrude/log_entry/null.rb +6 -0
- data/lib/evinrude/log_entry/state_machine_command.rb +13 -0
- data/lib/evinrude/logging_helpers.rb +40 -0
- data/lib/evinrude/message.rb +19 -0
- data/lib/evinrude/message/append_entries_reply.rb +13 -0
- data/lib/evinrude/message/append_entries_request.rb +18 -0
- data/lib/evinrude/message/command_reply.rb +13 -0
- data/lib/evinrude/message/command_request.rb +18 -0
- data/lib/evinrude/message/install_snapshot_reply.rb +13 -0
- data/lib/evinrude/message/install_snapshot_request.rb +18 -0
- data/lib/evinrude/message/join_reply.rb +13 -0
- data/lib/evinrude/message/join_request.rb +18 -0
- data/lib/evinrude/message/node_removal_reply.rb +13 -0
- data/lib/evinrude/message/node_removal_request.rb +18 -0
- data/lib/evinrude/message/read_reply.rb +13 -0
- data/lib/evinrude/message/read_request.rb +18 -0
- data/lib/evinrude/message/vote_reply.rb +13 -0
- data/lib/evinrude/message/vote_request.rb +18 -0
- data/lib/evinrude/messages.rb +14 -0
- data/lib/evinrude/metrics.rb +50 -0
- data/lib/evinrude/network.rb +69 -0
- data/lib/evinrude/network/connection.rb +144 -0
- data/lib/evinrude/network/protocol.rb +69 -0
- data/lib/evinrude/node_info.rb +35 -0
- data/lib/evinrude/peer.rb +50 -0
- data/lib/evinrude/resolver.rb +96 -0
- data/lib/evinrude/snapshot.rb +9 -0
- data/lib/evinrude/state_machine.rb +15 -0
- data/lib/evinrude/state_machine/register.rb +25 -0
- data/smoke_tests/001_single_node_cluster.rb +20 -0
- data/smoke_tests/002_three_node_cluster.rb +43 -0
- data/smoke_tests/003_spill.rb +25 -0
- data/smoke_tests/004_stale_read.rb +67 -0
- data/smoke_tests/005_sleepy_master.rb +28 -0
- data/smoke_tests/006_join_via_follower.rb +26 -0
- data/smoke_tests/007_snapshot_madness.rb +97 -0
- data/smoke_tests/008_downsizing.rb +43 -0
- data/smoke_tests/009_disaster_recovery.rb +46 -0
- data/smoke_tests/999_final_smoke_test.rb +279 -0
- data/smoke_tests/run +22 -0
- data/smoke_tests/smoke_test_helper.rb +199 -0
- metadata +318 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
require "rbnacl"
|
2
|
+
|
3
|
+
class Evinrude
|
4
|
+
class Network
|
5
|
+
module Protocol
|
6
|
+
class VersionError < Error; end
|
7
|
+
class DecryptionError < Error; end
|
8
|
+
|
9
|
+
class MessageTooBigError < Error; end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def max_message_size
|
14
|
+
@max_message_size ||= begin
|
15
|
+
if File.exist?("/proc/meminfo")
|
16
|
+
File.read("/proc/meminfo").split("\n").map do |l|
|
17
|
+
if l =~ /^MemTotal:\s+(\d+)\s+kB/
|
18
|
+
$1.to_i * 1024 / 10
|
19
|
+
else
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end.compact.first
|
23
|
+
else
|
24
|
+
100_000_000
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def box(msg)
|
30
|
+
logger.debug(logloc) { "Encrypting message #{msg.inspect} with key #{@keys.first.inspect}" }
|
31
|
+
RbNaCl::SimpleBox.from_secret_key(@keys.first).encrypt(msg)
|
32
|
+
end
|
33
|
+
|
34
|
+
def frame(chunk)
|
35
|
+
"\x00" + encode_length(chunk.length) + chunk
|
36
|
+
end
|
37
|
+
|
38
|
+
def unbox(ciphertext)
|
39
|
+
@keys.each do |k|
|
40
|
+
begin
|
41
|
+
return RbNaCl::SimpleBox.from_secret_key(k).decrypt(ciphertext)
|
42
|
+
rescue RbNaCl::CryptoError
|
43
|
+
# This just means "key failed"; nothing to get upset about
|
44
|
+
logger.debug(logloc) { "Decryption of #{ciphertext.inspect} failed with key #{k.inspect}" }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# ... but if all the keys fail, *that's* something to get upset about
|
49
|
+
raise DecryptionError,
|
50
|
+
"Received an undecryptable message from #{peer_info}: #{ciphertext[0, 20].inspect}..."
|
51
|
+
end
|
52
|
+
|
53
|
+
def encode_length(i)
|
54
|
+
if i < 128
|
55
|
+
return i.chr
|
56
|
+
else
|
57
|
+
s = ""
|
58
|
+
|
59
|
+
while i > 0
|
60
|
+
s << (i % 256).chr
|
61
|
+
i /= 256
|
62
|
+
end
|
63
|
+
|
64
|
+
(0x80 + s.length).chr + s.reverse
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Evinrude
|
2
|
+
class NodeInfo
|
3
|
+
attr_reader :name, :address, :port
|
4
|
+
|
5
|
+
def initialize(address:, port:, name:)
|
6
|
+
@address, @port, @name = address, port, name
|
7
|
+
end
|
8
|
+
|
9
|
+
# It's useful for a NodeInfo to be able to be treated like a hash sometimes.
|
10
|
+
def [](k)
|
11
|
+
case k
|
12
|
+
when :address
|
13
|
+
@address
|
14
|
+
when :port
|
15
|
+
@port
|
16
|
+
when :name
|
17
|
+
@name
|
18
|
+
else
|
19
|
+
raise ArgumentError, "Invalid key #k.inspect}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def eql?(o)
|
24
|
+
# I know hash equality doesn't mean "equality of hashes", but it amuses
|
25
|
+
# me nonetheless that that is a good way to implement it.
|
26
|
+
[Evinrude::NodeInfo, Hash].include?(o.class) && { address: @address, port: @port, name: @name}.eql?({ address: o[:address], port: o[:port], name: o[:name] })
|
27
|
+
end
|
28
|
+
|
29
|
+
alias :== :eql?
|
30
|
+
|
31
|
+
def hash
|
32
|
+
{ address: @address, port: @port, name: @name }.hash
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative "./message/append_entries_request"
|
2
|
+
|
3
|
+
class Evinrude
|
4
|
+
class Peer
|
5
|
+
attr_reader :conn, :next_index, :match_index, :node_info
|
6
|
+
|
7
|
+
def initialize(conn:, next_index:, node_info:, metrics:)
|
8
|
+
@conn, @next_index, @node_info, @metrics = conn, next_index, node_info, metrics
|
9
|
+
@match_index = 0
|
10
|
+
|
11
|
+
update_metrics
|
12
|
+
end
|
13
|
+
|
14
|
+
def failed_append(last_index = nil)
|
15
|
+
if last_index
|
16
|
+
@next_index = last_index + 1
|
17
|
+
else
|
18
|
+
@next_index = [1, @next_index - 1].max
|
19
|
+
end
|
20
|
+
|
21
|
+
update_metrics
|
22
|
+
end
|
23
|
+
|
24
|
+
def successful_append(last_index)
|
25
|
+
@next_index = last_index + 1
|
26
|
+
@match_index = last_index
|
27
|
+
|
28
|
+
update_metrics
|
29
|
+
end
|
30
|
+
|
31
|
+
def peer_info
|
32
|
+
conn.peer_info
|
33
|
+
end
|
34
|
+
|
35
|
+
def node_name
|
36
|
+
@node_info.name
|
37
|
+
end
|
38
|
+
|
39
|
+
def rpc(*a)
|
40
|
+
conn.rpc(*a)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def update_metrics
|
46
|
+
@metrics.next_index.set(@next_index, labels: { peer: peer_info, node_name: @node_name })
|
47
|
+
@metrics.match_index.set(0, labels: { peer: peer_info, node_name: @node_name })
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "async/dns"
|
2
|
+
require "async/dns/system"
|
3
|
+
|
4
|
+
class Evinrude
|
5
|
+
class Resolver
|
6
|
+
def initialize
|
7
|
+
@config = Config.new
|
8
|
+
p @config
|
9
|
+
@resolver = Async::DNS::Resolver.new(Async::DNS::System.standard_connections(@config.nameservers))
|
10
|
+
end
|
11
|
+
|
12
|
+
def getaddresses(name)
|
13
|
+
(getresources(name, Resolv::DNS::Resource::IN::AAAA) + getresources(name, Resolv::DNS::Resource::IN::A)).map(&:address).map(&:to_s)
|
14
|
+
end
|
15
|
+
|
16
|
+
def getresources(name, rtype)
|
17
|
+
search_candidates(name).each do |fqdn|
|
18
|
+
response = @resolver.query(fqdn, rtype)
|
19
|
+
|
20
|
+
if response.rcode != Resolv::DNS::RCode::NoError
|
21
|
+
next
|
22
|
+
end
|
23
|
+
|
24
|
+
return response.answer.map { |rr| rr[2] }
|
25
|
+
end
|
26
|
+
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def search_candidates(name)
|
33
|
+
ndots = name.each_char.grep(".").length
|
34
|
+
|
35
|
+
if ndots >= @config.ndots
|
36
|
+
[name] + @config.search_suffixes.map { |ss| "#{name}.#{ss}" }
|
37
|
+
else
|
38
|
+
@config.search_suffixes.map { |ss| "#{name}.#{ss}" }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Config
|
43
|
+
attr_reader :search_suffixes, :nameservers, :ndots
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@ndots = 1
|
47
|
+
|
48
|
+
if Object.const_defined?(:Win32)
|
49
|
+
# Taking a punt here
|
50
|
+
@search_suffixes, @nameservers = Win32::Resolv.get_resolv_info
|
51
|
+
else
|
52
|
+
if File.exist?(Async::DNS::System::RESOLV_CONF)
|
53
|
+
parse_resolv_conf(Async::DNS::System::RESOLV_CONF)
|
54
|
+
else
|
55
|
+
raise ArgumentError, "resolver config file #{Async::DNS::System::RESOLV_CONF} does not exist"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def parse_resolv_conf(filename)
|
63
|
+
@nameservers = []
|
64
|
+
|
65
|
+
File.open(filename) do |fd|
|
66
|
+
fd.each_line do |l|
|
67
|
+
case l
|
68
|
+
when /^domain\s+(.*)$/
|
69
|
+
@search_suffixes = [$1] if @search_suffixes.nil?
|
70
|
+
when /^search\s+(.*)$/
|
71
|
+
@search_suffixes = $1.split(/\s+/)
|
72
|
+
when /^nameserver\s+(.*)$/
|
73
|
+
@nameservers += $1.split(/\s+/)
|
74
|
+
when /^options\s+(.*)$/
|
75
|
+
$1.split(/\s+/).each do |opt|
|
76
|
+
if opt =~ /ndots:(\d+)/
|
77
|
+
@ndots = $1.to_i
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
if @search_suffixes.nil?
|
85
|
+
@search_suffixes = [Socket.gethostname.split(".", 2)[1].to_s]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private_constant :Config
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class Evinrude
|
2
|
+
class Snapshot
|
3
|
+
attr_reader :node_name, :state, :cluster_config, :cluster_config_index, :last_term, :last_index, :last_command_ids
|
4
|
+
|
5
|
+
def initialize(node_name:, state:, cluster_config:, cluster_config_index:, last_term:, last_index:, last_command_ids:)
|
6
|
+
@node_name, @state, @cluster_config, @cluster_config_index, @last_term, @last_index, @last_command_ids = node_name, state, cluster_config, cluster_config_index, last_term, last_index, last_command_ids
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative "../state_machine"
|
2
|
+
|
3
|
+
class Evinrude
|
4
|
+
class StateMachine
|
5
|
+
class Register < StateMachine
|
6
|
+
def initialize(**kwargs)
|
7
|
+
super
|
8
|
+
|
9
|
+
@state = kwargs.fetch(:snapshot, "")
|
10
|
+
end
|
11
|
+
|
12
|
+
def process_command(s)
|
13
|
+
@state = s
|
14
|
+
end
|
15
|
+
|
16
|
+
def current_state
|
17
|
+
@state
|
18
|
+
end
|
19
|
+
|
20
|
+
def snapshot
|
21
|
+
@state
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative "./smoke_test_helper"
|
4
|
+
|
5
|
+
require "evinrude"
|
6
|
+
|
7
|
+
Thread.current.name = "MT"
|
8
|
+
|
9
|
+
n = spawn_nodes(1).first
|
10
|
+
|
11
|
+
c = n.c
|
12
|
+
t = n.t
|
13
|
+
|
14
|
+
until c.leader?
|
15
|
+
t.join(0.001)
|
16
|
+
end
|
17
|
+
|
18
|
+
c.command("bob")
|
19
|
+
|
20
|
+
assert_equal c.state, "bob"
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative "./smoke_test_helper"
|
4
|
+
|
5
|
+
require "evinrude"
|
6
|
+
|
7
|
+
Thread.current.name = "MT"
|
8
|
+
|
9
|
+
nodes = spawn_nodes(3)
|
10
|
+
|
11
|
+
n1, n2, n3 = nodes
|
12
|
+
|
13
|
+
wait_for_stability(nodes)
|
14
|
+
wait_for_consensus(nodes)
|
15
|
+
|
16
|
+
assert_equal n1.c.nodes.map(&:name).sort, nodes.map(&:c).map(&:node_name).sort
|
17
|
+
assert_equal n2.c.nodes.map(&:name).sort, nodes.map(&:c).map(&:node_name).sort
|
18
|
+
assert_equal n3.c.nodes.map(&:name).sort, nodes.map(&:c).map(&:node_name).sort
|
19
|
+
|
20
|
+
n1.c.command("bob")
|
21
|
+
|
22
|
+
wait_for_consensus(nodes)
|
23
|
+
|
24
|
+
assert_equal n1.c.state, "bob"
|
25
|
+
assert_equal n2.c.state, "bob"
|
26
|
+
assert_equal n3.c.state, "bob"
|
27
|
+
|
28
|
+
n2.c.command("fred")
|
29
|
+
|
30
|
+
wait_for_consensus(nodes)
|
31
|
+
|
32
|
+
assert_equal n1.c.state, "fred"
|
33
|
+
assert_equal n2.c.state, "fred"
|
34
|
+
assert_equal n3.c.state, "fred"
|
35
|
+
|
36
|
+
[Thread.new { n1.c.command("c1") }, Thread.new { n2.c.command("c2") }, Thread.new { n3.c.command("c3") }].each(&:join)
|
37
|
+
|
38
|
+
wait_for_consensus(nodes)
|
39
|
+
|
40
|
+
val = n1.c.state
|
41
|
+
|
42
|
+
assert_equal val, n2.c.state
|
43
|
+
assert_equal val, n3.c.state
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative "./smoke_test_helper"
|
4
|
+
|
5
|
+
require "evinrude"
|
6
|
+
|
7
|
+
Thread.current.name = "MT"
|
8
|
+
|
9
|
+
nodes = spawn_nodes(5)
|
10
|
+
|
11
|
+
wait_for_stability(nodes)
|
12
|
+
wait_for_consensus(nodes)
|
13
|
+
|
14
|
+
prev_leader = nodes.find { |n| n.c.leader? }
|
15
|
+
prev_leader.t.kill
|
16
|
+
nodes.delete(prev_leader)
|
17
|
+
default_logger.info(logloc) { "Whacked leader #{prev_leader.t.name} to force election" }
|
18
|
+
|
19
|
+
wait_for_stability(nodes)
|
20
|
+
|
21
|
+
nodes.find { |n| n.c.follower? }.c.command("bob")
|
22
|
+
|
23
|
+
nodes.each do |n|
|
24
|
+
assert_equal n.c.state, "bob", n.t.name
|
25
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative "./smoke_test_helper"
|
4
|
+
|
5
|
+
require "evinrude"
|
6
|
+
|
7
|
+
Thread.current.name = "MT"
|
8
|
+
|
9
|
+
nodes = spawn_nodes(5)
|
10
|
+
|
11
|
+
wait_for_stability(nodes)
|
12
|
+
wait_for_consensus(nodes)
|
13
|
+
|
14
|
+
leader = nodes.find { |n| n.c.leader? }
|
15
|
+
|
16
|
+
leader.c.command("bob")
|
17
|
+
|
18
|
+
wait_for_consensus(nodes)
|
19
|
+
|
20
|
+
chosen_one = nodes.find { |n| n.c.follower? }
|
21
|
+
|
22
|
+
assert_equal "bob", chosen_one.c.state
|
23
|
+
|
24
|
+
victims = nodes.select { |n| n != leader && n != chosen_one }
|
25
|
+
|
26
|
+
victims.each do |v|
|
27
|
+
v.c.singleton_class.prepend(FaultInjector)
|
28
|
+
v.c.pause!(:process_append_entries_request)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Pop this in a separate thread because, if all goes well, this will block
|
32
|
+
cmd_t = Thread.new { Thread.current.name = "CT"; chosen_one.c.command("fred") }
|
33
|
+
cmd_t.abort_on_exception = true
|
34
|
+
|
35
|
+
# 640 milliseconds ought to be enough for anyone!
|
36
|
+
cmd_t.join(0.64)
|
37
|
+
|
38
|
+
assert_equal cmd_t.status, "sleep", "Command thread initial wait"
|
39
|
+
|
40
|
+
# This should also block
|
41
|
+
st_t = Thread.new { Thread.current.name = "ST"; chosen_one.c.state }
|
42
|
+
st_t.abort_on_exception = true
|
43
|
+
|
44
|
+
st_t.join(0.64)
|
45
|
+
|
46
|
+
assert_equal cmd_t.status, "sleep", "Command thread re-check"
|
47
|
+
assert_equal st_t.status, "sleep", "State retrieval thread"
|
48
|
+
|
49
|
+
# The fact that both of those threads are still waiting patiently indicates
|
50
|
+
# that the leader is being queried, and yet it hasn't achieved consensus
|
51
|
+
# (because a majority of the cluster is unresponsive). Now let's open up
|
52
|
+
# the floodgates and see what comes out!
|
53
|
+
|
54
|
+
victims.each { |v| v.c.unpause!(:process_append_entries_request) }
|
55
|
+
|
56
|
+
cmd_t.join(10)
|
57
|
+
|
58
|
+
assert_equal cmd_t.status, false, "Command thread completed"
|
59
|
+
|
60
|
+
nodes.each do |n|
|
61
|
+
assert_equal "fred", n.c.state, n.t.name
|
62
|
+
end
|
63
|
+
|
64
|
+
st_t.join(10)
|
65
|
+
|
66
|
+
assert_equal st_t.status, false, "State thread completed"
|
67
|
+
assert_equal st_t.value, "fred", "State thread gives new value"
|