Skip to content

Instantly share code, notes, and snippets.

@bmizerany
Forked from therealadam/doozer_lock.rb
Created August 28, 2011 21:39
Show Gist options
  • Save bmizerany/1177261 to your computer and use it in GitHub Desktop.
Save bmizerany/1177261 to your computer and use it in GitHub Desktop.
Locking with Doozer
require 'rubygems'
require 'fraggle/block'
# # Locks with doozerd
#
# Doozerd is a little coordination service. I think of it like Zookeeper
# Lite (TM). In today's episode, we'll naively manage exclusive advisory locks.
#
# Disclaimer: If part of this implementation is total lolscale,
# please to let me know what papers/sources I should read to avoid lolscaling.
#
# To run this example (assuming you have doozerd installed and the fraggle-block
# gem):
#
# $ doozerd # In shell 0
# $ ruby locksmith2.rb # In shell 1
# $ ruby locksmith2.rb # In shell 2
# $ doozer watch '/example' # In shell 3
#
# You should observe the two clients trading the lock. In the shell 3, you can
# watch the clients acquire and release the lock as doozerd sees it.
# Gracefully exit if we're in the middle of doing work.
$run = true
Signal.trap('TERM') { puts "Quittin' time, namean!"; $run = false }
Signal.trap('INT') { puts "Quittin' time, namean!"; $run = false }
# A locker is where you put your valuable state while you're doing
# something else.
module Locker
# We'll share a connection between all locks via a module instance variable.
@connection = nil
# Grab that connection.
def self.connection
@connection
end
# Set that connection.
def self.connection=(c)
@connection = c
end
end
# A shared, advisory lock. Kids, only use locks if all else fails.
class Locker::Lock
# This is the whole API. Specify a lock (`filename`) to wrap around a `block`
# of work. Doesn't handle timing out the lock, just retries until the heat
# death of the universe.
def self.with_lock(filename, &block)
lock = new
rev = lock.acquire(filename)
block.call
lock.release(filename, rev)
end
# Grabs the lock. It first grabs the lock file in doozer to see if anyone
# already has the lock. If it's already acquired, it sleeps for a second and
# tries again. Otherwise, it tries to set the lock. If the revision on the
# lock file is greater after setting the lock than when it was first
# observed, the lock was successfully acquired. Otherwise it tries again.
#
# This is using the client process's PID as the lock value. In a
# distributed system, you'd want to use something more unique, probably MAC
# address and PID/thread ID.
def acquire(filename)
rev = nil
holder = connection.get(filename)
while rev.nil?
if holder.value == ''
resp = connection.set(filename, Process.pid.to_s, holder.rev)
if resp.err_code.nil? && resp.rev > holder.rev
rev = resp.rev
else
end
else
sleep 1
holder = connection.get(filename)
end
end
rev
end
# Surrender the lock. Blindly assumes we really have the lock and deletes
# the file, passing along the revision so doozerd can verify we really own
# the lock.
def release(filename, rev)
resp = connection.del(filename, rev)
end
# Just a helper, nothing interesting here.
def connection
Locker.connection
end
end
if __FILE__ == $PROGRAM_NAME
# Connect to Doozerd with the non-EM, blocking driver.
Locker.connection = Fraggle::Block.connect
# Run until a signal is received.
while $run do
# Use the doozerd file '/example' as our coordination point.
Locker::Lock.with_lock('/example') do
# This block is executed whenever we hold the lock. Presumably we'd do
# something fancier than writing to STDOUT in a real system.
5.times { puts "Doing work"; sleep 1 }
end
# Sleeping here gives other clients a chance to acquire the lock. Not sure
# if doozerd has primitives that make it possible for other clients to
# watch the lock file while avoiding the herd effect.
sleep 1
end
# Once we're done running, disconnect from doozerd.
Locker.connection.disconnect
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment