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
data/README.md
ADDED
@@ -0,0 +1,410 @@
|
|
1
|
+
Evinrude is an opinionated but flexible implementation of the [Raft distributed
|
2
|
+
consensus algorithm](https://raft.github.io/). It is intended for use in any
|
3
|
+
situation where you need to be able to safely and securely achieve consensus
|
4
|
+
regarding the current state of a set of data in Ruby programs.
|
5
|
+
|
6
|
+
|
7
|
+
# Installation
|
8
|
+
|
9
|
+
It's a gem:
|
10
|
+
|
11
|
+
gem install evinrude
|
12
|
+
|
13
|
+
There's also the wonders of [the Gemfile](http://bundler.io):
|
14
|
+
|
15
|
+
gem 'evinrude'
|
16
|
+
|
17
|
+
If you're the sturdy type that likes to run from git:
|
18
|
+
|
19
|
+
rake install
|
20
|
+
|
21
|
+
Or, if you've eschewed the convenience of Rubygems entirely, then you
|
22
|
+
presumably know what to do already.
|
23
|
+
|
24
|
+
|
25
|
+
# Usage
|
26
|
+
|
27
|
+
In order to do its thing, {Evinrude} needs at least two things: the contact
|
28
|
+
details of an existing member of the cluster, and the shared secret key for the
|
29
|
+
cluster. Then, you create your {Evinrude} instance and set it running, like this:
|
30
|
+
|
31
|
+
```
|
32
|
+
c = Evinrude.new(join_hints: [{ address: "192.0.2.42", port: 31337 }], shared_keys: ["s3kr1t"])
|
33
|
+
c.run
|
34
|
+
```
|
35
|
+
|
36
|
+
The {Evinrude#run} method does not return in normal operation; you should call
|
37
|
+
it in a separate thread (or use a work supervisor such as
|
38
|
+
[Ultravisor](https://rubygems.org/gem/ultravisor)).
|
39
|
+
|
40
|
+
Once Evinrude is running, interaction with the data in the cluster is
|
41
|
+
straightforward. To cause a change to be made to the data set, you call
|
42
|
+
{Evinrude#command} with a message which describes how the shared state of the
|
43
|
+
cluster should be changed (an application-specific language which it is up to you
|
44
|
+
to define), while to retrieve the currently-agreed state of the data set you
|
45
|
+
call {Evinrude#state}.
|
46
|
+
|
47
|
+
By default, the data set that is managed by consensus is a "register" -- a single
|
48
|
+
atomic string value, wherein the single value is the value of the most recent
|
49
|
+
{Evinrude#command} call that has been committed by the cluster.
|
50
|
+
|
51
|
+
Since a single "last-write-wins" string is often not a particularly useful thing
|
52
|
+
to keep coordinated, Evinrude allows you to provide a more useful state machine
|
53
|
+
implementation which matches the data model you're working with.
|
54
|
+
|
55
|
+
|
56
|
+
## The Machines of State
|
57
|
+
|
58
|
+
Because Evinrude is merely a Raft *engine*, it makes no assumptions about the
|
59
|
+
semantics of the data that is being managed. For this reason, most non-trivial
|
60
|
+
uses of Evinrude will want to provide their own implementation of {Evinrude::StateMachine},
|
61
|
+
and provide it to the {Evinrude.new} call using the `state_machine` keyword
|
62
|
+
argument:
|
63
|
+
|
64
|
+
```
|
65
|
+
class MyStateMachine < Evinrude::StateMachine
|
66
|
+
# interesting things go here
|
67
|
+
end
|
68
|
+
|
69
|
+
c = Evinrude.new(join_hints: [...], shared_keys: [ ... ], state_machine: MyStateMachine)
|
70
|
+
|
71
|
+
# ...
|
72
|
+
```
|
73
|
+
|
74
|
+
While the current state of the state machine is what we, as consumers of the
|
75
|
+
replicated data, care about, behind the scenes Raft deals entirely in a log of commands.
|
76
|
+
Each command (along with its arguments) may cause some deterministic change to the
|
77
|
+
internal state variables. Exactly what commands are available, their arguments, and
|
78
|
+
what they do is up to your state machine implementation.
|
79
|
+
|
80
|
+
Thus, the core method in your state machine implementation is
|
81
|
+
{Evinrude::StateMachine#process_command}. Similar to {Evinrude#command}, this
|
82
|
+
method accepts a string of arbitrary data, which it is the responsibility of
|
83
|
+
your state machine to decode and action. In fact, the commands that your state
|
84
|
+
machine receives are the exact same ones that are provided to
|
85
|
+
{Evinrude#command}. The only difference is that the only commands your state
|
86
|
+
machine will receive are those that the cluster as a whole has committed.
|
87
|
+
|
88
|
+
The other side, of course, is retrieving the current state. That is handled by
|
89
|
+
{Evinrude::StateMachine#current_state}. This method, which takes no arguments, can
|
90
|
+
return an arbitrary Ruby object that represents the current state of the
|
91
|
+
machine.
|
92
|
+
|
93
|
+
You don't need to worry about concurrency issues inside your state machine, by the way;
|
94
|
+
all calls to all methods on the state machine instance will be serialized via mutex.
|
95
|
+
|
96
|
+
It is *crucially* important that your state machine take no input from anywhere
|
97
|
+
other than calls to `#command`, and do nothing but modify internal state
|
98
|
+
variables. If you start doing things like querying data in the outside world,
|
99
|
+
or interacting with anything outside the state machine in response to commands,
|
100
|
+
you will obliterate the guarantees of the replicated state machine model, and
|
101
|
+
all heck will, sooner or later, break loose.
|
102
|
+
|
103
|
+
One performance problem in a Raft state machine is the need to replay every log
|
104
|
+
message since the dawn of time in order to reproduce the current state when
|
105
|
+
(re)starting. Since that can take a long time (and involve a lot of storage
|
106
|
+
and/or network traffic), Raft has the concept of *shapshots*. These are string
|
107
|
+
representations of the entire current state of the machine. Thus, your state
|
108
|
+
machine has to implement {Evinrude::StateMachine#snapshot}, which serializes
|
109
|
+
the current state into a string. To load the state, a previously obtained
|
110
|
+
snapshot string will be passed to {Evinrude::StateMachine#initialize} in the
|
111
|
+
`snapshot` keyword argument.
|
112
|
+
|
113
|
+
... and that is the entire state machine interface.
|
114
|
+
|
115
|
+
|
116
|
+
## Persistent Storage
|
117
|
+
|
118
|
+
Whilst for toy systems you *can* get away with just storing everything in memory,
|
119
|
+
it's generally not considered good form for anything which you want to survive
|
120
|
+
long-term. For that reason, you'll generally want to specify the `storage_dir`
|
121
|
+
keyword argument, specifying a directory which is writable by the user running
|
122
|
+
the process that is calling reating the object.
|
123
|
+
|
124
|
+
If there is existing state in that directory, it will be loaded before Evinrude
|
125
|
+
attempts to re-join the cluster.
|
126
|
+
|
127
|
+
|
128
|
+
## The Madness of Networks
|
129
|
+
|
130
|
+
By default, Evinrude will listen on the `ANY` address, on a randomly assigned
|
131
|
+
high port, and will advertise itself as being available on the first sensible-looking
|
132
|
+
(ie non-loopback/link-local) address on the listening port.
|
133
|
+
|
134
|
+
In a sane and sensible network, that would be sufficient (with the possible
|
135
|
+
exception of the "listen on the random port" bit -- that can get annoying for
|
136
|
+
discovery purposes). However, so very, *very* few networks are sane and sensible,
|
137
|
+
and so there are knobs to tweak.
|
138
|
+
|
139
|
+
First off, if you need to control what address/port Evinrude listens on, you do
|
140
|
+
that via the `listen` keyword argument:
|
141
|
+
|
142
|
+
```
|
143
|
+
Evinrude.new(listen: { address: "192.0.2.42", port: 31337 }, ...)
|
144
|
+
```
|
145
|
+
|
146
|
+
Both `address` and `port` are optional; if left out, they'll be set to the appropriate
|
147
|
+
default. So you can just control the port to listen on, for instance, by
|
148
|
+
setting `listen: { port: 31337 }` if you like.
|
149
|
+
|
150
|
+
The other half of the network configuration is the *advertisement*. This is
|
151
|
+
needed because sometimes the address that Evinrude thinks it has is not the
|
152
|
+
address that other Evinrude instances must use to talk to it. Anywhere NAT
|
153
|
+
rears its ugly head is a candidate for this -- Docker containers where
|
154
|
+
publishing is in use, for instance, will almost certainly fall foul of this.
|
155
|
+
For this reason, you can override the advertised address and/or port using
|
156
|
+
the `advertise` keyword argument:
|
157
|
+
|
158
|
+
```
|
159
|
+
Evinrude.new(advertise: { address: "192.0.2.42", port: 31337 })
|
160
|
+
```
|
161
|
+
|
162
|
+
|
163
|
+
## Bootstrapping and Joining a Cluster
|
164
|
+
|
165
|
+
A Raft cluster bootstraps itself by having the "first" node recognise that it is
|
166
|
+
all alone in the world, and configure itself as the single node in the cluster.
|
167
|
+
After that, all other new nodes need to told the location of at least one other
|
168
|
+
node in the cluster. Existing cluster nodes that are restarted can *usually* use
|
169
|
+
the cluster configuration that is stored on disk, however a node which has been
|
170
|
+
offline while all other cluster nodes have changed addresses may still need to
|
171
|
+
use the join hints to find another node.
|
172
|
+
|
173
|
+
To signal to a node that it is the initial "bootstrap" node, you must explicitly
|
174
|
+
pass `join_hints: nil` to `Evinrude.new`:
|
175
|
+
|
176
|
+
```
|
177
|
+
# Bootstrap mode
|
178
|
+
c = Evinrude.new(join_hints: nil, ...)
|
179
|
+
```
|
180
|
+
|
181
|
+
Note that `nil` is *not* the default for `join_hints`; this is for safety, to avoid
|
182
|
+
any sort of configuration error causing havoc.
|
183
|
+
|
184
|
+
All other nodes in the cluster should be provided with the location of at least
|
185
|
+
one existing cluster member via `join_hints`. The usual form of the `join_hints`
|
186
|
+
is an array of one or more of the following entries:
|
187
|
+
|
188
|
+
* A hash containing `:address` and `:port` keys; `:address` can be either an
|
189
|
+
IPv4 or IPv6 literal address, or a hostname which the system is capable of
|
190
|
+
resolving into one or more IPv4 or IPv6 addresses, while `:port` must be
|
191
|
+
an integer representing a valid port number; *or*
|
192
|
+
|
193
|
+
* A string, which will be queried for `SRV` records.
|
194
|
+
|
195
|
+
An example, containing all of these:
|
196
|
+
|
197
|
+
```
|
198
|
+
c = Evinrude.new(join_hints: [
|
199
|
+
{ address: "192.0.2.42", port: 1234 },
|
200
|
+
{ address: "2001:db8::42", port: 4321 },
|
201
|
+
{ address: "cluster.example.com", port: 31337 },
|
202
|
+
"cluster._evinrude._tcp.example.com"
|
203
|
+
],
|
204
|
+
...)
|
205
|
+
```
|
206
|
+
|
207
|
+
As shown above, you can use all of the different forms together. They'll
|
208
|
+
be resolved and expanded into a big list of addresses as required.
|
209
|
+
|
210
|
+
|
211
|
+
## Encryption Key Management
|
212
|
+
|
213
|
+
To provide at least a modicum of security, all cluster network communications
|
214
|
+
are encrypted using a symmetric cipher. This requires a common key for
|
215
|
+
encryption and decryption, which you provide in the `shared_keys` keyword
|
216
|
+
argument:
|
217
|
+
|
218
|
+
```
|
219
|
+
c = Evinrude.new(shared_keys: ["s3krit"], ...)
|
220
|
+
```
|
221
|
+
|
222
|
+
The keys you use can be arbitrary strings of arbitrary length. Preferably, you
|
223
|
+
want the string to be completely random and have at least 128 bits of entropy.
|
224
|
+
For example, you could use a string of 16 binary characters, encoded in
|
225
|
+
hex: `SecureRandom.hex(16)`. The longer the better, but there's no point
|
226
|
+
having more than 256 bits of entropy, because your keys get hashed to 32 bytes
|
227
|
+
for use in the encryption algorithm.
|
228
|
+
|
229
|
+
As you can see from the above example, `shared_keys` is an *array* of strings,
|
230
|
+
not a single string. This is to facilitate *key rotation*, if you're into that
|
231
|
+
kind of thing.
|
232
|
+
|
233
|
+
Since you don't want to interrupt cluster operation, you can't take down
|
234
|
+
all the nodes simultaneously to change the key. Instead, you do the following,
|
235
|
+
assuming that you are using a secret key `"oldkey"`, and you want to
|
236
|
+
switch to using `"newkey"`:
|
237
|
+
|
238
|
+
1. Reconfigure each node, one by one, to set `shared_keys: ["oldkey", "newkey"]`
|
239
|
+
(Note the order there is important! `"oldkey"` first, then `"newkey"`)
|
240
|
+
|
241
|
+
2. When all nodes are running with the new configuration, then go around
|
242
|
+
and reconfigure each node again, to set `shared_keys: ["newkey", "oldkey"]`
|
243
|
+
(Again, *order is important*).
|
244
|
+
|
245
|
+
3. Finally, once all nodes are running with this second configuration, you
|
246
|
+
can remove `"oldkey"` from the configuration, and restart everything
|
247
|
+
with `shared_keys: ["newkey"]`, which retires the old key entirely.
|
248
|
+
|
249
|
+
This may seem like a lot of fiddling around, which is why you should always
|
250
|
+
use configuration management, which takes care of all the boring fiddling
|
251
|
+
around for you.
|
252
|
+
|
253
|
+
Why this works is because of how Evinrude uses the keys. The first key
|
254
|
+
in the list is the key with which all messages are encrypted. However any
|
255
|
+
received message can be decrypted with *any* key in the list. Hence, the
|
256
|
+
three step process:
|
257
|
+
|
258
|
+
1. While you're doing step 1, everyone is encrypting with `"oldkey"`, so nobody
|
259
|
+
will ever need to use `"newkey"` to decrypt anything, but that's OK.
|
260
|
+
|
261
|
+
2. While you're doing step 2, some nodes will be encrypting their messages with
|
262
|
+
`"oldkey"` and some will be encrypting with `"newkey"`. But since all the
|
263
|
+
nodes can decrypt anything encrypted with *either* `"oldkey"` *or*
|
264
|
+
`"newkey"` (because that's how they were configured in step 1), there's no
|
265
|
+
problem.
|
266
|
+
|
267
|
+
3. By the time you start step 3, everyone is encrypting everything with
|
268
|
+
`"newkey"`, so there's no problems with removing `"oldkey"` from the set of
|
269
|
+
shared keys.
|
270
|
+
|
271
|
+
|
272
|
+
## Managing and Decommissioning Nodes
|
273
|
+
|
274
|
+
Because Raft works on a "consensus" basis, a majority of nodes must always
|
275
|
+
be available to accept updates and agree on the current state of the cluster.
|
276
|
+
This is true for both writes (changes to the cluster state), *as well as reads*.
|
277
|
+
|
278
|
+
Once a node has joined the cluster, it is considered to be a part of the
|
279
|
+
cluster forever, unless it is explicitly removed. It is not safe for a node to
|
280
|
+
be removed automatically after some period of inactivity, because that node
|
281
|
+
could re-appear at any time and cause issues, including what is known as
|
282
|
+
"split-brain" (where there are two separate operational clusters, both of which
|
283
|
+
believe they know how things should be).
|
284
|
+
|
285
|
+
Evinrude makes some attempts to make the need to manually remove nodes rare. In
|
286
|
+
many raft implementations, a node is identified by its IP address and port. If
|
287
|
+
that changes, it counts as a new node. When you're using a "dynamic network"
|
288
|
+
system (like most cloud providers), every time a server restarts, it gets a new
|
289
|
+
IP address, which is counted as a new node, and so quickly there's more old, dead
|
290
|
+
nodes than currently living ones, and the cluster completely seizes up.
|
291
|
+
|
292
|
+
In contrast, Evinrude nodes have a name as well as the usual address/port pair. If a node
|
293
|
+
joins (or re-joins) the cluster with a name identical to that of a node already in
|
294
|
+
the cluster configuration, then the old node's address and port are replaced with
|
295
|
+
the address and port of the new one.
|
296
|
+
|
297
|
+
You can set one by hand, using the `node_name` keyword argument (although be
|
298
|
+
*really* sure to make them unique, or all heck will break loose), but if you
|
299
|
+
don't set one by hand, a new node will generate a UUID for its name. If a node
|
300
|
+
loads its state from disk on startup, it will use whatever name was stored on disk.
|
301
|
+
|
302
|
+
Thus, if you have servers backed by persistent storage, you don't have to do
|
303
|
+
anything special: let Evinrude generate a random name on first startup, write out
|
304
|
+
its node name to disk, and then on every restart thereafter, the shared cluster configuration
|
305
|
+
will be updated to keep the cluster state clean.
|
306
|
+
|
307
|
+
Even if you don't have persistant storage, as long as you can pass the same
|
308
|
+
node name to the cluster node each time it starts, everything will still be
|
309
|
+
fine: the fresh node will give its new address and port with its existing name,
|
310
|
+
the cluster configuration will be updated, the new node will be sent the
|
311
|
+
existing cluster state, and off it goes.
|
312
|
+
|
313
|
+
|
314
|
+
### Removing a Node
|
315
|
+
|
316
|
+
All that being said, there *are* times when a cluster node has to be forcibly
|
317
|
+
removed from the cluster. A few of the common cases are:
|
318
|
+
|
319
|
+
1. **Downsizing**: you were running a cluster of, say, nine nodes (because who
|
320
|
+
*doesn't* want N+4 redundancy?), but a management decree says that for
|
321
|
+
budget reasons, you can now only have five nodes (N+2 ought to be enough for
|
322
|
+
anyone!). In that case, you shut down four of the nodes, but the cluster
|
323
|
+
will need to be told that they've been removed, otherwise as soon as one
|
324
|
+
node crashes, the whole cluster will seize up.
|
325
|
+
|
326
|
+
2. **Operator error**: somehow (doesn't matter how, we all make mistakes
|
327
|
+
sometimes) an extra node managed to join the cluster. You nuked it before
|
328
|
+
it did any real damage, but the cluster config still thinks that node should
|
329
|
+
be part of the quorum. It needs to be removed before All Heck Breaks Loose.
|
330
|
+
|
331
|
+
3. **Totally Dynamic Environment**: if your cluster members have *no* state
|
332
|
+
persistence, not even being able to remember their name, nodes will need to
|
333
|
+
gracefully deregister themselves from the cluster when they shutdown.
|
334
|
+
**Note**: in this case, nodes that crash and burn without having a chance to
|
335
|
+
gracefully say "I'm outta here" will clog up the cluster, and sooner or
|
336
|
+
later you'll have more ex-nodes than live nodes, leading to eventual
|
337
|
+
Confusion and Delay. Make sure you've got some sort of "garbage collection"
|
338
|
+
background operation running, that can identify permanently-dead nodes and
|
339
|
+
remove them from the cluster before they cause downtime.
|
340
|
+
|
341
|
+
In any event, the way to remove a node is straightforward: from any node currently
|
342
|
+
in the cluster, call {Evinrude#remove_node}, passing the node's info:
|
343
|
+
|
344
|
+
```
|
345
|
+
c = Evinrude.new(...)
|
346
|
+
c.remove_node(Evinrude::NodeInfo.new(address: "2001:db8::42", port: 31337, name: "fred"))
|
347
|
+
```
|
348
|
+
|
349
|
+
This will notify the cluster leader of the node's departure, and the cluster
|
350
|
+
config will be updated.
|
351
|
+
|
352
|
+
Removing a node requires the cluster to still have consensus (half the
|
353
|
+
cluster nodes running), for the new configuration to take effect. This is so the
|
354
|
+
removal can be safe, by doing Raft Trickery to ensure that the removed node
|
355
|
+
can't cause split-brain issues on its way out the door.
|
356
|
+
|
357
|
+
|
358
|
+
### Emergency Removal of a Node
|
359
|
+
|
360
|
+
If your cluster has completely seized up, due to more than half of
|
361
|
+
the nodes in the cluster configuration being offline, things are somewhat trickier.
|
362
|
+
In this situation, you need to do the following:
|
363
|
+
|
364
|
+
1. Make 110% sure that the node (or nodes) you're removing aren't coming back any
|
365
|
+
time soon. If the nodes you're removing spontaneously reappear, you can end
|
366
|
+
up with split-brain.
|
367
|
+
|
368
|
+
2. Locate the current cluster leader node. The {Evinrude#leader?} method is your
|
369
|
+
friend here. If no node is the leader, then find a node which is a candidate
|
370
|
+
instead (with {Evinrude#candidate?} and use that.
|
371
|
+
|
372
|
+
3. Request the removal of a node with {Evinrude#remove_node}, but this time pass the
|
373
|
+
keyword argument `unsafe: true`. This bypasses the consensus checks.
|
374
|
+
|
375
|
+
The reason why you need to do this on the leader is because the new config that
|
376
|
+
doesn't have the removed node needs to propagate from the leader to the rest of
|
377
|
+
the cluster. When the cluster doesn't have a leader, removing the node from a
|
378
|
+
candidate allows that candidate to gather enough votes to consider itself a
|
379
|
+
leader, at which point it can propagate its configuration to the other nodes.
|
380
|
+
|
381
|
+
In almost all cases, you'll need to remove several nodes in order to get the
|
382
|
+
cluster working again. Just keep removing nodes until everything comes back.
|
383
|
+
|
384
|
+
Bear in mind that if your cluster split-brains as a result of passing `unsafe:
|
385
|
+
true`, you get to keep both pieces -- that's why the keyword's called `unsafe`!
|
386
|
+
|
387
|
+
|
388
|
+
# Contributing
|
389
|
+
|
390
|
+
Please see [CONTRIBUTING.md](CONTRIBUTING.md).
|
391
|
+
|
392
|
+
|
393
|
+
# Licence
|
394
|
+
|
395
|
+
Unless otherwise stated, everything in this repo is covered by the following
|
396
|
+
copyright notice:
|
397
|
+
|
398
|
+
Copyright (C) 2020 Matt Palmer <matt@hezmatt.org>
|
399
|
+
|
400
|
+
This program is free software: you can redistribute it and/or modify it
|
401
|
+
under the terms of the GNU General Public License version 3, as
|
402
|
+
published by the Free Software Foundation.
|
403
|
+
|
404
|
+
This program is distributed in the hope that it will be useful,
|
405
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
406
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
407
|
+
GNU General Public License for more details.
|
408
|
+
|
409
|
+
You should have received a copy of the GNU General Public License
|
410
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/evinrude.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
begin
|
2
|
+
require 'git-version-bump'
|
3
|
+
rescue LoadError
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "evinrude"
|
9
|
+
|
10
|
+
s.version = GVB.version rescue "0.0.0.1.NOGVB"
|
11
|
+
s.date = GVB.date rescue Time.now.strftime("%Y-%m-%d")
|
12
|
+
|
13
|
+
s.platform = Gem::Platform::RUBY
|
14
|
+
|
15
|
+
s.summary = "The Raft engine"
|
16
|
+
|
17
|
+
s.authors = ["Matt Palmer"]
|
18
|
+
s.email = ["theshed+evinrude@hezmatt.org"]
|
19
|
+
s.homepage = "https://github.com/mpalmer/evinrude"
|
20
|
+
|
21
|
+
s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(G|spec|Rakefile)/ }
|
22
|
+
|
23
|
+
s.required_ruby_version = ">= 2.5.0"
|
24
|
+
|
25
|
+
s.add_runtime_dependency "async"
|
26
|
+
s.add_runtime_dependency "async-dns"
|
27
|
+
s.add_runtime_dependency "async-io"
|
28
|
+
s.add_runtime_dependency "frankenstein", "~> 2.1"
|
29
|
+
s.add_runtime_dependency "prometheus-client", "~> 2.0"
|
30
|
+
s.add_runtime_dependency "rbnacl"
|
31
|
+
|
32
|
+
s.add_development_dependency 'bundler'
|
33
|
+
s.add_development_dependency 'github-release'
|
34
|
+
s.add_development_dependency 'guard-rspec'
|
35
|
+
s.add_development_dependency 'rake', '~> 10.4', '>= 10.4.2'
|
36
|
+
# Needed for guard
|
37
|
+
s.add_development_dependency 'rb-inotify', '~> 0.9'
|
38
|
+
s.add_development_dependency 'redcarpet'
|
39
|
+
s.add_development_dependency 'rspec'
|
40
|
+
s.add_development_dependency 'simplecov'
|
41
|
+
s.add_development_dependency 'yard'
|
42
|
+
end
|