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 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: []