24 Commits

Author SHA1 Message Date
Nick Elser
ce0a4d8d86 release 0.2.0 2015-04-12 22:33:19 -07:00
Nick Elser
1fd769eec2 refactor class methods into instance methods 2015-04-12 22:32:51 -07:00
Nick Elser
37be5ae27b bump version, update changelog 2015-04-12 20:58:18 -07:00
Nick Elser
a1a226fb59 account for variabilities in test memcached 2015-04-12 20:57:52 -07:00
Nick Elser
8d7ddaf35a move logic around loop counts to a more reasonable location 2015-04-12 20:57:29 -07:00
Nick Elser
1aacc0c1a1 properly throw lock LockClientError 2015-04-12 20:47:35 -07:00
Nick Elser
8166c6b51d specify ruby version 2015-04-12 19:53:47 -07:00
Nick Elser
7662743123 update changelog 2015-04-12 19:47:35 -07:00
Nick Elser
a2fa281b86 fix test cases when memcached is delayed 2015-04-12 19:47:23 -07:00
Nick Elser
887219b63d release 0.1.2 2015-04-12 19:43:59 -07:00
Nick Elser
c0905fef91 faster unpack without exceptions 2015-04-12 19:43:07 -07:00
Nick Elser
a54b795e20 simplify defaults logic and fix retry timeouts 2015-04-12 19:42:59 -07:00
Nick Elser
0828cd546b add gem info 2015-04-12 17:54:38 -07:00
Nick Elser
c2b9de4cf3 refactor shared logic into base class 2015-04-12 17:54:30 -07:00
Nick Elser
d4860423fa whitespace 2015-04-12 16:03:53 -07:00
Nick Elser
d8f8350d1c check timeout at the entry of the loop 2015-04-12 15:57:32 -07:00
Nick Elser
1668756a48 update changelog 2015-04-12 15:45:36 -07:00
Nick Elser
74e9e3de75 add build status 2015-04-12 15:41:31 -07:00
Nick Elser
75aad64c08 add minitest dependency 2015-04-12 15:35:37 -07:00
Nick Elser
2eb56a8eaa more gemspec updates 2015-04-12 15:34:05 -07:00
Nick Elser
d260160618 make test the default rake command 2015-04-12 15:31:19 -07:00
Nick Elser
57fad16e4b add license 2015-04-12 15:30:12 -07:00
Nick Elser
30639cae72 use msgpack for efficiency 2015-04-12 15:28:53 -07:00
Nick Elser
89061170ea start servers for travis 2015-04-12 15:28:35 -07:00
14 changed files with 317 additions and 401 deletions

View File

@@ -1,3 +1,6 @@
language: ruby language: ruby
rvm: rvm:
- 2.2.0 - 2.2.0
services:
- memcached
- redis-server

View File

@@ -1,3 +1,21 @@
## 0.2.0
- Refactor class methods into instance methods to simplify implementation.
- Increase thread safety with Memcached implementation.
## 0.1.3
- Properly throw Suo::LockClientError when the connection itself fails (Memcache server not reachable, etc.)
## 0.1.2
- Fix retry_timeout to properly use the full time (was being calculated incorrectly).
- Refactor client implementations to re-use more code.
## 0.1.1
- Use [MessagePack](https://github.com/msgpack/msgpack-ruby) for semaphore serialization.
## 0.1.0 ## 0.1.0
- First release - First release.

22
LICENSE.txt Normal file
View File

@@ -0,0 +1,22 @@
Copyright (c) 2015 Nick Elser
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,4 +1,4 @@
# Suo # Suo [![Build Status](https://travis-ci.org/nickelser/suo.svg?branch=master)](https://travis-ci.org/nickelser/suo) [![Gem Version](https://badge.fury.io/rb/suo.svg)](http://badge.fury.io/rb/suo)
:lock: Distributed semaphores using Memcached or Redis in Ruby. :lock: Distributed semaphores using Memcached or Redis in Ruby.
@@ -31,18 +31,27 @@ suo.lock("some_key") do
@puppies.pet! @puppies.pet!
end end
2.times do Thread.new { suo.lock("other_key", 2) { puts "One"; sleep 2 } }
Thread.new do Thread.new { suo.lock("other_key", 2) { puts "Two"; sleep 2 } }
# second argument is the number of resources - so this will run twice Thread.new { suo.lock("other_key", 2) { puts "Three" } }
suo.lock("other_key", 2, timeout: 0.5) { puts "Will run twice!" }
end # will print "One" "Two", but not "Three", as there are only 2 resources
end
# custom acquisition timeouts (time to acquire)
suo = Suo::Client::Memcached.new(client: some_dalli_client, acquisition_timeout: 1) # in seconds
# manually locking/unlocking
suo.lock("a_key")
foo.baz!
suo.unlock("a_key")
# custom stale lock cleanup (cleaning of dead clients)
suo = Suo::Client::Redis.new(client: some_redis_client, stale_lock_expiration: 60*5)
``` ```
## TODO ## TODO
- better stale key handling (refresh blocks) - better stale key handling (refresh blocks)
- more race condition tests - more race condition tests
- refactor clients to re-use more code
## History ## History

View File

@@ -1,7 +1,8 @@
require "bundler/gem_tasks" require "bundler/gem_tasks"
require "rake/testtask" require "rake/testtask"
task default: :test
Rake::TestTask.new do |t| Rake::TestTask.new do |t|
t.libs << "test" t.libs << "test"
t.pattern = "test/*_test.rb" t.pattern = "test/**/*_test.rb"
end end

View File

@@ -2,114 +2,187 @@ module Suo
module Client module Client
class Base class Base
DEFAULT_OPTIONS = { DEFAULT_OPTIONS = {
retry_count: 3, acquisition_timeout: 0.1,
retry_delay: 0.01, acquisition_delay: 0.01,
stale_lock_expiration: 3600 stale_lock_expiration: 3600
}.freeze }.freeze
attr_accessor :client
include MonitorMixin
def initialize(options = {}) def initialize(options = {})
@options = self.class.merge_defaults(options).merge(_initialized: true) fail "Client required" unless options[:client]
@options = DEFAULT_OPTIONS.merge(options)
@retry_count = (@options[:acquisition_timeout] / @options[:acquisition_delay].to_f).ceil
@client = @options[:client]
super()
end end
def lock(key, resources = 1, options = {}) def lock(key, resources = 1)
options = self.class.merge_defaults(@options.merge(options)) token = acquire_lock(key, resources)
token = self.class.lock(key, resources, options)
if token if block_given? && token
begin begin
yield if block_given? yield
ensure ensure
self.class.unlock(key, token, options) unlock(key, token)
end end
true
else else
false token
end end
end end
def locked?(key, resources = 1) def locked?(key, resources = 1)
self.class.locked?(key, resources, @options) locks(key).size >= resources
end end
class << self def locks(key)
def lock(key, resources = 1, options = {}) # rubocop:disable Lint/UnusedMethodArgument val, _ = get(key)
fail NotImplementedError locks = deserialize_locks(val)
end
def locked?(key, resources = 1, options = {}) locks
options = merge_defaults(options) end
client = options[:client]
locks = deserialize_locks(client.get(key))
locks.size >= resources def refresh(key, acquisition_token)
end retry_with_timeout(key) do
val, cas = get(key)
def locks(key, options) if val.nil?
options = merge_defaults(options) set_initial(key)
client = options[:client] next
locks = deserialize_locks(client.get(key))
locks.size
end
def refresh(key, acquisition_token, options = {}) # rubocop:disable Lint/UnusedMethodArgument
fail NotImplementedError
end
def unlock(key, acquisition_token, options = {}) # rubocop:disable Lint/UnusedMethodArgument
fail NotImplementedError
end
def clear(key, options = {}) # rubocop:disable Lint/UnusedMethodArgument
fail NotImplementedError
end
def merge_defaults(options = {})
unless options[:_initialized]
options = self::DEFAULT_OPTIONS.merge(options)
fail "Client required" unless options[:client]
end end
if options[:retry_timeout] locks = deserialize_and_clear_locks(val)
options[:retry_count] = (options[:retry_timeout] / options[:retry_delay].to_f).floor
refresh_lock(locks, acquisition_token)
break if set(key, serialize_locks(locks), cas)
end
end
def unlock(key, acquisition_token)
return unless acquisition_token
retry_with_timeout(key) do
val, cas = get(key)
break if val.nil?
locks = deserialize_and_clear_locks(val)
acquisition_lock = remove_lock(locks, acquisition_token)
break unless acquisition_lock
break if set(key, serialize_locks(locks), cas)
end
rescue LockClientError => _ # rubocop:disable Lint/HandleExceptions
# ignore - assume success due to optimistic locking
end
def clear(key) # rubocop:disable Lint/UnusedMethodArgument
fail NotImplementedError
end
private
def acquire_lock(key, resources = 1)
acquisition_token = nil
token = SecureRandom.base64(16)
retry_with_timeout(key) do
val, cas = get(key)
if val.nil?
set_initial(key)
next
end end
options locks = deserialize_and_clear_locks(val)
end
private if locks.size < resources
add_lock(locks, token)
def serialize_locks(locks) newval = serialize_locks(locks)
locks.map { |time, token| [time.to_f, token].join(":") }.join(",")
end
def deserialize_locks(str) if set(key, newval, cas)
str.split(",").map do |s| acquisition_token = token
time, token = s.split(":", 2) break
[Time.at(time.to_f), token] end
end end
end end
def clear_expired_locks(locks, options) acquisition_token
expired = Time.now - options[:stale_lock_expiration] end
locks.reject { |time, _| time < expired }
end
def add_lock(locks, token) def get(key) # rubocop:disable Lint/UnusedMethodArgument
locks << [Time.now.to_f, token] fail NotImplementedError
end end
def remove_lock(locks, acquisition_token) def set(key, newval, oldval) # rubocop:disable Lint/UnusedMethodArgument
lock = locks.find { |_, token| token == acquisition_token } fail NotImplementedError
locks.delete(lock) end
end
def refresh_lock(locks, acquisition_token) def set_initial(key) # rubocop:disable Lint/UnusedMethodArgument
remove_lock(locks, acquisition_token) fail NotImplementedError
add_lock(locks, token) end
def synchronize(key) # rubocop:disable Lint/UnusedMethodArgument
mon_synchronize { yield }
end
def retry_with_timeout(key)
start = Time.now.to_f
@retry_count.times do
now = Time.now.to_f
break if now - start > @options[:acquisition_timeout]
synchronize(key) do
yield
end
sleep(rand(@options[:acquisition_delay] * 1000).to_f / 1000)
end end
rescue => _
raise LockClientError
end
def serialize_locks(locks)
MessagePack.pack(locks.map { |time, token| [time.to_f, token] })
end
def deserialize_and_clear_locks(val)
clear_expired_locks(deserialize_locks(val))
end
def deserialize_locks(val)
unpacked = (val.nil? || val == "") ? [] : MessagePack.unpack(val)
unpacked.map do |time, token|
[Time.at(time), token]
end
rescue EOFError => _
[]
end
def clear_expired_locks(locks)
expired = Time.now - @options[:stale_lock_expiration]
locks.reject { |time, _| time < expired }
end
def add_lock(locks, token)
locks << [Time.now.to_f, token]
end
def remove_lock(locks, acquisition_token)
lock = locks.find { |_, token| token == acquisition_token }
locks.delete(lock)
end
def refresh_lock(locks, acquisition_token)
remove_lock(locks, acquisition_token)
add_lock(locks, token)
end end
end end
end end

View File

@@ -1,7 +0,0 @@
module Suo
module Client
module Errors
class FailedToAcquireLock < StandardError; end
end
end
end

View File

@@ -6,131 +6,22 @@ module Suo
super super
end end
class << self def clear(key)
def lock(key, resources = 1, options = {}) @client.delete(key)
options = merge_defaults(options) end
acquisition_token = nil
token = SecureRandom.base64(16)
client = options[:client]
begin private
start = Time.now.to_f
options[:retry_count].times do |i| def get(key)
val, cas = client.get_cas(key) @client.get_cas(key)
end
# no key has been set yet; we could simply set it, but would lead to race conditions on the initial setting def set(key, newval, cas)
if val.nil? @client.set_cas(key, newval, cas)
client.set(key, "") end
next
end
locks = clear_expired_locks(deserialize_locks(val.to_s), options) def set_initial(key)
@client.set(key, "")
if locks.size < resources
add_lock(locks, token)
newval = serialize_locks(locks)
if client.set_cas(key, newval, cas)
acquisition_token = token
break
end
end
if options[:retry_timeout]
now = Time.now.to_f
break if now - start > options[:retry_timeout]
end
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
end
rescue => _
raise FailedToAcquireLock
end
acquisition_token
end
def refresh(key, acquisition_token, options = {})
options = merge_defaults(options)
client = options[:client]
begin
start = Time.now.to_f
options[:retry_count].times do
val, cas = client.get_cas(key)
# much like with initial set - ensure the key is here
if val.nil?
client.set(key, "")
next
end
locks = clear_expired_locks(deserialize_locks(val), options)
refresh_lock(locks, acquisition_token)
newval = serialize_locks(locks)
break if client.set_cas(key, newval, cas)
if options[:retry_timeout]
now = Time.now.to_f
break if now - start > options[:retry_timeout]
end
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
end
rescue => _
raise FailedToAcquireLock
end
end
def unlock(key, acquisition_token, options = {})
options = merge_defaults(options)
client = options[:client]
return unless acquisition_token
begin
start = Time.now.to_f
options[:retry_count].times do
val, cas = client.get_cas(key)
break if val.nil? # lock has expired totally
locks = clear_expired_locks(deserialize_locks(val), options)
acquisition_lock = remove_lock(locks, acquisition_token)
break unless acquisition_lock
newval = serialize_locks(locks)
break if client.set_cas(key, newval, cas)
# another client cleared a token in the interim - try again!
if options[:retry_timeout]
now = Time.now.to_f
break if now - start > options[:retry_timeout]
end
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
end
rescue => boom # rubocop:disable Lint/HandleExceptions
# since it's optimistic locking - fine if we are unable to release
raise boom if ENV["SUO_TEST"]
end
end
def clear(key, options = {})
options = merge_defaults(options)
options[:client].delete(key)
end
end end
end end
end end

View File

@@ -6,161 +6,34 @@ module Suo
super super
end end
class << self def clear(key)
def lock(key, resources = 1, options = {}) @client.del(key)
options = merge_defaults(options) end
acquisition_token = nil
token = SecureRandom.base64(16)
client = options[:client]
begin private
start = Time.now.to_f
options[:retry_count].times do def get(key)
client.watch(key) do [@client.get(key), nil]
begin end
val = client.get(key)
locks = clear_expired_locks(deserialize_locks(val.to_s), options) def set(key, newval, _)
ret = @client.multi do |multi|
if locks.size < resources multi.set(key, newval)
add_lock(locks, token)
newval = serialize_locks(locks)
ret = client.multi do |multi|
multi.set(key, newval)
end
acquisition_token = token if ret[0] == "OK"
end
ensure
client.unwatch
end
end
break if acquisition_token
if options[:retry_timeout]
now = Time.now.to_f
break if now - start > options[:retry_timeout]
end
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
end
rescue => boom
raise boom
raise Suo::Client::FailedToAcquireLock
end
acquisition_token
end end
def refresh(key, acquisition_token, options = {}) ret[0] == "OK"
options = merge_defaults(options) end
client = options[:client]
refreshed = false
begin def synchronize(key)
start = Time.now.to_f @client.watch(key) do
yield
options[:retry_count].times do
client.watch(key) do
begin
val = client.get(key)
locks = clear_expired_locks(deserialize_locks(val), options)
refresh_lock(locks, acquisition_token)
newval = serialize_locks(locks)
ret = client.multi do |multi|
multi.set(key, newval)
end
refreshed = ret[0] == "OK"
ensure
client.unwatch
end
end
break if refreshed
if options[:retry_timeout]
now = Time.now.to_f
break if now - start > options[:retry_timeout]
end
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
end
rescue => _
raise Suo::Client::FailedToAcquireLock
end
end end
ensure
@client.unwatch
end
def unlock(key, acquisition_token, options = {}) def set_initial(key)
options = merge_defaults(options) @client.set(key, "")
client = options[:client]
return unless acquisition_token
begin
start = Time.now.to_f
options[:retry_count].times do
cleared = false
client.watch(key) do
begin
val = client.get(key)
if val.nil?
cleared = true
break
end
locks = clear_expired_locks(deserialize_locks(val), options)
acquisition_lock = remove_lock(locks, acquisition_token)
unless acquisition_lock
# token was already cleared
cleared = true
break
end
newval = serialize_locks(locks)
ret = client.multi do |multi|
multi.set(key, newval)
end
cleared = ret[0] == "OK"
ensure
client.unwatch
end
end
break if cleared
if options[:retry_timeout]
now = Time.now.to_f
break if now - start > options[:retry_timeout]
end
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
end
rescue => boom # rubocop:disable Lint/HandleExceptions
# since it's optimistic locking - fine if we are unable to release
raise boom if ENV["SUO_TEST"]
end
end
def clear(key, options = {})
options = merge_defaults(options)
options[:client].del(key)
end
end end
end end
end end

View File

@@ -6,7 +6,9 @@ require "dalli/cas/client"
require "redis" require "redis"
require "suo/client/errors" require "msgpack"
require "suo/errors"
require "suo/client/base" require "suo/client/base"
require "suo/client/memcached" require "suo/client/memcached"
require "suo/client/redis" require "suo/client/redis"

3
lib/suo/errors.rb Normal file
View File

@@ -0,0 +1,3 @@
module Suo
class LockClientError < StandardError; end
end

View File

@@ -1,3 +1,3 @@
module Suo module Suo
VERSION = "0.1.0" VERSION = "0.2.0"
end end

View File

@@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
lib = File.expand_path('../lib', __FILE__) lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'suo/version' require "suo/version"
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.name = "suo" spec.name = "suo"
@@ -10,14 +10,18 @@ Gem::Specification.new do |spec|
spec.email = ["nick.elser@gmail.com"] spec.email = ["nick.elser@gmail.com"]
spec.summary = %q(Distributed semaphores using Memcached or Redis.) spec.summary = %q(Distributed semaphores using Memcached or Redis.)
# spec.description = %q{TODO: Long description} spec.description = %q(Distributed semaphores using Memcached or Redis.)
spec.homepage = "https://github.com/nickelser/suo" spec.homepage = "https://github.com/nickelser/suo"
spec.license = "MIT"
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } spec.files = `git ls-files -z`.split("\x0")
spec.bindir = "bin" spec.bindir = "bin"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"] spec.require_paths = ["lib"]
spec.required_ruby_version = "~> 2.0"
spec.add_dependency "dalli" spec.add_dependency "dalli"
spec.add_dependency "redis" spec.add_dependency "redis"
spec.add_dependency "msgpack" spec.add_dependency "msgpack"
@@ -25,4 +29,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler", "~> 1.5" spec.add_development_dependency "bundler", "~> 1.5"
spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rubocop", "~> 0.30.0" spec.add_development_dependency "rubocop", "~> 0.30.0"
spec.add_development_dependency "minitest", "~> 5.5.0"
end end

View File

@@ -3,55 +3,55 @@ require "test_helper"
TEST_KEY = "suo_test_key".freeze TEST_KEY = "suo_test_key".freeze
module ClientTests module ClientTests
def test_requires_client def test_throws_failed_error_on_bad_client
exception = assert_raises(RuntimeError) do assert_raises(Suo::LockClientError) do
@klass.lock(TEST_KEY, 1) client = @client.class.new(client: {})
client.lock(TEST_KEY, 1)
end end
assert_equal "Client required", exception.message
end end
def test_class_single_resource_locking def test_class_single_resource_locking
lock1 = @klass.lock(TEST_KEY, 1, client: @klass_client) lock1 = @client.lock(TEST_KEY, 1)
refute_nil lock1 refute_nil lock1
locked = @klass.locked?(TEST_KEY, 1, client: @klass_client) locked = @client.locked?(TEST_KEY, 1)
assert_equal true, locked assert_equal true, locked
lock2 = @klass.lock(TEST_KEY, 1, client: @klass_client) lock2 = @client.lock(TEST_KEY, 1)
assert_nil lock2 assert_nil lock2
@klass.unlock(TEST_KEY, lock1, client: @klass_client) @client.unlock(TEST_KEY, lock1)
locked = @client.locked?(TEST_KEY, 1)
locked = @klass.locked?(TEST_KEY, 1, client: @klass_client)
assert_equal false, locked assert_equal false, locked
end end
def test_class_multiple_resource_locking def test_class_multiple_resource_locking
lock1 = @klass.lock(TEST_KEY, 2, client: @klass_client) lock1 = @client.lock(TEST_KEY, 2)
refute_nil lock1 refute_nil lock1
locked = @klass.locked?(TEST_KEY, 2, client: @klass_client) locked = @client.locked?(TEST_KEY, 2)
assert_equal false, locked assert_equal false, locked
lock2 = @klass.lock(TEST_KEY, 2, client: @klass_client) lock2 = @client.lock(TEST_KEY, 2)
refute_nil lock2 refute_nil lock2
locked = @klass.locked?(TEST_KEY, 2, client: @klass_client) locked = @client.locked?(TEST_KEY, 2)
assert_equal true, locked assert_equal true, locked
@klass.unlock(TEST_KEY, lock1, client: @klass_client) @client.unlock(TEST_KEY, lock1)
locked = @klass.locked?(TEST_KEY, 1, client: @klass_client) locked = @client.locked?(TEST_KEY, 1)
assert_equal true, locked assert_equal true, locked
@klass.unlock(TEST_KEY, lock2, client: @klass_client) @client.unlock(TEST_KEY, lock2)
locked = @klass.locked?(TEST_KEY, 1, client: @klass_client) locked = @client.locked?(TEST_KEY, 1)
assert_equal false, locked assert_equal false, locked
end end
def test_instance_single_resource_locking def test_block_single_resource_locking
locked = false locked = false
@client.lock(TEST_KEY, 1) { locked = true } @client.lock(TEST_KEY, 1) { locked = true }
@@ -59,23 +59,46 @@ module ClientTests
assert_equal true, locked assert_equal true, locked
end end
def test_instance_unlocks_on_exception def test_block_unlocks_on_exception
assert_raises(RuntimeError) do assert_raises(RuntimeError) do
@client.lock(TEST_KEY, 1) { fail "Test" } @client.lock(TEST_KEY, 1) { fail "Test" }
end end
locked = @klass.locked?(TEST_KEY, 1, client: @klass_client) locked = @client.locked?(TEST_KEY, 1)
assert_equal false, locked assert_equal false, locked
end end
def test_readme_example
output = Queue.new
threads = []
threads << Thread.new { @client.lock(TEST_KEY, 2) { output << "One"; sleep 2 } }
threads << Thread.new { @client.lock(TEST_KEY, 2) { output << "Two"; sleep 2 } }
threads << Thread.new { @client.lock(TEST_KEY, 2) { output << "Three" } }
threads.map(&:join)
ret = []
ret << output.pop
ret << output.pop
ret.sort!
assert_equal 0, output.size
assert_equal ["One", "Two"], ret
end
def test_instance_multiple_resource_locking def test_instance_multiple_resource_locking
success_counter = Queue.new success_counter = Queue.new
failure_counter = Queue.new failure_counter = Queue.new
client = @client.class.new(acquisition_timeout: 0.9, client: @client.client)
100.times.map do |i| 100.times.map do |i|
Thread.new do Thread.new do
success = @client.lock(TEST_KEY, 50, retry_timeout: 0.9) do success = @client.lock(TEST_KEY, 50) do
sleep(1) sleep(3)
success_counter << i success_counter << i
end end
@@ -91,10 +114,12 @@ module ClientTests
success_counter = Queue.new success_counter = Queue.new
failure_counter = Queue.new failure_counter = Queue.new
client = @client.class.new(acquisition_timeout: 3, client: @client.client)
100.times.map do |i| 100.times.map do |i|
Thread.new do Thread.new do
success = @client.lock(TEST_KEY, 50, retry_timeout: 2) do success = client.lock(TEST_KEY, 50) do
sleep(1) sleep(0.5)
success_counter << i success_counter << i
end end
@@ -109,12 +134,12 @@ end
class TestBaseClient < Minitest::Test class TestBaseClient < Minitest::Test
def setup def setup
@klass = Suo::Client::Base @client = Suo::Client::Base.new(client: {})
end end
def test_not_implemented def test_not_implemented
assert_raises(NotImplementedError) do assert_raises(NotImplementedError) do
@klass.lock(TEST_KEY, 1) @client.send(:get, TEST_KEY)
end end
end end
end end
@@ -123,13 +148,12 @@ class TestMemcachedClient < Minitest::Test
include ClientTests include ClientTests
def setup def setup
@klass = Suo::Client::Memcached @dalli = Dalli::Client.new("127.0.0.1:11211")
@client = @klass.new @client = Suo::Client::Memcached.new
@klass_client = Dalli::Client.new("127.0.0.1:11211")
end end
def teardown def teardown
@klass_client.delete(TEST_KEY) @dalli.delete(TEST_KEY)
end end
end end
@@ -137,13 +161,12 @@ class TestRedisClient < Minitest::Test
include ClientTests include ClientTests
def setup def setup
@klass = Suo::Client::Redis @redis = Redis.new
@client = @klass.new @client = Suo::Client::Redis.new
@klass_client = Redis.new
end end
def teardown def teardown
@klass_client.del(TEST_KEY) @redis.del(TEST_KEY)
end end
end end