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.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/lib/raft.rb +212 -58
  3. data/lib/raft/goliath.rb +145 -56
  4. metadata +79 -14
@@ -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
@@ -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
- attr_accessor :node_ids
7
+ attr_reader :node_ids
8
+
9
+ def initialize
10
+ @node_ids = []
11
+ end
6
12
 
7
13
  def quorum
8
- @node_ids.size / 2 + 1 # integer division rounds down
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
- PersistentState = Struct.new(:current_term, :voted_for, :log)
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
- TemporaryState = Struct.new(:commit_index, :leader_id)
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
- @timeout = Time.now + timeout
71
- reset!
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(0, nil, [])
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
- request = RequestVoteRequest.new(@persistent_state.current_term, @id, @persistent_state.log.last.index, @persistent_state.log.last.term)
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
- elected = @config.rpc_provider.request_votes(request, @cluster) do |_, response|
139
- if response.term > @persistent_state.current_term
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
- return false
219
+ elected = false
142
220
  elsif response.vote_granted
143
221
  votes_for += 1
144
- return false if votes_for >= quorum
222
+ elected = true if votes_for >= quorum
145
223
  else
146
224
  votes_against += 1
147
- return false if votes_against >= quorum
225
+ elected = false if votes_against >= quorum
148
226
  end
149
- nil # no majority result yet
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 elected
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
- @temporary_state.commit_index = @leadership_state.followers.values.
165
- select {|follower_state| follower_state.succeeded}.
166
- map {|follower_state| follower_state.next_index}.
167
- sort[@cluster.quorum - 1]
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 + 1
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
- @persistent_state.log.size - 1,
187
- @persistent_state.log.last.term,
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 response.success
199
- @leadership_state.followers[node_id].next_index = request.log.last.index
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
- prev_log_index = request.prev_log_index > 1 ? request.prev_log_index - 1 : nil
204
- prev_log_term = nil
205
- entries = @persistent_state.log
206
- unless prev_log_index.nil?
207
- prev_log_term = @persistent_state.log[prev_log_index].term
208
- entries = @persistent_state.log.slice((prev_log_index + 1)..-1)
209
- end
210
- next_request = AppendEntriesRequest.new(
211
- @persistent_state.current_term,
212
- @id,
213
- prev_log_index,
214
- prev_log_term,
215
- entries,
216
- @temporary_state.commit_index)
217
- @config.rpc_provider.append_entries_to_follower(next_request, @temporary_state.leader_id) do |node_id, response|
218
- append_entries_to_follower(node_id, next_request, response)
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 request.last_log_term == @persistent_state.log.last.term &&
241
- request.last_log_index < @persistent_state.log.last.index
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
- raise "Cannot truncate committed logs" if abs_log_index < @temporary_state.commit_index
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
- response = @config.rpc_provider.command(request, @temporary_state.leader_id)
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
- log_entry = LogEntry.new(@persistent_state.current_term, @persistent_state.log.last.index + 1, request.command)
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 - 1]
302
- @temporary_state.commit_index >= log_entry.index &&
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(commit_index)
350
- return false if @temporary_state.commit_index && @temporary_state.commit_index > commit_index
351
- @temporary_state.commit_index = 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
@@ -1,26 +1,35 @@
1
- require 'lib/raft'
1
+ require_relative '../raft'
2
2
 
3
3
  require 'goliath'
4
4
 
5
5
  module Raft
6
6
  class Goliath
7
- class HttpJsonRpcResponder < Goliath::API
8
- use Goliath::Rack::Render, 'json'
9
- use Goliath::Rack::Validation::RequestMethod, %w(POST)
10
- use Goliath::Rack::Params
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, {}, {'term' => response.term, 'vote_granted' => response.vote_granted}]
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::AppendEntriesRequest.new(
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
- params['entries'],
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, {}, {'term' => response.term, 'success' => response.success}]
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, {}, {'success' => response.success}]
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.message)
71
+ error_response(422, se)
61
72
  rescue Exception => e
62
- error_response(500, e.message)
73
+ error_response(500, e)
63
74
  end
64
75
 
65
- def error_response(code, message)
66
- [code, {}, {'error' => message}]
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 :url_generator
102
+ attr_reader :uri_generator
73
103
 
74
- def initialize(url_generator)
75
- @url_generator = url_generator
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 request_votes(request, cluster)
79
- #multi = EventMachine::Synchrony::Multi.new
80
- #cluster.node_ids.each do |node_id|
81
- # multi.add node_id, EventMachine::HttpRequest.new(url_generator.call(node_id)).apost
82
- #end
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 append_entries(request, cluster)
86
- raise "Your RpcProvider subclass must implement #append_entries"
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 append_entries_to_follower(request, node_id)
90
- raise "Your RpcProvider subclass must implement #append_entries_to_follower"
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
- raise "Your RpcProvider subclass must implement #command"
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 = Fiber.current
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
- runner = self
134
- @update_fiber = Fiber.new do
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
- @running = false
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.2
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-02 00:00:00.000000000 Z
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.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.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: 1.8.25
124
+ rubygems_version: 2.0.3
59
125
  signing_key:
60
- specification_version: 3
126
+ specification_version: 4
61
127
  summary: A simple Raft distributed consensus implementation
62
128
  test_files: []
63
- has_rdoc: