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,13 @@
1
+ require_relative "../message"
2
+
3
+ class Evinrude
4
+ class Message
5
+ class AppendEntriesReply < Message
6
+ attr_reader :term, :success, :last_index
7
+
8
+ def initialize(term:, success:, last_index: nil)
9
+ @term, @success, @last_index = term, success, last_index
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "../message"
2
+ require_relative "./append_entries_reply"
3
+
4
+ class Evinrude
5
+ class Message
6
+ class AppendEntriesRequest < Message
7
+ attr_reader :term, :leader_info, :leader_commit, :prev_log_index, :prev_log_term, :entries
8
+
9
+ def initialize(term:, leader_info:, leader_commit:, prev_log_index:, prev_log_term:, entries:)
10
+ @term, @leader_info, @leader_commit, @prev_log_index, @prev_log_term, @entries = term, leader_info, leader_commit, prev_log_index, prev_log_term, entries
11
+ end
12
+
13
+ def expected_reply_types
14
+ [AppendEntriesReply]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "../message"
2
+
3
+ class Evinrude
4
+ class Message
5
+ class CommandReply < Message
6
+ attr_reader :success, :leader_info
7
+
8
+ def initialize(success:, leader_info: nil)
9
+ @success, @leader_info = success, leader_info
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "../message"
2
+ require_relative "./command_reply"
3
+
4
+ class Evinrude
5
+ class Message
6
+ class CommandRequest < Message
7
+ attr_reader :command, :id, :node_name
8
+
9
+ def initialize(command:, id:, node_name:)
10
+ @command, @id, @node_name = command, id, node_name
11
+ end
12
+
13
+ def expected_reply_types
14
+ [CommandReply]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "../message"
2
+
3
+ class Evinrude
4
+ class Message
5
+ class InstallSnapshotReply < Message
6
+ attr_reader :term
7
+
8
+ def initialize(term:)
9
+ @term = term
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "../message"
2
+ require_relative "./install_snapshot_reply"
3
+
4
+ class Evinrude
5
+ class Message
6
+ class InstallSnapshotRequest < Message
7
+ attr_reader :term, :leader_info, :last_included_index, :last_included_term, :data
8
+
9
+ def initialize(term:, leader_info:, last_included_index:, last_included_term:, data:)
10
+ @term, @leader_info, @last_included_index, @last_included_term, @data = term, leader_info, last_included_index, last_included_term, data
11
+ end
12
+
13
+ def expected_reply_types
14
+ [InstallSnapshotReply]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "../message"
2
+
3
+ class Evinrude
4
+ class Message
5
+ class JoinReply < Message
6
+ attr_reader :success, :leader_info
7
+
8
+ def initialize(success:, leader_info: nil)
9
+ @success, @leader_info = success, leader_info
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "../message"
2
+ require_relative "./join_reply"
3
+
4
+ class Evinrude
5
+ class Message
6
+ class JoinRequest < Message
7
+ attr_reader :node_info
8
+
9
+ def initialize(node_info:)
10
+ @node_info = node_info
11
+ end
12
+
13
+ def expected_reply_types
14
+ [JoinReply]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "../message"
2
+
3
+ class Evinrude
4
+ class Message
5
+ class NodeRemovalReply < Message
6
+ attr_reader :success, :leader_info
7
+
8
+ def initialize(success:, leader_info: nil)
9
+ @success, @leader_info = success, leader_info
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "../message"
2
+ require_relative "./node_removal_reply"
3
+
4
+ class Evinrude
5
+ class Message
6
+ class NodeRemovalRequest < Message
7
+ attr_reader :node_info, :unsafe
8
+
9
+ def initialize(node_info:, unsafe: false)
10
+ @node_info, @unsafe = node_info, unsafe
11
+ end
12
+
13
+ def expected_reply_types
14
+ [NodeRemovalReply]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "../message"
2
+
3
+ class Evinrude
4
+ class Message
5
+ class ReadReply < Message
6
+ attr_reader :success, :leader_info
7
+
8
+ def initialize(success:, leader_info: nil)
9
+ @success, @leader_info = success, leader_info
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "../message"
2
+ require_relative "./read_reply"
3
+
4
+ class Evinrude
5
+ class Message
6
+ class ReadRequest < Message
7
+ attr_reader :commit_index
8
+
9
+ def initialize(commit_index:)
10
+ @commit_index = commit_index
11
+ end
12
+
13
+ def expected_reply_types
14
+ [ReadReply]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "../message"
2
+
3
+ class Evinrude
4
+ class Message
5
+ class VoteReply < Message
6
+ attr_reader :term, :vote_granted
7
+
8
+ def initialize(term:, vote_granted:)
9
+ @term, @vote_granted = term, vote_granted
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "../message"
2
+ require_relative "./vote_reply"
3
+
4
+ class Evinrude
5
+ class Message
6
+ class VoteRequest < Message
7
+ attr_reader :term, :candidate_info, :last_log_index, :last_log_term
8
+
9
+ def initialize(term:, candidate_info:, last_log_index:, last_log_term:)
10
+ @term, @candidate_info, @last_log_index, @last_log_term = term, candidate_info, last_log_index, last_log_term
11
+ end
12
+
13
+ def expected_reply_types
14
+ [VoteReply]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "./message/append_entries_reply"
2
+ require_relative "./message/append_entries_request"
3
+ require_relative "./message/command_reply"
4
+ require_relative "./message/command_request"
5
+ require_relative "./message/install_snapshot_reply"
6
+ require_relative "./message/install_snapshot_request"
7
+ require_relative "./message/join_reply"
8
+ require_relative "./message/join_request"
9
+ require_relative "./message/node_removal_reply"
10
+ require_relative "./message/node_removal_request"
11
+ require_relative "./message/read_reply"
12
+ require_relative "./message/read_request"
13
+ require_relative "./message/vote_reply"
14
+ require_relative "./message/vote_request"
@@ -0,0 +1,50 @@
1
+ require "prometheus/client"
2
+ require "frankenstein"
3
+ require "frankenstein/remove_time_series"
4
+
5
+ class Evinrude
6
+ class Metrics
7
+ attr_reader :command_execution, :commit_index, :info, :joint_configuration,
8
+ :log_entries_persisted, :log_file_size, :log_loaded_from_disk,
9
+ :match_index, :messages_received, :next_index, :node_count,
10
+ :read_state, :remove_node, :replication_majority, :rpc,
11
+ :rpc_exception, :snapshot_file_size, :start_time, :state, :term
12
+
13
+ def initialize(registry)
14
+ @registry = registry
15
+
16
+ @command_execution = Frankenstein::Request.new(:evinrude_command, description: "state machine command", registry: @registry)
17
+ @commit_index = get_or_create(:gauge, :evinrude_commit_index, docstring: "The index of the last log entry committed to the state machine")
18
+ @info = get_or_create(:gauge, :evinrude_node_info, docstring: "Basic information about this Evinrude node in labels", labels: %i{node_name listen_address listen_port advertise_address advertise_port})
19
+ @joint_configuration = get_or_create(:gauge, :evinrude_joint_configuration_action, docstring: "Whether or not this node is currently using a 'joint' configuration for consensus")
20
+ @log_entries_persisted = get_or_create(:counter, :evinrude_log_entries_persisted_total, docstring: "How many log entries have been persisted to disk")
21
+ @log_file_size = get_or_create(:gauge, :evinrude_log_file_size_bytes, docstring: "The current size of the append-only log file")
22
+ @log_loaded_from_disk = get_or_create(:gauge, :evinrude_log_loaded_from_disk, docstring: "Whether (1) or not (0) this Evinrude node was initialized from data on disk")
23
+ @match_index = get_or_create(:gauge, :evinrude_follower_match_index, docstring: "The last log index known to be replicated to this follower", labels: %i{peer node_name})
24
+ @messages_received = get_or_create(:counter, :evinrude_messages_received_total, docstring: "How many unsolicited (RPC) messages have been received", labels: %i{type})
25
+ @next_index = get_or_create(:gauge, :evinrude_follower_next_index, docstring: "The index of the next log entry to be sent to a follower node", labels: %i{peer node_name})
26
+ @node_count = get_or_create(:gauge, :evinrude_node_count, docstring: "How many distinct nodes, active or otherwise, are currently in the cluster configuration")
27
+ @read_state = Frankenstein::Request.new(:evinrude_read_state, description: "read state check", registry: @registry)
28
+ @remove_node = Frankenstein::Request.new(:evinrude_remove_node, description: "remove node request", registry: @registry)
29
+ @replication_majority = get_or_create(:gauge, :evinrude_replication_majority_index, docstring: "The last log index that has been replicated to a majority of nodes")
30
+ @rpc = Frankenstein::Request.new(:evinrude_rpc, description: "remote procedure call", registry: @registry, labels: %i{target}, duration_labels: %i{target result})
31
+ @rpc_exception = get_or_create(:counter, :evinrude_rpc_to_leader_exceptions_total, docstring: "How many exceptions have been raised whilst trying to do RPCs to the cluster leader", labels: %i{node_name target class})
32
+ @snapshot_file_size = get_or_create(:gauge, :evinrude_snapshot_file_size_bytes, docstring: "The current size of the snapshot data file")
33
+ @start_time = get_or_create(:gauge, :evinrude_node_start_time, docstring: "The number of seconds since the Unix epoch at which this Evinrude node commenced operation")
34
+ @state = get_or_create(:gauge, :evinrude_node_state, docstring: "The current state of the node; 0=init, 1=candidate, 2=follower, 3=leader")
35
+ @term = get_or_create(:gauge, :evinrude_current_term, docstring: "The current Raft election 'term' that this node is operating in")
36
+ end
37
+
38
+ def clear_peer_metrics
39
+ [@match_index, @next_index, @replication_majority].each do |metric|
40
+ metric.values.keys.each { |ls| metric.remove(ls) }
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def get_or_create(type, name, docstring:, labels: [])
47
+ @registry.get(name) || @registry.__send__(type, name, docstring: docstring, labels: labels)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,69 @@
1
+ require "async/io"
2
+ require "socket"
3
+
4
+ require_relative "./message/join_request"
5
+ require_relative "./network/connection"
6
+
7
+ class Evinrude
8
+ class Network
9
+ class ConnectionTimeoutError < Error; end
10
+
11
+ include Evinrude::LoggingHelpers
12
+
13
+ def initialize(keys:, logger:, metrics:, listen:, advertise:)
14
+ @keys, @logger, @metrics, @advertise, @listen = keys, logger, metrics, advertise, listen
15
+
16
+ @endpoint = Async::IO::Endpoint.tcp(@listen[:address], @listen[:port])
17
+ end
18
+
19
+ def start
20
+ @socket = @endpoint.bind.first
21
+ @socket.listen(Socket::SOMAXCONN)
22
+ self
23
+ end
24
+
25
+ def advertised_address
26
+ @advertise[:address] ||
27
+ (
28
+ Socket.ip_address_list.select { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }.first ||
29
+ Socket.ip_address_list.select { |a| a.ipv4? && !a.ipv4_loopback? }.first
30
+ ).ip_address
31
+ end
32
+
33
+ def advertised_port
34
+ @advertise[:port] ||
35
+ @socket&.instance_variable_get(:@io)&.local_address&.ip_port
36
+ end
37
+
38
+ def listen_address
39
+ @listen[:address]
40
+ end
41
+
42
+ def listen_port
43
+ @listen[:port] == 0 ? advertised_port : @listen[:port]
44
+ end
45
+
46
+ def each_message(&blk)
47
+ unless @socket
48
+ bind
49
+ end
50
+
51
+ @socket.listen(Socket::SOMAXCONN)
52
+
53
+ @socket.accept_each do |sock|
54
+ conn = Network::Connection.new(socket: sock, keys: @keys, logger: logger, metrics: @metrics)
55
+
56
+ conn.each_message do |msg|
57
+ blk.call(msg, conn)
58
+ end
59
+ end
60
+ end
61
+
62
+ def connect(address:, port:)
63
+ Connection.connect(address: address, port: port, keys: @keys, logger: logger, metrics: @metrics).tap do |conn|
64
+ logger.debug(logloc) { "New connection #{conn} to #{address}:#{port}" }
65
+ yield conn if block_given?
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,144 @@
1
+ require "async/semaphore"
2
+ require "digest/sha2"
3
+ require "yaml"
4
+
5
+ require_relative "./protocol"
6
+
7
+ class Evinrude
8
+ class Network
9
+ class Connection
10
+ include Evinrude::LoggingHelpers
11
+ include Evinrude::Network::Protocol
12
+
13
+ class ConnectionError < Error; end
14
+
15
+ attr_reader :peer_address, :peer_port
16
+
17
+ def self.connect(address:, port:, keys:, logger:, metrics:)
18
+ backoff = Evinrude::Backoff.new
19
+
20
+ begin
21
+ sock = Async::IO::Endpoint.tcp(address, port).connect
22
+ rescue Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ETIMEDOUT => ex
23
+ logger.info("Evinrude::Network::Connection.connect") { "Could not connect to #{address}:#{port}: #{ex.class}" }
24
+ raise ConnectionError,
25
+ "#{ex.class}"
26
+ end
27
+
28
+ new(socket: sock, logger: logger, metrics: metrics, keys: keys)
29
+ end
30
+
31
+ def initialize(socket:, logger:, metrics:, keys:)
32
+ @socket, @logger, @metrics = socket, logger, metrics
33
+
34
+ @keys = keys.map { |k| Digest::SHA256.digest(k) }
35
+
36
+ @sem = Async::Semaphore.new
37
+
38
+ @peer_address = @socket.remote_address.ip_address
39
+ @peer_port = @socket.remote_address.ip_port
40
+ end
41
+
42
+ def peer_info
43
+ "#{peer_address}:#{peer_port}"
44
+ end
45
+
46
+ def rpc(msg)
47
+ @metrics.rpc.measure(target: peer_info) do |labels|
48
+ begin
49
+ @sem.acquire do
50
+ logger.debug(logloc) { "Sending RPC request #{msg.inspect} to #{peer_info}" }
51
+ begin
52
+ @socket.write(frame(box(msg.to_yaml)))
53
+ rescue Errno::EPIPE, IOError, Errno::ECONNRESET => ex
54
+ logger.debug(logloc) { "Failed to send RPC request to #{peer_info}: #{ex.message} (#{ex.class})" }
55
+ labels[:result] = ex.class.to_s
56
+ return nil
57
+ end
58
+
59
+ logger.debug(logloc) { "Request sent; now we wait" }
60
+
61
+ begin
62
+ read_message
63
+ rescue Protocol::VersionError, Errno::ECONNRESET, Errno::EPIPE, IOError => ex
64
+ logger.debug(logloc) { "I/O exception #{ex.class} while reading RPC reply" }
65
+ labels[:result] = ex.class.to_s
66
+ @socket.close
67
+ nil
68
+ end
69
+ end.tap { labels[:result] = "success" }
70
+ rescue Async::Wrapper::Cancelled
71
+ labels[:result] = "cancelled"
72
+ nil
73
+ end
74
+ end
75
+ end
76
+
77
+ def each_message
78
+ begin
79
+ loop do
80
+ @sem.acquire do
81
+ yield read_message
82
+ end
83
+ end
84
+ rescue Async::Wrapper::Cancelled
85
+ # This is fine
86
+ nil
87
+ rescue Evinrude::Error, SystemCallError, IOError => ex
88
+ # This is... not so fine, but there's not much we can do about it
89
+ log_exception(ex) { "Reading message" }
90
+ @socket.close
91
+ nil
92
+ end
93
+ end
94
+
95
+ def send_reply(msg)
96
+ @socket.write(frame(box(msg.to_yaml)))
97
+ end
98
+
99
+ def close
100
+ @socket.close
101
+ end
102
+
103
+ def inspect
104
+ "#<#{self.class}:0x#{object_id.to_s(16)} " +
105
+ instance_variables.map do |iv|
106
+ next nil if %i{@logger @metrics @socket}.include?(iv)
107
+ "#{iv}=#{instance_variable_get(iv).inspect}"
108
+ end.compact.join(" ")
109
+ end
110
+
111
+ private
112
+
113
+ def read_message
114
+ v = @socket.read(1)
115
+
116
+ if v.nil?
117
+ logger.debug(logloc) { "Connection to #{peer_info} closed" }
118
+ raise Async::Wrapper::Cancelled
119
+ end
120
+
121
+ unless v == "\x00"
122
+ raise Protocol::VersionError, "Expected 0, got #{v.inspect}"
123
+ end
124
+
125
+ lenlen = @socket.read(1).ord
126
+
127
+ if (lenlen & 0x80) == 0
128
+ len = lenlen
129
+ else
130
+ lenlen &= 0x7f
131
+ len = @socket.read(lenlen).split(//).inject(0) { |a, c| a * 256 + c.ord }
132
+ end
133
+
134
+ if len > max_message_size
135
+ raise MessageTooBigError
136
+ end
137
+
138
+ box = @socket.read(len)
139
+
140
+ YAML.safe_load(unbox(box), permitted_classes: Message.classes + LogEntry.classes + [NodeInfo, ClusterConfiguration, Symbol], aliases: true)
141
+ end
142
+ end
143
+ end
144
+ end