Permalink
Browse files

works with Rails

  • Loading branch information...
1 parent 8072cec commit 999d04cbd12ddcde4b0d6a80b554d4d6b557f297 Andrew Cantino & Chris Alvarado-Dryden committed Apr 7, 2012
Showing with 310 additions and 0 deletions.
  1. +5 −0 .gitignore
  2. +1 −0 .rvmrc
  3. +7 −0 Gemfile
  4. 0 README
  5. +51 −0 README.markdown
  6. +7 −0 Rakefile
  7. +2 −0 lib/memlock.rb
  8. +21 −0 lib/morlock/base.rb
  9. +39 −0 lib/morlock/gem_client.rb
  10. +17 −0 lib/morlock/rails.rb
  11. +3 −0 lib/morlock/version.rb
  12. +25 −0 morlock.gemspec
  13. +124 −0 spec/morlock_spec.rb
  14. +8 −0 spec/spec_helper.rb
View
@@ -0,0 +1,5 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
+.idea
View
@@ -0,0 +1 @@
+rvm use 1.9.3@morlock --create
View
@@ -0,0 +1,7 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in morlock.gemspec
+gemspec
+
+#gem "memcache-client"
+#gem 'dalli'
View
No changes.
View
@@ -0,0 +1,51 @@
+# Morlock
+
+Morlock turns your memcached server into a distributed, conflict-eating machine. Rails integration is dug in.
+
+## Usage
+
+### Creating a new Morlock instance
+
+#### Ruby
+
+ require 'memcache-client'
+ require 'morlock'
+
+ mem_cache_client = MemCache.new("memcached.you.com")
+ morlock = Morlock.new(mem_cache_client)
+
+If you prefer Dalli, use that instead:
+
+ require 'dalli'
+ dc = Dalli::Client.new('localhost:11211')
+ morlock = Morlock.new(dc)
+
+#### Rails
+
+If you're already using MemCacheStore in your Rails app, using Morlock is trivial. Morlock will automatically use the memcached server that is backing Rails.cache.
+
+With Bundler:
+
+ gem 'morlock', :require => 'morlock/rails'
+
+Or in any script after Rails has loaded:
+
+ require 'morlock/rails'
+
+At this point, `Rails.morlock` should be defined and available. Use it instead of `morlock` in the examples below.
+
+### Distributed Locking
+
+Possible usages:
+
+ handle_failed_lock unless morlock.lock(key) do
+ # We have the lock
+ end
+
+ morlock.lock(key) { # We have the lock } || raise "Unable to lock!"
+
+ morlock.lock(key, :failure => failure_proc) do
+ # We have the lock
+ end
+
+ morlock.lock(key, :failure => failure_proc, :success => success_proc)
View
@@ -0,0 +1,7 @@
+require "rubygems"
+require "bundler/gem_tasks"
+
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new('spec')
+task :default => :spec
View
@@ -0,0 +1,2 @@
+require "morlock/version"
+require 'morlock/base'
View
@@ -0,0 +1,21 @@
+require 'morlock/gem_client'
+
+class Morlock
+ DEFAULT_EXPIRATION = 60
+
+ attr_accessor :client
+
+ def initialize(client)
+ @client = Morlock::GemClient.wrap(client)
+ end
+
+ def lock(key, options = {})
+ lock_obtained = @client.add(key, options[:expiration] || DEFAULT_EXPIRATION)
+ yield if lock_obtained && block_given?
+ options[:success].call if lock_obtained && options[:success]
+ options[:failure].call if !lock_obtained && options[:failure]
+ lock_obtained
+ ensure
+ @client.delete(key) if lock_obtained
+ end
+end
@@ -0,0 +1,39 @@
+class Morlock
+ class UnknownGemClient < StandardError; end
+
+ class GemClient
+ GEM_CLIENTS = []
+
+ def initialize(client)
+ @client = client
+ end
+
+ def self.wrap(client)
+ GEM_CLIENTS.each do |gem, gem_client|
+ if (eval(gem) rescue false) && client.is_a?(eval(gem))
+ return gem_client.new(client)
+ end
+ end
+
+ raise UnknownGemClient.new("You provided Morlock a memcached client of an unknown type: #{client.class}")
+ end
+
+ def delete(key)
+ @client.delete(key)
+ end
+ end
+
+ class DalliGemClient < GemClient
+ def add(key, expiration)
+ @client.add(key, 1, expiration)
+ end
+ end
+ GemClient::GEM_CLIENTS << ["Dalli::Client", DalliGemClient]
+
+ class MemcacheGemClient < GemClient
+ def add(key, expiration)
+ @client.add(key, 1, expiration, true) !~ /NOT_STORED/
+ end
+ end
+ GemClient::GEM_CLIENTS << ["MemCache", MemcacheGemClient]
+end
View
@@ -0,0 +1,17 @@
+class Morlock
+ class MorlockRailtie < ::Rails::Railtie
+ config.after_initialize do
+ if defined?(ActiveSupport::Cache::MemCacheStore) && Rails.cache.is_a?(ActiveSupport::Cache::MemCacheStore) && Rails.cache.instance_variable_get(:@data)
+ Rails.module_eval do
+ class << self
+ def morlock
+ @@morlock ||= Morlock.new(Rails.cache.instance_variable_get(:@data))
+ end
+ end
+ end
+ else
+ Rails.logger.warn "WARNING: Morlock detected that you are not using the Rails ActiveSupport::Cache::MemCacheStore. Rails.morlock will not be setup."
+ end
+ end
+ end
+end
@@ -0,0 +1,3 @@
+class Morlock
+ VERSION = "0.0.1"
+end
View
@@ -0,0 +1,25 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "morlock/version"
+
+Gem::Specification.new do |s|
+ s.name = "morlock"
+ s.version = Morlock::VERSION
+ s.authors = ["Andrew Cantino & Chris Alvarado-Dryden"]
+ s.email = ["pair+andrew+chris@mavenlink.com"]
+ s.homepage = ""
+ s.summary = %q{TODO: Write a gem summary}
+ s.description = %q{TODO: Write a gem description}
+
+ s.rubyforge_project = "morlock"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ # specify any dependencies here; for example:
+ s.add_development_dependency "rspec"
+ s.add_development_dependency "rr"
+ #s.add_runtime_dependency "memcache-client"
+end
View
@@ -0,0 +1,124 @@
+require 'spec_helper'
+
+describe Morlock do
+ describe "#lock" do
+ module Dalli
+ class Client; end
+ end
+ class MemCache; end
+
+ context "working with different memcached client gems" do
+ describe "with Dalli" do
+ before do
+ @mock_client = Dalli::Client.new
+ stub(@mock_client).delete(anything)
+ end
+
+ it "should issue a memcached add command on the given key" do
+ mock(@mock_client).add("some_key", 1, 60)
+ morlock = Morlock.new(@mock_client)
+ morlock.lock("some_key", :expiration => 60)
+ end
+
+ it "should return true when the lock is acquired" do
+ mock(@mock_client).add("some_key", 1, 60) { true }
+ morlock = Morlock.new(@mock_client)
+ morlock.lock("some_key", :expiration => 60).should == true
+ end
+
+ it "should return false when the lock is not acquired" do
+ mock(@mock_client).add("some_key", 1, 60) { false }
+ morlock = Morlock.new(@mock_client)
+ morlock.lock("some_key", :expiration => 60).should == false
+ end
+ end
+
+ describe "with MemCache" do
+ before do
+ @mock_client = MemCache.new
+ stub(@mock_client).delete(anything)
+ end
+
+ it "should issue a memcached add command on the given key" do
+ mock(@mock_client).add("some_key", 1, 60, true)
+ morlock = Morlock.new(@mock_client)
+ morlock.lock("some_key", :expiration => 60)
+ end
+
+ it "should return true when the lock is acquired" do
+ mock(@mock_client).add("some_key", 1, 60, true) { "STORED\r\n" }
+ morlock = Morlock.new(@mock_client)
+ morlock.lock("some_key", :expiration => 60).should == true
+ end
+
+ it "should return false when the lock is not acquired" do
+ mock(@mock_client).add("some_key", 1, 60, true) { "NOT_STORED\r\n" }
+ morlock = Morlock.new(@mock_client)
+ morlock.lock("some_key", :expiration => 60).should == false
+ end
+ end
+ end
+
+ context "general behavior" do
+ before do
+ @mock_client = Dalli::Client.new
+ @morlock = Morlock.new(@mock_client)
+ end
+
+ def lock_will_succeed
+ key = nil
+ mock(@mock_client).add(anything, anything, anything) { |k| key = k; true }
+ mock(@mock_client).delete(anything) { |k| k.should == key }
+ end
+
+ def lock_will_fail
+ mock(@mock_client).add(anything, anything, anything) { |k| false }
+ do_not_allow(@mock_client).delete
+ end
+
+ it "should yield on success" do
+ lock_will_succeed
+ yielded = false
+ @morlock.lock("some_key") do
+ yielded = true
+ end
+ yielded.should be_true
+ end
+
+ it "should not yield on failure" do
+ lock_will_fail
+ yielded = false
+ @morlock.lock("some_key") do
+ yielded = true
+ end
+ yielded.should be_false
+ end
+
+ it "should accept :success and :failure procs and call :success on success" do
+ lock_will_succeed
+ failed, succeeded = nil, nil
+ @morlock.lock("some_key", :failure => lambda { failed = true }, :success => lambda { succeeded = true })
+ failed.should be_nil
+ succeeded.should be_true
+ end
+
+ it "should accept :success and :failure procs and call :failure on failure" do
+ lock_will_fail
+ failed, succeeded = nil, nil
+ @morlock.lock("some_key", :failure => lambda { failed = true }, :success => lambda { succeeded = true })
+ failed.should be_true
+ succeeded.should be_nil
+ end
+
+ it "should return false on failure" do
+ lock_will_fail
+ @morlock.lock("some_key").should be_false
+ end
+
+ it "should return true on success" do
+ lock_will_succeed
+ @morlock.lock("some_key").should be_true
+ end
+ end
+ end
+end
View
@@ -0,0 +1,8 @@
+require 'rubygems'
+require 'bundler/setup'
+require 'morlock'
+require 'rr'
+
+RSpec.configure do |config|
+ config.mock_with :rr
+end

0 comments on commit 999d04c

Please sign in to comment.