suo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 200ad3e821080a0c0674af2abd4622de1c5088fb
4
+ data.tar.gz: 81a59e78a7396bd76b4a5d9c1e3cab46b0eac260
5
+ SHA512:
6
+ metadata.gz: f5ee6c325526dbe4ea6f4a68cd60a310c33b563a3b05495de4197093061634086581702ab25973df67410ef28345484a0ddf224a6d580ab863fe7ce732f777eb
7
+ data.tar.gz: 6f308c0243d9af16a65b33e8c35be667c27ce070500264ed236deb128c3bdfb4f9563aa879ece10d22386f4a92c4d56a42f2242003674a9038a585950f3d5839
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,216 @@
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/TrailingComma:
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/SpaceAfterControlKeyword:
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: 8
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
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0
2
+
3
+ - First release
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # 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.
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(connection: "127.0.0.1:11211")
22
+
23
+ # Redis
24
+ suo = Suo::Client::Redis.new(connection: {host: "10.0.1.1"})
25
+
26
+ # Pre-existing client
27
+ suo = Suo::Client::Memcached.new(client: some_dalli_client)
28
+
29
+ suo.lock("some_key") do
30
+ # critical code here
31
+ @puppies.pet!
32
+ end
33
+
34
+ 2.times do
35
+ Thread.new do
36
+ # second argument is the number of resources - so this will run twice
37
+ suo.lock("other_key", 2, timeout: 0.5) { puts "Will run twice!" }
38
+ end
39
+ end
40
+ ```
41
+
42
+ ## TODO
43
+ - better stale key handling (refresh blocks)
44
+ - more race condition tests
45
+ - refactor clients to re-use more code
46
+
47
+ ## History
48
+
49
+ View the [changelog](https://github.com/nickelser/suo/blob/master/CHANGELOG.md)
50
+
51
+ ## Contributing
52
+
53
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
54
+
55
+ - [Report bugs](https://github.com/nickelser/suo/issues)
56
+ - Fix bugs and [submit pull requests](https://github.com/nickelser/suo/pulls)
57
+ - Write, clarify, or fix documentation
58
+ - Suggest or add new features
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.pattern = "test/*_test.rb"
7
+ 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,116 @@
1
+ module Suo
2
+ module Client
3
+ class Base
4
+ DEFAULT_OPTIONS = {
5
+ retry_count: 3,
6
+ retry_delay: 0.01,
7
+ stale_lock_expiration: 3600
8
+ }.freeze
9
+
10
+ def initialize(options = {})
11
+ @options = self.class.merge_defaults(options).merge(_initialized: true)
12
+ end
13
+
14
+ def lock(key, resources = 1, options = {})
15
+ options = self.class.merge_defaults(@options.merge(options))
16
+ token = self.class.lock(key, resources, options)
17
+
18
+ if token
19
+ begin
20
+ yield if block_given?
21
+ ensure
22
+ self.class.unlock(key, token, options)
23
+ end
24
+
25
+ true
26
+ else
27
+ false
28
+ end
29
+ end
30
+
31
+ def locked?(key, resources = 1)
32
+ self.class.locked?(key, resources, @options)
33
+ end
34
+
35
+ class << self
36
+ def lock(key, resources = 1, options = {}) # rubocop:disable Lint/UnusedMethodArgument
37
+ fail NotImplementedError
38
+ end
39
+
40
+ def locked?(key, resources = 1, options = {})
41
+ options = merge_defaults(options)
42
+ client = options[:client]
43
+ locks = deserialize_locks(client.get(key))
44
+
45
+ locks.size >= resources
46
+ end
47
+
48
+ def locks(key, options)
49
+ options = merge_defaults(options)
50
+ client = options[:client]
51
+ locks = deserialize_locks(client.get(key))
52
+
53
+ locks.size
54
+ end
55
+
56
+ def refresh(key, acquisition_token, options = {}) # rubocop:disable Lint/UnusedMethodArgument
57
+ fail NotImplementedError
58
+ end
59
+
60
+ def unlock(key, acquisition_token, options = {}) # rubocop:disable Lint/UnusedMethodArgument
61
+ fail NotImplementedError
62
+ end
63
+
64
+ def clear(key, options = {}) # rubocop:disable Lint/UnusedMethodArgument
65
+ fail NotImplementedError
66
+ end
67
+
68
+ def merge_defaults(options = {})
69
+ unless options[:_initialized]
70
+ options = self::DEFAULT_OPTIONS.merge(options)
71
+
72
+ fail "Client required" unless options[:client]
73
+ end
74
+
75
+ if options[:retry_timeout]
76
+ options[:retry_count] = (options[:retry_timeout] / options[:retry_delay].to_f).floor
77
+ end
78
+
79
+ options
80
+ end
81
+
82
+ private
83
+
84
+ def serialize_locks(locks)
85
+ locks.map { |time, token| [time.to_f, token].join(":") }.join(",")
86
+ end
87
+
88
+ def deserialize_locks(str)
89
+ str.split(",").map do |s|
90
+ time, token = s.split(":", 2)
91
+ [Time.at(time.to_f), token]
92
+ end
93
+ end
94
+
95
+ def clear_expired_locks(locks, options)
96
+ expired = Time.now - options[:stale_lock_expiration]
97
+ locks.reject { |time, _| time < expired }
98
+ end
99
+
100
+ def add_lock(locks, token)
101
+ locks << [Time.now.to_f, token]
102
+ end
103
+
104
+ def remove_lock(locks, acquisition_token)
105
+ lock = locks.find { |_, token| token == acquisition_token }
106
+ locks.delete(lock)
107
+ end
108
+
109
+ def refresh_lock(locks, acquisition_token)
110
+ remove_lock(locks, acquisition_token)
111
+ add_lock(locks, token)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,7 @@
1
+ module Suo
2
+ module Client
3
+ module Errors
4
+ class FailedToAcquireLock < StandardError; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,137 @@
1
+ module Suo
2
+ module Client
3
+ class Memcached < Base
4
+ def initialize(options = {})
5
+ options[:client] ||= Dalli::Client.new(options[:connection] || ENV["MEMCACHE_SERVERS"] || "127.0.0.1:11211")
6
+ super
7
+ end
8
+
9
+ class << self
10
+ def lock(key, resources = 1, options = {})
11
+ options = merge_defaults(options)
12
+ acquisition_token = nil
13
+ token = SecureRandom.base64(16)
14
+ client = options[:client]
15
+
16
+ begin
17
+ start = Time.now.to_f
18
+
19
+ options[:retry_count].times do |i|
20
+ val, cas = client.get_cas(key)
21
+
22
+ # no key has been set yet; we could simply set it, but would lead to race conditions on the initial setting
23
+ if val.nil?
24
+ client.set(key, "")
25
+ next
26
+ end
27
+
28
+ locks = clear_expired_locks(deserialize_locks(val.to_s), options)
29
+
30
+ if locks.size < resources
31
+ add_lock(locks, token)
32
+
33
+ newval = serialize_locks(locks)
34
+
35
+ if client.set_cas(key, newval, cas)
36
+ acquisition_token = token
37
+ break
38
+ end
39
+ end
40
+
41
+ if options[:retry_timeout]
42
+ now = Time.now.to_f
43
+ break if now - start > options[:retry_timeout]
44
+ end
45
+
46
+ sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
47
+ end
48
+ rescue => _
49
+ raise FailedToAcquireLock
50
+ end
51
+
52
+ acquisition_token
53
+ end
54
+
55
+ def refresh(key, acquisition_token, options = {})
56
+ options = merge_defaults(options)
57
+ client = options[:client]
58
+
59
+ begin
60
+ start = Time.now.to_f
61
+
62
+ options[:retry_count].times do
63
+ val, cas = client.get_cas(key)
64
+
65
+ # much like with initial set - ensure the key is here
66
+ if val.nil?
67
+ client.set(key, "")
68
+ next
69
+ end
70
+
71
+ locks = clear_expired_locks(deserialize_locks(val), options)
72
+
73
+ refresh_lock(locks, acquisition_token)
74
+
75
+ newval = serialize_locks(locks)
76
+
77
+ break if client.set_cas(key, newval, cas)
78
+
79
+ if options[:retry_timeout]
80
+ now = Time.now.to_f
81
+ break if now - start > options[:retry_timeout]
82
+ end
83
+
84
+ sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
85
+ end
86
+ rescue => _
87
+ raise FailedToAcquireLock
88
+ end
89
+ end
90
+
91
+ def unlock(key, acquisition_token, options = {})
92
+ options = merge_defaults(options)
93
+ client = options[:client]
94
+
95
+ return unless acquisition_token
96
+
97
+ begin
98
+ start = Time.now.to_f
99
+
100
+ options[:retry_count].times do
101
+ val, cas = client.get_cas(key)
102
+
103
+ break if val.nil? # lock has expired totally
104
+
105
+ locks = clear_expired_locks(deserialize_locks(val), options)
106
+
107
+ acquisition_lock = remove_lock(locks, acquisition_token)
108
+
109
+ break unless acquisition_lock
110
+
111
+ newval = serialize_locks(locks)
112
+
113
+ break if client.set_cas(key, newval, cas)
114
+
115
+ # another client cleared a token in the interim - try again!
116
+
117
+ if options[:retry_timeout]
118
+ now = Time.now.to_f
119
+ break if now - start > options[:retry_timeout]
120
+ end
121
+
122
+ sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
123
+ end
124
+ rescue => boom # rubocop:disable Lint/HandleExceptions
125
+ # since it's optimistic locking - fine if we are unable to release
126
+ raise boom if ENV["SUO_TEST"]
127
+ end
128
+ end
129
+
130
+ def clear(key, options = {})
131
+ options = merge_defaults(options)
132
+ options[:client].delete(key)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,167 @@
1
+ module Suo
2
+ module Client
3
+ class Redis < Base
4
+ def initialize(options = {})
5
+ options[:client] ||= ::Redis.new(options[:connection] || {})
6
+ super
7
+ end
8
+
9
+ class << self
10
+ def lock(key, resources = 1, options = {})
11
+ options = merge_defaults(options)
12
+ acquisition_token = nil
13
+ token = SecureRandom.base64(16)
14
+ client = options[:client]
15
+
16
+ begin
17
+ start = Time.now.to_f
18
+
19
+ options[:retry_count].times do
20
+ client.watch(key) do
21
+ begin
22
+ val = client.get(key)
23
+
24
+ locks = clear_expired_locks(deserialize_locks(val.to_s), options)
25
+
26
+ if locks.size < resources
27
+ add_lock(locks, token)
28
+
29
+ newval = serialize_locks(locks)
30
+
31
+ ret = client.multi do |multi|
32
+ multi.set(key, newval)
33
+ end
34
+
35
+ acquisition_token = token if ret[0] == "OK"
36
+ end
37
+ ensure
38
+ client.unwatch
39
+ end
40
+ end
41
+
42
+ break if acquisition_token
43
+
44
+ if options[:retry_timeout]
45
+ now = Time.now.to_f
46
+ break if now - start > options[:retry_timeout]
47
+ end
48
+
49
+ sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
50
+ end
51
+ rescue => boom
52
+ raise boom
53
+ raise Suo::Client::FailedToAcquireLock
54
+ end
55
+
56
+ acquisition_token
57
+ end
58
+
59
+ def refresh(key, acquisition_token, options = {})
60
+ options = merge_defaults(options)
61
+ client = options[:client]
62
+ refreshed = false
63
+
64
+ begin
65
+ start = Time.now.to_f
66
+
67
+ options[:retry_count].times do
68
+ client.watch(key) do
69
+ begin
70
+ val = client.get(key)
71
+
72
+ locks = clear_expired_locks(deserialize_locks(val), options)
73
+
74
+ refresh_lock(locks, acquisition_token)
75
+
76
+ newval = serialize_locks(locks)
77
+
78
+ ret = client.multi do |multi|
79
+ multi.set(key, newval)
80
+ end
81
+
82
+ refreshed = ret[0] == "OK"
83
+ ensure
84
+ client.unwatch
85
+ end
86
+ end
87
+
88
+ break if refreshed
89
+
90
+ if options[:retry_timeout]
91
+ now = Time.now.to_f
92
+ break if now - start > options[:retry_timeout]
93
+ end
94
+
95
+ sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
96
+ end
97
+ rescue => _
98
+ raise Suo::Client::FailedToAcquireLock
99
+ end
100
+ end
101
+
102
+ def unlock(key, acquisition_token, options = {})
103
+ options = merge_defaults(options)
104
+ client = options[:client]
105
+
106
+ return unless acquisition_token
107
+
108
+ begin
109
+ start = Time.now.to_f
110
+
111
+ options[:retry_count].times do
112
+ cleared = false
113
+
114
+ client.watch(key) do
115
+ begin
116
+ val = client.get(key)
117
+
118
+ if val.nil?
119
+ cleared = true
120
+ break
121
+ end
122
+
123
+ locks = clear_expired_locks(deserialize_locks(val), options)
124
+
125
+ acquisition_lock = remove_lock(locks, acquisition_token)
126
+
127
+ unless acquisition_lock
128
+ # token was already cleared
129
+ cleared = true
130
+ break
131
+ end
132
+
133
+ newval = serialize_locks(locks)
134
+
135
+ ret = client.multi do |multi|
136
+ multi.set(key, newval)
137
+ end
138
+
139
+ cleared = ret[0] == "OK"
140
+ ensure
141
+ client.unwatch
142
+ end
143
+ end
144
+
145
+ break if cleared
146
+
147
+ if options[:retry_timeout]
148
+ now = Time.now.to_f
149
+ break if now - start > options[:retry_timeout]
150
+ end
151
+
152
+ sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
153
+ end
154
+ rescue => boom # rubocop:disable Lint/HandleExceptions
155
+ # since it's optimistic locking - fine if we are unable to release
156
+ raise boom if ENV["SUO_TEST"]
157
+ end
158
+ end
159
+
160
+ def clear(key, options = {})
161
+ options = merge_defaults(options)
162
+ options[:client].del(key)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,12 @@
1
+ require "securerandom"
2
+ require "monitor"
3
+
4
+ require "dalli"
5
+ require "dalli/cas/client"
6
+
7
+ require "redis"
8
+
9
+ require "suo/client/errors"
10
+ require "suo/client/base"
11
+ require "suo/client/memcached"
12
+ require "suo/client/redis"
@@ -0,0 +1,3 @@
1
+ module Suo
2
+ VERSION = "0.1.0"
3
+ end
data/lib/suo.rb ADDED
@@ -0,0 +1,2 @@
1
+ require "suo/version"
2
+ require "suo/clients"
data/suo.gemspec ADDED
@@ -0,0 +1,28 @@
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"
8
+ spec.version = Suo::VERSION
9
+ spec.authors = ["Nick Elser"]
10
+ spec.email = ["nick.elser@gmail.com"]
11
+
12
+ spec.summary = %q(Distributed semaphores using Memcached or Redis.)
13
+ # spec.description = %q{TODO: Long description}
14
+ spec.homepage = "https://github.com/nickelser/suo"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "bin"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "dalli"
22
+ spec.add_dependency "redis"
23
+ spec.add_dependency "msgpack"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.5"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rubocop", "~> 0.30.0"
28
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: suo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Elser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dalli
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: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: msgpack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.30.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.30.0
97
+ description:
98
+ email:
99
+ - nick.elser@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rubocop.yml"
106
+ - ".travis.yml"
107
+ - CHANGELOG.md
108
+ - Gemfile
109
+ - README.md
110
+ - Rakefile
111
+ - bin/console
112
+ - bin/setup
113
+ - lib/suo.rb
114
+ - lib/suo/client/base.rb
115
+ - lib/suo/client/errors.rb
116
+ - lib/suo/client/memcached.rb
117
+ - lib/suo/client/redis.rb
118
+ - lib/suo/clients.rb
119
+ - lib/suo/version.rb
120
+ - suo.gemspec
121
+ homepage: https://github.com/nickelser/suo
122
+ licenses: []
123
+ metadata: {}
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirements: []
139
+ rubyforge_project:
140
+ rubygems_version: 2.4.5
141
+ signing_key:
142
+ specification_version: 4
143
+ summary: Distributed semaphores using Memcached or Redis.
144
+ test_files: []