pontoon 0.1.0

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