raft 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|