evinrude 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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"