evinrude 0.0.1

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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +23 -0
  3. data/.gitignore +6 -0
  4. data/.yardopts +1 -0
  5. data/CODE_OF_CONDUCT.md +49 -0
  6. data/CONTRIBUTING.md +10 -0
  7. data/LICENCE +674 -0
  8. data/README.md +410 -0
  9. data/evinrude.gemspec +42 -0
  10. data/lib/evinrude.rb +1233 -0
  11. data/lib/evinrude/backoff.rb +19 -0
  12. data/lib/evinrude/cluster_configuration.rb +162 -0
  13. data/lib/evinrude/config_change_queue_entry.rb +19 -0
  14. data/lib/evinrude/config_change_queue_entry/add_node.rb +13 -0
  15. data/lib/evinrude/config_change_queue_entry/remove_node.rb +14 -0
  16. data/lib/evinrude/freedom_patches/range.rb +5 -0
  17. data/lib/evinrude/log.rb +102 -0
  18. data/lib/evinrude/log_entries.rb +3 -0
  19. data/lib/evinrude/log_entry.rb +13 -0
  20. data/lib/evinrude/log_entry/cluster_configuration.rb +15 -0
  21. data/lib/evinrude/log_entry/null.rb +6 -0
  22. data/lib/evinrude/log_entry/state_machine_command.rb +13 -0
  23. data/lib/evinrude/logging_helpers.rb +40 -0
  24. data/lib/evinrude/message.rb +19 -0
  25. data/lib/evinrude/message/append_entries_reply.rb +13 -0
  26. data/lib/evinrude/message/append_entries_request.rb +18 -0
  27. data/lib/evinrude/message/command_reply.rb +13 -0
  28. data/lib/evinrude/message/command_request.rb +18 -0
  29. data/lib/evinrude/message/install_snapshot_reply.rb +13 -0
  30. data/lib/evinrude/message/install_snapshot_request.rb +18 -0
  31. data/lib/evinrude/message/join_reply.rb +13 -0
  32. data/lib/evinrude/message/join_request.rb +18 -0
  33. data/lib/evinrude/message/node_removal_reply.rb +13 -0
  34. data/lib/evinrude/message/node_removal_request.rb +18 -0
  35. data/lib/evinrude/message/read_reply.rb +13 -0
  36. data/lib/evinrude/message/read_request.rb +18 -0
  37. data/lib/evinrude/message/vote_reply.rb +13 -0
  38. data/lib/evinrude/message/vote_request.rb +18 -0
  39. data/lib/evinrude/messages.rb +14 -0
  40. data/lib/evinrude/metrics.rb +50 -0
  41. data/lib/evinrude/network.rb +69 -0
  42. data/lib/evinrude/network/connection.rb +144 -0
  43. data/lib/evinrude/network/protocol.rb +69 -0
  44. data/lib/evinrude/node_info.rb +35 -0
  45. data/lib/evinrude/peer.rb +50 -0
  46. data/lib/evinrude/resolver.rb +96 -0
  47. data/lib/evinrude/snapshot.rb +9 -0
  48. data/lib/evinrude/state_machine.rb +15 -0
  49. data/lib/evinrude/state_machine/register.rb +25 -0
  50. data/smoke_tests/001_single_node_cluster.rb +20 -0
  51. data/smoke_tests/002_three_node_cluster.rb +43 -0
  52. data/smoke_tests/003_spill.rb +25 -0
  53. data/smoke_tests/004_stale_read.rb +67 -0
  54. data/smoke_tests/005_sleepy_master.rb +28 -0
  55. data/smoke_tests/006_join_via_follower.rb +26 -0
  56. data/smoke_tests/007_snapshot_madness.rb +97 -0
  57. data/smoke_tests/008_downsizing.rb +43 -0
  58. data/smoke_tests/009_disaster_recovery.rb +46 -0
  59. data/smoke_tests/999_final_smoke_test.rb +279 -0
  60. data/smoke_tests/run +22 -0
  61. data/smoke_tests/smoke_test_helper.rb +199 -0
  62. 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,15 @@
1
+ class Evinrude
2
+ class StateMachine
3
+ def initialize(**kwargs)
4
+ end
5
+
6
+ def process_command(s)
7
+ end
8
+
9
+ def current_state
10
+ end
11
+
12
+ def snapshot
13
+ end
14
+ end
15
+ 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"