simple_mutex 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 003ab1f3d7a72c1ed87d20272d4e7e84c8b486fd7dc3a34539ee8ce21688199d
4
- data.tar.gz: e392b0b025b830ea18f062e402c1608562ad3992d3c8c100a266e77f7392dbb5
3
+ metadata.gz: f81fc4fca6f67910c3920de849bdce7e3e3317389a82fbf2f7ff9162b50e688e
4
+ data.tar.gz: ab559f0fea8d0ee4708e2135866d310577906ee643ee725564b38298a326d53d
5
5
  SHA512:
6
- metadata.gz: 4385df59ccb07e0781ff83575d2137907ebc022e2971d2cb9e5f000c70075f7685585e5f93ac3b8dcf5d8798dbb28400441f1bf263c6d34167a22740f1661edd
7
- data.tar.gz: 1c0bd82fce378dfe91f8d778792a325f14191547ce3e6d464cb1b472299480378299283a28340c4299ff5a1d4dd63ab49204e825d31ba85adcc7e7f0f8719784
6
+ metadata.gz: 36cf9cc634a114964b550c518cb15f3c01f959230e990d2a135c5aed911a36ce4b7802e4513cee59d3a6b86c2f8d908165c8e9c8f7176cd1fa2e026875aae167
7
+ data.tar.gz: d7f258c60572617fa5435825b1a23727b5763d44e5aa811c86676dc6f2146f21cfc95a1d9f78b263ec619bc8d93022033a265d341fdd0a2a4eb780cada5539ee
@@ -12,7 +12,7 @@ jobs:
12
12
  strategy:
13
13
  fail-fast: false
14
14
  matrix:
15
- ruby: ["2.6", "2.7", "3.0"]
15
+ ruby: ["2.6", "2.7", "3.0", "3.1"]
16
16
 
17
17
  name: ${{ matrix.ruby }}
18
18
 
@@ -24,7 +24,7 @@ jobs:
24
24
  ruby-version: ${{ matrix.ruby }}
25
25
  bundler-cache: true
26
26
 
27
- - run: bundle exec rake bundle:audit
27
+ - run: bundle exec bundle-audit check --update
28
28
  - run: bundle exec rubocop
29
29
  - run: bundle exec rspec
30
30
 
data/.rubocop.yml CHANGED
@@ -1,6 +1,9 @@
1
1
  inherit_gem:
2
2
  rubocop-config-umbrellio: lib/rubocop.yml
3
3
 
4
+ AllCops:
5
+ TargetRubyVersion: 2.6
6
+
4
7
  Layout/HashAlignment:
5
8
  EnforcedHashRocketStyle:
6
9
  - key
@@ -12,3 +15,6 @@ Layout/HashAlignment:
12
15
  Naming/VariableNumber:
13
16
  Exclude:
14
17
  - 'spec/**/*'
18
+
19
+ Performance/StringIdentifierArgument:
20
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.0.2]
6
+ - Redis-namespace dependency now requires version 1.8.2 or more recent, because thread safety was broken in 1.8.1
7
+ - Adds processing for case when `watch` fails in redis transactions (during unlocking).
8
+
5
9
  ## [1.0.1]
6
10
 
7
11
  - Bugfix with active job/batches memoization in `SimpleMutex::SidekiqSupport::JobCleaner` and
data/Gemfile.lock CHANGED
@@ -1,91 +1,90 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- simple_mutex (1.0.1)
4
+ simple_mutex (1.0.2)
5
5
  redis
6
- redis-namespace
6
+ redis-namespace (>= 1.8.2)
7
7
  sidekiq
8
8
 
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- activesupport (6.1.4.1)
12
+ activesupport (6.1.6)
13
13
  concurrent-ruby (~> 1.0, >= 1.0.2)
14
14
  i18n (>= 1.6, < 2)
15
15
  minitest (>= 5.1)
16
16
  tzinfo (~> 2.0)
17
17
  zeitwerk (~> 2.3)
18
18
  ast (2.4.2)
19
- bundler-audit (0.9.0.1)
19
+ bundler-audit (0.9.1)
20
20
  bundler (>= 1.2.0, < 3)
21
21
  thor (~> 1.0)
22
- concurrent-ruby (1.1.9)
22
+ concurrent-ruby (1.1.10)
23
23
  connection_pool (2.2.5)
24
- diff-lcs (1.4.4)
24
+ diff-lcs (1.5.0)
25
25
  docile (1.4.0)
26
- i18n (1.8.10)
26
+ i18n (1.10.0)
27
27
  concurrent-ruby (~> 1.0)
28
- minitest (5.14.4)
29
- mock_redis (0.29.0)
28
+ minitest (5.16.2)
29
+ mock_redis (0.32.0)
30
30
  ruby2_keywords
31
- parallel (1.21.0)
32
- parser (3.0.2.0)
31
+ parallel (1.22.1)
32
+ parser (3.1.2.0)
33
33
  ast (~> 2.4.1)
34
- rack (2.2.3)
35
- rainbow (3.0.0)
36
- redis (4.5.1)
37
- redis-namespace (1.8.1)
34
+ rack (2.2.4)
35
+ rainbow (3.1.1)
36
+ redis (4.7.1)
37
+ redis-namespace (1.8.2)
38
38
  redis (>= 3.0.4)
39
- regexp_parser (2.1.1)
39
+ regexp_parser (2.5.0)
40
40
  rexml (3.2.5)
41
- rspec (3.10.0)
42
- rspec-core (~> 3.10.0)
43
- rspec-expectations (~> 3.10.0)
44
- rspec-mocks (~> 3.10.0)
45
- rspec-core (3.10.1)
46
- rspec-support (~> 3.10.0)
47
- rspec-expectations (3.10.1)
41
+ rspec (3.11.0)
42
+ rspec-core (~> 3.11.0)
43
+ rspec-expectations (~> 3.11.0)
44
+ rspec-mocks (~> 3.11.0)
45
+ rspec-core (3.11.0)
46
+ rspec-support (~> 3.11.0)
47
+ rspec-expectations (3.11.0)
48
48
  diff-lcs (>= 1.2.0, < 2.0)
49
- rspec-support (~> 3.10.0)
50
- rspec-mocks (3.10.2)
49
+ rspec-support (~> 3.11.0)
50
+ rspec-mocks (3.11.1)
51
51
  diff-lcs (>= 1.2.0, < 2.0)
52
- rspec-support (~> 3.10.0)
53
- rspec-support (3.10.2)
54
- rubocop (1.17.0)
52
+ rspec-support (~> 3.11.0)
53
+ rspec-support (3.11.0)
54
+ rubocop (1.30.1)
55
55
  parallel (~> 1.10)
56
- parser (>= 3.0.0.0)
56
+ parser (>= 3.1.0.0)
57
57
  rainbow (>= 2.2.2, < 4.0)
58
58
  regexp_parser (>= 1.8, < 3.0)
59
- rexml
60
- rubocop-ast (>= 1.7.0, < 2.0)
59
+ rexml (>= 3.2.5, < 4.0)
60
+ rubocop-ast (>= 1.18.0, < 2.0)
61
61
  ruby-progressbar (~> 1.7)
62
62
  unicode-display_width (>= 1.4.0, < 3.0)
63
- rubocop-ast (1.12.0)
64
- parser (>= 3.0.1.1)
65
- rubocop-config-umbrellio (1.17.0.53)
66
- rubocop (= 1.17.0)
67
- rubocop-performance (= 1.10.0)
68
- rubocop-rails (= 2.9.1)
69
- rubocop-rake (= 0.5.1)
70
- rubocop-rspec (= 2.2.0)
71
- rubocop-sequel (= 0.2.0)
72
- rubocop-performance (1.10.0)
73
- rubocop (>= 0.90.0, < 2.0)
63
+ rubocop-ast (1.18.0)
64
+ parser (>= 3.1.1.0)
65
+ rubocop-config-umbrellio (1.30.0.65)
66
+ rubocop (~> 1.30.0)
67
+ rubocop-performance (~> 1.14.0)
68
+ rubocop-rails (~> 2.14.2)
69
+ rubocop-rake (~> 0.6.0)
70
+ rubocop-rspec (~> 2.11.1)
71
+ rubocop-sequel (~> 0.3.3)
72
+ rubocop-performance (1.14.2)
73
+ rubocop (>= 1.7.0, < 2.0)
74
74
  rubocop-ast (>= 0.4.0)
75
- rubocop-rails (2.9.1)
75
+ rubocop-rails (2.14.2)
76
76
  activesupport (>= 4.2.0)
77
77
  rack (>= 1.1)
78
- rubocop (>= 0.90.0, < 2.0)
79
- rubocop-rake (0.5.1)
80
- rubocop
81
- rubocop-rspec (2.2.0)
78
+ rubocop (>= 1.7.0, < 2.0)
79
+ rubocop-rake (0.6.0)
82
80
  rubocop (~> 1.0)
83
- rubocop-ast (>= 1.1.0)
84
- rubocop-sequel (0.2.0)
81
+ rubocop-rspec (2.11.1)
82
+ rubocop (~> 1.19)
83
+ rubocop-sequel (0.3.4)
85
84
  rubocop (~> 1.0)
86
85
  ruby-progressbar (1.11.0)
87
86
  ruby2_keywords (0.0.5)
88
- sidekiq (6.3.1)
87
+ sidekiq (6.5.1)
89
88
  connection_pool (>= 2.2.2)
90
89
  rack (~> 2.0)
91
90
  redis (>= 4.2.0)
@@ -95,16 +94,16 @@ GEM
95
94
  simplecov_json_formatter (~> 0.1)
96
95
  simplecov-html (0.12.3)
97
96
  simplecov-lcov (0.8.0)
98
- simplecov_json_formatter (0.1.3)
99
- thor (1.1.0)
100
- timecop (0.9.4)
97
+ simplecov_json_formatter (0.1.4)
98
+ thor (1.2.1)
99
+ timecop (0.9.5)
101
100
  tzinfo (2.0.4)
102
101
  concurrent-ruby (~> 1.0)
103
- unicode-display_width (2.1.0)
104
- zeitwerk (2.5.1)
102
+ unicode-display_width (2.2.0)
103
+ zeitwerk (2.6.0)
105
104
 
106
105
  PLATFORMS
107
- ruby
106
+ x86_64-linux
108
107
 
109
108
  DEPENDENCIES
110
109
  bundler
@@ -120,4 +119,4 @@ DEPENDENCIES
120
119
  timecop
121
120
 
122
121
  BUNDLED WITH
123
- 2.2.22
122
+ 2.3.17
@@ -2,35 +2,54 @@
2
2
 
3
3
  module SimpleMutex
4
4
  class BaseCleaner
5
+ MAX_DEL_ATTEMPTS = 3
6
+
7
+ class SynchronizationAnomalyError < ::StandardError; end
8
+
9
+ # rubocop:disable Metrics/MethodLength
5
10
  def unlock
6
11
  ::SimpleMutex.redis_check!
7
12
 
8
13
  logger&.info(start_msg)
9
14
 
10
15
  redis.keys.select do |lock_key|
11
- redis.watch(lock_key) do
12
- raw_data = redis.get(lock_key)
16
+ attempt = 0
17
+
18
+ begin
19
+ redis.watch(lock_key) do
20
+ raw_data = redis.get(lock_key)
21
+ raw_data = raw_data.value if raw_data.is_a?(Redis::Future)
22
+
23
+ next if raw_data.nil?
24
+
25
+ parsed_data = safe_parse(raw_data)
13
26
 
14
- next redis.unwatch if raw_data.nil?
27
+ next unless parsed_data&.dig("payload", "type") == type
15
28
 
16
- parsed_data = safe_parse(raw_data)
29
+ entity_id = parsed_data&.dig(*path_to_entity_id)
17
30
 
18
- next redis.unwatch unless parsed_data&.dig("payload", "type") == type
31
+ next if entity_id.nil? || active?(entity_id)
19
32
 
20
- entity_id = parsed_data&.dig(*path_to_entity_id)
33
+ return_value = redis.multi { |multi| multi.del(lock_key) }
21
34
 
22
- next redis.unwatch if entity_id.nil? || active?(entity_id)
35
+ log_iteration(lock_key, raw_data, return_value) unless logger.nil?
23
36
 
24
- return_value = redis.multi { |multi| multi.del(lock_key) }
37
+ result = return_value&.first
25
38
 
26
- log_iteration(lock_key, raw_data, return_value) unless logger.nil?
39
+ raise SynchronizationAnomalyError, "Sync anomaly." unless result.is_a?(Integer)
27
40
 
28
- return_value&.first&.positive?
41
+ result.positive?
42
+ ensure
43
+ redis.unwatch
44
+ end
45
+ rescue SynchronizationAnomalyError
46
+ retry if (attempt += 1) < MAX_DEL_ATTEMPTS
29
47
  end
30
48
  end
31
49
 
32
50
  logger&.info(end_msg)
33
51
  end
52
+ # rubocop:enable Metrics/MethodLength
34
53
 
35
54
  private
36
55
 
@@ -75,8 +94,8 @@ module SimpleMutex
75
94
 
76
95
  def generate_log_msg(lock_key, raw_data, return_value)
77
96
  "Trying to delete row with key <#{lock_key.inspect}> "\
78
- "and value <#{raw_data.inspect}>. "\
79
- "MULTI returned value <#{return_value.inspect}>."
97
+ "and value <#{raw_data.inspect}>. "\
98
+ "MULTI returned value <#{return_value.inspect}>."
80
99
  end
81
100
 
82
101
  def redis
@@ -29,39 +29,37 @@ module SimpleMutex
29
29
  }
30
30
  end
31
31
 
32
- # rubocop:disable Metrics/MethodLength, Style/HashEachMethods, Performance/CollectionLiteralInLoop
32
+ # rubocop:disable Style/HashEachMethods
33
33
  def list(mode: :default)
34
34
  check_mode(mode)
35
35
 
36
36
  result = []
37
+ job_types = %i[job default]
38
+ batch_types = %i[batch default]
37
39
 
38
40
  redis.keys.each do |lock_key|
39
- redis.watch(lock_key) do
40
- raw_data = redis.get(lock_key)
41
-
42
- unless raw_data.nil?
43
- parsed_data = safe_parse(raw_data)
44
-
45
- if parsed_data.nil?
46
- result << { key: lock_key, value: raw_data } if mode == :all
47
- else
48
- lock_type = parsed_data&.dig("payload", "type")
49
-
50
- if (mode == :all) ||
51
- (lock_type == "Job" && %i[job default].include?(mode)) ||
52
- (lock_type == "Batch" && %i[batch default].include?(mode))
53
- result << { key: lock_key, value: parsed_data }
54
- end
55
- end
56
- end
41
+ raw_data = redis.get(lock_key)
42
+
43
+ next if raw_data.nil?
44
+
45
+ parsed_data = safe_parse(raw_data)
57
46
 
58
- redis.unwatch
47
+ if parsed_data.nil?
48
+ result << { key: lock_key, value: raw_data } if mode == :all
49
+ else
50
+ lock_type = parsed_data&.dig("payload", "type")
51
+
52
+ if (mode == :all) ||
53
+ (lock_type == "Job" && job_types.include?(mode)) ||
54
+ (lock_type == "Batch" && batch_types.include?(mode))
55
+ result << { key: lock_key, value: parsed_data }
56
+ end
59
57
  end
60
58
  end
61
59
 
62
60
  result
63
61
  end
64
- # rubocop:enable Metrics/MethodLength, Style/HashEachMethods, Performance/CollectionLiteralInLoop
62
+ # rubocop:enable Style/HashEachMethods
65
63
 
66
64
  private
67
65
 
@@ -6,8 +6,9 @@ require "json"
6
6
  module SimpleMutex
7
7
  class Mutex
8
8
  DEFAULT_EXPIRES_IN = 60 * 60 # 1 hour
9
+ MAX_DEL_ATTEMPTS = 3
9
10
 
10
- ERR_MSGS = {
11
+ ERROR_MESSAGES = {
11
12
  unlock: {
12
13
  unknown: lambda do |lock_key|
13
14
  "something when wrong when deleting lock key <#{lock_key}>."
@@ -18,6 +19,9 @@ module SimpleMutex
18
19
  signature_mismatch: lambda do |lock_key|
19
20
  "signature mismatch for lock key <#{lock_key}>."
20
21
  end,
22
+ repeated_synchronization_anomaly: lambda do |lock_key|
23
+ "repeated synchronization anomaly for <#{lock_key}>."
24
+ end,
21
25
  }.freeze,
22
26
  lock: {
23
27
  basic: lambda do |lock_key|
@@ -26,6 +30,8 @@ module SimpleMutex
26
30
  }.freeze,
27
31
  }.freeze
28
32
 
33
+ SynchronizationAnomalyError = Class.new(::StandardError)
34
+
29
35
  BaseError = Class.new(::StandardError) do
30
36
  attr_reader :lock_key
31
37
 
@@ -54,41 +60,62 @@ module SimpleMutex
54
60
 
55
61
  redis = ::SimpleMutex.redis
56
62
 
57
- redis.watch(lock_key) do
58
- raw_data = redis.get(lock_key)
63
+ attempt = 0
64
+
65
+ begin
66
+ redis.watch(lock_key) do
67
+ raw_data = redis.get(lock_key)
68
+ raw_data = raw_data.value if raw_data.is_a?(Redis::Future)
69
+
70
+ return false if raw_data.nil?
71
+ return false unless force || signature_valid?(raw_data, signature)
59
72
 
60
- if raw_data && (force || signature_valid?(raw_data, signature))
61
- redis.multi { |multi| multi.del(lock_key) }.first.positive?
62
- else
73
+ result = redis.multi { |multi| multi.del(lock_key) }.first
74
+
75
+ raise SynchronizationAnomalyError, "Sync anomaly." unless result.is_a?(Integer)
76
+
77
+ result.positive?
78
+ ensure
63
79
  redis.unwatch
64
- false
65
80
  end
81
+ rescue SynchronizationAnomalyError
82
+ retry if (attempt += 1) < MAX_DEL_ATTEMPTS
83
+ raise_error(UnlockError, :repeated_synchronization_anomaly, lock_key)
66
84
  end
67
85
  end
68
86
 
87
+ # rubocop:disable Metrics/MethodLength
69
88
  def unlock!(lock_key, signature: nil, force: false)
70
89
  ::SimpleMutex.redis_check!
71
90
 
72
91
  redis = ::SimpleMutex.redis
73
92
 
74
- redis.watch(lock_key) do
75
- raw_data = redis.get(lock_key)
93
+ attempt = 0
94
+
95
+ begin
96
+ redis.watch(lock_key) do
97
+ raw_data = redis.get(lock_key)
98
+ raw_data = raw_data.value if raw_data.is_a?(Redis::Future)
76
99
 
77
- begin
78
- raise_error(UnlockError, :key_not_found, lock_key) unless raw_data
100
+ raise_error(UnlockError, :key_not_found, lock_key) if raw_data.nil?
79
101
 
80
102
  unless force || signature_valid?(raw_data, signature)
81
103
  raise_error(UnlockError, :signature_mismatch, lock_key)
82
104
  end
83
105
 
84
- success = redis.multi { |multi| multi.del(lock_key) }.first.positive?
106
+ result = redis.multi { |multi| multi.del(lock_key) }.first
85
107
 
86
- raise_error(UnlockError, :unknown, lock_key) unless success
108
+ raise SynchronizationAnomalyError, "Sync anomaly." unless result.is_a?(Integer)
109
+ raise_error(UnlockError, :unknown, lock_key) unless result.positive?
87
110
  ensure
88
111
  redis.unwatch
89
112
  end
113
+ rescue SynchronizationAnomalyError
114
+ retry if (attempt += 1) < MAX_DEL_ATTEMPTS
115
+ raise_error(UnlockError, :repeated_synchronization_anomaly, lock_key)
90
116
  end
91
117
  end
118
+ # rubocop:enable Metrics/MethodLength
92
119
 
93
120
  def with_lock(lock_key, **options, &block)
94
121
  new(lock_key, **options).with_lock(&block)
@@ -96,7 +123,7 @@ module SimpleMutex
96
123
 
97
124
  def raise_error(error_class, msg_template, lock_key)
98
125
  template_base = error_class.name.split("::").last.gsub("Error", "").downcase.to_sym
99
- error_msg = ERR_MSGS[template_base][msg_template].call(lock_key)
126
+ error_msg = ERROR_MESSAGES[template_base][msg_template].call(lock_key)
100
127
 
101
128
  raise(error_class.new(error_msg, lock_key))
102
129
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleMutex
4
- VERSION = "1.0.1"
4
+ VERSION = "1.0.2"
5
5
  end
data/simple_mutex.gemspec CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/umbrellio/simple_mutex"
14
14
  spec.license = "MIT"
15
15
 
16
- spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
16
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
17
17
 
18
18
  # Specify which files should be added to the gem when it is released.
19
19
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.require_paths = ["lib"]
27
27
 
28
28
  spec.add_runtime_dependency "redis"
29
- spec.add_runtime_dependency "redis-namespace"
29
+ spec.add_runtime_dependency "redis-namespace", ">= 1.8.2"
30
30
  spec.add_runtime_dependency "sidekiq"
31
31
 
32
32
  spec.add_development_dependency "bundler"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_mutex
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - bob-umbr
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-19 00:00:00.000000000 Z
11
+ date: 2022-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: 1.8.2
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: 1.8.2
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: sidekiq
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -235,14 +235,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
235
235
  requirements:
236
236
  - - ">="
237
237
  - !ruby/object:Gem::Version
238
- version: 2.5.0
238
+ version: 2.6.0
239
239
  required_rubygems_version: !ruby/object:Gem::Requirement
240
240
  requirements:
241
241
  - - ">="
242
242
  - !ruby/object:Gem::Version
243
243
  version: '0'
244
244
  requirements: []
245
- rubygems_version: 3.2.3
245
+ rubygems_version: 3.3.0.dev
246
246
  signing_key:
247
247
  specification_version: 4
248
248
  summary: Redis-based mutex library for using with sidekiq jobs and batches.