Skip to content

Instantly share code, notes, and snippets.

@therealadam
Created August 28, 2011 21:03
Show Gist options
  • Select an option

  • Save therealadam/1177228 to your computer and use it in GitHub Desktop.

Select an option

Save therealadam/1177228 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
@bmizerany
Copy link
Copy Markdown

The sleep(1) is unnecessary if use WATCH with holder.rev.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment