redstruct 0.1.7 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +15 -11
- data/Rakefile +5 -5
- data/lib/redstruct/all.rb +14 -0
- data/lib/redstruct/configuration.rb +9 -6
- data/lib/redstruct/connection_proxy.rb +123 -0
- data/lib/redstruct/counter.rb +96 -0
- data/lib/redstruct/error.rb +2 -0
- data/lib/redstruct/factory/object.rb +31 -0
- data/lib/redstruct/factory.rb +94 -55
- data/lib/redstruct/hash.rb +123 -0
- data/lib/redstruct/list.rb +315 -0
- data/lib/redstruct/lock.rb +183 -0
- data/lib/redstruct/script.rb +104 -0
- data/lib/redstruct/set.rb +155 -0
- data/lib/redstruct/sorted_set/slice.rb +124 -0
- data/lib/redstruct/sorted_set.rb +153 -0
- data/lib/redstruct/string.rb +66 -0
- data/lib/redstruct/struct.rb +87 -0
- data/lib/redstruct/utils/coercion.rb +14 -8
- data/lib/redstruct/utils/inspectable.rb +8 -4
- data/lib/redstruct/utils/iterable.rb +52 -0
- data/lib/redstruct/utils/scriptable.rb +32 -6
- data/lib/redstruct/version.rb +4 -1
- data/lib/redstruct.rb +17 -51
- data/lib/yard/defscript_handler.rb +5 -3
- data/test/redstruct/configuration_test.rb +13 -0
- data/test/redstruct/connection_proxy_test.rb +85 -0
- data/test/redstruct/counter_test.rb +108 -0
- data/test/redstruct/factory/object_test.rb +21 -0
- data/test/redstruct/factory_test.rb +136 -0
- data/test/redstruct/hash_test.rb +138 -0
- data/test/redstruct/list_test.rb +244 -0
- data/test/redstruct/lock_test.rb +108 -0
- data/test/redstruct/script_test.rb +53 -0
- data/test/redstruct/set_test.rb +219 -0
- data/test/redstruct/sorted_set/slice_test.rb +10 -0
- data/test/redstruct/sorted_set_test.rb +219 -0
- data/test/redstruct/string_test.rb +8 -0
- data/test/redstruct/struct_test.rb +61 -0
- data/test/redstruct/utils/coercion_test.rb +33 -0
- data/test/redstruct/utils/inspectable_test.rb +31 -0
- data/test/redstruct/utils/iterable_test.rb +94 -0
- data/test/redstruct/utils/scriptable_test.rb +67 -0
- data/test/redstruct_test.rb +14 -0
- data/test/test_helper.rb +77 -1
- metadata +58 -26
- data/lib/redstruct/connection.rb +0 -47
- data/lib/redstruct/factory/creation.rb +0 -95
- data/lib/redstruct/factory/deserialization.rb +0 -7
- data/lib/redstruct/hls/lock.rb +0 -175
- data/lib/redstruct/hls/queue.rb +0 -29
- data/lib/redstruct/hls.rb +0 -2
- data/lib/redstruct/types/base.rb +0 -36
- data/lib/redstruct/types/counter.rb +0 -65
- data/lib/redstruct/types/hash.rb +0 -72
- data/lib/redstruct/types/list.rb +0 -76
- data/lib/redstruct/types/script.rb +0 -56
- data/lib/redstruct/types/set.rb +0 -96
- data/lib/redstruct/types/sorted_set.rb +0 -129
- data/lib/redstruct/types/string.rb +0 -64
- data/lib/redstruct/types/struct.rb +0 -58
- data/lib/releaser/logger.rb +0 -15
- data/lib/releaser/repository.rb +0 -32
- data/lib/tasks/release.rake +0 -49
- data/test/redstruct/restruct_test.rb +0 -4
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'test_helper'
|
6
|
+
|
7
|
+
module Redstruct
|
8
|
+
module Utils
|
9
|
+
class ScriptableTest < Redstruct::Test
|
10
|
+
def test_defscript
|
11
|
+
script = <<~LUA
|
12
|
+
local sum = 0
|
13
|
+
for i, key in ipairs(KEYS) do
|
14
|
+
local value = tonumber(ARGV[i])
|
15
|
+
redis.call("set", key, value)
|
16
|
+
sum = sum + value
|
17
|
+
end
|
18
|
+
|
19
|
+
return sum
|
20
|
+
LUA
|
21
|
+
script = script.strip
|
22
|
+
sha1 = Digest::SHA1.hexdigest(script)
|
23
|
+
|
24
|
+
klass = Class.new(Redstruct::Factory::Object) do
|
25
|
+
include Redstruct::Utils::Scriptable
|
26
|
+
defscript :test, script
|
27
|
+
end
|
28
|
+
|
29
|
+
assert_equal({ script: script, sha1: sha1 }, klass::SCRIPT_TEST, 'should have defined the constant properly')
|
30
|
+
|
31
|
+
factory = create_factory
|
32
|
+
keys = [factory.string(SecureRandom.hex(4)).key, factory.string(SecureRandom.hex(4)).key]
|
33
|
+
argv = [SecureRandom.random_number(10), SecureRandom.random_number(10)]
|
34
|
+
assert_equal argv.reduce(&:+), klass.new(factory: factory).test(keys: keys, argv: argv)
|
35
|
+
|
36
|
+
keys.each_with_index do |key, index|
|
37
|
+
assert_equal argv[index], factory.string(key).get.to_i, 'script should have set the value properly'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_const_defined
|
42
|
+
klass = Class.new { include Redstruct::Utils::Scriptable }
|
43
|
+
klass.const_set('SCRIPT_TEST', true)
|
44
|
+
|
45
|
+
stdout, = capture_subprocess_io { klass.defscript('test', 'return 0') }
|
46
|
+
assert_match(/WARN/, stdout, 'should produce a warning if the constant is already defined')
|
47
|
+
refute klass.method_defined?('test')
|
48
|
+
assert_equal true, klass::SCRIPT_TEST, 'should still be the old value'
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_method_defined
|
52
|
+
klass = Class.new do
|
53
|
+
include Redstruct::Utils::Scriptable
|
54
|
+
|
55
|
+
def test
|
56
|
+
return true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
stdout, = capture_subprocess_io { klass.defscript('test', 'return 0') }
|
61
|
+
assert_match(/WARN/, stdout, 'should produce a warning if the method is already defined')
|
62
|
+
refute klass.const_defined?('SCRIPT_TEST')
|
63
|
+
assert_equal true, klass.new.test, 'should still be the old value'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class RedstructTest < Redstruct::Test
|
6
|
+
# Test singleton property of the config
|
7
|
+
def test_config
|
8
|
+
config = Redstruct.config
|
9
|
+
assert_kind_of Redstruct::Configuration, config, 'Should return an instance of the configuration class'
|
10
|
+
|
11
|
+
reference = Redstruct.config
|
12
|
+
assert_equal config, reference, 'Should be a singleton object'
|
13
|
+
end
|
14
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -1,4 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
4
|
+
require 'securerandom'
|
2
5
|
require 'bundler/setup'
|
3
|
-
require 'redstruct'
|
4
6
|
require 'minitest/autorun'
|
7
|
+
require 'flexmock/minitest'
|
8
|
+
|
9
|
+
ci_build = ENV['CI_BUILD'].to_i.positive?
|
10
|
+
|
11
|
+
bundler_groups = %i[default test]
|
12
|
+
bundler_groups << (ci_build ? :ci : :debug)
|
13
|
+
Bundler.require(*bundler_groups)
|
14
|
+
|
15
|
+
# Start coverage
|
16
|
+
Codacy::Reporter.start if ci_build
|
17
|
+
|
18
|
+
# Default Redstruct config
|
19
|
+
require 'redstruct/all'
|
20
|
+
Redstruct.config.default_namespace = "redstruct:test:#{SecureRandom.uuid}"
|
21
|
+
Redstruct.config.default_connection = ConnectionPool.new(size: 5, timeout: 2) do
|
22
|
+
Redis.new(host: ENV.fetch('REDIS_HOST', '127.0.0.1'), port: ENV.fetch('REDIS_PORT', 6379).to_i, db: ENV.fetch('REDIS_DB', 0).to_i)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Setup cleanup hook
|
26
|
+
Minitest.after_run do
|
27
|
+
Redstruct.config.default_connection.with do |conn|
|
28
|
+
conn.flushdb
|
29
|
+
conn.script(:flush)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Small class used to generate thread-safe sequence when creating per-test
|
34
|
+
# factories
|
35
|
+
class AtomicInteger
|
36
|
+
def initialize
|
37
|
+
@lock = Mutex.new
|
38
|
+
@current = 0
|
39
|
+
end
|
40
|
+
|
41
|
+
def incr
|
42
|
+
value = nil
|
43
|
+
@lock.synchronize do
|
44
|
+
value = @current
|
45
|
+
@current += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
return value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module Redstruct
|
53
|
+
# Base class for all Redstruct tests. Configures the gem, provides a default factory, and makes sure to clean it up
|
54
|
+
# at the end
|
55
|
+
class Test < Minitest::Test
|
56
|
+
@@counter = AtomicInteger.new # rubocop: disable Style/ClassVars
|
57
|
+
|
58
|
+
parallelize_me!
|
59
|
+
make_my_diffs_pretty!
|
60
|
+
|
61
|
+
# Use this helper to create a factory that the test class will keep track of and remove at the end
|
62
|
+
def create_factory(namespace = nil)
|
63
|
+
namespace ||= "#{Redstruct.config.default_namespace}:#{@@counter.incr}"
|
64
|
+
return Redstruct::Factory.new(namespace: namespace)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Helper when trying to ensure a particular redis-rb command was called
|
68
|
+
# while still calling it. This allows for testing things outside of our
|
69
|
+
# control (e.g. srandmember returning random items)
|
70
|
+
# The reason we don't simply just mock the return value is to ensure
|
71
|
+
# that tests will break if a command (e.g. srandmember) changes its return
|
72
|
+
# value
|
73
|
+
def ensure_command_called(object, command, *args, allow: true)
|
74
|
+
mock = flexmock(object.connection).should_receive(command).with(object.key, *args)
|
75
|
+
mock = mock.pass_thru if allow
|
76
|
+
|
77
|
+
return mock
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redstruct
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nicolas Pepin-Perreault
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-05-
|
11
|
+
date: 2017-05-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -50,14 +50,14 @@ dependencies:
|
|
50
50
|
requirements:
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '1.
|
53
|
+
version: '1.13'
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version: '1.
|
60
|
+
version: '1.13'
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
62
|
name: rake
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -78,14 +78,14 @@ dependencies:
|
|
78
78
|
requirements:
|
79
79
|
- - "~>"
|
80
80
|
- !ruby/object:Gem::Version
|
81
|
-
version: '5.
|
81
|
+
version: '5.1'
|
82
82
|
type: :development
|
83
83
|
prerelease: false
|
84
84
|
version_requirements: !ruby/object:Gem::Requirement
|
85
85
|
requirements:
|
86
86
|
- - "~>"
|
87
87
|
- !ruby/object:Gem::Version
|
88
|
-
version: '5.
|
88
|
+
version: '5.1'
|
89
89
|
description: Provides higher level data structures in Ruby using standard Redis commands.
|
90
90
|
Also provides basic object mapping for pre-existing types.
|
91
91
|
email:
|
@@ -97,33 +97,47 @@ files:
|
|
97
97
|
- README.md
|
98
98
|
- Rakefile
|
99
99
|
- lib/redstruct.rb
|
100
|
+
- lib/redstruct/all.rb
|
100
101
|
- lib/redstruct/configuration.rb
|
101
|
-
- lib/redstruct/
|
102
|
+
- lib/redstruct/connection_proxy.rb
|
103
|
+
- lib/redstruct/counter.rb
|
102
104
|
- lib/redstruct/error.rb
|
103
105
|
- lib/redstruct/factory.rb
|
104
|
-
- lib/redstruct/factory/
|
105
|
-
- lib/redstruct/
|
106
|
-
- lib/redstruct/
|
107
|
-
- lib/redstruct/
|
108
|
-
- lib/redstruct/
|
109
|
-
- lib/redstruct/
|
110
|
-
- lib/redstruct/
|
111
|
-
- lib/redstruct/
|
112
|
-
- lib/redstruct/
|
113
|
-
- lib/redstruct/
|
114
|
-
- lib/redstruct/types/set.rb
|
115
|
-
- lib/redstruct/types/sorted_set.rb
|
116
|
-
- lib/redstruct/types/string.rb
|
117
|
-
- lib/redstruct/types/struct.rb
|
106
|
+
- lib/redstruct/factory/object.rb
|
107
|
+
- lib/redstruct/hash.rb
|
108
|
+
- lib/redstruct/list.rb
|
109
|
+
- lib/redstruct/lock.rb
|
110
|
+
- lib/redstruct/script.rb
|
111
|
+
- lib/redstruct/set.rb
|
112
|
+
- lib/redstruct/sorted_set.rb
|
113
|
+
- lib/redstruct/sorted_set/slice.rb
|
114
|
+
- lib/redstruct/string.rb
|
115
|
+
- lib/redstruct/struct.rb
|
118
116
|
- lib/redstruct/utils/coercion.rb
|
119
117
|
- lib/redstruct/utils/inspectable.rb
|
118
|
+
- lib/redstruct/utils/iterable.rb
|
120
119
|
- lib/redstruct/utils/scriptable.rb
|
121
120
|
- lib/redstruct/version.rb
|
122
|
-
- lib/releaser/logger.rb
|
123
|
-
- lib/releaser/repository.rb
|
124
|
-
- lib/tasks/release.rake
|
125
121
|
- lib/yard/defscript_handler.rb
|
126
|
-
- test/redstruct/
|
122
|
+
- test/redstruct/configuration_test.rb
|
123
|
+
- test/redstruct/connection_proxy_test.rb
|
124
|
+
- test/redstruct/counter_test.rb
|
125
|
+
- test/redstruct/factory/object_test.rb
|
126
|
+
- test/redstruct/factory_test.rb
|
127
|
+
- test/redstruct/hash_test.rb
|
128
|
+
- test/redstruct/list_test.rb
|
129
|
+
- test/redstruct/lock_test.rb
|
130
|
+
- test/redstruct/script_test.rb
|
131
|
+
- test/redstruct/set_test.rb
|
132
|
+
- test/redstruct/sorted_set/slice_test.rb
|
133
|
+
- test/redstruct/sorted_set_test.rb
|
134
|
+
- test/redstruct/string_test.rb
|
135
|
+
- test/redstruct/struct_test.rb
|
136
|
+
- test/redstruct/utils/coercion_test.rb
|
137
|
+
- test/redstruct/utils/inspectable_test.rb
|
138
|
+
- test/redstruct/utils/iterable_test.rb
|
139
|
+
- test/redstruct/utils/scriptable_test.rb
|
140
|
+
- test/redstruct_test.rb
|
127
141
|
- test/test_helper.rb
|
128
142
|
homepage: https://npepinpe.github.com/redstruct/
|
129
143
|
licenses:
|
@@ -150,5 +164,23 @@ signing_key:
|
|
150
164
|
specification_version: 4
|
151
165
|
summary: Higher level data structures for Redis.
|
152
166
|
test_files:
|
153
|
-
- test/redstruct/
|
167
|
+
- test/redstruct/configuration_test.rb
|
168
|
+
- test/redstruct/connection_proxy_test.rb
|
169
|
+
- test/redstruct/counter_test.rb
|
170
|
+
- test/redstruct/factory/object_test.rb
|
171
|
+
- test/redstruct/factory_test.rb
|
172
|
+
- test/redstruct/hash_test.rb
|
173
|
+
- test/redstruct/list_test.rb
|
174
|
+
- test/redstruct/lock_test.rb
|
175
|
+
- test/redstruct/script_test.rb
|
176
|
+
- test/redstruct/set_test.rb
|
177
|
+
- test/redstruct/sorted_set/slice_test.rb
|
178
|
+
- test/redstruct/sorted_set_test.rb
|
179
|
+
- test/redstruct/string_test.rb
|
180
|
+
- test/redstruct/struct_test.rb
|
181
|
+
- test/redstruct/utils/coercion_test.rb
|
182
|
+
- test/redstruct/utils/inspectable_test.rb
|
183
|
+
- test/redstruct/utils/iterable_test.rb
|
184
|
+
- test/redstruct/utils/scriptable_test.rb
|
185
|
+
- test/redstruct_test.rb
|
154
186
|
- test/test_helper.rb
|
data/lib/redstruct/connection.rb
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
module Redstruct
|
2
|
-
class Connection
|
3
|
-
# @return [Array<Symbol>] List of methods from the Redis class that we don't want to delegate to.
|
4
|
-
NON_COMMAND_METHODS = [:[], :[]=, :_eval, :_scan, :method_missing, :call, :dup, :inspect, :to_s].freeze
|
5
|
-
|
6
|
-
attr_reader :pool
|
7
|
-
|
8
|
-
def initialize(pool)
|
9
|
-
raise(Redstruct::Error, 'Requires a ConnectionPool to proxy to') unless pool.is_a?(ConnectionPool)
|
10
|
-
@pool = pool
|
11
|
-
end
|
12
|
-
|
13
|
-
# Executes the given block by first fixing a thread local connection from the pool,
|
14
|
-
# such that all redis commands executed within the block are on the same connection.
|
15
|
-
# This is necessary when doing pipelining, or multi/exec stuff
|
16
|
-
# @return [Object] whatever the passed block evaluates to, nil otherwise
|
17
|
-
def with
|
18
|
-
result = nil
|
19
|
-
@pool.with do |c|
|
20
|
-
begin
|
21
|
-
Thread.current[:__redstruct_connection] = c
|
22
|
-
result = yield(c) if block_given?
|
23
|
-
ensure
|
24
|
-
Thread.current[:__redstruct_connection] = nil
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
return result
|
29
|
-
end
|
30
|
-
|
31
|
-
# While slower on load, defining all methods that we want to pipe to one of the connections results in
|
32
|
-
# faster calls at runtime, and gives us the convenience of not going through the pool.with everytime.
|
33
|
-
Redis.public_instance_methods(false).each do |method|
|
34
|
-
next if NON_COMMAND_METHODS.include?(method)
|
35
|
-
class_eval <<~METHOD, __FILE__, __LINE__ + 1
|
36
|
-
def #{method}(*args, &block)
|
37
|
-
connection = Thread.current[:__redstruct_connection]
|
38
|
-
if connection.nil?
|
39
|
-
with { |c| c.#{method}(*args, &block) }
|
40
|
-
else
|
41
|
-
return connection.#{method}(*args, &block)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
METHOD
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
@@ -1,95 +0,0 @@
|
|
1
|
-
module Redstruct
|
2
|
-
class Factory
|
3
|
-
# Module to hold all the factory creation methods.
|
4
|
-
module Creation
|
5
|
-
# Builds a struct with the given key (namespaced) and sharing the factory connection
|
6
|
-
# Building a struct is only really useful if you plan on making only basic operations,
|
7
|
-
# such as delete, expire, etc. It is however recommended to always build your objects
|
8
|
-
# in the same way, e.g. if it's a lock, use Factory#lock
|
9
|
-
# @param [::String] key base key to use
|
10
|
-
# @return [Redstruct::Types::Struct] base struct pointing to that key
|
11
|
-
def struct(key, **options)
|
12
|
-
return create(Redstruct::Types::Struct, key, **options)
|
13
|
-
end
|
14
|
-
|
15
|
-
# Builds a Redis string struct from the key
|
16
|
-
# @param [::String] key base key to use
|
17
|
-
# @return [Redstruct::Types::String]
|
18
|
-
def string(key, **options)
|
19
|
-
return create(Redstruct::Types::String, key, **options)
|
20
|
-
end
|
21
|
-
|
22
|
-
# Builds a Redis list struct from the key
|
23
|
-
# @param [::String] key base key to use
|
24
|
-
# @return [Redstruct::Types::List]
|
25
|
-
def list(key, **options)
|
26
|
-
return create(Redstruct::Types::List, key, **options)
|
27
|
-
end
|
28
|
-
|
29
|
-
# Builds a Redis set struct from the key
|
30
|
-
# @param [::String] key base key to use
|
31
|
-
# @return [Redstruct::Types::Set]
|
32
|
-
def set(key, **options)
|
33
|
-
return create(Redstruct::Types::Set, key, **options)
|
34
|
-
end
|
35
|
-
|
36
|
-
# Builds a Redis sorted set (zset) struct from the key
|
37
|
-
# @param [::String] key base key to use
|
38
|
-
# @return [Redstruct::Types::SortedSet]
|
39
|
-
def sorted_set(key, **options)
|
40
|
-
return create(Redstruct::Types::SortedSet, key, **options)
|
41
|
-
end
|
42
|
-
|
43
|
-
# Builds a Redis hash struct from the key
|
44
|
-
# @param [::String] key base key to use
|
45
|
-
# @return [Redstruct::Types::Hash]
|
46
|
-
def hash(key, **options)
|
47
|
-
return create(Redstruct::Types::Hash, key, **options)
|
48
|
-
end
|
49
|
-
|
50
|
-
# Builds a Redis backed lock from the key
|
51
|
-
# @param [::String] key base key to use
|
52
|
-
# @return [Redstruct::Hls::Lock]
|
53
|
-
def lock(key, **options)
|
54
|
-
return create(Redstruct::Hls::Lock, key, **options)
|
55
|
-
end
|
56
|
-
|
57
|
-
# Builds a Redis counter struct from the key
|
58
|
-
# @param [::String] key base key to use
|
59
|
-
# @return [Redstruct::Types::Counter]
|
60
|
-
def counter(key, **options)
|
61
|
-
return create(Redstruct::Types::Counter, key, **options)
|
62
|
-
end
|
63
|
-
|
64
|
-
# Builds a Redis backed queue from the key
|
65
|
-
# @param [::String] key base key to use
|
66
|
-
# @return [Redstruct::Hls::Queue]
|
67
|
-
def queue(key)
|
68
|
-
return create(Redstruct::Hls::Queue, key)
|
69
|
-
end
|
70
|
-
|
71
|
-
# @todo The script cache is actually based on the database you will connect to. Therefore, it might be smarter to move it to the connection used?
|
72
|
-
# Caveat: if the script with the given ID exists in the cache, we don't bother updating it.
|
73
|
-
# So if the script actually changed since the first call, the one sent during the first call will
|
74
|
-
def script(id, script)
|
75
|
-
return @script_cache.synchronize do
|
76
|
-
@script_cache[id] = Redstruct::Types::Script.new(key: id, script: script, factory: self) if @script_cache[id].nil?
|
77
|
-
@script_cache[id]
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
# Returns a factory with an isolated namespace.
|
82
|
-
# @example Given a factory `f` with namespace fact:first
|
83
|
-
# f.factory('second') # => Redstruct::Factory: namespace: <"fact:first:second">, script_cache: <[]>
|
84
|
-
# @return [Factory] namespaced factory
|
85
|
-
def factory(namespace)
|
86
|
-
return self.class.new(connection: @connection, namespace: isolate(namespace))
|
87
|
-
end
|
88
|
-
|
89
|
-
def create(type, key, **options)
|
90
|
-
return type.new(key: isolate(key), factory: self, **options)
|
91
|
-
end
|
92
|
-
private :create
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
data/lib/redstruct/hls/lock.rb
DELETED
@@ -1,175 +0,0 @@
|
|
1
|
-
require 'securerandom'
|
2
|
-
|
3
|
-
module Redstruct
|
4
|
-
module Hls
|
5
|
-
# Implementation of a simple binary lock (locked/not locked), with option to block and wait for the lock.
|
6
|
-
# Uses two redis structures: a string for the lease, and a list for blocking operations.
|
7
|
-
# @see #acquire
|
8
|
-
# @see #release
|
9
|
-
# @see #locked
|
10
|
-
# @attr_reader [::String, nil] token the current token or nil
|
11
|
-
# @attr_reader [Fixnum] expiry expiry of the underlying redis structures in milliseconds
|
12
|
-
# @attr_reader [Fixnum, nil] timeout the timeout to wait when attempting to acquire the lock, in seconds
|
13
|
-
class Lock < Redstruct::Types::Base
|
14
|
-
include Redstruct::Utils::Scriptable, Redstruct::Utils::Coercion
|
15
|
-
|
16
|
-
# The default expiry on the underlying redis keys, in milliseconds
|
17
|
-
DEFAULT_EXPIRY = 1000
|
18
|
-
|
19
|
-
# The default timeout when blocking, in seconds; a nil value means it is non-blocking
|
20
|
-
DEFAULT_TIMEOUT = nil
|
21
|
-
|
22
|
-
attr_reader :token, :expiry, :timeout
|
23
|
-
|
24
|
-
# @param [Integer] expiry in milliseconds; to prevent infinite locking, each mutex is released after a certain expiry time
|
25
|
-
# @param [Integer] timeout in seconds; if > 0, will block for this amount of time when trying to obtain the lock
|
26
|
-
def initialize(expiry: DEFAULT_EXPIRY, timeout: DEFAULT_TIMEOUT, **options)
|
27
|
-
super(**options)
|
28
|
-
|
29
|
-
@token = nil
|
30
|
-
@expiry = expiry
|
31
|
-
@timeout = timeout.to_i
|
32
|
-
|
33
|
-
create do |factory|
|
34
|
-
@lease = factory.string('lease')
|
35
|
-
@tokens = factory.list('tokens')
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# Executes the given block if the lock can be acquired
|
40
|
-
# @yield Block to be executed if the lock is acquired
|
41
|
-
def locked
|
42
|
-
yield if acquire
|
43
|
-
ensure
|
44
|
-
release
|
45
|
-
end
|
46
|
-
|
47
|
-
# Whether or not the lock will block when attempting to acquire it
|
48
|
-
# @return [Boolean]
|
49
|
-
def blocking?
|
50
|
-
return @timeout.positive?
|
51
|
-
end
|
52
|
-
|
53
|
-
# Attempts to acquire the lock. First attempts to grab the lease (a redis string).
|
54
|
-
# If the current token is already the lease token, the lock is considered acquired.
|
55
|
-
# If there is no current lease, then sets it to the current token.
|
56
|
-
# If there is a current lease that is not the current token, then:
|
57
|
-
# 1) If this not a blocking lock (see Lock#blocking?), return false
|
58
|
-
# 2) If this is a blocking lock, block and wait for the next token to be pushed on the tokens list
|
59
|
-
# 3) If a token was pushed, set it as our token and refresh the expiry
|
60
|
-
# @return [Boolean] True if acquired, false otherwise
|
61
|
-
def acquire
|
62
|
-
acquired = false
|
63
|
-
token = non_blocking_acquire(@token)
|
64
|
-
token = blocking_acquire if token.nil? && blocking?
|
65
|
-
|
66
|
-
unless token.nil?
|
67
|
-
@token = token
|
68
|
-
acquired = true
|
69
|
-
end
|
70
|
-
|
71
|
-
return acquired
|
72
|
-
end
|
73
|
-
|
74
|
-
# Releases the lock only if the current token is the value of the lease.
|
75
|
-
# If the lock is a blocking lock (see Lock#blocking?), push the next token on the tokens list.
|
76
|
-
# @return [Boolean] True if released, false otherwise
|
77
|
-
def release
|
78
|
-
return false if @token.nil?
|
79
|
-
|
80
|
-
next_token = SecureRandom.uuid
|
81
|
-
return coerce_bool(release_script(keys: [@lease.key, @tokens.key], argv: [@token, next_token, @expiry]))
|
82
|
-
end
|
83
|
-
|
84
|
-
def non_blocking_acquire(token = nil)
|
85
|
-
token ||= generate_token
|
86
|
-
return acquire_script(keys: @lease.key, argv: [token, @expiry])
|
87
|
-
end
|
88
|
-
private :non_blocking_acquire
|
89
|
-
|
90
|
-
def blocking_acquire
|
91
|
-
timeout = @timeout == Float::INFINITY ? 0 : @timeout
|
92
|
-
token = @tokens.pop(timeout: timeout)
|
93
|
-
|
94
|
-
# Attempt to reacquire in a non blocking way to:
|
95
|
-
# 1) assert we do own the lock (edge case)
|
96
|
-
# 2) touch the lock expiry
|
97
|
-
token = non_blocking_acquire(token) unless token.nil?
|
98
|
-
|
99
|
-
return token
|
100
|
-
end
|
101
|
-
private :blocking_acquire
|
102
|
-
|
103
|
-
# The acquire script attempts to set the lease (keys[1]) to the given token (argv[1]), only
|
104
|
-
# if it wasn't already set. It then compares to check if the value of the lease is that of the token,
|
105
|
-
# and if so refreshes the expiry (argv[2]) time of the lease.
|
106
|
-
# @param [Array<(::String)>] keys The lease key specifying who owns the mutex at the moment
|
107
|
-
# @param [Array<(::String, Fixnum)>] argv The current token; the expiry time in milliseconds
|
108
|
-
# @return [::String] Returns the token if acquired, nil otherwise.
|
109
|
-
defscript :acquire_script, <<~LUA
|
110
|
-
local token = ARGV[1]
|
111
|
-
local expiry = tonumber(ARGV[2])
|
112
|
-
|
113
|
-
redis.call('set', KEYS[1], token, 'NX')
|
114
|
-
if redis.call('get', KEYS[1]) == token then
|
115
|
-
redis.call('pexpire', KEYS[1], expiry)
|
116
|
-
return token
|
117
|
-
end
|
118
|
-
|
119
|
-
return false
|
120
|
-
LUA
|
121
|
-
|
122
|
-
# The release script compares the given token (argv[1]) with the lease value (keys[1]); if they are the same,
|
123
|
-
# then a new token (argv[2]) is set as the lease, and pushed on the tokens (keys[2]) list
|
124
|
-
# for the next acquire request.
|
125
|
-
# @param [Array<(::String, ::String)>] keys The lease key; the tokens list key
|
126
|
-
# @param [Array<(::String, ::String, Fixnum)>] argv The current token; the next token to push; the expiry time of both keys
|
127
|
-
# @return [Fixnum] 1 if released, 0 otherwise
|
128
|
-
defscript :release_script, <<~LUA
|
129
|
-
local currentToken = ARGV[1]
|
130
|
-
local nextToken = ARGV[2]
|
131
|
-
local expiry = tonumber(ARGV[3])
|
132
|
-
|
133
|
-
if redis.call('get', KEYS[1]) == currentToken then
|
134
|
-
redis.call('set', KEYS[1], nextToken, 'PX', expiry)
|
135
|
-
redis.call('lpush', KEYS[2], nextToken)
|
136
|
-
redis.call('pexpire', KEYS[2], expiry)
|
137
|
-
return true
|
138
|
-
end
|
139
|
-
|
140
|
-
return false
|
141
|
-
LUA
|
142
|
-
|
143
|
-
# @!group Serialization
|
144
|
-
# Returns a hash representation of the object
|
145
|
-
# @see Lock#from_h
|
146
|
-
# @return [Hash<Symbol, Object>] hash representation of the lock
|
147
|
-
def to_h
|
148
|
-
return super.merge(token: @token, expiry: @expiry, timeout: @timeout)
|
149
|
-
end
|
150
|
-
|
151
|
-
class << self
|
152
|
-
# Builds a lock from a hash.
|
153
|
-
# @see Lock#to_h
|
154
|
-
# @see Factory#create_from_h
|
155
|
-
# @param [Hash] hash hash generated by calling Lock#to_h. Ensure beforehand that keys are symbols.
|
156
|
-
# @return [Lock]
|
157
|
-
def from_h(hash, factory)
|
158
|
-
hash[:factory] = factory
|
159
|
-
return new(**hash)
|
160
|
-
end
|
161
|
-
end
|
162
|
-
# @!endgroup
|
163
|
-
|
164
|
-
def generate_token
|
165
|
-
return SecureRandom.uuid
|
166
|
-
end
|
167
|
-
private :generate_token
|
168
|
-
|
169
|
-
# Helper method for easy inspection
|
170
|
-
def inspectable_attributes
|
171
|
-
super.merge(expiry: @expiry, blocking: blocking?)
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
data/lib/redstruct/hls/queue.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
module Redstruct
|
2
|
-
module Hls
|
3
|
-
class Queue < Redstruct::Types::List
|
4
|
-
include Redstruct::Utils::Scriptable
|
5
|
-
|
6
|
-
def enqueue(*elements)
|
7
|
-
self.connection.rpush(@key, elements)
|
8
|
-
end
|
9
|
-
|
10
|
-
def dequeue(length: 1)
|
11
|
-
elements = dequeue_script(keys: @key, argv: length)
|
12
|
-
length == 1 ? elements.first : elements
|
13
|
-
end
|
14
|
-
|
15
|
-
# Dequeues up to argv[1] amount of items from the list at keys[1]
|
16
|
-
# @param [Array<(::String)>] keys The key of the list to dequeue from
|
17
|
-
# @param [Array<(Fixnum)>] argv The number of items to dequeue
|
18
|
-
# @return [Array] An array of items dequeued or an empty array
|
19
|
-
defscript :dequeue_script, <<~LUA
|
20
|
-
local length = tonumber(ARGV[1])
|
21
|
-
local elements = redis.call('lrange', KEYS[1], 0, length - 1)
|
22
|
-
redis.call('ltrim', KEYS[1], length, -1)
|
23
|
-
|
24
|
-
return elements
|
25
|
-
LUA
|
26
|
-
protected :dequeue_script
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
data/lib/redstruct/hls.rb
DELETED