suo-dependless 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a874831b56f527f3e7bd154e5b7811be14580c7d
4
+ data.tar.gz: 03eb5515ae43d4ac7bb6fc1c6da82629f9831cb2
5
+ SHA512:
6
+ metadata.gz: 851915fd21541fc735e2c7053d88e47d5cdb02fd8036fa56ce2cccc492fe2c39477916fda2dffa443d186913e09e9aac71fb7036f9fa91bd13825f6dfea14e0c
7
+ data.tar.gz: 3c1f9ebd4f654d64eef795ecfe81a15ec335e617c71b1af01f08e1b59693c75b81a73aa453106f6afab03c8de80b7f6c726e422d32e5882c18bf1f457d79dc9c
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ .DS_Store
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rubocop.yml ADDED
@@ -0,0 +1,219 @@
1
+ AllCops:
2
+ Exclude:
3
+ - .git/**/*
4
+ - tmp/**/*
5
+ - suo.gemspec
6
+
7
+ Lint/DuplicateMethods:
8
+ Enabled: true
9
+
10
+ Lint/DeprecatedClassMethods:
11
+ Enabled: true
12
+
13
+ Style/TrailingWhitespace:
14
+ Enabled: true
15
+
16
+ Style/Tab:
17
+ Enabled: true
18
+
19
+ Style/TrailingBlankLines:
20
+ Enabled: true
21
+
22
+ Style/NilComparison:
23
+ Enabled: true
24
+
25
+ Style/NonNilCheck:
26
+ Enabled: true
27
+
28
+ Style/Not:
29
+ Enabled: true
30
+
31
+ Style/RedundantReturn:
32
+ Enabled: true
33
+
34
+ Style/ClassCheck:
35
+ Enabled: true
36
+
37
+ Style/EmptyLines:
38
+ Enabled: true
39
+
40
+ Style/EmptyLiteral:
41
+ Enabled: true
42
+
43
+ Style/Alias:
44
+ Enabled: true
45
+
46
+ Style/MethodCallParentheses:
47
+ Enabled: true
48
+
49
+ Style/MethodDefParentheses:
50
+ Enabled: true
51
+
52
+ Style/SpaceBeforeBlockBraces:
53
+ Enabled: true
54
+
55
+ Style/SpaceInsideBlockBraces:
56
+ Enabled: true
57
+
58
+ Style/SpaceInsideParens:
59
+ Enabled: true
60
+
61
+ Style/DeprecatedHashMethods:
62
+ Enabled: true
63
+
64
+ Style/HashSyntax:
65
+ Enabled: true
66
+
67
+ Style/SpaceInsideHashLiteralBraces:
68
+ Enabled: true
69
+ EnforcedStyle: no_space
70
+
71
+ Style/SpaceInsideBrackets:
72
+ Enabled: true
73
+
74
+ Style/AndOr:
75
+ Enabled: false
76
+
77
+ Style/TrailingCommaInLiteral:
78
+ Enabled: true
79
+
80
+ Style/SpaceBeforeComma:
81
+ Enabled: true
82
+
83
+ Style/SpaceBeforeComment:
84
+ Enabled: true
85
+
86
+ Style/SpaceBeforeSemicolon:
87
+ Enabled: true
88
+
89
+ Style/SpaceAroundBlockParameters:
90
+ Enabled: true
91
+
92
+ Style/SpaceAroundOperators:
93
+ Enabled: true
94
+
95
+ Style/SpaceAfterColon:
96
+ Enabled: true
97
+
98
+ Style/SpaceAfterComma:
99
+ Enabled: true
100
+
101
+ Style/SpaceAroundKeyword:
102
+ Enabled: true
103
+
104
+ Style/SpaceAfterNot:
105
+ Enabled: true
106
+
107
+ Style/SpaceAfterSemicolon:
108
+ Enabled: true
109
+
110
+ Lint/UselessComparison:
111
+ Enabled: true
112
+
113
+ Lint/InvalidCharacterLiteral:
114
+ Enabled: true
115
+
116
+ Lint/LiteralInInterpolation:
117
+ Enabled: true
118
+
119
+ Lint/LiteralInCondition:
120
+ Enabled: true
121
+
122
+ Lint/UnusedBlockArgument:
123
+ Enabled: true
124
+
125
+ Style/VariableInterpolation:
126
+ Enabled: true
127
+
128
+ Style/RedundantSelf:
129
+ Enabled: true
130
+
131
+ Style/ParenthesesAroundCondition:
132
+ Enabled: true
133
+
134
+ Style/WhileUntilDo:
135
+ Enabled: true
136
+
137
+ Style/EmptyLineBetweenDefs:
138
+ Enabled: true
139
+
140
+ Style/EmptyLinesAroundAccessModifier:
141
+ Enabled: true
142
+
143
+ Style/EmptyLinesAroundMethodBody:
144
+ Enabled: true
145
+
146
+ Style/ColonMethodCall:
147
+ Enabled: true
148
+
149
+ Lint/SpaceBeforeFirstArg:
150
+ Enabled: true
151
+
152
+ Lint/UnreachableCode:
153
+ Enabled: true
154
+
155
+ Style/UnlessElse:
156
+ Enabled: true
157
+
158
+ Style/ClassVars:
159
+ Enabled: true
160
+
161
+ Style/StringLiterals:
162
+ Enabled: true
163
+ EnforcedStyle: double_quotes
164
+
165
+ Metrics/CyclomaticComplexity:
166
+ Max: 10
167
+
168
+ Metrics/LineLength:
169
+ Max: 128
170
+
171
+ Metrics/MethodLength:
172
+ Max: 32
173
+
174
+ Metrics/PerceivedComplexity:
175
+ Max: 8
176
+
177
+ # Disabled
178
+
179
+ Style/EvenOdd:
180
+ Enabled: false
181
+
182
+ Style/AsciiComments:
183
+ Enabled: false
184
+
185
+ Style/NumericLiterals:
186
+ Enabled: false
187
+
188
+ Style/UnneededPercentQ:
189
+ Enabled: false
190
+
191
+ Style/SpecialGlobalVars:
192
+ Enabled: false
193
+
194
+ Style/TrivialAccessors:
195
+ Enabled: false
196
+
197
+ Style/PerlBackrefs:
198
+ Enabled: false
199
+
200
+ Metrics/AbcSize:
201
+ Enabled: false
202
+
203
+ Metrics/BlockNesting:
204
+ Enabled: false
205
+
206
+ Metrics/ClassLength:
207
+ Enabled: false
208
+
209
+ Metrics/MethodLength:
210
+ Enabled: false
211
+
212
+ Metrics/ParameterLists:
213
+ Enabled: false
214
+
215
+ Metrics/PerceivedComplexity:
216
+ Enabled: false
217
+
218
+ Style/Documentation:
219
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ - 2.3.1
5
+ services:
6
+ - memcached
7
+ - redis-server
data/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ ## 0.3.2
2
+
3
+ - Custom lock tokens (thanks to avokhmin).
4
+
5
+ ## 0.3.1
6
+
7
+ - Slight memory leak fix.
8
+
9
+ ## 0.3.0
10
+
11
+ - Dramatically simplify the interface by forcing clients to specify the key & resources at lock initialization instead of every method call.
12
+
13
+ ## 0.2.3
14
+
15
+ - Clarify documentation further with respect to semaphores.
16
+
17
+ ## 0.2.2
18
+
19
+ - Fix bug with refresh - typo would've prevented real use.
20
+ - Clean up code.
21
+ - Improve documentation a bit.
22
+ - 100% test coverage.
23
+
24
+ ## 0.2.1
25
+
26
+ - Fix bug when dealing with real-world Redis error conditions.
27
+
28
+ ## 0.2.0
29
+
30
+ - Refactor class methods into instance methods to simplify implementation.
31
+ - Increase thread safety with Memcached implementation.
32
+
33
+ ## 0.1.3
34
+
35
+ - Properly throw Suo::LockClientError when the connection itself fails (Memcache server not reachable, etc.)
36
+
37
+ ## 0.1.2
38
+
39
+ - Fix retry_timeout to properly use the full time (was being calculated incorrectly).
40
+ - Refactor client implementations to re-use more code.
41
+
42
+ ## 0.1.1
43
+
44
+ - Use [MessagePack](https://github.com/msgpack/msgpack-ruby) for lock serialization.
45
+
46
+ ## 0.1.0
47
+
48
+ - First release.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Nick Elser
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Suo [![Build Status](https://travis-ci.org/nickelser/suo.svg?branch=master)](https://travis-ci.org/nickelser/suo) [![Code Climate](https://codeclimate.com/github/nickelser/suo/badges/gpa.svg)](https://codeclimate.com/github/nickelser/suo) [![Test Coverage](https://codeclimate.com/github/nickelser/suo/badges/coverage.svg)](https://codeclimate.com/github/nickelser/suo) [![Gem Version](https://badge.fury.io/rb/suo.svg)](http://badge.fury.io/rb/suo)
2
+
3
+ :lock: Distributed semaphores using Memcached or Redis in Ruby.
4
+
5
+ Suo provides a very performant distributed lock solution using Compare-And-Set (`CAS`) commands in Memcached, and `WATCH/MULTI` in Redis. It allows locking both single exclusion (like a mutex - sharing one resource), as well as multiple resources.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application’s Gemfile:
10
+
11
+ ```ruby
12
+ gem 'suo'
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Basic
18
+
19
+ ```ruby
20
+ # Memcached
21
+ suo = Suo::Client::Memcached.new("foo_resource", connection: "127.0.0.1:11211")
22
+
23
+ # Redis
24
+ suo = Suo::Client::Redis.new("baz_resource", connection: {host: "10.0.1.1"})
25
+
26
+ # Pre-existing client
27
+ suo = Suo::Client::Memcached.new("bar_resource", client: some_dalli_client)
28
+
29
+ suo.lock do
30
+ # critical code here
31
+ @puppies.pet!
32
+ end
33
+
34
+ # The resources argument is the number of resources the semaphore will allow to lock (defaulting to one - a mutex)
35
+ suo = Suo::Client::Memcached.new("bar_resource", client: some_dalli_client, resources: 2)
36
+
37
+ Thread.new { suo.lock { puts "One"; sleep 2 } }
38
+ Thread.new { suo.lock { puts "Two"; sleep 2 } }
39
+ Thread.new { suo.lock { puts "Three" } }
40
+
41
+ # will print "One" "Two", but not "Three", as there are only 2 resources
42
+
43
+ # custom acquisition timeouts (time to acquire)
44
+ suo = Suo::Client::Memcached.new("protected_key", client: some_dalli_client, acquisition_timeout: 1) # in seconds
45
+
46
+ # manually locking/unlocking
47
+ # the return value from lock without a block is a unique token valid only for the current lock
48
+ # which must be unlocked manually
49
+ token = suo.lock
50
+ foo.baz!
51
+ suo.unlock(token)
52
+
53
+ # custom stale lock expiration (cleaning of dead locks)
54
+ suo = Suo::Client::Redis.new("other_key", client: some_redis_client, stale_lock_expiration: 60*5)
55
+ ```
56
+
57
+ ### Stale locks
58
+
59
+ "Stale locks" - those acquired more than `stale_lock_expiration` (defaulting to 3600 or one hour) ago - are automatically cleared during any operation on the key (`lock`, `unlock`, `refresh`). The `locked?` method will not return true if only stale locks exist, but will not modify the key itself.
60
+
61
+ To re-acquire a lock in the middle of a block, you can use the refresh method on client.
62
+
63
+ ```ruby
64
+ suo = Suo::Client::Redis.new("foo")
65
+
66
+ # lock is the same token as seen in the manual example, above
67
+ suo.lock do |token|
68
+ 5.times do
69
+ baz.bar!
70
+ suo.refresh(token)
71
+ end
72
+ end
73
+ ```
74
+
75
+ ## TODO
76
+ - more race condition tests
77
+
78
+ ## History
79
+
80
+ View the [changelog](https://github.com/nickelser/suo/blob/master/CHANGELOG.md).
81
+
82
+ ## Contributing
83
+
84
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
85
+
86
+ - [Report bugs](https://github.com/nickelser/suo/issues)
87
+ - Fix bugs and [submit pull requests](https://github.com/nickelser/suo/pulls)
88
+ - Write, clarify, or fix documentation
89
+ - Suggest or add new features
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task default: :test
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test"
7
+ t.pattern = "test/**/*_test.rb"
8
+ end
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "suo"
5
+ require "irb"
6
+
7
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
@@ -0,0 +1,194 @@
1
+ module Suo
2
+ module Client
3
+ class Base
4
+ DEFAULT_OPTIONS = {
5
+ acquisition_timeout: 0.1,
6
+ acquisition_delay: 0.01,
7
+ stale_lock_expiration: 3600,
8
+ resources: 1
9
+ }.freeze
10
+
11
+ BLANK_STR = "".freeze
12
+
13
+ attr_accessor :client, :key, :resources, :options
14
+
15
+ include MonitorMixin
16
+
17
+ def initialize(key, options = {})
18
+ fail "Client required" unless options[:client]
19
+
20
+ @options = DEFAULT_OPTIONS.merge(options)
21
+ @retry_count = (@options[:acquisition_timeout] / @options[:acquisition_delay].to_f).ceil
22
+ @client = @options[:client]
23
+ @resources = @options[:resources].to_i
24
+ @key = key
25
+
26
+ super() # initialize Monitor mixin for thread safety
27
+ end
28
+
29
+ def lock(custom_token = nil)
30
+ token = acquire_lock(custom_token)
31
+
32
+ if block_given? && token
33
+ begin
34
+ yield
35
+ ensure
36
+ unlock(token)
37
+ end
38
+ else
39
+ token
40
+ end
41
+ end
42
+
43
+ def locked?
44
+ locks.size >= resources
45
+ end
46
+
47
+ def locks
48
+ val, _ = get
49
+ cleared_locks = deserialize_and_clear_locks(val)
50
+
51
+ cleared_locks
52
+ end
53
+
54
+ def refresh(token)
55
+ retry_with_timeout do
56
+ val, cas = get
57
+
58
+ if val.nil?
59
+ initial_set
60
+ next
61
+ end
62
+
63
+ cleared_locks = deserialize_and_clear_locks(val)
64
+
65
+ refresh_lock(cleared_locks, token)
66
+
67
+ break if set(serialize_locks(cleared_locks), cas)
68
+ end
69
+ end
70
+
71
+ def unlock(token)
72
+ return unless token
73
+
74
+ retry_with_timeout do
75
+ val, cas = get
76
+
77
+ break if val.nil?
78
+
79
+ cleared_locks = deserialize_and_clear_locks(val)
80
+
81
+ acquisition_lock = remove_lock(cleared_locks, token)
82
+
83
+ break unless acquisition_lock
84
+ break if set(serialize_locks(cleared_locks), cas)
85
+ end
86
+ rescue LockClientError => _ # rubocop:disable Lint/HandleExceptions
87
+ # ignore - assume success due to optimistic locking
88
+ end
89
+
90
+ def clear
91
+ fail NotImplementedError
92
+ end
93
+
94
+ private
95
+
96
+ attr_accessor :retry_count
97
+
98
+ def acquire_lock(token = nil)
99
+ token ||= SecureRandom.base64(16)
100
+
101
+ retry_with_timeout do
102
+ val, cas = get
103
+
104
+ if val.nil?
105
+ initial_set
106
+ next
107
+ end
108
+
109
+ cleared_locks = deserialize_and_clear_locks(val)
110
+
111
+ if cleared_locks.size < resources
112
+ add_lock(cleared_locks, token)
113
+
114
+ newval = serialize_locks(cleared_locks)
115
+
116
+ return token if set(newval, cas)
117
+ end
118
+ end
119
+
120
+ nil
121
+ end
122
+
123
+ def get
124
+ fail NotImplementedError
125
+ end
126
+
127
+ def set(newval, cas) # rubocop:disable Lint/UnusedMethodArgument
128
+ fail NotImplementedError
129
+ end
130
+
131
+ def initial_set(val = BLANK_STR) # rubocop:disable Lint/UnusedMethodArgument
132
+ fail NotImplementedError
133
+ end
134
+
135
+ def synchronize
136
+ mon_synchronize { yield }
137
+ end
138
+
139
+ def retry_with_timeout
140
+ start = Time.now.to_f
141
+
142
+ retry_count.times do
143
+ elapsed = Time.now.to_f - start
144
+ break if elapsed >= options[:acquisition_timeout]
145
+
146
+ synchronize do
147
+ yield
148
+ end
149
+
150
+ sleep(rand(options[:acquisition_delay] * 1000).to_f / 1000)
151
+ end
152
+ rescue => _
153
+ raise LockClientError
154
+ end
155
+
156
+ def serialize_locks(locks)
157
+ MessagePack.pack(locks.map { |time, token| [time.to_f, token] })
158
+ end
159
+
160
+ def deserialize_and_clear_locks(val)
161
+ clear_expired_locks(deserialize_locks(val))
162
+ end
163
+
164
+ def deserialize_locks(val)
165
+ unpacked = (val.nil? || val == BLANK_STR) ? [] : MessagePack.unpack(val)
166
+
167
+ unpacked.map do |time, token|
168
+ [Time.at(time), token]
169
+ end
170
+ rescue EOFError, MessagePack::MalformedFormatError => _
171
+ []
172
+ end
173
+
174
+ def clear_expired_locks(locks)
175
+ expired = Time.now - options[:stale_lock_expiration]
176
+ locks.reject { |time, _| time < expired }
177
+ end
178
+
179
+ def add_lock(locks, token, time = Time.now.to_f)
180
+ locks << [time, token]
181
+ end
182
+
183
+ def remove_lock(locks, acquisition_token)
184
+ lock = locks.find { |_, token| token == acquisition_token }
185
+ locks.delete(lock)
186
+ end
187
+
188
+ def refresh_lock(locks, acquisition_token)
189
+ remove_lock(locks, acquisition_token)
190
+ add_lock(locks, acquisition_token)
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,28 @@
1
+ module Suo
2
+ module Client
3
+ class Memcached < Base
4
+ def initialize(key, options = {})
5
+ options[:client] ||= Dalli::Client.new(options[:connection] || ENV["MEMCACHE_SERVERS"] || "127.0.0.1:11211")
6
+ super
7
+ end
8
+
9
+ def clear
10
+ @client.delete(@key)
11
+ end
12
+
13
+ private
14
+
15
+ def get
16
+ @client.get_cas(@key)
17
+ end
18
+
19
+ def set(newval, cas)
20
+ @client.set_cas(@key, newval, cas)
21
+ end
22
+
23
+ def initial_set(val = BLANK_STR)
24
+ @client.set(@key, val)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ module Suo
2
+ module Client
3
+ class Redis < Base
4
+ OK_STR = "OK".freeze
5
+
6
+ def initialize(key, options = {})
7
+ options[:client] ||= ::Redis.new(options[:connection] || {})
8
+ super
9
+ end
10
+
11
+ def clear
12
+ @client.del(@key)
13
+ end
14
+
15
+ private
16
+
17
+ def get
18
+ [@client.get(@key), nil]
19
+ end
20
+
21
+ def set(newval, _)
22
+ ret = @client.multi do |multi|
23
+ multi.set(@key, newval)
24
+ end
25
+
26
+ ret && ret[0] == OK_STR
27
+ end
28
+
29
+ def synchronize
30
+ @client.watch(@key) do
31
+ yield
32
+ end
33
+ ensure
34
+ @client.unwatch
35
+ end
36
+
37
+ def initial_set(val = BLANK_STR)
38
+ @client.set(@key, val)
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/suo/errors.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Suo
2
+ class LockClientError < StandardError; end
3
+ end
@@ -0,0 +1,3 @@
1
+ module Suo
2
+ VERSION = "0.3.2".freeze
3
+ end
data/lib/suo.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "securerandom"
2
+ require "monitor"
3
+
4
+ require "dalli"
5
+ require "dalli/cas/client"
6
+
7
+ require "redis"
8
+
9
+ require "msgpack"
10
+
11
+ require "suo/version"
12
+
13
+ require "suo/errors"
14
+ require "suo/client/base"
15
+ require "suo/client/memcached"
16
+ require "suo/client/redis"
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "suo/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "suo-dependless"
8
+ spec.version = Suo::VERSION
9
+ spec.authors = ["Nick Elser"]
10
+ spec.email = ["nick.elser@gmail.com"]
11
+
12
+ spec.summary = %q(Distributed locks (mutexes & semaphores) using Memcached or Redis.)
13
+ spec.description = %q(Distributed locks (mutexes & semaphores) using Memcached or Redis.)
14
+ spec.homepage = "https://github.com/bernstein/suo"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.bindir = "bin"
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.required_ruby_version = "~> 2.0"
24
+
25
+ spec.add_dependency "msgpack"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.5"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ spec.add_development_dependency "rubocop", "~> 0.30.0"
30
+ spec.add_development_dependency "minitest", "~> 5.5.0"
31
+ spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4.7"
32
+ end
@@ -0,0 +1,397 @@
1
+ require "test_helper"
2
+
3
+ TEST_KEY = "suo_test_key".freeze
4
+
5
+ module ClientTests
6
+ def client(options = {})
7
+ @client.class.new(options[:key] || TEST_KEY, options.merge(client: @client.client))
8
+ end
9
+
10
+ def test_throws_failed_error_on_bad_client
11
+ assert_raises(Suo::LockClientError) do
12
+ client = @client.class.new(TEST_KEY, client: {})
13
+ client.lock
14
+ end
15
+ end
16
+
17
+ def test_single_resource_locking
18
+ lock1 = @client.lock
19
+ refute_nil lock1
20
+
21
+ locked = @client.locked?
22
+ assert_equal true, locked
23
+
24
+ lock2 = @client.lock
25
+ assert_nil lock2
26
+
27
+ @client.unlock(lock1)
28
+
29
+ locked = @client.locked?
30
+
31
+ assert_equal false, locked
32
+ end
33
+
34
+ def test_lock_with_custom_token
35
+ token = 'foo-bar'
36
+ lock = @client.lock token
37
+ assert_equal lock, token
38
+ end
39
+
40
+ def test_empty_lock_on_invalid_data
41
+ @client.send(:initial_set, "bad value")
42
+ assert_equal false, @client.locked?
43
+ end
44
+
45
+ def test_clear
46
+ lock1 = @client.lock
47
+ refute_nil lock1
48
+
49
+ @client.clear
50
+
51
+ assert_equal false, @client.locked?
52
+ end
53
+
54
+ def test_multiple_resource_locking
55
+ @client = client(resources: 2)
56
+
57
+ lock1 = @client.lock
58
+ refute_nil lock1
59
+
60
+ assert_equal false, @client.locked?
61
+
62
+ lock2 = @client.lock
63
+ refute_nil lock2
64
+
65
+ assert_equal true, @client.locked?
66
+
67
+ @client.unlock(lock1)
68
+
69
+ assert_equal false, @client.locked?
70
+
71
+ assert_equal 1, @client.locks.size
72
+
73
+ @client.unlock(lock2)
74
+
75
+ assert_equal false, @client.locked?
76
+ assert_equal 0, @client.locks.size
77
+ end
78
+
79
+ def test_block_single_resource_locking
80
+ locked = false
81
+
82
+ @client.lock { locked = true }
83
+
84
+ assert_equal true, locked
85
+ end
86
+
87
+ def test_block_unlocks_on_exception
88
+ assert_raises(RuntimeError) do
89
+ @client.lock{ fail "Test" }
90
+ end
91
+
92
+ assert_equal false, @client.locked?
93
+ end
94
+
95
+ def test_readme_example
96
+ output = Queue.new
97
+ @client = client(resources: 2)
98
+ threads = []
99
+
100
+ threads << Thread.new { @client.lock { output << "One"; sleep 0.5 } }
101
+ threads << Thread.new { @client.lock { output << "Two"; sleep 0.5 } }
102
+ sleep 0.1
103
+ threads << Thread.new { @client.lock { output << "Three" } }
104
+
105
+ threads.each(&:join)
106
+
107
+ ret = []
108
+
109
+ ret << (output.size > 0 ? output.pop : nil)
110
+ ret << (output.size > 0 ? output.pop : nil)
111
+
112
+ ret.sort!
113
+
114
+ assert_equal 0, output.size
115
+ assert_equal %w(One Two), ret
116
+ assert_equal false, @client.locked?
117
+ end
118
+
119
+ def test_block_multiple_resource_locking
120
+ success_counter = Queue.new
121
+ failure_counter = Queue.new
122
+
123
+ @client = client(acquisition_timeout: 0.9, resources: 50)
124
+
125
+ 100.times.map do |i|
126
+ Thread.new do
127
+ success = @client.lock do
128
+ sleep(3)
129
+ success_counter << i
130
+ end
131
+
132
+ failure_counter << i unless success
133
+ end
134
+ end.each(&:join)
135
+
136
+ assert_equal 50, success_counter.size
137
+ assert_equal 50, failure_counter.size
138
+ assert_equal false, @client.locked?
139
+ end
140
+
141
+ def test_block_multiple_resource_locking_longer_timeout
142
+ success_counter = Queue.new
143
+ failure_counter = Queue.new
144
+
145
+ @client = client(acquisition_timeout: 3, resources: 50)
146
+
147
+ 100.times.map do |i|
148
+ Thread.new do
149
+ success = @client.lock do
150
+ sleep(0.5)
151
+ success_counter << i
152
+ end
153
+
154
+ failure_counter << i unless success
155
+ end
156
+ end.each(&:join)
157
+
158
+ assert_equal 100, success_counter.size
159
+ assert_equal 0, failure_counter.size
160
+ assert_equal false, @client.locked?
161
+ end
162
+
163
+ def test_unstale_lock_acquisition
164
+ success_counter = Queue.new
165
+ failure_counter = Queue.new
166
+
167
+ @client = client(stale_lock_expiration: 0.5)
168
+
169
+ t1 = Thread.new { @client.lock { sleep 0.6; success_counter << 1 } }
170
+ sleep 0.3
171
+ t2 = Thread.new do
172
+ locked = @client.lock { success_counter << 1 }
173
+ failure_counter << 1 unless locked
174
+ end
175
+
176
+ [t1, t2].each(&:join)
177
+
178
+ assert_equal 1, success_counter.size
179
+ assert_equal 1, failure_counter.size
180
+ assert_equal false, @client.locked?
181
+ end
182
+
183
+ def test_stale_lock_acquisition
184
+ success_counter = Queue.new
185
+ failure_counter = Queue.new
186
+
187
+ @client = client(stale_lock_expiration: 0.5)
188
+
189
+ t1 = Thread.new { @client.lock { sleep 0.6; success_counter << 1 } }
190
+ sleep 0.55
191
+ t2 = Thread.new do
192
+ locked = @client.lock { success_counter << 1 }
193
+ failure_counter << 1 unless locked
194
+ end
195
+
196
+ [t1, t2].each(&:join)
197
+
198
+ assert_equal 2, success_counter.size
199
+ assert_equal 0, failure_counter.size
200
+ assert_equal false, @client.locked?
201
+ end
202
+
203
+ def test_refresh
204
+ @client = client(stale_lock_expiration: 0.5)
205
+
206
+ lock1 = @client.lock
207
+
208
+ assert_equal true, @client.locked?
209
+
210
+ @client.refresh(lock1)
211
+
212
+ assert_equal true, @client.locked?
213
+
214
+ sleep 0.55
215
+
216
+ assert_equal false, @client.locked?
217
+
218
+ lock2 = @client.lock
219
+
220
+ @client.refresh(lock1)
221
+
222
+ assert_equal true, @client.locked?
223
+
224
+ @client.unlock(lock1)
225
+
226
+ # edge case with refresh lock in the middle
227
+ assert_equal true, @client.locked?
228
+
229
+ @client.clear
230
+
231
+ assert_equal false, @client.locked?
232
+
233
+ @client.refresh(lock2)
234
+
235
+ assert_equal true, @client.locked?
236
+
237
+ @client.unlock(lock2)
238
+
239
+ # now finally unlocked
240
+ assert_equal false, @client.locked?
241
+ end
242
+
243
+ def test_block_refresh
244
+ success_counter = Queue.new
245
+ failure_counter = Queue.new
246
+
247
+ @client = client(stale_lock_expiration: 0.5)
248
+
249
+ t1 = Thread.new do
250
+ @client.lock do |token|
251
+ sleep 0.6
252
+ @client.refresh(token)
253
+ sleep 1
254
+ success_counter << 1
255
+ end
256
+ end
257
+
258
+ t2 = Thread.new do
259
+ sleep 0.8
260
+ locked = @client.lock { success_counter << 1 }
261
+ failure_counter << 1 unless locked
262
+ end
263
+
264
+ [t1, t2].each(&:join)
265
+
266
+ assert_equal 1, success_counter.size
267
+ assert_equal 1, failure_counter.size
268
+ assert_equal false, @client.locked?
269
+ end
270
+
271
+ def test_refresh_multi
272
+ success_counter = Queue.new
273
+ failure_counter = Queue.new
274
+
275
+ @client = client(stale_lock_expiration: 0.5, resources: 2)
276
+
277
+ t1 = Thread.new do
278
+ @client.lock do |token|
279
+ sleep 0.4
280
+ @client.refresh(token)
281
+ success_counter << 1
282
+ sleep 0.5
283
+ end
284
+ end
285
+
286
+ t2 = Thread.new do
287
+ sleep 0.55
288
+ locked = @client.lock do
289
+ success_counter << 1
290
+ sleep 0.5
291
+ end
292
+
293
+ failure_counter << 1 unless locked
294
+ end
295
+
296
+ t3 = Thread.new do
297
+ sleep 0.75
298
+ locked = @client.lock { success_counter << 1 }
299
+ failure_counter << 1 unless locked
300
+ end
301
+
302
+ [t1, t2, t3].each(&:join)
303
+
304
+ assert_equal 2, success_counter.size
305
+ assert_equal 1, failure_counter.size
306
+ assert_equal false, @client.locked?
307
+ end
308
+
309
+ def test_increment_reused_client
310
+ i = 0
311
+
312
+ threads = 2.times.map do
313
+ Thread.new do
314
+ @client.lock { i += 1 }
315
+ end
316
+ end
317
+
318
+ threads.each(&:join)
319
+
320
+ assert_equal 2, i
321
+ assert_equal false, @client.locked?
322
+ end
323
+
324
+ def test_increment_new_client
325
+ i = 0
326
+
327
+ threads = 2.times.map do
328
+ Thread.new do
329
+ # note this is the method that generates a *new* client
330
+ client.lock { i += 1 }
331
+ end
332
+ end
333
+
334
+ threads.each(&:join)
335
+
336
+ assert_equal 2, i
337
+ assert_equal false, @client.locked?
338
+ end
339
+ end
340
+
341
+ class TestBaseClient < Minitest::Test
342
+ def setup
343
+ @client = Suo::Client::Base.new(TEST_KEY, client: {})
344
+ end
345
+
346
+ def test_not_implemented
347
+ assert_raises(NotImplementedError) do
348
+ @client.send(:get)
349
+ end
350
+
351
+ assert_raises(NotImplementedError) do
352
+ @client.send(:set, "", "")
353
+ end
354
+
355
+ assert_raises(NotImplementedError) do
356
+ @client.send(:initial_set)
357
+ end
358
+
359
+ assert_raises(NotImplementedError) do
360
+ @client.send(:clear)
361
+ end
362
+ end
363
+ end
364
+
365
+ class TestMemcachedClient < Minitest::Test
366
+ include ClientTests
367
+
368
+ def setup
369
+ @dalli = Dalli::Client.new("127.0.0.1:11211")
370
+ @client = Suo::Client::Memcached.new(TEST_KEY)
371
+ teardown
372
+ end
373
+
374
+ def teardown
375
+ @dalli.delete(TEST_KEY)
376
+ end
377
+ end
378
+
379
+ class TestRedisClient < Minitest::Test
380
+ include ClientTests
381
+
382
+ def setup
383
+ @redis = Redis.new
384
+ @client = Suo::Client::Redis.new(TEST_KEY)
385
+ teardown
386
+ end
387
+
388
+ def teardown
389
+ @redis.del(TEST_KEY)
390
+ end
391
+ end
392
+
393
+ class TestLibrary < Minitest::Test
394
+ def test_that_it_has_a_version_number
395
+ refute_nil ::Suo::VERSION
396
+ end
397
+ end
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
+
3
+ if ENV["CODECLIMATE_REPO_TOKEN"]
4
+ require "codeclimate-test-reporter"
5
+ CodeClimate::TestReporter.start
6
+ end
7
+
8
+ require "suo"
9
+ require "thread"
10
+ require "minitest/autorun"
11
+ require "minitest/benchmark"
12
+
13
+ ENV["SUO_TEST"] = "true"
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: suo-dependless
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.2
5
+ platform: ruby
6
+ authors:
7
+ - Nick Elser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-05-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: msgpack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.30.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.30.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 5.5.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 5.5.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: codeclimate-test-reporter
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.4.7
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.4.7
97
+ description: Distributed locks (mutexes & semaphores) using Memcached or Redis.
98
+ email:
99
+ - nick.elser@gmail.com
100
+ executables:
101
+ - console
102
+ - setup
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".rubocop.yml"
108
+ - ".travis.yml"
109
+ - CHANGELOG.md
110
+ - Gemfile
111
+ - LICENSE.txt
112
+ - README.md
113
+ - Rakefile
114
+ - bin/console
115
+ - bin/setup
116
+ - lib/suo.rb
117
+ - lib/suo/client/base.rb
118
+ - lib/suo/client/memcached.rb
119
+ - lib/suo/client/redis.rb
120
+ - lib/suo/errors.rb
121
+ - lib/suo/version.rb
122
+ - suo-dependless.gemspec
123
+ - test/client_test.rb
124
+ - test/test_helper.rb
125
+ homepage: https://github.com/bernstein/suo
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '2.0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 2.6.9
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: Distributed locks (mutexes & semaphores) using Memcached or Redis.
149
+ test_files:
150
+ - test/client_test.rb
151
+ - test/test_helper.rb