raft 0.0.2 → 0.0.3
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.
- checksums.yaml +7 -0
- data/lib/raft.rb +212 -58
- data/lib/raft/goliath.rb +145 -56
- metadata +79 -14
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b93e1acac92fc590e669fd765e13da401bd03649
|
4
|
+
data.tar.gz: 292865e0af4c1a662a3b07fbfeb7d4905af8e195
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: eb2440b0e811a0a557ae32da8d3024b80e61c8fb45b870836adb15a5d36e0fd578e54459234f113d220f6442504347ed5b3c0cbdb405879cca4c4d2d3b9ef1b0
|
7
|
+
data.tar.gz: f97c12da642745009b5811e38ec584ed40b66687a44ff10effb58d2182a0078787126c3baefc7ba4b5ea351d3e7fa4ebeda16c03cdb3de98a0e20ba2696395e4
|
data/lib/raft.rb
CHANGED
@@ -1,17 +1,72 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
1
3
|
module Raft
|
2
4
|
Config = Struct.new(:rpc_provider, :async_provider, :election_timeout, :update_interval, :heartbeat_interval)
|
3
5
|
|
4
6
|
class Cluster
|
5
|
-
|
7
|
+
attr_reader :node_ids
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@node_ids = []
|
11
|
+
end
|
6
12
|
|
7
13
|
def quorum
|
8
|
-
@node_ids.
|
14
|
+
@node_ids.count / 2 + 1 # integer division rounds down
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class LogEntry
|
19
|
+
attr_reader :term, :index, :command
|
20
|
+
|
21
|
+
def initialize(term, index, command)
|
22
|
+
@term, @index, @command = term, index, command
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Log < DelegateClass(Array)
|
27
|
+
def last(*args)
|
28
|
+
self.any? ? super(*args) : LogEntry.new(nil, nil, nil)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class PersistentState #= Struct.new(:current_term, :voted_for, :log)
|
33
|
+
attr_reader :current_term, :voted_for, :log
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@current_term = 0
|
37
|
+
@voted_for = nil
|
38
|
+
@log = Log.new([])
|
39
|
+
end
|
40
|
+
|
41
|
+
def current_term=(new_term)
|
42
|
+
raise 'cannot restart an old term' unless @current_term < new_term
|
43
|
+
@current_term = new_term
|
44
|
+
@voted_for = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def voted_for=(new_votee)
|
48
|
+
raise 'cannot change vote for this term' unless @voted_for.nil?
|
49
|
+
@voted_for = new_votee
|
50
|
+
end
|
51
|
+
|
52
|
+
def log=(new_log)
|
53
|
+
@log = Log.new(new_log)
|
9
54
|
end
|
10
55
|
end
|
11
56
|
|
12
|
-
|
57
|
+
class TemporaryState
|
58
|
+
attr_reader :commit_index
|
59
|
+
attr_accessor :leader_id
|
60
|
+
|
61
|
+
def initialize(commit_index, leader_id)
|
62
|
+
@commit_index, @leader_id = commit_index, leader_id
|
63
|
+
end
|
13
64
|
|
14
|
-
|
65
|
+
def commit_index=(new_commit_index)
|
66
|
+
raise 'cannot uncommit log entries' unless @commit_index.nil? || @commit_index <= new_commit_index
|
67
|
+
@commit_index = new_commit_index
|
68
|
+
end
|
69
|
+
end
|
15
70
|
|
16
71
|
class LeadershipState
|
17
72
|
def followers
|
@@ -27,12 +82,20 @@ module Raft
|
|
27
82
|
|
28
83
|
FollowerState = Struct.new(:next_index, :succeeded)
|
29
84
|
|
30
|
-
LogEntry = Struct.new(:term, :index, :command)
|
31
|
-
|
32
85
|
RequestVoteRequest = Struct.new(:term, :candidate_id, :last_log_index, :last_log_term)
|
33
86
|
|
87
|
+
#class RequestVoteRequest < Struct.new(:term, :candidate_id, :last_log_index, :last_log_term)
|
88
|
+
# def term; @term.to_i; end
|
89
|
+
# def last_log_index; @last_log_index.to_i; end
|
90
|
+
# def last_log_term; @last_log_term.to_i; end
|
91
|
+
#end
|
92
|
+
|
34
93
|
RequestVoteResponse = Struct.new(:term, :vote_granted)
|
35
94
|
|
95
|
+
#class RequestVoteResponse < Struct.new(:term, :vote_granted)
|
96
|
+
# def term; @term.to_i; end
|
97
|
+
#end
|
98
|
+
|
36
99
|
AppendEntriesRequest = Struct.new(:term, :leader_id, :prev_log_index, :prev_log_term, :entries, :commit_index)
|
37
100
|
|
38
101
|
AppendEntriesResponse = Struct.new(:term, :success)
|
@@ -67,8 +130,8 @@ module Raft
|
|
67
130
|
|
68
131
|
class Timer
|
69
132
|
def initialize(interval)
|
70
|
-
@
|
71
|
-
|
133
|
+
@interval = interval
|
134
|
+
@start = Time.now - interval
|
72
135
|
end
|
73
136
|
|
74
137
|
def reset!
|
@@ -97,17 +160,22 @@ module Raft
|
|
97
160
|
CANDIDATE_ROLE = 1
|
98
161
|
LEADER_ROLE = 2
|
99
162
|
|
100
|
-
def initialize(id, config, cluster)
|
163
|
+
def initialize(id, config, cluster, commit_handler=nil, &block)
|
101
164
|
@id = id
|
102
165
|
@role = FOLLOWER_ROLE
|
103
166
|
@config = config
|
104
167
|
@cluster = cluster
|
105
|
-
@persistent_state = PersistentState.new
|
168
|
+
@persistent_state = PersistentState.new
|
106
169
|
@temporary_state = TemporaryState.new(nil, nil)
|
107
170
|
@election_timer = Timer.new(config.election_timeout)
|
171
|
+
@commit_handler = commit_handler || (block.to_proc if block_given?)
|
108
172
|
end
|
109
173
|
|
110
174
|
def update
|
175
|
+
return if @updating
|
176
|
+
@updating = true
|
177
|
+
indent = "\t" * (@id.to_i % 3)
|
178
|
+
#STDOUT.write("\n\n#{indent}update #{@id}, role #{@role}, log length #{@persistent_state.log.count}\n\n")
|
111
179
|
case @role
|
112
180
|
when FOLLOWER_ROLE
|
113
181
|
follower_update
|
@@ -116,6 +184,7 @@ module Raft
|
|
116
184
|
when LEADER_ROLE
|
117
185
|
leader_update
|
118
186
|
end
|
187
|
+
@updating = false
|
119
188
|
end
|
120
189
|
|
121
190
|
def follower_update
|
@@ -124,6 +193,7 @@ module Raft
|
|
124
193
|
candidate_update
|
125
194
|
end
|
126
195
|
end
|
196
|
+
|
127
197
|
protected :follower_update
|
128
198
|
|
129
199
|
def candidate_update
|
@@ -131,60 +201,102 @@ module Raft
|
|
131
201
|
@persistent_state.current_term += 1
|
132
202
|
@persistent_state.voted_for = @id
|
133
203
|
reset_election_timeout
|
134
|
-
|
204
|
+
last_log_entry = @persistent_state.log.last
|
205
|
+
log_index = last_log_entry ? last_log_entry.index : nil
|
206
|
+
log_term = last_log_entry ? last_log_entry.term : nil
|
207
|
+
request = RequestVoteRequest.new(@persistent_state.current_term, @id, log_index, log_term)
|
135
208
|
votes_for = 1 # candidate always votes for self
|
136
209
|
votes_against = 0
|
137
210
|
quorum = @cluster.quorum
|
138
|
-
|
139
|
-
|
211
|
+
#STDOUT.write("\n\t\t#{@id} requests votes for term #{@persistent_state.current_term}\n\n")
|
212
|
+
@config.rpc_provider.request_votes(request, @cluster) do |_, request, response|
|
213
|
+
#STDOUT.write("\n\t\t#{@id} receives vote #{response.vote_granted}\n\n")
|
214
|
+
elected = nil # no majority result yet
|
215
|
+
if request.term != @persistent_state.current_term
|
216
|
+
# this is a response to an out-of-date request, just ignore it
|
217
|
+
elsif response.term > @persistent_state.current_term
|
140
218
|
@role = FOLLOWER_ROLE
|
141
|
-
|
219
|
+
elected = false
|
142
220
|
elsif response.vote_granted
|
143
221
|
votes_for += 1
|
144
|
-
|
222
|
+
elected = true if votes_for >= quorum
|
145
223
|
else
|
146
224
|
votes_against += 1
|
147
|
-
|
225
|
+
elected = false if votes_against >= quorum
|
148
226
|
end
|
149
|
-
|
227
|
+
#STDOUT.write("\n\t\t#{@id} receives vote #{response.vote_granted}, elected is #{elected.inspect}\n\n")
|
228
|
+
elected
|
150
229
|
end
|
151
|
-
if
|
230
|
+
if votes_for >= quorum
|
231
|
+
#STDOUT.write("\n#{@id} becomes leader for term #{@persistent_state.current_term}\n\n")
|
152
232
|
@role = LEADER_ROLE
|
153
233
|
establish_leadership
|
234
|
+
else
|
235
|
+
#STDOUT.write("\n\t\t#{@id} not elected leader (for #{votes_for}, against #{votes_against})\n\n")
|
154
236
|
end
|
155
237
|
end
|
156
238
|
end
|
239
|
+
|
157
240
|
protected :candidate_update
|
158
241
|
|
159
242
|
def leader_update
|
243
|
+
#STDOUT.write("\nLEADER UPDATE BEGINS\n")
|
160
244
|
if @leadership_state.update_timer.timed_out?
|
161
245
|
@leadership_state.update_timer.reset!
|
162
246
|
send_heartbeats
|
163
247
|
end
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
248
|
+
if @leadership_state.followers.any?
|
249
|
+
new_commit_index = @leadership_state.followers.values.
|
250
|
+
select { |follower_state| follower_state.succeeded }.
|
251
|
+
map { |follower_state| follower_state.next_index - 1 }.
|
252
|
+
sort[@cluster.quorum - 1]
|
253
|
+
else
|
254
|
+
new_commit_index = @persistent_state.log.size - 1
|
255
|
+
end
|
256
|
+
handle_commits(new_commit_index)
|
257
|
+
#STDOUT.write("\nLEADER UPDATE ENDS\n")
|
168
258
|
end
|
259
|
+
|
169
260
|
protected :leader_update
|
170
261
|
|
262
|
+
def handle_commits(new_commit_index)
|
263
|
+
#STDOUT.write("\nnode #{@id} handle_commits(new_commit_index = #{new_commit_index}) (@temporary_state.commit_index = #{@temporary_state.commit_index}\n")
|
264
|
+
return if new_commit_index == @temporary_state.commit_index
|
265
|
+
next_commit = @temporary_state.commit_index.nil? ? 0 : @temporary_state.commit_index + 1
|
266
|
+
while next_commit <= new_commit_index
|
267
|
+
@commit_handler.call(@persistent_state.log[next_commit].command) if @commit_handler
|
268
|
+
@temporary_state.commit_index = next_commit
|
269
|
+
next_commit += 1
|
270
|
+
#STDOUT.write("\n\tnode #{@id} handle_commits(new_commit_index = #{new_commit_index}) (new @temporary_state.commit_index = #{@temporary_state.commit_index}\n")
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
protected :handle_commits
|
275
|
+
|
171
276
|
def establish_leadership
|
172
277
|
@leadership_state = LeadershipState.new(@config.update_interval)
|
278
|
+
@temporary_state.leader_id = @id
|
173
279
|
@cluster.node_ids.each do |node_id|
|
280
|
+
next if node_id == @id
|
174
281
|
follower_state = (@leadership_state.followers[node_id] ||= FollowerState.new)
|
175
|
-
follower_state.next_index = @persistent_state.log.size
|
282
|
+
follower_state.next_index = @persistent_state.log.size
|
176
283
|
follower_state.succeeded = false
|
177
284
|
end
|
178
285
|
send_heartbeats
|
179
286
|
end
|
287
|
+
|
180
288
|
protected :establish_leadership
|
181
289
|
|
182
290
|
def send_heartbeats
|
291
|
+
#STDOUT.write("\nsending heartbeats\n")
|
292
|
+
last_log_entry = @persistent_state.log.last
|
293
|
+
log_index = last_log_entry ? last_log_entry.index : nil
|
294
|
+
log_term = last_log_entry ? last_log_entry.term : nil
|
183
295
|
request = AppendEntriesRequest.new(
|
184
296
|
@persistent_state.current_term,
|
185
297
|
@id,
|
186
|
-
|
187
|
-
|
298
|
+
log_index,
|
299
|
+
log_term,
|
188
300
|
[],
|
189
301
|
@temporary_state.commit_index)
|
190
302
|
|
@@ -192,34 +304,45 @@ module Raft
|
|
192
304
|
append_entries_to_follower(node_id, request, response)
|
193
305
|
end
|
194
306
|
end
|
307
|
+
|
195
308
|
protected :send_heartbeats
|
196
309
|
|
197
310
|
def append_entries_to_follower(node_id, request, response)
|
198
|
-
if
|
199
|
-
|
311
|
+
if @role != LEADER_ROLE
|
312
|
+
# we lost the leadership
|
313
|
+
elsif response.success
|
314
|
+
#STDOUT.write("\nappend_entries_to_follower #{node_id} request #{request.pretty_inspect} succeeded\n")
|
315
|
+
@leadership_state.followers[node_id].next_index = (request.prev_log_index || -1) + request.entries.count + 1
|
200
316
|
@leadership_state.followers[node_id].succeeded = true
|
201
317
|
elsif response.term <= @persistent_state.current_term
|
318
|
+
#STDOUT.write("\nappend_entries_to_follower #{node_id} request failed (#{request.pretty_inspect}) and responded with #{response.pretty_inspect}\n")
|
202
319
|
@config.rpc_provider.append_entries_to_follower(request, node_id) do |node_id, response|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
320
|
+
if @role == LEADER_ROLE # make sure leadership wasn't lost since the request
|
321
|
+
#STDOUT.write("\nappend_entries_to_follower #{node_id} callback...\n")
|
322
|
+
prev_log_index = (request.prev_log_index.nil? || request.prev_log_index <= 0) ? nil : request.prev_log_index - 1
|
323
|
+
prev_log_term = nil
|
324
|
+
entries = @persistent_state.log
|
325
|
+
unless prev_log_index.nil?
|
326
|
+
prev_log_term = @persistent_state.log[prev_log_index].term
|
327
|
+
entries = @persistent_state.log.slice((prev_log_index + 1)..-1)
|
328
|
+
end
|
329
|
+
next_request = AppendEntriesRequest.new(
|
330
|
+
@persistent_state.current_term,
|
331
|
+
@id,
|
332
|
+
prev_log_index,
|
333
|
+
prev_log_term,
|
334
|
+
entries,
|
335
|
+
@temporary_state.commit_index)
|
336
|
+
#STDOUT.write("\nappend_entries_to_follower #{node_id} request #{request.pretty_inspect} failed...\n")
|
337
|
+
#STDOUT.write("sending updated request #{next_request.pretty_inspect}\n")
|
338
|
+
@config.rpc_provider.append_entries_to_follower(next_request, node_id) do |node_id, response|
|
339
|
+
append_entries_to_follower(node_id, next_request, response)
|
340
|
+
end
|
219
341
|
end
|
220
342
|
end
|
221
343
|
end
|
222
344
|
end
|
345
|
+
|
223
346
|
protected :append_entries_to_follower
|
224
347
|
|
225
348
|
def handle_request_vote(request)
|
@@ -237,12 +360,15 @@ module Raft
|
|
237
360
|
if @persistent_state.voted_for == request.candidate_id
|
238
361
|
response.vote_granted = success
|
239
362
|
elsif @persistent_state.voted_for.nil?
|
240
|
-
if
|
241
|
-
|
363
|
+
if @persistent_state.log.empty?
|
364
|
+
# this node has no log so it can't be ahead
|
365
|
+
@persistent_state.voted_for = request.candidate_id
|
366
|
+
response.vote_granted = true
|
367
|
+
elsif request.last_log_term == @persistent_state.log.last.term &&
|
368
|
+
request.last_log_index && request.last_log_index < @persistent_state.log.last.index
|
242
369
|
# candidate's log is incomplete compared to this node
|
243
|
-
elsif request.last_log_term < @persistent_state.log.last.term
|
370
|
+
elsif request.last_log_term && request.last_log_term < @persistent_state.log.last.term
|
244
371
|
# candidate's log is incomplete compared to this node
|
245
|
-
@persistent_state.voted_for = request.candidate_id
|
246
372
|
else
|
247
373
|
@persistent_state.voted_for = request.candidate_id
|
248
374
|
response.vote_granted = true
|
@@ -255,11 +381,13 @@ module Raft
|
|
255
381
|
end
|
256
382
|
|
257
383
|
def handle_append_entries(request)
|
384
|
+
#STDOUT.write("\n\nnode #{@id} handle_append_entries: #{request.entries.pretty_inspect}\n\n") if request.prev_log_index.nil?
|
258
385
|
response = AppendEntriesResponse.new
|
259
386
|
response.term = @persistent_state.current_term
|
260
387
|
response.success = false
|
261
388
|
|
262
389
|
return response if request.term < @persistent_state.current_term
|
390
|
+
#STDOUT.write("\n\nnode #{@id} handle_append_entries stage 2\n") if request.prev_log_index.nil?
|
263
391
|
|
264
392
|
step_down_if_new_term(request.term)
|
265
393
|
|
@@ -269,12 +397,17 @@ module Raft
|
|
269
397
|
|
270
398
|
abs_log_index = abs_log_index_for(request.prev_log_index, request.prev_log_term)
|
271
399
|
return response if abs_log_index.nil? && !request.prev_log_index.nil? && !request.prev_log_term.nil?
|
272
|
-
|
273
|
-
|
400
|
+
#STDOUT.write("\n\nnode #{@id} handle_append_entries stage 3\n") if request.prev_log_index.nil?
|
401
|
+
if @temporary_state.commit_index &&
|
402
|
+
abs_log_index &&
|
403
|
+
abs_log_index < @temporary_state.commit_index
|
404
|
+
raise "Cannot truncate committed logs; @temporary_state.commit_index = #{@temporary_state.commit_index}; abs_log_index = #{abs_log_index}"
|
405
|
+
end
|
274
406
|
|
275
407
|
truncate_and_update_log(abs_log_index, request.entries)
|
276
408
|
|
277
409
|
return response unless update_commit_index(request.commit_index)
|
410
|
+
#STDOUT.write("\n\nnode #{@id} handle_append_entries stage 4\n") if request.prev_log_index.nil?
|
278
411
|
|
279
412
|
response.success = true
|
280
413
|
response
|
@@ -284,52 +417,68 @@ module Raft
|
|
284
417
|
response = CommandResponse.new(false)
|
285
418
|
case @role
|
286
419
|
when FOLLOWER_ROLE
|
287
|
-
|
420
|
+
await_leader
|
421
|
+
if @role == LEADER_ROLE
|
422
|
+
handle_command(request)
|
423
|
+
else
|
424
|
+
# forward the command to the leader
|
425
|
+
response = @config.rpc_provider.command(request, @temporary_state.leader_id)
|
426
|
+
end
|
288
427
|
when CANDIDATE_ROLE
|
289
428
|
await_leader
|
290
429
|
response = handle_command(request)
|
291
430
|
when LEADER_ROLE
|
292
|
-
|
431
|
+
last_log = @persistent_state.log.last
|
432
|
+
log_entry = LogEntry.new(@persistent_state.current_term, last_log.index ? last_log.index + 1 : 0, request.command)
|
293
433
|
@persistent_state.log << log_entry
|
294
434
|
await_consensus(log_entry)
|
435
|
+
response = CommandResponse.new(true)
|
295
436
|
end
|
296
437
|
response
|
297
438
|
end
|
298
439
|
|
299
440
|
def await_consensus(log_entry)
|
300
441
|
@config.async_provider.await do
|
301
|
-
persisted_log_entry = @persistent_state.log[log_entry.index
|
302
|
-
|
442
|
+
persisted_log_entry = @persistent_state.log[log_entry.index]
|
443
|
+
!@temporary_state.commit_index.nil? &&
|
444
|
+
@temporary_state.commit_index >= log_entry.index &&
|
303
445
|
persisted_log_entry.term == log_entry.term &&
|
304
446
|
persisted_log_entry.command == log_entry.command
|
305
447
|
end
|
306
448
|
end
|
449
|
+
|
307
450
|
protected :await_consensus
|
308
451
|
|
309
452
|
def await_leader
|
453
|
+
if @temporary_state.leader_id.nil?
|
454
|
+
@role = CANDIDATE_ROLE
|
455
|
+
end
|
310
456
|
@config.async_provider.await do
|
311
457
|
@role != CANDIDATE_ROLE && !@temporary_state.leader_id.nil?
|
312
458
|
end
|
313
459
|
end
|
460
|
+
|
314
461
|
protected :await_leader
|
315
462
|
|
316
463
|
def step_down_if_new_term(request_term)
|
317
464
|
if request_term > @persistent_state.current_term
|
318
465
|
@persistent_state.current_term = request_term
|
319
|
-
@persistent_state.voted_for = nil
|
320
466
|
@role = FOLLOWER_ROLE
|
321
467
|
end
|
322
468
|
end
|
469
|
+
|
323
470
|
protected :step_down_if_new_term
|
324
471
|
|
325
472
|
def reset_election_timeout
|
326
473
|
@election_timer.reset!
|
327
474
|
end
|
475
|
+
|
328
476
|
protected :reset_election_timeout
|
329
477
|
|
330
478
|
def abs_log_index_for(prev_log_index, prev_log_term)
|
331
|
-
@persistent_state.log.rindex {|log_entry| log_entry.index == prev_log_index && log_entry.term == prev_log_term}
|
479
|
+
@persistent_state.log.rindex { |log_entry| log_entry.index == prev_log_index && log_entry.term == prev_log_term }
|
332
480
|
end
|
481
|
+
|
333
482
|
protected :abs_log_index_for
|
334
483
|
|
335
484
|
def truncate_and_update_log(abs_log_index, entries)
|
@@ -341,15 +490,20 @@ module Raft
|
|
341
490
|
else
|
342
491
|
log = log.slice(0..abs_log_index)
|
343
492
|
end
|
493
|
+
#STDOUT.write("\n\nentries is: #{entries.pretty_inspect}\n\n")
|
344
494
|
log = log.concat(entries) unless entries.empty?
|
345
495
|
@persistent_state.log = log
|
346
496
|
end
|
497
|
+
|
347
498
|
protected :truncate_and_update_log
|
348
499
|
|
349
|
-
def update_commit_index(
|
350
|
-
|
351
|
-
@temporary_state.commit_index
|
500
|
+
def update_commit_index(new_commit_index)
|
501
|
+
#STDOUT.write("\n\n%%%%%%%%%%%%%%%%%%%%% node #{@id} update_commit_index(new_commit_index = #{new_commit_index})\n")
|
502
|
+
return false if @temporary_state.commit_index && @temporary_state.commit_index > new_commit_index
|
503
|
+
handle_commits(new_commit_index)
|
504
|
+
true
|
352
505
|
end
|
506
|
+
|
353
507
|
protected :update_commit_index
|
354
508
|
end
|
355
509
|
end
|
data/lib/raft/goliath.rb
CHANGED
@@ -1,26 +1,35 @@
|
|
1
|
-
|
1
|
+
require_relative '../raft'
|
2
2
|
|
3
3
|
require 'goliath'
|
4
4
|
|
5
5
|
module Raft
|
6
6
|
class Goliath
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
|
8
|
+
def self.log(message)
|
9
|
+
STDOUT.write("\n\n")
|
10
|
+
STDOUT.write(message)
|
11
|
+
STDOUT.write("\n\n")
|
12
|
+
end
|
13
|
+
|
14
|
+
class HttpJsonRpcResponder < ::Goliath::API
|
15
|
+
use ::Goliath::Rack::Render, 'json'
|
16
|
+
use ::Goliath::Rack::Validation::RequestMethod, %w(POST)
|
17
|
+
use ::Goliath::Rack::Params
|
11
18
|
|
12
19
|
def initialize(node)
|
13
20
|
@node = node
|
14
21
|
end
|
15
22
|
|
23
|
+
HEADERS = { 'Content-Type' => 'application/json' }
|
24
|
+
|
16
25
|
def response(env)
|
17
26
|
case env['REQUEST_PATH']
|
18
27
|
when '/request_vote'
|
19
|
-
handle_errors {request_vote_response(env['params'])}
|
28
|
+
handle_errors { request_vote_response(env['params']) }
|
20
29
|
when '/append_entries'
|
21
|
-
handle_errors {append_entries_response(env['params'])}
|
30
|
+
handle_errors { append_entries_response(env['params']) }
|
22
31
|
when '/command'
|
23
|
-
handle_errors {command_response(env['params'])}
|
32
|
+
handle_errors { command_response(env['params']) }
|
24
33
|
else
|
25
34
|
error_response(404, 'not found')
|
26
35
|
end
|
@@ -33,82 +42,168 @@ module Raft
|
|
33
42
|
params['last_log_index'],
|
34
43
|
params['last_log_term'])
|
35
44
|
response = @node.handle_request_vote(request)
|
36
|
-
[200,
|
45
|
+
[200, HEADERS, { 'term' => response.term, 'vote_granted' => response.vote_granted }]
|
37
46
|
end
|
38
47
|
|
39
48
|
def append_entries_response(params)
|
40
|
-
Raft::
|
49
|
+
entries = params['entries'].map {|entry| Raft::LogEntry.new(entry['term'], entry['index'], entry['command'])}
|
50
|
+
request = Raft::AppendEntriesRequest.new(
|
41
51
|
params['term'],
|
42
52
|
params['leader_id'],
|
43
53
|
params['prev_log_index'],
|
44
54
|
params['prev_log_term'],
|
45
|
-
|
55
|
+
entries,
|
46
56
|
params['commit_index'])
|
57
|
+
#STDOUT.write("\nnode #{@node.id} received entries: #{request.entries.pretty_inspect}\n")
|
47
58
|
response = @node.handle_append_entries(request)
|
48
|
-
[200,
|
59
|
+
[200, HEADERS, { 'term' => response.term, 'success' => response.success }]
|
49
60
|
end
|
50
61
|
|
51
62
|
def command_response(params)
|
52
|
-
Raft::CommandRequest.new(params['command'])
|
63
|
+
request = Raft::CommandRequest.new(params['command'])
|
53
64
|
response = @node.handle_command(request)
|
54
|
-
[response.success ? 200 : 409,
|
65
|
+
[response.success ? 200 : 409, HEADERS, { 'success' => response.success }]
|
55
66
|
end
|
56
67
|
|
57
68
|
def handle_errors
|
58
69
|
yield
|
59
70
|
rescue StandardError => se
|
60
|
-
error_response(422, se
|
71
|
+
error_response(422, se)
|
61
72
|
rescue Exception => e
|
62
|
-
error_response(500, e
|
73
|
+
error_response(500, e)
|
63
74
|
end
|
64
75
|
|
65
|
-
def
|
66
|
-
|
76
|
+
def error_message(exception)
|
77
|
+
"#{exception.message}\n\t#{exception.backtrace.join("\n\t")}".tap {|m| STDOUT.write("\n\n\t#{m}\n\n")}
|
78
|
+
end
|
79
|
+
|
80
|
+
def error_response(code, exception)
|
81
|
+
[code, HEADERS, { 'error' => error_message(exception) }]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
module HashMarshalling
|
86
|
+
def self.hash_to_object(hash, klass)
|
87
|
+
object = klass.new
|
88
|
+
hash.each_pair do |k, v|
|
89
|
+
object.send("#{k}=", v)
|
90
|
+
end
|
91
|
+
object
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.object_to_hash(object, attrs)
|
95
|
+
attrs.reduce({}) { |hash, attr|
|
96
|
+
hash[attr] = object.send(attr); hash
|
97
|
+
}
|
67
98
|
end
|
68
99
|
end
|
69
100
|
|
70
|
-
#TODO: implement HttpJsonRpcProvider!
|
71
101
|
class HttpJsonRpcProvider < Raft::RpcProvider
|
72
|
-
attr_reader :
|
102
|
+
attr_reader :uri_generator
|
73
103
|
|
74
|
-
def initialize(
|
75
|
-
@
|
104
|
+
def initialize(uri_generator)
|
105
|
+
@uri_generator = uri_generator
|
106
|
+
end
|
107
|
+
|
108
|
+
def request_votes(request, cluster, &block)
|
109
|
+
sent_hash = HashMarshalling.object_to_hash(request, %w(term candidate_id last_log_index last_log_term))
|
110
|
+
sent_json = MultiJson.dump(sent_hash)
|
111
|
+
deferred_calls = []
|
112
|
+
EM.synchrony do
|
113
|
+
cluster.node_ids.each do |node_id|
|
114
|
+
next if node_id == request.candidate_id
|
115
|
+
http = EventMachine::HttpRequest.new(uri_generator.call(node_id, 'request_vote')).apost(
|
116
|
+
:body => sent_json,
|
117
|
+
:head => { 'Content-Type' => 'application/json' })
|
118
|
+
http.callback do
|
119
|
+
if http.response_header.status == 200
|
120
|
+
received_hash = MultiJson.load(http.response)
|
121
|
+
response = HashMarshalling.hash_to_object(received_hash, Raft::RequestVoteResponse)
|
122
|
+
#STDOUT.write("\n\t#{node_id} responded #{response.vote_granted} to #{request.candidate_id}\n\n")
|
123
|
+
yield node_id, request, response
|
124
|
+
else
|
125
|
+
Raft::Goliath.log("request_vote failed for node '#{node_id}' with code #{http.response_header.status}")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
deferred_calls << http
|
129
|
+
end
|
130
|
+
end
|
131
|
+
deferred_calls.each do |http|
|
132
|
+
EM::Synchrony.sync http
|
133
|
+
end
|
76
134
|
end
|
77
135
|
|
78
|
-
def
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
136
|
+
def append_entries(request, cluster, &block)
|
137
|
+
deferred_calls = []
|
138
|
+
EM.synchrony do
|
139
|
+
cluster.node_ids.each do |node_id|
|
140
|
+
next if node_id == request.leader_id
|
141
|
+
deferred_calls << create_append_entries_to_follower_request(request, node_id, &block)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
deferred_calls.each do |http|
|
145
|
+
EM::Synchrony.sync http
|
146
|
+
end
|
83
147
|
end
|
84
148
|
|
85
|
-
def
|
86
|
-
|
149
|
+
def append_entries_to_follower(request, node_id, &block)
|
150
|
+
# EM.synchrony do
|
151
|
+
create_append_entries_to_follower_request(request, node_id, &block)
|
152
|
+
# end
|
87
153
|
end
|
88
154
|
|
89
|
-
def
|
90
|
-
|
155
|
+
def create_append_entries_to_follower_request(request, node_id, &block)
|
156
|
+
sent_hash = HashMarshalling.object_to_hash(request, %w(term leader_id prev_log_index prev_log_term entries commit_index))
|
157
|
+
sent_hash['entries'] = sent_hash['entries'].map {|obj| HashMarshalling.object_to_hash(obj, %w(term index command))}
|
158
|
+
sent_json = MultiJson.dump(sent_hash)
|
159
|
+
raise "replicating to self!" if request.leader_id == node_id
|
160
|
+
#STDOUT.write("\nleader #{request.leader_id} replicating entries to #{node_id}: #{sent_hash.pretty_inspect}\n")#"\t#{caller[0..4].join("\n\t")}")
|
161
|
+
|
162
|
+
http = EventMachine::HttpRequest.new(uri_generator.call(node_id, 'append_entries')).apost(
|
163
|
+
:body => sent_json,
|
164
|
+
:head => { 'Content-Type' => 'application/json' })
|
165
|
+
http.callback do
|
166
|
+
#STDOUT.write("\nleader #{request.leader_id} calling back to #{node_id} to append entries\n")
|
167
|
+
if http.response_header.status == 200
|
168
|
+
received_hash = MultiJson.load(http.response)
|
169
|
+
response = HashMarshalling.hash_to_object(received_hash, Raft::AppendEntriesResponse)
|
170
|
+
yield node_id, response
|
171
|
+
else
|
172
|
+
Raft::Goliath.log("append_entries failed for node '#{node_id}' with code #{http.response_header.status}")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
http
|
91
176
|
end
|
92
177
|
|
93
178
|
def command(request, node_id)
|
94
|
-
|
179
|
+
sent_hash = HashMarshalling.object_to_hash(request, %w(command))
|
180
|
+
sent_json = MultiJson.dump(sent_hash)
|
181
|
+
http = EventMachine::HttpRequest.new(uri_generator.call(node_id, 'command')).apost(
|
182
|
+
:body => sent_json,
|
183
|
+
:head => { 'Content-Type' => 'application/json' })
|
184
|
+
http = EM::Synchrony.sync(http)
|
185
|
+
if http.response_header.status == 200
|
186
|
+
received_hash = MultiJson.load(http.response)
|
187
|
+
HashMarshalling.hash_to_object(received_hash, Raft::CommandResponse)
|
188
|
+
else
|
189
|
+
Raft::Goliath.log("command failed for node '#{node_id}' with code #{http.response_header.status}")
|
190
|
+
CommandResponse.new(false)
|
191
|
+
end
|
95
192
|
end
|
96
193
|
end
|
97
194
|
|
98
195
|
class EventMachineAsyncProvider < Raft::AsyncProvider
|
99
196
|
def await
|
197
|
+
f = Fiber.current
|
100
198
|
until yield
|
101
|
-
f
|
102
|
-
EventMachine::add_timer(0.1) do
|
103
|
-
f.resume
|
104
|
-
end
|
199
|
+
EM.next_tick {f.resume}
|
105
200
|
Fiber.yield
|
106
201
|
end
|
107
202
|
end
|
108
203
|
end
|
109
204
|
|
110
|
-
def self.rpc_provider
|
111
|
-
HttpJsonRpcProvider.new
|
205
|
+
def self.rpc_provider(uri_generator)
|
206
|
+
HttpJsonRpcProvider.new(uri_generator)
|
112
207
|
end
|
113
208
|
|
114
209
|
def self.async_provider
|
@@ -116,39 +211,33 @@ module Raft
|
|
116
211
|
end
|
117
212
|
|
118
213
|
def initialize(node)
|
119
|
-
|
214
|
+
@node = node
|
120
215
|
end
|
121
216
|
|
217
|
+
attr_reader :node
|
122
218
|
attr_reader :update_fiber
|
123
219
|
attr_reader :running
|
124
220
|
|
125
|
-
def start
|
126
|
-
@runner = Goliath::Runner.new(ARGV, nil)
|
221
|
+
def start(options = {})
|
222
|
+
@runner = ::Goliath::Runner.new(ARGV, nil)
|
127
223
|
@runner.api = HttpJsonRpcResponder.new(node)
|
128
|
-
@runner.app = Goliath::Rack::Builder.build(HttpJsonRpcResponder, runner.api)
|
224
|
+
@runner.app = ::Goliath::Rack::Builder.build(HttpJsonRpcResponder, @runner.api)
|
225
|
+
@runner.address = options[:address] if options[:address]
|
226
|
+
@runner.port = options[:port] if options[:port]
|
129
227
|
@runner.run
|
130
|
-
|
131
228
|
@running = true
|
132
229
|
|
133
|
-
|
134
|
-
|
135
|
-
while runner.running?
|
136
|
-
EventMachine.add_timer node.config.update_interval, Proc.new do
|
137
|
-
runner.update_fiber.resume if runner.running?
|
138
|
-
end
|
230
|
+
update_proc = Proc.new do
|
231
|
+
EM.synchrony do
|
139
232
|
@node.update
|
140
|
-
Fiber.yield
|
141
233
|
end
|
142
234
|
end
|
235
|
+
@update_timer = EventMachine.add_periodic_timer(node.config.update_interval, update_proc)
|
236
|
+
# @node.update
|
143
237
|
end
|
144
238
|
|
145
239
|
def stop
|
146
|
-
@
|
147
|
-
f = Fiber.current
|
148
|
-
while @update_fiber.alive?
|
149
|
-
EventMachine.add_timer 0.1, proc { f.resume }
|
150
|
-
Fiber.yield
|
151
|
-
end
|
240
|
+
@update_timer.cancel
|
152
241
|
end
|
153
242
|
end
|
154
243
|
end
|
metadata
CHANGED
@@ -1,32 +1,99 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: raft
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.0.3
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Harry Wilkinson
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2013-06-
|
11
|
+
date: 2013-06-16 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: goliath
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
17
|
- - ~>
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version: 1.0
|
19
|
+
version: '1.0'
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
24
|
- - ~>
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version: 1.0
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: multi_json
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: cucumber
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: em-http-request
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: oj
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.0'
|
30
97
|
description: A simple Raft distributed consensus implementation
|
31
98
|
email: hwilkinson@mdsol.com
|
32
99
|
executables: []
|
@@ -37,27 +104,25 @@ files:
|
|
37
104
|
- lib/raft/goliath.rb
|
38
105
|
homepage: http://github.com/harryw/raft
|
39
106
|
licenses: []
|
107
|
+
metadata: {}
|
40
108
|
post_install_message:
|
41
109
|
rdoc_options: []
|
42
110
|
require_paths:
|
43
111
|
- lib
|
44
112
|
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
-
none: false
|
46
113
|
requirements:
|
47
|
-
- -
|
114
|
+
- - '>='
|
48
115
|
- !ruby/object:Gem::Version
|
49
116
|
version: '0'
|
50
117
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
-
none: false
|
52
118
|
requirements:
|
53
|
-
- -
|
119
|
+
- - '>='
|
54
120
|
- !ruby/object:Gem::Version
|
55
121
|
version: '0'
|
56
122
|
requirements: []
|
57
123
|
rubyforge_project:
|
58
|
-
rubygems_version:
|
124
|
+
rubygems_version: 2.0.3
|
59
125
|
signing_key:
|
60
|
-
specification_version:
|
126
|
+
specification_version: 4
|
61
127
|
summary: A simple Raft distributed consensus implementation
|
62
128
|
test_files: []
|
63
|
-
has_rdoc:
|