raft 0.0.2 → 0.0.3

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