rails_soft_lock 0.2.0 → 0.3.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 +4 -4
- data/.reek.yml +4 -0
- data/.ruby-version +1 -1
- data/Rakefile +5 -1
- data/lib/rails_soft_lock/configuration.rb +17 -20
- data/lib/rails_soft_lock/lock_object.rb +15 -12
- data/lib/rails_soft_lock/model_extensions.rb +87 -42
- data/lib/rails_soft_lock/redis_adapter.rb +5 -17
- data/lib/rails_soft_lock/redis_config.rb +9 -15
- data/lib/rails_soft_lock/version.rb +1 -1
- data/lib/rails_soft_lock.rb +4 -0
- metadata +9 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28c11572b431bdc87a2f4771c63b6aef7665f619761c84949d0265b73f2976d1
|
|
4
|
+
data.tar.gz: a81700f090a280d04f43d96dbac00603dc64fbd7912a7e22f69a76321792a57d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 824828fb615e0e17866a4ebf266f493f3283876e6e83fb485afdfde89beae8a748ba9ab6e7afec273344572385b0853b2707943fc4f64692961174a8baae6c4e
|
|
7
|
+
data.tar.gz: 1dcea30302c83b22c3782aeabf7307a3c931a7c6355b39f62726dd277630a472eb441200369426fa64a55dc04d124b65f18033a3a8524f24ebaf5ba5b65439e6
|
data/.reek.yml
ADDED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby-
|
|
1
|
+
ruby-4.0.1
|
data/Rakefile
CHANGED
|
@@ -2,41 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
# lib/rails_soft_lock/configuration.rb
|
|
4
4
|
module RailsSoftLock
|
|
5
|
-
# Configuration
|
|
5
|
+
# Configuration for global settings like adapter and user class.
|
|
6
6
|
class Configuration
|
|
7
|
-
#
|
|
7
|
+
# Supported storage adapters.
|
|
8
8
|
VALID_ADAPTERS = %i[redis nats memcached].freeze
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
attr_accessor :adapter_options
|
|
9
|
+
|
|
10
|
+
attr_reader :adapter, :adapter_options
|
|
12
11
|
# :reek:Attribute
|
|
13
12
|
attr_writer :locked_by_class
|
|
14
13
|
|
|
14
|
+
# Initializes configuration with default values.
|
|
15
15
|
def initialize
|
|
16
|
-
@adapter = :redis
|
|
17
|
-
@adapter_options
|
|
16
|
+
@adapter = :redis
|
|
17
|
+
@adapter_options = RedisConfig.default_adapter_options
|
|
18
18
|
@locked_by_class = locked_by_class || "User"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def
|
|
22
|
-
@
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def acts_as_locked_attribute
|
|
26
|
-
@acts_as_locked_options&.[](:by) || :lock_attribute
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def acts_as_locked_scope
|
|
30
|
-
@acts_as_locked_options&.[](:scope)&.call || "none"
|
|
21
|
+
def adapter_options=(options = {})
|
|
22
|
+
@adapter_options = options.empty? ? RedisConfig.default_adapter_options : options
|
|
31
23
|
end
|
|
32
24
|
|
|
25
|
+
# Returns the locker class constant (e.g., User).
|
|
26
|
+
#
|
|
27
|
+
# @return [Class]
|
|
33
28
|
def locked_by_class
|
|
34
29
|
@locked_by_class.is_a?(String) ? @locked_by_class.constantize : @locked_by_class
|
|
35
30
|
end
|
|
36
31
|
|
|
37
|
-
# Sets the adapter
|
|
38
|
-
#
|
|
39
|
-
# @
|
|
32
|
+
# Sets the adapter (e.g., :redis, :nats, :memcached).
|
|
33
|
+
#
|
|
34
|
+
# @param value [Symbol] one of the VALID_ADAPTERS
|
|
35
|
+
# @raise [ArgumentError] if adapter is not valid
|
|
36
|
+
# @return [void]
|
|
40
37
|
def adapter=(value)
|
|
41
38
|
raise ArgumentError, "Adapter must be one of: #{VALID_ADAPTERS.join(", ")}" unless VALID_ADAPTERS.include?(value)
|
|
42
39
|
|
|
@@ -9,17 +9,20 @@ module RailsSoftLock
|
|
|
9
9
|
# - object_key: The identifier of the lock instance, typically a unique database record ID
|
|
10
10
|
# - object_value: The identifier of the locker that locked the record
|
|
11
11
|
class LockObject
|
|
12
|
-
#
|
|
12
|
+
# Maps adapter symbols to their respective modules
|
|
13
|
+
ADAPTER_MAP = {
|
|
14
|
+
redis: RailsSoftLock::RedisAdapter
|
|
15
|
+
# nats: RailsSoftLock::NatsAdapter,
|
|
16
|
+
# memcached: RailsSoftLock::MemcachedAdapter
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# Selects and returns the adapter module based on current configuration.
|
|
20
|
+
#
|
|
21
|
+
# @return [Module] adapter module (e.g., RedisAdapter)
|
|
22
|
+
# @raise [ArgumentError] if adapter is not recognized
|
|
13
23
|
def self.adapter
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
RailsSoftLock::RedisAdapter
|
|
17
|
-
when :nats
|
|
18
|
-
RailsSoftLock::NatsAdapter
|
|
19
|
-
when :memcached
|
|
20
|
-
RailsSoftLock::MemcachedAdapter
|
|
21
|
-
else
|
|
22
|
-
raise ArgumentError, "Unknown adapter: #{RailsSoftLock.configuration.adapter}"
|
|
24
|
+
ADAPTER_MAP.fetch(RailsSoftLock.configuration.adapter) do |key|
|
|
25
|
+
raise ArgumentError, "Unknown adapter: #{key}"
|
|
23
26
|
end
|
|
24
27
|
end
|
|
25
28
|
|
|
@@ -30,7 +33,7 @@ module RailsSoftLock
|
|
|
30
33
|
def initialize(object_name:, object_key: nil, object_value: nil)
|
|
31
34
|
@object_name = object_name
|
|
32
35
|
@object_key = object_key
|
|
33
|
-
@object_value = object_value.
|
|
36
|
+
@object_value = object_value.presence&.to_s # Convert to string for consistency
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
# Returns the ID of the locker who locked the object
|
|
@@ -43,7 +46,7 @@ module RailsSoftLock
|
|
|
43
46
|
# @return [Hash] { has_locked: Boolean, locked_by: String or nil }
|
|
44
47
|
def lock_or_find
|
|
45
48
|
locked_object = get
|
|
46
|
-
{ has_locked: create, locked_by: locked_object || object_value }
|
|
49
|
+
{ has_locked: !create, locked_by: locked_object || object_value }
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
# Unlocks the object
|
|
@@ -1,74 +1,119 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# lib/rails_soft_lock/model_extensions.rb
|
|
4
|
-
|
|
5
4
|
module RailsSoftLock
|
|
6
5
|
# Extend model and give methods from gem
|
|
7
6
|
module ModelExtensions
|
|
8
7
|
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
class_attribute :locked_attribute, default: :id
|
|
11
|
+
class_attribute :lock_scope_proc, default: -> { "none" }
|
|
12
|
+
|
|
13
|
+
delegate :object_name, to: :class
|
|
14
|
+
end
|
|
15
|
+
|
|
9
16
|
class_methods do
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
# Defines which attribute to use for locking and an optional scoping block.
|
|
18
|
+
#
|
|
19
|
+
# @param attribute [Symbol] the attribute used for locking (default: :id)
|
|
20
|
+
# @param scope [Proc] a proc returning the scoping value (default: -> { "none" })
|
|
21
|
+
# @return [void]
|
|
22
|
+
def acts_as_locked_by(attribute = nil, scope: nil)
|
|
23
|
+
unless attribute
|
|
24
|
+
raise InvalidArgumentError,
|
|
25
|
+
"[RailsSoftLock.acts_as_locked_by] Argument 'attribute' is required"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self.locked_attribute = attribute
|
|
29
|
+
self.lock_scope_proc = scope if scope
|
|
13
30
|
end
|
|
14
31
|
|
|
32
|
+
# Fetches all locks for the model.
|
|
33
|
+
#
|
|
34
|
+
# @return [Hash]
|
|
15
35
|
def all_locks
|
|
16
|
-
|
|
36
|
+
LockObject.new(object_name: object_name).all_locks
|
|
17
37
|
end
|
|
18
38
|
|
|
39
|
+
# Unlocks a specific object key.
|
|
40
|
+
#
|
|
41
|
+
# @param object_key [String, Integer]
|
|
42
|
+
# @return [Boolean]
|
|
19
43
|
def unlock(object_key)
|
|
20
|
-
|
|
44
|
+
LockObject.new(object_name: object_name, object_key: object_key).unlock
|
|
21
45
|
end
|
|
22
46
|
|
|
47
|
+
# Returns the composed object name (model + scope).
|
|
48
|
+
#
|
|
49
|
+
# @return [String]
|
|
23
50
|
def object_name
|
|
24
|
-
|
|
25
|
-
"#{name}::#{scope}"
|
|
51
|
+
"#{name}::#{lock_scope_proc&.call}"
|
|
26
52
|
end
|
|
27
53
|
end
|
|
28
54
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
# Attempts to acquire a lock for the given user.
|
|
56
|
+
#
|
|
57
|
+
# @param user [User]
|
|
58
|
+
# @return [Hash]
|
|
59
|
+
def lock_or_find(user)
|
|
60
|
+
lock_object_for(user).lock_or_find
|
|
61
|
+
end
|
|
35
62
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
# Unlocks the object for the given user.
|
|
64
|
+
#
|
|
65
|
+
# @param user [User]
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def unlock(user)
|
|
68
|
+
lock_object_for(user).unlock
|
|
69
|
+
end
|
|
39
70
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
71
|
+
# Checks if the object is locked.
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def locked?
|
|
75
|
+
locked_by.present?
|
|
76
|
+
end
|
|
43
77
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
# Returns the user who locked the object, if any.
|
|
79
|
+
#
|
|
80
|
+
# @return [User, nil]
|
|
81
|
+
def locked_by
|
|
82
|
+
user_id = base_lock_object.locked_by&.to_i
|
|
83
|
+
user_id ? RailsSoftLock.configuration.locked_by_class.find_by(id: user_id) : nil
|
|
84
|
+
end
|
|
48
85
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
86
|
+
# Returns the lock attribute value for the instance.
|
|
87
|
+
#
|
|
88
|
+
# @return [Object]
|
|
89
|
+
def object_key
|
|
90
|
+
model = self.class
|
|
91
|
+
attribute = model.locked_attribute
|
|
52
92
|
|
|
53
|
-
|
|
93
|
+
if (value = try(attribute))
|
|
94
|
+
value
|
|
95
|
+
else
|
|
96
|
+
raise NoMethodError,
|
|
97
|
+
"[RailsSoftLock.object_key] Model #{model} does not respond to :#{attribute}"
|
|
54
98
|
end
|
|
99
|
+
end
|
|
55
100
|
|
|
56
|
-
|
|
101
|
+
private
|
|
57
102
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
object_key: object_key,
|
|
62
|
-
object_value: user.id.to_s
|
|
63
|
-
)
|
|
64
|
-
end
|
|
103
|
+
def lock_object_for(user)
|
|
104
|
+
build_lock_object(object_value: user.id.to_s)
|
|
105
|
+
end
|
|
65
106
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
def base_lock_object
|
|
108
|
+
build_lock_object
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def build_lock_object(object_value: nil)
|
|
112
|
+
RailsSoftLock::LockObject.new(
|
|
113
|
+
object_name: object_name,
|
|
114
|
+
object_key: object_key,
|
|
115
|
+
object_value: object_value
|
|
116
|
+
)
|
|
72
117
|
end
|
|
73
118
|
end
|
|
74
119
|
end
|
|
@@ -9,21 +9,9 @@ module RailsSoftLock
|
|
|
9
9
|
# Adapter for store lock in redis
|
|
10
10
|
module RedisAdapter
|
|
11
11
|
# Initialize Redis client
|
|
12
|
-
def redis_client
|
|
12
|
+
def redis_client
|
|
13
13
|
@redis_client ||= begin
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Set config defaults
|
|
17
|
-
defaults = {
|
|
18
|
-
url: "redis://localhost:6379/0",
|
|
19
|
-
timeout: 5
|
|
20
|
-
}
|
|
21
|
-
# Merge config options
|
|
22
|
-
config = defaults.merge(redis_config)
|
|
23
|
-
|
|
24
|
-
ConnectionPool::Wrapper.new do
|
|
25
|
-
Redis.new(**config)
|
|
26
|
-
end
|
|
14
|
+
ConnectionPool::Wrapper.new { Redis.new(**RailsSoftLock.configuration.adapter_options[:redis]) }
|
|
27
15
|
rescue Redis::CannotConnectError => e
|
|
28
16
|
raise RailsSoftLock::Error, "Failed to connect to Redis: #{e.message}"
|
|
29
17
|
end
|
|
@@ -42,19 +30,19 @@ module RailsSoftLock
|
|
|
42
30
|
transaction.hsetnx(@object_name, @object_key, @object_value)
|
|
43
31
|
transaction.hget(@object_name, @object_key)
|
|
44
32
|
end
|
|
45
|
-
|
|
33
|
+
result.first # true on creation, false otherwise
|
|
46
34
|
end
|
|
47
35
|
|
|
48
36
|
# Updates the value for an existing key or creates a new key-value pair
|
|
49
37
|
# @return [Boolean] true if the key was updated, false if it was created
|
|
50
|
-
def update
|
|
38
|
+
def update # rubocop:disable Naming/PredicateMethod
|
|
51
39
|
result = redis_client.hset(@object_name, @object_key, @object_value)
|
|
52
40
|
result.zero?
|
|
53
41
|
end
|
|
54
42
|
|
|
55
43
|
# Deletes a key from the specified hash
|
|
56
44
|
# @return [Boolean] true if the key was deleted, false if it did not exist
|
|
57
|
-
def delete
|
|
45
|
+
def delete # rubocop:disable Naming/PredicateMethod
|
|
58
46
|
result = redis_client.hdel(@object_name, @object_key)
|
|
59
47
|
!result.zero?
|
|
60
48
|
end
|
|
@@ -18,6 +18,13 @@ module RailsSoftLock
|
|
|
18
18
|
{ redis: config_with_defaults }
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# The default Redis connection settings
|
|
22
|
+
# @return [Hash]
|
|
23
|
+
# @api private
|
|
24
|
+
def default_settings
|
|
25
|
+
{ url: "redis://localhost:6379/0", timeout: 5 }
|
|
26
|
+
end
|
|
27
|
+
|
|
21
28
|
# Merges default settings with any Rails-specific configuration
|
|
22
29
|
# @return [Hash] Complete Redis configuration
|
|
23
30
|
# @note Will return just defaults if Rails isn't available
|
|
@@ -29,13 +36,7 @@ module RailsSoftLock
|
|
|
29
36
|
# Checks if Rails environment is available and properly configured
|
|
30
37
|
# @return [Boolean]
|
|
31
38
|
def rails_available?
|
|
32
|
-
|
|
33
|
-
Rails.respond_to?(:application) &&
|
|
34
|
-
Rails.application
|
|
35
|
-
true
|
|
36
|
-
else
|
|
37
|
-
false
|
|
38
|
-
end
|
|
39
|
+
!!(defined?(Rails) && Rails.try(:application).present?)
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
# Attempts to load Redis config from Rails application
|
|
@@ -55,14 +56,7 @@ module RailsSoftLock
|
|
|
55
56
|
# @return [Boolean]
|
|
56
57
|
# @api private
|
|
57
58
|
def rails_config_available?
|
|
58
|
-
Rails.application.
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# The default Redis connection settings
|
|
62
|
-
# @return [Hash]
|
|
63
|
-
# @api private
|
|
64
|
-
def default_settings
|
|
65
|
-
{ url: "redis://localhost:6379/0", timeout: 5 }
|
|
59
|
+
!!Rails.application.try(:config_for, :redis)
|
|
66
60
|
end
|
|
67
61
|
end
|
|
68
62
|
end
|
data/lib/rails_soft_lock.rb
CHANGED
|
@@ -8,6 +8,10 @@ require "zeitwerk"
|
|
|
8
8
|
module RailsSoftLock
|
|
9
9
|
# Error class for gem
|
|
10
10
|
class Error < StandardError; end
|
|
11
|
+
# Error class for Argument required error
|
|
12
|
+
class InvalidArgumentError < Error; end
|
|
13
|
+
# Error class for undef attribute for Model class
|
|
14
|
+
class NoMethodError < Error; end
|
|
11
15
|
|
|
12
16
|
def self.lock_manager(object_name:, object_key: nil, object_value: nil)
|
|
13
17
|
@lock_manager ||= LockObject.new(
|
metadata
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_soft_lock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergey Arkhipov
|
|
8
|
+
- Georgy Shcherbakov
|
|
8
9
|
- Vladimir Peskov
|
|
9
10
|
bindir: lib
|
|
10
11
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
13
|
dependencies:
|
|
13
14
|
- !ruby/object:Gem::Dependency
|
|
14
15
|
name: connection_pool
|
|
@@ -16,14 +17,14 @@ dependencies:
|
|
|
16
17
|
requirements:
|
|
17
18
|
- - "~>"
|
|
18
19
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
20
|
+
version: '3.0'
|
|
20
21
|
type: :runtime
|
|
21
22
|
prerelease: false
|
|
22
23
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
24
|
requirements:
|
|
24
25
|
- - "~>"
|
|
25
26
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
27
|
+
version: '3.0'
|
|
27
28
|
- !ruby/object:Gem::Dependency
|
|
28
29
|
name: zeitwerk
|
|
29
30
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -41,11 +42,13 @@ dependencies:
|
|
|
41
42
|
description: Using In-Memory Databases to Work with Rails Active Record Locks
|
|
42
43
|
email:
|
|
43
44
|
- sergey-arkhipov@ya.ru
|
|
45
|
+
- lordsynergymail@gmail.com
|
|
44
46
|
- v.peskov@mail.ru
|
|
45
47
|
executables: []
|
|
46
48
|
extensions: []
|
|
47
49
|
extra_rdoc_files: []
|
|
48
50
|
files:
|
|
51
|
+
- ".reek.yml"
|
|
49
52
|
- ".rspec"
|
|
50
53
|
- ".rubocop.yml"
|
|
51
54
|
- ".ruby-version"
|
|
@@ -81,14 +84,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
81
84
|
requirements:
|
|
82
85
|
- - ">="
|
|
83
86
|
- !ruby/object:Gem::Version
|
|
84
|
-
version: 3.
|
|
87
|
+
version: '3.4'
|
|
85
88
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
89
|
requirements:
|
|
87
90
|
- - ">="
|
|
88
91
|
- !ruby/object:Gem::Version
|
|
89
92
|
version: '0'
|
|
90
93
|
requirements: []
|
|
91
|
-
rubygems_version:
|
|
94
|
+
rubygems_version: 4.0.3
|
|
92
95
|
specification_version: 4
|
|
93
96
|
summary: Lock Active record by attribyte using in-memory adapters
|
|
94
97
|
test_files: []
|