simple_mutex 1.0.0
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/.github/worklows/ci.yml +33 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +113 -0
- data/LICENSE +21 -0
- data/README.md +364 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/simple_mutex/base_cleaner.rb +97 -0
- data/lib/simple_mutex/helper.rb +83 -0
- data/lib/simple_mutex/mutex.rb +177 -0
- data/lib/simple_mutex/sidekiq_support/batch.rb +78 -0
- data/lib/simple_mutex/sidekiq_support/batch_callbacks.rb +15 -0
- data/lib/simple_mutex/sidekiq_support/batch_cleaner.rb +32 -0
- data/lib/simple_mutex/sidekiq_support/job_cleaner.rb +27 -0
- data/lib/simple_mutex/sidekiq_support/job_mixin.rb +65 -0
- data/lib/simple_mutex/sidekiq_support/job_wrapper.rb +52 -0
- data/lib/simple_mutex/version.rb +5 -0
- data/lib/simple_mutex.rb +47 -0
- data/simple_mutex.gemspec +40 -0
- metadata +220 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 386f8c08c9a13c4402812145555a49a710fe28bf016455491bb779aa58cd7a90
|
|
4
|
+
data.tar.gz: 1a7e87acc3d54d3a90fd31a515c92c861823e10c054511c1466303fd5afc5e1f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c94e26f6ac88dd2d3be0b7a788051ef0bf8cda52acd41173d18dabb53521c9bb1feb639ba76deac1feccae666a0e3c359563569a5c5fd81175980d87bd324eac
|
|
7
|
+
data.tar.gz: bc0f67a008af4a252e118673584c691de022ad5568b83a088654e9070ad45a20c3fec5c77b2b2abd1f1208ca4831b36a4e276a20fbe49d3854886eda48dfd81f
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
|
|
9
|
+
# We want to run on external PRs, but not on our own internal PRs as they'll be run on push event
|
|
10
|
+
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'umbrellio/table_sync'
|
|
11
|
+
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
ruby: ["2.6", "2.7", "3.0"]
|
|
16
|
+
|
|
17
|
+
name: ${{ matrix.ruby }}
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v2
|
|
21
|
+
|
|
22
|
+
- uses: ruby/setup-ruby@v1
|
|
23
|
+
with:
|
|
24
|
+
ruby-version: ${{ matrix.ruby }}
|
|
25
|
+
bundler-cache: true
|
|
26
|
+
|
|
27
|
+
- run: bundle exec rake bundle:audit
|
|
28
|
+
- run: bundle exec rubocop
|
|
29
|
+
- run: bundle exec rspec
|
|
30
|
+
|
|
31
|
+
- uses: coverallsapp/github-action@v1.1.2
|
|
32
|
+
with:
|
|
33
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
simple_mutex (1.0.0)
|
|
5
|
+
redis
|
|
6
|
+
redis-namespace
|
|
7
|
+
sidekiq
|
|
8
|
+
|
|
9
|
+
GEM
|
|
10
|
+
remote: https://rubygems.org/
|
|
11
|
+
specs:
|
|
12
|
+
activesupport (6.1.4.1)
|
|
13
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
14
|
+
i18n (>= 1.6, < 2)
|
|
15
|
+
minitest (>= 5.1)
|
|
16
|
+
tzinfo (~> 2.0)
|
|
17
|
+
zeitwerk (~> 2.3)
|
|
18
|
+
ast (2.4.2)
|
|
19
|
+
bundler-audit (0.9.0.1)
|
|
20
|
+
bundler (>= 1.2.0, < 3)
|
|
21
|
+
thor (~> 1.0)
|
|
22
|
+
concurrent-ruby (1.1.9)
|
|
23
|
+
connection_pool (2.2.5)
|
|
24
|
+
diff-lcs (1.4.4)
|
|
25
|
+
i18n (1.8.10)
|
|
26
|
+
concurrent-ruby (~> 1.0)
|
|
27
|
+
minitest (5.14.4)
|
|
28
|
+
mock_redis (0.29.0)
|
|
29
|
+
ruby2_keywords
|
|
30
|
+
parallel (1.21.0)
|
|
31
|
+
parser (3.0.2.0)
|
|
32
|
+
ast (~> 2.4.1)
|
|
33
|
+
rack (2.2.3)
|
|
34
|
+
rainbow (3.0.0)
|
|
35
|
+
redis (4.5.1)
|
|
36
|
+
redis-namespace (1.8.1)
|
|
37
|
+
redis (>= 3.0.4)
|
|
38
|
+
regexp_parser (2.1.1)
|
|
39
|
+
rexml (3.2.5)
|
|
40
|
+
rspec (3.10.0)
|
|
41
|
+
rspec-core (~> 3.10.0)
|
|
42
|
+
rspec-expectations (~> 3.10.0)
|
|
43
|
+
rspec-mocks (~> 3.10.0)
|
|
44
|
+
rspec-core (3.10.1)
|
|
45
|
+
rspec-support (~> 3.10.0)
|
|
46
|
+
rspec-expectations (3.10.1)
|
|
47
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
48
|
+
rspec-support (~> 3.10.0)
|
|
49
|
+
rspec-mocks (3.10.2)
|
|
50
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
51
|
+
rspec-support (~> 3.10.0)
|
|
52
|
+
rspec-support (3.10.2)
|
|
53
|
+
rubocop (1.17.0)
|
|
54
|
+
parallel (~> 1.10)
|
|
55
|
+
parser (>= 3.0.0.0)
|
|
56
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
57
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
58
|
+
rexml
|
|
59
|
+
rubocop-ast (>= 1.7.0, < 2.0)
|
|
60
|
+
ruby-progressbar (~> 1.7)
|
|
61
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
|
62
|
+
rubocop-ast (1.12.0)
|
|
63
|
+
parser (>= 3.0.1.1)
|
|
64
|
+
rubocop-config-umbrellio (1.17.0.53)
|
|
65
|
+
rubocop (= 1.17.0)
|
|
66
|
+
rubocop-performance (= 1.10.0)
|
|
67
|
+
rubocop-rails (= 2.9.1)
|
|
68
|
+
rubocop-rake (= 0.5.1)
|
|
69
|
+
rubocop-rspec (= 2.2.0)
|
|
70
|
+
rubocop-sequel (= 0.2.0)
|
|
71
|
+
rubocop-performance (1.10.0)
|
|
72
|
+
rubocop (>= 0.90.0, < 2.0)
|
|
73
|
+
rubocop-ast (>= 0.4.0)
|
|
74
|
+
rubocop-rails (2.9.1)
|
|
75
|
+
activesupport (>= 4.2.0)
|
|
76
|
+
rack (>= 1.1)
|
|
77
|
+
rubocop (>= 0.90.0, < 2.0)
|
|
78
|
+
rubocop-rake (0.5.1)
|
|
79
|
+
rubocop
|
|
80
|
+
rubocop-rspec (2.2.0)
|
|
81
|
+
rubocop (~> 1.0)
|
|
82
|
+
rubocop-ast (>= 1.1.0)
|
|
83
|
+
rubocop-sequel (0.2.0)
|
|
84
|
+
rubocop (~> 1.0)
|
|
85
|
+
ruby-progressbar (1.11.0)
|
|
86
|
+
ruby2_keywords (0.0.5)
|
|
87
|
+
sidekiq (6.2.2)
|
|
88
|
+
connection_pool (>= 2.2.2)
|
|
89
|
+
rack (~> 2.0)
|
|
90
|
+
redis (>= 4.2.0)
|
|
91
|
+
thor (1.1.0)
|
|
92
|
+
timecop (0.9.4)
|
|
93
|
+
tzinfo (2.0.4)
|
|
94
|
+
concurrent-ruby (~> 1.0)
|
|
95
|
+
unicode-display_width (2.1.0)
|
|
96
|
+
zeitwerk (2.5.1)
|
|
97
|
+
|
|
98
|
+
PLATFORMS
|
|
99
|
+
ruby
|
|
100
|
+
|
|
101
|
+
DEPENDENCIES
|
|
102
|
+
bundler
|
|
103
|
+
bundler-audit
|
|
104
|
+
mock_redis
|
|
105
|
+
rspec
|
|
106
|
+
rubocop
|
|
107
|
+
rubocop-config-umbrellio
|
|
108
|
+
rubocop-rspec
|
|
109
|
+
simple_mutex!
|
|
110
|
+
timecop
|
|
111
|
+
|
|
112
|
+
BUNDLED WITH
|
|
113
|
+
2.2.22
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Umbrellio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# SimpleMutex
|
|
2
|
+
|
|
3
|
+
`SimpleMutex::Mutex` - Redis-based locks with ability to store custom data inside them.
|
|
4
|
+
|
|
5
|
+
`SimpleMutex::SidekiqSupport::JobWrapper` - wrapper for Sidekiq jobs that generates locks using
|
|
6
|
+
job's class name and arguments (optional)
|
|
7
|
+
|
|
8
|
+
`SimpleMutex:SidekiqSupport::JobMixin` - mixin for Sidekiq jobs with DSL simplifying usage
|
|
9
|
+
of `SimpleMutex::SidekiqSupport::JobWrapper`
|
|
10
|
+
|
|
11
|
+
`SimpleMutex::SidekiqSupport::JobCleaner` - cleaner for leftover locks created by SimpleMutex::Job
|
|
12
|
+
if Sidekiq dies unexpectedly.
|
|
13
|
+
|
|
14
|
+
`SimpleMutex::SidekiqSupport::Batch` - wrapper for Sidekiq Pro batches that use SimpleMutex::Mutex
|
|
15
|
+
to prevent running multiple batch instances.
|
|
16
|
+
|
|
17
|
+
`SimpleMutex:SidekiqSupport::BatchCleaner` - cleaner for leftover lock created by SimpleMutex::Batch
|
|
18
|
+
if Sidekiq dies unexpectedly.
|
|
19
|
+
|
|
20
|
+
`SimpleMutex::Helper` - auxiliary class for debugging purposes. Allows to inspect existing locks.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
Providing Redis instance before using gem is mandatory.
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
SimpleMutex.redis = Redis.new(
|
|
29
|
+
# ...
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Providing logger is optional (used by `SimpleMutex::SidekiqSupport::JobCleaner` and
|
|
34
|
+
`SimpleMutex::SidekiqSupport::BatchCleaner`).
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
SimpleMutex.logger = Logger.new(
|
|
38
|
+
# ...
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
When using gem with Ruby on Rails you can set those in initializers
|
|
43
|
+
|
|
44
|
+
## SimpleMutex::Mutex Usage
|
|
45
|
+
|
|
46
|
+
### Initialization
|
|
47
|
+
|
|
48
|
+
#### Arguments
|
|
49
|
+
|
|
50
|
+
##### mandatory
|
|
51
|
+
|
|
52
|
+
* `lock_key` - string that identifies lock, mandatory. Two pieces of locked code can't be run
|
|
53
|
+
simultaneously if they use same `lock_key`. They don't interfere with each other if different
|
|
54
|
+
`lock_key`'s are used
|
|
55
|
+
|
|
56
|
+
##### optional
|
|
57
|
+
|
|
58
|
+
Keyword arguments are used for optional args.
|
|
59
|
+
|
|
60
|
+
* `expires_in:` - mutex TTL in second (or ActiveSupport::Numeric time interval), lock will be
|
|
61
|
+
removed by redis automatically when expired, lock will expire in 1 hour (`3600`) if not provided
|
|
62
|
+
* `signature:` - string used to determine ownership of lock, checked when manually deleting lock,
|
|
63
|
+
will be generated by `SecureRandom.uuid` if not provided
|
|
64
|
+
* `payload:` - any object that can be serialized as JSON, `nil` if not provided
|
|
65
|
+
|
|
66
|
+
#### Example
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
SimpleMutex::Mutex
|
|
70
|
+
.new(
|
|
71
|
+
"some_lock_key",
|
|
72
|
+
expires_in: 3600,
|
|
73
|
+
signature: "qwe123",
|
|
74
|
+
payload: { "started_at" => Time.now }
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Wrapping block in mutex
|
|
79
|
+
|
|
80
|
+
You can use method `#with_lock` to wrap code block in mutex
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
SimpleMutex::Mutex
|
|
84
|
+
.new(
|
|
85
|
+
"some_lock_key",
|
|
86
|
+
expires_in: 3600,
|
|
87
|
+
signature: "qwe123",
|
|
88
|
+
payload: { "started_at" => Time.now }
|
|
89
|
+
).with_lock do
|
|
90
|
+
# your code
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Method has delegator defined on class, so it can be used without manual instantiation
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
SimpleMutex::Mutex
|
|
98
|
+
.with_lock(
|
|
99
|
+
"some_lock_key",
|
|
100
|
+
expires_in: 3600,
|
|
101
|
+
signature: "qwe123",
|
|
102
|
+
payload: { "started_at" => Time.now }
|
|
103
|
+
) do
|
|
104
|
+
# your code
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Manual lock control
|
|
109
|
+
|
|
110
|
+
##### Using mutex instance
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
mutex = SimpleMutex::Mutex.new(
|
|
114
|
+
"some_lock_key",
|
|
115
|
+
expires_in: 3600,
|
|
116
|
+
signature: "qwe123",
|
|
117
|
+
payload: { "started_at" => Time.now }
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
mutex.lock!
|
|
121
|
+
# your code
|
|
122
|
+
mutex.unlock!
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
If you for some reason don't want exceptions to be raised when obtaining/deleting lock is failed,
|
|
126
|
+
you can use non-! methods.
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
mutex = SimpleMutex::Mutex.new("some_lock_key")
|
|
130
|
+
# obtaining of lock is not guaranteed
|
|
131
|
+
mutex.lock
|
|
132
|
+
# but you can check if it is obtained (true if lock with correct signature exists)
|
|
133
|
+
mutex.lock_obtained?
|
|
134
|
+
# releasing of lock is not guaranteed
|
|
135
|
+
mutex.unlock
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
##### Using without instance
|
|
139
|
+
|
|
140
|
+
There are `::lock`/`::lock!`/`::unlock`/`::unlock!` methods defined on class if you don't want to
|
|
141
|
+
explicitly use initializer (though it still will be used behind the scenes as `::lock` and `::lock!`
|
|
142
|
+
class methods are just delegators).
|
|
143
|
+
|
|
144
|
+
Mutexes have random `signature` stored inside to determine ownership. By default it prevents
|
|
145
|
+
deleting locks with signature different from provided. You can use `force: true` to ignore
|
|
146
|
+
signature check.
|
|
147
|
+
|
|
148
|
+
`::lock` and `::lock!` class methods accept same arguments as in `::new`
|
|
149
|
+
|
|
150
|
+
`::unlock` and `::unlock!` accept next arguments:
|
|
151
|
+
|
|
152
|
+
* `lock_key` - same as in `::new`
|
|
153
|
+
* `signature:` - same as in `::new`
|
|
154
|
+
* `force:` - boolean, signature will be ignored if `true`, optional, `false` by default
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
SimpleMutex::Mutex.lock!("some_lock_key", signature: "abra_kadabra")
|
|
158
|
+
|
|
159
|
+
# This will work because signature is same as in lock
|
|
160
|
+
SimpleMutex::Mutex.unlock!("some_lock_key", signature: "abra_kadabra")
|
|
161
|
+
|
|
162
|
+
# This won't work, because signature is missing
|
|
163
|
+
SimpleMutex::Mutex.unlock!("some_lock_key")
|
|
164
|
+
|
|
165
|
+
# This won't work, because signature is different
|
|
166
|
+
SimpleMutex::Mutex.unlock!("some_lock_key", signature: "alakazam")
|
|
167
|
+
|
|
168
|
+
# This will work because of force: true
|
|
169
|
+
SimpleMutex::Mutex.unlock!("some_lock_key", force: true)
|
|
170
|
+
|
|
171
|
+
# This will work because of force: true
|
|
172
|
+
SimpleMutex::Mutex.unlock!("some_lock_key", signature: "alakazam", force: true)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Getting signature from instance
|
|
176
|
+
|
|
177
|
+
You can get signature from instance if you want. By default it is UUID generated by SecureRandom.
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
mutex = SimpleMutex::Mutex.new("some_lock_key")
|
|
181
|
+
mutex.signature
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## SimpleMutex::SidekiqSupport::JobWrapper Usage
|
|
185
|
+
|
|
186
|
+
This class made to simplify usage for locking of sidekiq jobs. It will create lock with
|
|
187
|
+
`lock_key` based on job's `class.name` and it's arguments if `lock_with_params: true`.
|
|
188
|
+
|
|
189
|
+
Job's ID (`jid`) and time when job's execution is started will be stored inside mutex value.
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
class SomeJob
|
|
193
|
+
include Sidekiq::Worker
|
|
194
|
+
|
|
195
|
+
def perform(*args)
|
|
196
|
+
SimpleMutex::SidekiqSupport::JobWrapper.new(
|
|
197
|
+
self,
|
|
198
|
+
params: args,
|
|
199
|
+
lock_with_params: true,
|
|
200
|
+
expires_in: 1.hour,
|
|
201
|
+
payload: { this_is_optional: true }
|
|
202
|
+
).with_redlock do
|
|
203
|
+
# your code
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`params` will be used to generate `lock_key` if `lock_with_params: true`.
|
|
210
|
+
|
|
211
|
+
`expires_in:` is in seconds, optional, 5 hours by default.
|
|
212
|
+
|
|
213
|
+
`payload:` optional serializable object.
|
|
214
|
+
|
|
215
|
+
## SimpleMutex::SidekiqSupport::Batch Usage
|
|
216
|
+
|
|
217
|
+
This is wrapper for `Sidekiq::Batch` (from Sidekiq Pro) that helps to prevent running two
|
|
218
|
+
similar batches.
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
batch = SimpleMutex::SidekiqSupport::Batch.new(
|
|
222
|
+
lock_key: "my_batch",
|
|
223
|
+
expires_in: 23.hours.to_i,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
batch.description = "batch of MyJobs"
|
|
227
|
+
batch.on(:success, self.class, {}) # you can add custom callbacks like with Sidekiq::Batch
|
|
228
|
+
batch.on(:death , self.class, {})
|
|
229
|
+
|
|
230
|
+
batch.jobs do
|
|
231
|
+
set_of_job_attributes.each do |job_attributes|
|
|
232
|
+
MyJob.perform(job_attributes)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
* `lock_key` - manatory lock key
|
|
238
|
+
* `expires_in:` - optional TTL, 6 hours if not provided
|
|
239
|
+
|
|
240
|
+
## SimpleMutex::SidekiqSupport::JobCleaner Usage
|
|
241
|
+
|
|
242
|
+
If you use SimpleMutex for locking jobs via `SimpleMutex::SidekiqSupport::Job`, when Sidekiq dies
|
|
243
|
+
unexpectedely, there can be leftover mutexes for dead jobs. To delete them you can use:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
SimpleMutex::SidekiqSupport::JobCleaner.unlock_dead_jobs
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
## SimpleMutex::SidekiqSupport::BatchCleaner Usage
|
|
251
|
+
|
|
252
|
+
If you use SimpleMutex for locking Batches via `SimpleMutex::SidekiqSupport::Batch`, when Sidekiq
|
|
253
|
+
dies unexpectedely, there can be leftover mutexes for dead batches. To delete them you can use:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
SimpleMutex::SidekiqSupport::BatchCleaner.unlock_dead_batches
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## SimpleMutex::Helper Usage
|
|
260
|
+
|
|
261
|
+
Getting lock by `lock_key` (returns nil if no such lock)
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
SimpleMutex::Helper.get("some_lock_key")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Listing existing locks.
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
SimpleMutex::Helper.list(mode: :default)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`mode:` paramater allows to filter locks by type:
|
|
274
|
+
* `:all` - all locks including manual
|
|
275
|
+
* `:job` - job locks
|
|
276
|
+
* `:batch` - batch locks
|
|
277
|
+
* `:default` - job and batch locks
|
|
278
|
+
|
|
279
|
+
## SimpleMutex::SidekiqSupport::JobMixin Usage
|
|
280
|
+
|
|
281
|
+
Base Job class
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
class ApplicationJob
|
|
285
|
+
include Sidekiq::Worker
|
|
286
|
+
include SimpleMutex::SidekiqSupport::JobMixin
|
|
287
|
+
|
|
288
|
+
class << self
|
|
289
|
+
def inherited(job_class)
|
|
290
|
+
# Setting default timeout for mutex.
|
|
291
|
+
job_class.set_job_timeout(5 * 60 * 60) # 5 hours
|
|
292
|
+
|
|
293
|
+
job_class.prepend(
|
|
294
|
+
Module.new do
|
|
295
|
+
def perform(*args)
|
|
296
|
+
with_redlock(args) { super }
|
|
297
|
+
end
|
|
298
|
+
end,
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
DSL:
|
|
306
|
+
- `locking!` - enables locking with simple_mutex for jobs of this class
|
|
307
|
+
- `lock_with_params!` - locks are specific for set of arguments. Same job with other arguments
|
|
308
|
+
can still be called.
|
|
309
|
+
- `skip_locking_error?` - suppresses `SimpleMutex::Mutex::LockError`
|
|
310
|
+
- `set_job_timeout` - redis mutex TTL in seconds (will be removed by redis itself on timeout)
|
|
311
|
+
|
|
312
|
+
Example:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
class SpecificJob < ApplicaionJob
|
|
316
|
+
locking!
|
|
317
|
+
lock_with_params!
|
|
318
|
+
set_job_timeout 6 * 60 * 60
|
|
319
|
+
|
|
320
|
+
def perform
|
|
321
|
+
# ...
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
You can also override error processing for SimpleMutex::Mutex::LockError
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
# DEFAULT ERROR PROCESSING
|
|
330
|
+
# def process_locking_error(error)
|
|
331
|
+
# raise error unless self.class.skip_locking_error?
|
|
332
|
+
# end
|
|
333
|
+
|
|
334
|
+
class SpecificJob < ApplicaionJob
|
|
335
|
+
locking!
|
|
336
|
+
|
|
337
|
+
def perform
|
|
338
|
+
# ...
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def process_locking_error(error)
|
|
342
|
+
SomeLogger.error(error.msg)
|
|
343
|
+
raise error unless self.class.skip_locking_error?
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Contributing
|
|
349
|
+
|
|
350
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/simple_mutex.
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
Released under MIT License.
|
|
355
|
+
|
|
356
|
+
## Authors
|
|
357
|
+
|
|
358
|
+
Team Umbrellio
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
<a href="https://github.com/umbrellio/">
|
|
363
|
+
<img style="float: left;" src="https://umbrellio.github.io/Umbrellio/supported_by_umbrellio.svg" alt="Supported by Umbrellio" width="439" height="72">
|
|
364
|
+
</a>
|
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "simple_mutex"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleMutex
|
|
4
|
+
class BaseCleaner
|
|
5
|
+
def unlock
|
|
6
|
+
::SimpleMutex.redis_check!
|
|
7
|
+
|
|
8
|
+
logger&.info(start_msg)
|
|
9
|
+
|
|
10
|
+
redis.keys.select do |lock_key|
|
|
11
|
+
redis.watch(lock_key) do
|
|
12
|
+
raw_data = redis.get(lock_key)
|
|
13
|
+
|
|
14
|
+
next redis.unwatch if raw_data.nil?
|
|
15
|
+
|
|
16
|
+
parsed_data = safe_parse(raw_data)
|
|
17
|
+
|
|
18
|
+
next redis.unwatch unless parsed_data&.dig("payload", "type") == type
|
|
19
|
+
|
|
20
|
+
entity_id = parsed_data&.dig(*path_to_entity_id)
|
|
21
|
+
|
|
22
|
+
next redis.unwatch if entity_id.nil? || active?(entity_id)
|
|
23
|
+
|
|
24
|
+
return_value = redis.multi { redis.del(lock_key) }
|
|
25
|
+
|
|
26
|
+
log_iteration(lock_key, raw_data, return_value) unless logger.nil?
|
|
27
|
+
|
|
28
|
+
return_value.first.positive?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
logger&.info(end_msg)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def active?(entity_id)
|
|
38
|
+
active_entity_ids.include?(entity_id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def type
|
|
42
|
+
raise NoMethodError
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def path_to_entity_id
|
|
46
|
+
raise NoMethodError
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_active_entity_ids
|
|
50
|
+
raise NoMethodError
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def safe_parse(raw_data)
|
|
54
|
+
JSON.parse(raw_data)
|
|
55
|
+
rescue JSON::ParserError, TypeError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def active_entity_ids
|
|
60
|
+
return @active_entity_ids if defined? @active_entity_ids
|
|
61
|
+
|
|
62
|
+
@active_entity_ids = get_active_entity_ids
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def log_iteration(lock_key, raw_data, return_value)
|
|
66
|
+
log_msg = generate_log_msg(lock_key, raw_data, return_value)
|
|
67
|
+
|
|
68
|
+
if return_value&.first.is_a?(Integer)
|
|
69
|
+
logger.info(log_msg)
|
|
70
|
+
else
|
|
71
|
+
logger.error(log_msg)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def start_msg
|
|
76
|
+
"START #{self.class.name}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def end_msg
|
|
80
|
+
"END #{self.class.name}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def generate_log_msg(lock_key, raw_data, return_value)
|
|
84
|
+
"Trying to delete row with key <#{lock_key.inspect}> "\
|
|
85
|
+
"and value <#{raw_data.inspect}>. "\
|
|
86
|
+
"MULTI returned value <#{return_value.inspect}>."
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def redis
|
|
90
|
+
::SimpleMutex.redis
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def logger
|
|
94
|
+
::SimpleMutex.logger
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|