slock 0.0.1

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
+ 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
+ [![Build Status](https://travis-ci.org/zoer/slock.svg)](https://travis-ci.org/zoer/slock)
3
+ [![Code Climate](https://codeclimate.com/github/zoer/slock/badges/gpa.svg)](https://codeclimate.com/github/zoer/slock)
4
+ [![Version Eye](https://www.versioneye.com/ruby/slock/badge.png)](https://www.versioneye.com/ruby/slock)
5
+ [![Inline docs](http://inch-ci.org/github/zoer/slock.png)](http://inch-ci.org/github/zoer/slock)
6
+ [![Gem Version](https://badge.fury.io/rb/slock.svg)](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
+ ```
@@ -0,0 +1,8 @@
1
+ module Slock
2
+ module Errors
3
+ class BaseError < StandardError; end
4
+ class TimeoutError < BaseError; end
5
+ class WrongLockOwnerError < BaseError; end
6
+ class TokenOutOffSemaphoreSizeError < BaseError; end
7
+ end
8
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Slock
2
+ VERSION = '0.0.1'.freeze
3
+ end
data/lib/slock.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'securerandom'
2
+ require 'forwardable'
3
+
4
+ module Slock
5
+ autoload :Semaphore, 'slock/semaphore'
6
+ autoload :Errors, 'slock/errors'
7
+ end
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: []