rails_soft_lock 0.2.0 → 0.2.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 +4 -4
- data/.reek.yml +4 -0
- data/Rakefile +5 -1
- data/lib/rails_soft_lock/configuration.rb +17 -20
- data/lib/rails_soft_lock/lock_object.rb +14 -11
- data/lib/rails_soft_lock/model_extensions.rb +87 -42
- data/lib/rails_soft_lock/redis_adapter.rb +3 -15
- 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 +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '059033a8b8115c0c301d705a1cfdeb412038ca9d7c5901fdba21400c591592c1'
|
4
|
+
data.tar.gz: febced8d04b074e5acb9a62d913e55d3e1e1b0e6c4328d957249e7bd7f98cb60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f0b8ea015e43aafd3c624831ad2fbbbde4fa50b733268ec51f2e00654e76165c3653a5112177b79cc537da785cc1603af8eb273227bac012856c69d6ce01ac2b
|
7
|
+
data.tar.gz: 42658703a0637fb10038f0bac0bdba3e3c06a710b60bb1539e2435c59ea1f27917c25ba8d06476672c2e64864c6f2cd50aaec52da6392dc3ce89057debeba91f
|
data/.reek.yml
ADDED
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
|
|
@@ -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,7 +30,7 @@ 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
|
@@ -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.2.
|
4
|
+
version: 0.2.1
|
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: 2025-
|
12
|
+
date: 2025-05-17 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: connection_pool
|
@@ -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"
|