simple_mutex 1.0.1 → 1.0.2

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 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.