redis_queued_locks 0.0.20 → 0.0.22
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/CHANGELOG.md +8 -0
- data/README.md +14 -1
- data/lib/redis_queued_locks/acquier/{delay.rb → acquire_lock/delay_execution.rb} +2 -2
- data/lib/redis_queued_locks/acquier/{try.rb → acquire_lock/try_to_lock.rb} +1 -1
- data/lib/redis_queued_locks/acquier/acquire_lock/with_acq_timeout.rb +29 -0
- data/lib/redis_queued_locks/acquier/{yield_expire.rb → acquire_lock/yield_with_expire.rb} +9 -2
- data/lib/redis_queued_locks/acquier/acquire_lock.rb +291 -0
- data/lib/redis_queued_locks/acquier/extend_lock_ttl.rb +19 -0
- data/lib/redis_queued_locks/acquier/is_locked.rb +18 -0
- data/lib/redis_queued_locks/acquier/is_queued.rb +18 -0
- data/lib/redis_queued_locks/acquier/keys.rb +26 -0
- data/lib/redis_queued_locks/acquier/lock_info.rb +57 -0
- data/lib/redis_queued_locks/acquier/locks.rb +26 -0
- data/lib/redis_queued_locks/acquier/queue_info.rb +50 -0
- data/lib/redis_queued_locks/acquier/queues.rb +26 -0
- data/lib/redis_queued_locks/acquier/release_all_locks.rb +88 -0
- data/lib/redis_queued_locks/acquier/release_lock.rb +76 -0
- data/lib/redis_queued_locks/acquier.rb +11 -557
- data/lib/redis_queued_locks/client.rb +34 -19
- data/lib/redis_queued_locks/instrument/active_support.rb +1 -1
- data/lib/redis_queued_locks/instrument/void_notifier.rb +1 -1
- data/lib/redis_queued_locks/instrument.rb +3 -3
- data/lib/redis_queued_locks/logging/void_logger.rb +43 -0
- data/lib/redis_queued_locks/logging.rb +57 -0
- data/lib/redis_queued_locks/utilities.rb +16 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- data/lib/redis_queued_locks.rb +3 -0
- metadata +20 -6
- data/lib/redis_queued_locks/acquier/release.rb +0 -60
@@ -9,18 +9,16 @@ class RedisQueuedLocks::Client
|
|
9
9
|
|
10
10
|
configuration do
|
11
11
|
setting :retry_count, 3
|
12
|
-
setting :retry_delay, 200 # NOTE: milliseconds
|
13
|
-
setting :retry_jitter, 25 # NOTE: milliseconds
|
14
|
-
setting :try_to_lock_timeout, 10 # NOTE: seconds
|
15
|
-
setting :default_lock_ttl, 5_000 # NOTE: milliseconds
|
16
|
-
setting :default_queue_ttl, 15 # NOTE: seconds
|
12
|
+
setting :retry_delay, 200 # NOTE: in milliseconds
|
13
|
+
setting :retry_jitter, 25 # NOTE: in milliseconds
|
14
|
+
setting :try_to_lock_timeout, 10 # NOTE: in seconds
|
15
|
+
setting :default_lock_ttl, 5_000 # NOTE: in milliseconds
|
16
|
+
setting :default_queue_ttl, 15 # NOTE: in seconds
|
17
17
|
setting :lock_release_batch_size, 100
|
18
18
|
setting :key_extraction_batch_size, 500
|
19
19
|
setting :instrumenter, RedisQueuedLocks::Instrument::VoidNotifier
|
20
20
|
setting :uniq_identifier, -> { RedisQueuedLocks::Resource.calc_uniq_identity }
|
21
|
-
|
22
|
-
# TODO: setting :logger, Logger.new(IO::NULL)
|
23
|
-
# TODO: setting :debug, true/false
|
21
|
+
setting :logger, RedisQueuedLocks::Logging::VoidLogger
|
24
22
|
|
25
23
|
validate('retry_count') { |val| val == nil || (val.is_a?(::Integer) && val >= 0) }
|
26
24
|
validate('retry_delay') { |val| val.is_a?(::Integer) && val >= 0 }
|
@@ -30,6 +28,7 @@ class RedisQueuedLocks::Client
|
|
30
28
|
validate('default_queue_ttl', :integer)
|
31
29
|
validate('lock_release_batch_size', :integer)
|
32
30
|
validate('instrumenter') { |val| RedisQueuedLocks::Instrument.valid_interface?(val) }
|
31
|
+
validate('logger') { |val| RedisQueuedLocks::Logging.valid_interface?(val) }
|
33
32
|
validate('uniq_identifier', :proc)
|
34
33
|
end
|
35
34
|
|
@@ -117,7 +116,7 @@ class RedisQueuedLocks::Client
|
|
117
116
|
metadata: nil,
|
118
117
|
&block
|
119
118
|
)
|
120
|
-
RedisQueuedLocks::Acquier.acquire_lock
|
119
|
+
RedisQueuedLocks::Acquier::AcquireLock.acquire_lock(
|
121
120
|
redis_client,
|
122
121
|
lock_name,
|
123
122
|
process_id: RedisQueuedLocks::Resource.get_process_id,
|
@@ -136,6 +135,7 @@ class RedisQueuedLocks::Client
|
|
136
135
|
identity:,
|
137
136
|
fail_fast:,
|
138
137
|
metadata:,
|
138
|
+
logger: config[:logger],
|
139
139
|
&block
|
140
140
|
)
|
141
141
|
end
|
@@ -183,7 +183,12 @@ class RedisQueuedLocks::Client
|
|
183
183
|
# @api public
|
184
184
|
# @since 0.1.0
|
185
185
|
def unlock(lock_name)
|
186
|
-
RedisQueuedLocks::Acquier.release_lock
|
186
|
+
RedisQueuedLocks::Acquier::ReleaseLock.release_lock(
|
187
|
+
redis_client,
|
188
|
+
lock_name,
|
189
|
+
config[:instrumenter],
|
190
|
+
config[:logger]
|
191
|
+
)
|
187
192
|
end
|
188
193
|
|
189
194
|
# @param lock_name [String]
|
@@ -192,7 +197,7 @@ class RedisQueuedLocks::Client
|
|
192
197
|
# @api public
|
193
198
|
# @since 0.1.0
|
194
199
|
def locked?(lock_name)
|
195
|
-
RedisQueuedLocks::Acquier.locked?(redis_client, lock_name)
|
200
|
+
RedisQueuedLocks::Acquier::IsLocked.locked?(redis_client, lock_name)
|
196
201
|
end
|
197
202
|
|
198
203
|
# @param lock_name [String]
|
@@ -201,7 +206,7 @@ class RedisQueuedLocks::Client
|
|
201
206
|
# @api public
|
202
207
|
# @since 0.1.0
|
203
208
|
def queued?(lock_name)
|
204
|
-
RedisQueuedLocks::Acquier.queued?(redis_client, lock_name)
|
209
|
+
RedisQueuedLocks::Acquier::IsQueued.queued?(redis_client, lock_name)
|
205
210
|
end
|
206
211
|
|
207
212
|
# @param lock_name [String]
|
@@ -210,7 +215,7 @@ class RedisQueuedLocks::Client
|
|
210
215
|
# @api public
|
211
216
|
# @since 0.1.0
|
212
217
|
def lock_info(lock_name)
|
213
|
-
RedisQueuedLocks::Acquier.lock_info(redis_client, lock_name)
|
218
|
+
RedisQueuedLocks::Acquier::LockInfo.lock_info(redis_client, lock_name)
|
214
219
|
end
|
215
220
|
|
216
221
|
# @param lock_name [String]
|
@@ -219,7 +224,7 @@ class RedisQueuedLocks::Client
|
|
219
224
|
# @api public
|
220
225
|
# @since 0.1.0
|
221
226
|
def queue_info(lock_name)
|
222
|
-
RedisQueuedLocks::Acquier.queue_info(redis_client, lock_name)
|
227
|
+
RedisQueuedLocks::Acquier::QueueInfo.queue_info(redis_client, lock_name)
|
223
228
|
end
|
224
229
|
|
225
230
|
# @param lock_name [String]
|
@@ -229,7 +234,12 @@ class RedisQueuedLocks::Client
|
|
229
234
|
# @api public
|
230
235
|
# @since 0.1.0
|
231
236
|
def extend_lock_ttl(lock_name, milliseconds)
|
232
|
-
RedisQueuedLocks::Acquier.extend_lock_ttl(
|
237
|
+
RedisQueuedLocks::Acquier::ExtendLockTTL.extend_lock_ttl(
|
238
|
+
redis_client,
|
239
|
+
lock_name,
|
240
|
+
milliseconds,
|
241
|
+
config[:logger]
|
242
|
+
)
|
233
243
|
end
|
234
244
|
|
235
245
|
# @option batch_size [Integer]
|
@@ -239,7 +249,12 @@ class RedisQueuedLocks::Client
|
|
239
249
|
# @api public
|
240
250
|
# @since 0.1.0
|
241
251
|
def clear_locks(batch_size: config[:lock_release_batch_size])
|
242
|
-
RedisQueuedLocks::Acquier.release_all_locks
|
252
|
+
RedisQueuedLocks::Acquier::ReleaseAllLocks.release_all_locks(
|
253
|
+
redis_client,
|
254
|
+
batch_size,
|
255
|
+
config[:instrumenter],
|
256
|
+
config[:logger]
|
257
|
+
)
|
243
258
|
end
|
244
259
|
|
245
260
|
# @option scan_size [Integer]
|
@@ -248,7 +263,7 @@ class RedisQueuedLocks::Client
|
|
248
263
|
# @api public
|
249
264
|
# @since 0.1.0
|
250
265
|
def locks(scan_size: config[:key_extraction_batch_size])
|
251
|
-
RedisQueuedLocks::Acquier.locks(redis_client, scan_size:)
|
266
|
+
RedisQueuedLocks::Acquier::Locks.locks(redis_client, scan_size:)
|
252
267
|
end
|
253
268
|
|
254
269
|
# @option scan_size [Integer]
|
@@ -257,7 +272,7 @@ class RedisQueuedLocks::Client
|
|
257
272
|
# @api public
|
258
273
|
# @since 0.1.0
|
259
274
|
def queues(scan_size: config[:key_extraction_batch_size])
|
260
|
-
RedisQueuedLocks::Acquier.queues(redis_client, scan_size:)
|
275
|
+
RedisQueuedLocks::Acquier::Queues.queues(redis_client, scan_size:)
|
261
276
|
end
|
262
277
|
|
263
278
|
# @option scan_size [Integer]
|
@@ -266,7 +281,7 @@ class RedisQueuedLocks::Client
|
|
266
281
|
# @api public
|
267
282
|
# @since 0.1.0
|
268
283
|
def keys(scan_size: config[:key_extraction_batch_size])
|
269
|
-
RedisQueuedLocks::Acquier.keys(redis_client, scan_size:)
|
284
|
+
RedisQueuedLocks::Acquier::Keys.keys(redis_client, scan_size:)
|
270
285
|
end
|
271
286
|
end
|
272
287
|
# rubocop:enable Metrics/ClassLength
|
@@ -8,7 +8,7 @@ module RedisQueuedLocks::Instrument::ActiveSupport
|
|
8
8
|
# @param payload [Hash<String|Symbol,Any>]
|
9
9
|
# @return [void]
|
10
10
|
#
|
11
|
-
# @api
|
11
|
+
# @api public
|
12
12
|
# @since 0.1.0
|
13
13
|
def notify(event, payload = {})
|
14
14
|
::ActiveSupport::Notifications.instrument(event, payload)
|
@@ -25,10 +25,10 @@ module RedisQueuedLocks::Instrument
|
|
25
25
|
m_obj = instrumenter.method(:notify)
|
26
26
|
m_sig = m_obj.parameters
|
27
27
|
|
28
|
-
f_prm = m_sig[0][0]
|
29
|
-
s_prm = m_sig[1][0]
|
30
|
-
|
31
28
|
if m_sig.size == 2
|
29
|
+
f_prm = m_sig[0][0]
|
30
|
+
s_prm = m_sig[1][0]
|
31
|
+
|
32
32
|
# rubocop:disable Layout/MultilineOperationIndentation
|
33
33
|
# NOTE: check the signature vairants
|
34
34
|
f_prm == :req && s_prm == :req ||
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api public
|
4
|
+
# @since 0.1.0
|
5
|
+
module RedisQueuedLocks::Logging::VoidLogger
|
6
|
+
class << self
|
7
|
+
# @api public
|
8
|
+
# @since 0.1.0
|
9
|
+
def warn(progname = nil, &block); end
|
10
|
+
|
11
|
+
# @api public
|
12
|
+
# @since 0.1.0
|
13
|
+
def unknown(progname = nil, &block); end
|
14
|
+
|
15
|
+
# @api public
|
16
|
+
# @since 0.1.0
|
17
|
+
def log(progname = nil, &block); end
|
18
|
+
|
19
|
+
# @api public
|
20
|
+
# @since 0.1.0
|
21
|
+
def info(progname = nil, &block); end
|
22
|
+
|
23
|
+
# @api public
|
24
|
+
# @since 0.1.0
|
25
|
+
def error(progname = nil, &block); end
|
26
|
+
|
27
|
+
# @api public
|
28
|
+
# @since 0.1.0
|
29
|
+
def fatal(progname = nil, &block); end
|
30
|
+
|
31
|
+
# @api public
|
32
|
+
# @since 0.1.0
|
33
|
+
def debug(progname = nil, &block); end
|
34
|
+
|
35
|
+
# @api public
|
36
|
+
# @since 0.1.0
|
37
|
+
def add(*, &block); end
|
38
|
+
|
39
|
+
# @api public
|
40
|
+
# @since 0.1.0
|
41
|
+
def <<(message); end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api public
|
4
|
+
# @since 0.1.0
|
5
|
+
module RedisQueuedLocks::Logging
|
6
|
+
require_relative 'logging/void_logger'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# @param logger [#debug]
|
10
|
+
# @return [Boolean]
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
# @since 0.1.0
|
14
|
+
def valid_interface?(logger)
|
15
|
+
return true if logger.is_a?(::Logger)
|
16
|
+
|
17
|
+
# NOTE: should provide `#debug` method.
|
18
|
+
return false unless logger.respond_to?(:debug)
|
19
|
+
|
20
|
+
# NOTE:
|
21
|
+
# `#debug` method should have appropriated signature `(progname, &block)`
|
22
|
+
# Required method signature (progname, &block):
|
23
|
+
# => [[:opt, :progname], [:block, :block]]
|
24
|
+
# => [[:req, :progname], [:block, :block]]
|
25
|
+
# => [[:opt, :progname]]
|
26
|
+
# => [[:req, :progname]]
|
27
|
+
# => [[:rest], [:block, :block]]
|
28
|
+
# => [[:rest]]
|
29
|
+
|
30
|
+
m_obj = logger.method(:debug)
|
31
|
+
m_sig = m_obj.parameters
|
32
|
+
|
33
|
+
if m_sig.size == 2
|
34
|
+
# => [[:opt, :progname], [:block, :block]]
|
35
|
+
# => [[:req, :progname], [:block, :block]]
|
36
|
+
# => [[:rest], [:block, :block]]
|
37
|
+
f_prm = m_sig[0][0]
|
38
|
+
s_prm = m_sig[1][0]
|
39
|
+
|
40
|
+
# rubocop:disable Layout/MultilineOperationIndentation
|
41
|
+
f_prm == :opt && s_prm == :block ||
|
42
|
+
f_prm == :req && s_prm == :block ||
|
43
|
+
f_prm == :rest && s_prm == :block
|
44
|
+
# rubocop:enable Layout/MultilineOperationIndentation
|
45
|
+
elsif m_sig.size == 1
|
46
|
+
# => [[:opt, :progname]]
|
47
|
+
# => [[:req, :progname]]
|
48
|
+
# => [[:rest]]
|
49
|
+
prm = m_sig[0][0]
|
50
|
+
|
51
|
+
prm == :opt || prm == :req || prm == :rest
|
52
|
+
else
|
53
|
+
false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 0.1.0
|
5
|
+
module RedisQueuedLocks::Utilities
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# @param block [Block]
|
9
|
+
# @return [Any]
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
# @since 0.1.0
|
13
|
+
def run_non_critical(&block)
|
14
|
+
yield rescue nil
|
15
|
+
end
|
16
|
+
end
|
data/lib/redis_queued_locks.rb
CHANGED
@@ -4,12 +4,15 @@ require 'redis-client'
|
|
4
4
|
require 'qonfig'
|
5
5
|
require 'timeout'
|
6
6
|
require 'securerandom'
|
7
|
+
require 'logger'
|
7
8
|
|
8
9
|
# @api public
|
9
10
|
# @since 0.1.0
|
10
11
|
module RedisQueuedLocks
|
11
12
|
require_relative 'redis_queued_locks/version'
|
12
13
|
require_relative 'redis_queued_locks/errors'
|
14
|
+
require_relative 'redis_queued_locks/utilities'
|
15
|
+
require_relative 'redis_queued_locks/logging'
|
13
16
|
require_relative 'redis_queued_locks/data'
|
14
17
|
require_relative 'redis_queued_locks/debugger'
|
15
18
|
require_relative 'redis_queued_locks/resource'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis_queued_locks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.22
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rustam Ibragimov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-03-
|
11
|
+
date: 2024-03-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
@@ -56,10 +56,21 @@ files:
|
|
56
56
|
- Rakefile
|
57
57
|
- lib/redis_queued_locks.rb
|
58
58
|
- lib/redis_queued_locks/acquier.rb
|
59
|
-
- lib/redis_queued_locks/acquier/
|
60
|
-
- lib/redis_queued_locks/acquier/
|
61
|
-
- lib/redis_queued_locks/acquier/
|
62
|
-
- lib/redis_queued_locks/acquier/
|
59
|
+
- lib/redis_queued_locks/acquier/acquire_lock.rb
|
60
|
+
- lib/redis_queued_locks/acquier/acquire_lock/delay_execution.rb
|
61
|
+
- lib/redis_queued_locks/acquier/acquire_lock/try_to_lock.rb
|
62
|
+
- lib/redis_queued_locks/acquier/acquire_lock/with_acq_timeout.rb
|
63
|
+
- lib/redis_queued_locks/acquier/acquire_lock/yield_with_expire.rb
|
64
|
+
- lib/redis_queued_locks/acquier/extend_lock_ttl.rb
|
65
|
+
- lib/redis_queued_locks/acquier/is_locked.rb
|
66
|
+
- lib/redis_queued_locks/acquier/is_queued.rb
|
67
|
+
- lib/redis_queued_locks/acquier/keys.rb
|
68
|
+
- lib/redis_queued_locks/acquier/lock_info.rb
|
69
|
+
- lib/redis_queued_locks/acquier/locks.rb
|
70
|
+
- lib/redis_queued_locks/acquier/queue_info.rb
|
71
|
+
- lib/redis_queued_locks/acquier/queues.rb
|
72
|
+
- lib/redis_queued_locks/acquier/release_all_locks.rb
|
73
|
+
- lib/redis_queued_locks/acquier/release_lock.rb
|
63
74
|
- lib/redis_queued_locks/client.rb
|
64
75
|
- lib/redis_queued_locks/data.rb
|
65
76
|
- lib/redis_queued_locks/debugger.rb
|
@@ -68,7 +79,10 @@ files:
|
|
68
79
|
- lib/redis_queued_locks/instrument.rb
|
69
80
|
- lib/redis_queued_locks/instrument/active_support.rb
|
70
81
|
- lib/redis_queued_locks/instrument/void_notifier.rb
|
82
|
+
- lib/redis_queued_locks/logging.rb
|
83
|
+
- lib/redis_queued_locks/logging/void_logger.rb
|
71
84
|
- lib/redis_queued_locks/resource.rb
|
85
|
+
- lib/redis_queued_locks/utilities.rb
|
72
86
|
- lib/redis_queued_locks/version.rb
|
73
87
|
- redis_queued_locks.gemspec
|
74
88
|
homepage: https://github.com/0exp/redis_queued_locks
|
@@ -1,60 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# @api private
|
4
|
-
# @since 0.1.0
|
5
|
-
module RedisQueuedLocks::Acquier::Release
|
6
|
-
# Realease the lock: clear the lock queue and expire the lock.
|
7
|
-
#
|
8
|
-
# @param redis [RedisClient]
|
9
|
-
# @param lock_key [String]
|
10
|
-
# @param lock_key_queue [String]
|
11
|
-
# @return [RedisQueuedLocks::Data,Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
|
12
|
-
#
|
13
|
-
# @api private
|
14
|
-
# @since 0.1.0
|
15
|
-
def fully_release_lock(redis, lock_key, lock_key_queue)
|
16
|
-
result = redis.multi do |transact|
|
17
|
-
transact.call('ZREMRANGEBYSCORE', lock_key_queue, '-inf', '+inf')
|
18
|
-
transact.call('EXPIRE', lock_key, '0')
|
19
|
-
end
|
20
|
-
|
21
|
-
RedisQueuedLocks::Data[ok: true, result:]
|
22
|
-
end
|
23
|
-
|
24
|
-
# Release all locks: clear all lock queus and expire all locks.
|
25
|
-
#
|
26
|
-
# @param redis [RedisClient]
|
27
|
-
# @param batch_size [Integer]
|
28
|
-
# @return [RedisQueuedLocks::Data,Hash<Symbol,Any>] Format: { ok: true/false, result: Any }
|
29
|
-
#
|
30
|
-
# @api private
|
31
|
-
# @since 0.1.0
|
32
|
-
def fully_release_all_locks(redis, batch_size)
|
33
|
-
result = redis.pipelined do |pipeline|
|
34
|
-
# Step A: release all queus and their related locks
|
35
|
-
redis.scan(
|
36
|
-
'MATCH',
|
37
|
-
RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN,
|
38
|
-
count: batch_size
|
39
|
-
) do |lock_queue|
|
40
|
-
# TODO: reduce unnecessary iterations
|
41
|
-
pipeline.call('ZREMRANGEBYSCORE', lock_queue, '-inf', '+inf')
|
42
|
-
pipeline.call('EXPIRE', RedisQueuedLocks::Resource.lock_key_from_queue(lock_queue), '0')
|
43
|
-
end
|
44
|
-
|
45
|
-
# Step B: release all locks
|
46
|
-
redis.scan(
|
47
|
-
'MATCH',
|
48
|
-
RedisQueuedLocks::Resource::LOCK_PATTERN,
|
49
|
-
count: batch_size
|
50
|
-
) do |lock_key|
|
51
|
-
# TODO: reduce unnecessary iterations
|
52
|
-
pipeline.call('EXPIRE', lock_key, '0')
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
rel_keys = result.count { |red_res| red_res == 0 }
|
57
|
-
|
58
|
-
RedisQueuedLocks::Data[ok: true, result: { rel_keys: rel_keys }]
|
59
|
-
end
|
60
|
-
end
|