Class: ZK::Election::Candidate

Inherits:
Base
  • Object
show all
Defined in:
lib/z_k/election.rb

Overview

This class is for registering candidates in the leader election. This instance will participate in votes for becoming the leader and will be notified in the case where it needs to take over.

if data is given, it will be used as the content of both our ballot and the leader acknowledgement node if and when we become the leader.

Instance Attribute Summary

Attributes inherited from Base

#root_election_node, #vote_path, #zk

Instance Method Summary collapse

Methods inherited from Base

#cast_ballot!, #create_root_path!, #digit, #leader_ack_path, #leader_acked?, #leader_data, #on_leader_ack, #root_vote_path, #safe_call, #vote_basename

Constructor Details

#initialize(client, name, opts = {}) ⇒ Candidate

Returns a new instance of Candidate.



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/z_k/election.rb', line 175

def initialize(client, name, opts={})
  super(client, name, opts)
  opts = DEFAULT_OPTS.merge(opts)

  @leader     = nil 
  @data       = opts[:data] || ''
  @vote_path  = nil
 
  @winner_callbacks = []
  @loser_callbacks = []

  @next_node_ballot_sub = nil # the subscription for next-node failure
end

Instance Method Details

#acknowledge_win!Object (protected)

the inauguration, as it were



233
234
235
# File 'lib/z_k/election.rb', line 233

def acknowledge_win!
  @zk.create(leader_ack_path, @data, :ephemeral => true) rescue Exceptions::NodeExists
end

#check_election_results!Object (protected)

if watch_next is true, we register a watcher for the next-lowest index number in the list of ballots



247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/z_k/election.rb', line 247

def check_election_results!
  #return if leader?         # we already know we're the leader
  ballots = get_ballots()

  our_idx = ballots.index(vote_basename)
  
  if our_idx == 0           # if we have the lowest number
    logger.info { "ZK: We have become leader, data: #{@data.inspect}" }
    handle_winning_election
  else
    logger.info { "ZK: we are not the leader, data: #{@data.inspect}" }
    handle_losing_election(our_idx, ballots)
  end
end

#clear_next_node_ballot_sub!Object (protected)



302
303
304
305
306
307
# File 'lib/z_k/election.rb', line 302

def clear_next_node_ballot_sub!
  if @next_node_ballot_sub
    @next_node_ballot_sub.unsubscribe 
    @next_node_ballot_sub = nil
  end
end

#fire_losing_callbacks!Object (protected)



313
314
315
# File 'lib/z_k/election.rb', line 313

def fire_losing_callbacks!
  safe_call(*@loser_callbacks)
end

#fire_winning_callbacks!Object (protected)



309
310
311
# File 'lib/z_k/election.rb', line 309

def fire_winning_callbacks!
  safe_call(*@winner_callbacks)
end

#get_ballotsObject (protected)

return the list of ephemeral vote nodes



238
239
240
241
242
# File 'lib/z_k/election.rb', line 238

def get_ballots
  @zk.children(root_vote_path).grep(/^ballot/).tap do |ballots|
    ballots.sort! {|a,b| digit(a) <=> digit(b) }
  end
end

#handle_losing_election(our_idx, ballots) ⇒ Object (protected)



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/z_k/election.rb', line 268

def handle_losing_election(our_idx, ballots)
  @leader = false

  on_leader_ack do
    fire_losing_callbacks!

    next_ballot = File.join(root_vote_path, ballots[our_idx - 1])

    logger.info { "ZK: following #{next_ballot} for changes, #{@data.inspect}" }

    @next_node_ballot_sub ||= @zk.watcher.register(next_ballot) do |event| 
      if event.node_deleted? 
        logger.debug { "#{next_ballot} was deleted, voting, #{@data.inspect}" }
        vote! 
      else
        # this takes care of the race condition where the leader ballot would
        # have been deleted before we could re-register to receive updates
        # if zk.stat returns false, it means the path was deleted
        unless @zk.exists?(next_ballot, :watch => true)
          logger.debug { "#{next_ballot} was deleted (detected on re-watch), voting, #{@data.inspect}" }
          vote! 
        end
      end
    end

    # this catches a possible race condition, where the leader has died before
    # our callback has fired. In this case, retry and do this procedure again
    unless @zk.stat(next_ballot, :watch => true).exists?
      logger.debug { "#{@data.inspect}: the node #{next_ballot} did not exist, retrying" }
      vote!
    end
  end
end

#handle_winning_electionObject (protected)



262
263
264
265
266
# File 'lib/z_k/election.rb', line 262

def handle_winning_election
  @leader = true  
  fire_winning_callbacks!
  acknowledge_win!
end

#leader?Boolean

Returns:

  • (Boolean)


189
190
191
# File 'lib/z_k/election.rb', line 189

def leader?
  false|@leader
end

#on_losing_election(&block) ⇒ Object

When we lose the election and are relegated to the shadows, waiting for the leader to make one small misstep, where we can finally claim what is rightfully ours! MWUAHAHAHAHAHA(cough)



207
208
209
# File 'lib/z_k/election.rb', line 207

def on_losing_election(&block)
  @loser_callbacks << block
end

#on_takeover_errorObject

These procs should be run in the case of an error when trying to assume the leadership role. This should probably be a “hara-kiri” or STONITH type procedure (i.e. kill the candidate)

Raises:

  • (NotImplementedError)


215
216
217
# File 'lib/z_k/election.rb', line 215

def on_takeover_error #:nodoc:
  raise NotImplementedError
end

#on_winning_election(&block) ⇒ Object

When we win the election, we will call the procs registered using this method.



200
201
202
# File 'lib/z_k/election.rb', line 200

def on_winning_election(&block)
  @winner_callbacks << block
end

#vote!Object

volunteer to become the leader. if we win, on_winning_election blocks will be called, otherwise, wait for next election

data will be placed in the znode representing our vote



223
224
225
226
227
228
229
# File 'lib/z_k/election.rb', line 223

def vote!
  @mutex.synchronize do
    clear_next_node_ballot_sub!
    cast_ballot!(@data) unless @vote_path
    check_election_results!
  end
end

#voted?Boolean

true if leader has been determined at least once (used in tests)

Returns:

  • (Boolean)


194
195
196
# File 'lib/z_k/election.rb', line 194

def voted? #:nodoc:
  !@leader.nil?
end