slock 0.0.1
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 +7 -0
- data/README.md +51 -0
- data/lib/slock/errors.rb +8 -0
- data/lib/slock/semaphore/health.rb +59 -0
- data/lib/slock/semaphore/lock.rb +145 -0
- data/lib/slock/semaphore/singleton.rb +34 -0
- data/lib/slock/semaphore.rb +103 -0
- data/lib/slock/version.rb +3 -0
- data/lib/slock.rb +7 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d2858fe738070faddd813106acfe33aec8d486f60017666fcd077f9c54d2f242
|
4
|
+
data.tar.gz: 8b016b82dcbf3da5624479df233c72fdbca117ee56115aa0628c0b102ba8441e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f55eec7a7a46345958bb85bfcc4282b0b5cb24156131903eacf4d7298c9506e5ffda5e92d9e02e07f5824b0a7832ea662ff8597467fe78cd0993a54f89bb3e22
|
7
|
+
data.tar.gz: 14e8361b3a4ea823406192f19373eb9303bfab95f35baf55e534d4d9f2dd1ff18878708aa40b3aefcb1fbcdb075ee5fcf22fcc909f00a532fd8375f0adcb88b9
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Slock
|
2
|
+
[](https://travis-ci.org/zoer/slock)
|
3
|
+
[](https://codeclimate.com/github/zoer/slock)
|
4
|
+
[](https://www.versioneye.com/ruby/slock)
|
5
|
+
[](http://inch-ci.org/github/zoer/slock)
|
6
|
+
[](http://badge.fury.io/rb/slock)
|
7
|
+
|
8
|
+
Slock implements Semaphore via Redis.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'slock'
|
16
|
+
```
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
### Singleton Class
|
21
|
+
```ruby
|
22
|
+
class MySemaphore
|
23
|
+
include Slock::Semaphore::Singleton
|
24
|
+
|
25
|
+
SIZE = 2 # max count of simultaneous locks
|
26
|
+
LIFETIME = 600 # max time that lock lives after acquring (in seconds)
|
27
|
+
TIMEOUT = 900 # max time that semaphore waits for lock to acquire before raising an error
|
28
|
+
|
29
|
+
def semaphore_opts
|
30
|
+
{
|
31
|
+
redis: Redis.new(url: ENV['REDIS_URL']),
|
32
|
+
size: SIZE,
|
33
|
+
lifetime: LIFETIME,
|
34
|
+
timeout: TIMEOUT
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
MySemaphore.acquire { do_something }
|
40
|
+
```
|
41
|
+
|
42
|
+
|
43
|
+
### Simple
|
44
|
+
```ruby
|
45
|
+
sempahore = Slock::Semaphore.new 'uniq_semaphore_key',
|
46
|
+
redis: Redis.new(ENV['REDIS_URL']),
|
47
|
+
lifetime: 600,
|
48
|
+
timeout: 900
|
49
|
+
|
50
|
+
semaphroe.acquire { do_something }
|
51
|
+
```
|
data/lib/slock/errors.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Slock
|
2
|
+
class Semaphore
|
3
|
+
class Health
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
# @return [Integer]
|
7
|
+
HEALTHCHECK_TIMEOUT = 10
|
8
|
+
|
9
|
+
def_delegators :semaphore, :key, :size, :tokens_path, :client
|
10
|
+
|
11
|
+
# @return [Slock::Semaphore]
|
12
|
+
attr_reader :semaphore
|
13
|
+
|
14
|
+
#
|
15
|
+
# @param [Slock::Semaphore] semaphore
|
16
|
+
#
|
17
|
+
def initialize(semaphore)
|
18
|
+
@semaphore = semaphore
|
19
|
+
end
|
20
|
+
|
21
|
+
def check!
|
22
|
+
check if lock
|
23
|
+
end
|
24
|
+
|
25
|
+
def check
|
26
|
+
client.watch(tokens_path) do
|
27
|
+
missing_tokens.shuffle.each do |token|
|
28
|
+
lock = Semaphore::Lock.new(self, token)
|
29
|
+
lock.fix! unless lock.live?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
ensure
|
33
|
+
client.del(healthlock_path)
|
34
|
+
client.unwatch
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# @return [Array<String>]
|
39
|
+
#
|
40
|
+
def missing_tokens
|
41
|
+
size.times.map(&:to_s) - client.lrange(tokens_path, 0, -1)
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# @return [Boolean]
|
46
|
+
#
|
47
|
+
def lock
|
48
|
+
!!client.set(healthlock_path, 1, nx: true, ex: HEALTHCHECK_TIMEOUT)
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# @return [String]
|
53
|
+
#
|
54
|
+
def healthlock_path
|
55
|
+
key(:healthlock)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Slock
|
2
|
+
class Semaphore
|
3
|
+
class Lock
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
attr_reader :id
|
7
|
+
attr_reader :semaphore, :token, :lifetime
|
8
|
+
|
9
|
+
def_delegators :semaphore, :key, :client, :tokens_path
|
10
|
+
|
11
|
+
#
|
12
|
+
# @param [Slock::Semaphore] semaphore
|
13
|
+
# @param [String] token
|
14
|
+
# @param [Hash] opts
|
15
|
+
# @option opts [Integer] :timeout
|
16
|
+
# @option opts [Integer] :lifetime
|
17
|
+
#
|
18
|
+
def initialize(semaphore, token, opts = {})
|
19
|
+
@semaphore = semaphore
|
20
|
+
@token = token
|
21
|
+
@id = SecureRandom.uuid
|
22
|
+
@lifetime = opts.delete(:lifetime) || (10 * 60)
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# @return [Boolean]
|
27
|
+
#
|
28
|
+
def locked?
|
29
|
+
client.get(id_path) == id
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.lock(semaphore, opts = {})
|
33
|
+
_, token = semaphore.client.blpop(semaphore.tokens_path, timeout: 0)
|
34
|
+
raise Errors::TokenOutOffSemaphoreSizeError if token.to_i >= semaphore.size
|
35
|
+
|
36
|
+
new(semaphore, token, opts).tap(&:lock)
|
37
|
+
rescue Redis::TimeoutError, Errors::WrongLockOwnerError,
|
38
|
+
Errors::TokenOutOffSemaphoreSizeError
|
39
|
+
|
40
|
+
retry
|
41
|
+
end
|
42
|
+
|
43
|
+
def lock
|
44
|
+
change do
|
45
|
+
check_owner!(true)
|
46
|
+
renew
|
47
|
+
own
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def release
|
52
|
+
change { owned? ? _release : false }
|
53
|
+
end
|
54
|
+
|
55
|
+
def _release
|
56
|
+
client.del(id_path)
|
57
|
+
client.del(live_path)
|
58
|
+
client.rpush(tokens_path, token)
|
59
|
+
end
|
60
|
+
|
61
|
+
def renew
|
62
|
+
client.set(live_path, id, ex: lifetime)
|
63
|
+
end
|
64
|
+
|
65
|
+
def own
|
66
|
+
client.set(id_path, id)
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# @param [String] allow_empty
|
71
|
+
#
|
72
|
+
# @return [Boolean]
|
73
|
+
#
|
74
|
+
def owned?(allow_empty = false)
|
75
|
+
owner = client.get(id_path)
|
76
|
+
return true if owner.nil? && allow_empty
|
77
|
+
|
78
|
+
owner == id
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# @param [Boolean] allow_empty
|
83
|
+
#
|
84
|
+
# @raise [Semaphore::Errors::WrongLockOwnerError]
|
85
|
+
#
|
86
|
+
def check_owner!(allow_empty = false)
|
87
|
+
return if owned?(allow_empty)
|
88
|
+
|
89
|
+
raise Errors::WrongLockOwnerError, token
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# @return [Boolean]
|
94
|
+
#
|
95
|
+
def live?
|
96
|
+
client.exists?(live_path)
|
97
|
+
end
|
98
|
+
|
99
|
+
def fix!
|
100
|
+
change do
|
101
|
+
client.multi do |tx|
|
102
|
+
tx.del(id_path)
|
103
|
+
tx.del(live_path)
|
104
|
+
tx.lpush(tokens_path, token)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def change
|
110
|
+
return yield if @changable
|
111
|
+
|
112
|
+
begin
|
113
|
+
sleep(0.1) until client.set(lock_path, 1, nx: true, ex: 3)
|
114
|
+
@changable = true
|
115
|
+
|
116
|
+
yield
|
117
|
+
ensure
|
118
|
+
@changable = false
|
119
|
+
client.del(lock_path)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# @return [String]
|
125
|
+
#
|
126
|
+
def lock_path
|
127
|
+
key(:tokens, token, :lock)
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# @return [String]
|
132
|
+
#
|
133
|
+
def id_path
|
134
|
+
key(:tokens, token, :id)
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# @return [String]
|
139
|
+
#
|
140
|
+
def live_path
|
141
|
+
key(:tokens, token, :live)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Slock
|
2
|
+
class Semaphore
|
3
|
+
module Singleton
|
4
|
+
module ClassMethods
|
5
|
+
def acquire(*args, &block)
|
6
|
+
instance.semaphore.acquire(*args, &block)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.include ::Singleton
|
12
|
+
base.extend ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# @return [Slock::Semaphore]
|
17
|
+
#
|
18
|
+
def semaphore
|
19
|
+
@semaphore ||= begin
|
20
|
+
opts = semaphore_opts.dup
|
21
|
+
key = opts.delete(:key) || "semaphore:#{self.class.name.underscore}"
|
22
|
+
Slock::Semaphore.new(key, opts)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# @return [Hash{Symbol => Object}]
|
28
|
+
#
|
29
|
+
def semaphore_opts
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Slock
|
2
|
+
class Semaphore
|
3
|
+
autoload :Lock, 'slock/semaphore/lock'
|
4
|
+
autoload :Health, 'slock/semaphore/health'
|
5
|
+
autoload :Singleton, 'slock/semaphore/singleton'
|
6
|
+
|
7
|
+
# @return [Redis]
|
8
|
+
attr_reader :client
|
9
|
+
# @return [Integer]
|
10
|
+
attr_reader :size
|
11
|
+
|
12
|
+
#
|
13
|
+
# @param [String, Symbol] key
|
14
|
+
# @param [Hash] opts
|
15
|
+
# @option opts [Integer] :size
|
16
|
+
# @option opts [Integer] :timeout
|
17
|
+
# @option opts [Integer] :lifetime
|
18
|
+
#
|
19
|
+
def initialize(key, opts = {})
|
20
|
+
@key = key
|
21
|
+
@client = opts.delete(:redis) || Redis.new
|
22
|
+
@size = opts.delete(:size) || 1
|
23
|
+
@opts = opts
|
24
|
+
|
25
|
+
initialize_semaphore
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize_semaphore
|
29
|
+
return if client.getset(init_path, '1') == '1'
|
30
|
+
|
31
|
+
client.del(tokens_path) if client.exists?(tokens_path)
|
32
|
+
size.times { |n| client.rpush(tokens_path, n) }
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# @param [Hash] opts
|
37
|
+
# @option opts [Integer] :timeout
|
38
|
+
# @option opts [Integer] :lifetime
|
39
|
+
#
|
40
|
+
# @return [Slock::Semaphore::Lock] returns lock's handler when no block is provided
|
41
|
+
# @return [Object] returns yielded block resulst when block is provided
|
42
|
+
#
|
43
|
+
def acquire(opts = {})
|
44
|
+
check_health!
|
45
|
+
opts = @opts.merge(opts)
|
46
|
+
lock = opts[:timeout] ? acquire_timeout(opts) : acquire_notimeout(opts)
|
47
|
+
return lock unless block_given?
|
48
|
+
|
49
|
+
yield(lock) if lock.locked?
|
50
|
+
ensure
|
51
|
+
lock&.release if block_given?
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# @param [Hash] opts
|
56
|
+
# @option opts [Integer] :timeout
|
57
|
+
# @option opts [Integer] :lifetime
|
58
|
+
#
|
59
|
+
# @return [Slock::Semaphore::Lock]
|
60
|
+
#
|
61
|
+
def acquire_timeout(opts = {})
|
62
|
+
Timeout.timeout(opts[:timeout]) { acquire_notimeout(opts) }
|
63
|
+
rescue Timeout::Error
|
64
|
+
raise Errors::TimeoutError
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# @param [Hash] opts
|
69
|
+
# @option opts [Integer] :timeout
|
70
|
+
# @option opts [Integer] :lifetime
|
71
|
+
#
|
72
|
+
# @return [Slock::Semaphore::Lock]
|
73
|
+
#
|
74
|
+
def acquire_notimeout(opts = {})
|
75
|
+
Semaphore::Lock.lock(self, opts)
|
76
|
+
end
|
77
|
+
|
78
|
+
def check_health!
|
79
|
+
Semaphore::Health.new(self).check!
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# @param [Arra<String, Symbol, nil, Integer>] postfixes
|
84
|
+
#
|
85
|
+
def key(*postfixes)
|
86
|
+
[@key, *postfixes].compact.join(':')
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# @return [String]
|
91
|
+
#
|
92
|
+
def tokens_path
|
93
|
+
key(:tokens)
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# @return [String]
|
98
|
+
#
|
99
|
+
def init_path
|
100
|
+
key(:init)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/slock.rb
ADDED
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: slock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oleg Yashchuk
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-04-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
description: Gem provide Semaphore lock via Redis
|
28
|
+
email: oazoer@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- README.md
|
34
|
+
- lib/slock.rb
|
35
|
+
- lib/slock/errors.rb
|
36
|
+
- lib/slock/semaphore.rb
|
37
|
+
- lib/slock/semaphore/health.rb
|
38
|
+
- lib/slock/semaphore/lock.rb
|
39
|
+
- lib/slock/semaphore/singleton.rb
|
40
|
+
- lib/slock/version.rb
|
41
|
+
homepage: https://github.com/zoer/slock
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
metadata:
|
45
|
+
rubygems_mfa_required: 'true'
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.6.0
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.1.6
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Sempahore Lock
|
65
|
+
test_files: []
|