redstruct 0.1.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 +7 -0
- data/README.md +42 -0
- data/Rakefile +18 -0
- data/lib/redstruct/configuration.rb +14 -0
- data/lib/redstruct/connection.rb +29 -0
- data/lib/redstruct/error.rb +5 -0
- data/lib/redstruct/factory/creation.rb +95 -0
- data/lib/redstruct/factory/deserialization.rb +7 -0
- data/lib/redstruct/factory.rb +45 -0
- data/lib/redstruct/hls/lock.rb +175 -0
- data/lib/redstruct/hls/queue.rb +29 -0
- data/lib/redstruct/hls.rb +2 -0
- data/lib/redstruct/types/base.rb +47 -0
- data/lib/redstruct/types/counter.rb +65 -0
- data/lib/redstruct/types/hash.rb +72 -0
- data/lib/redstruct/types/list.rb +78 -0
- data/lib/redstruct/types/script.rb +56 -0
- data/lib/redstruct/types/set.rb +96 -0
- data/lib/redstruct/types/sorted_set.rb +15 -0
- data/lib/redstruct/types/string.rb +64 -0
- data/lib/redstruct/types/struct.rb +41 -0
- data/lib/redstruct/utils/coercion.rb +33 -0
- data/lib/redstruct/utils/inspectable.rb +21 -0
- data/lib/redstruct/utils/scriptable.rb +21 -0
- data/lib/redstruct/version.rb +3 -0
- data/lib/redstruct/yard/defscript_handler.rb +32 -0
- data/lib/redstruct.rb +65 -0
- data/test/redstruct/restruct_test.rb +4 -0
- data/test/test_helper.rb +4 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f2a542b5597811e35ed17b643ae0df3fb01fb226
|
4
|
+
data.tar.gz: bc58fb20c1cc5d2b4a502c3ba085f2d2d1304056
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8a98538b8e61c874e1b42d487d46a83176cb563ede67eefa78131e577f6a16627678658fd314e11a7981c0c6f7ab0e9022ca82c61e6723e4620c0a3bb099fa1b
|
7
|
+
data.tar.gz: 7a230c6f9cc89433e0580efa59ddfd716bdaf569ae2f2f20c53dc2b1909d1d8f332f2b789870a834f90a9fbcc5953cc275639478ff842b40368fa41608da7add
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Redstruct
|
2
|
+
|
3
|
+
Provides higher level data structures in Ruby using standard Redis commands. Also provides basic object mapping for pre-existing types.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'redstruct'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install redstruct
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Development
|
26
|
+
|
27
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
28
|
+
|
29
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
30
|
+
|
31
|
+
*Note*
|
32
|
+
|
33
|
+
Avoid using transactions; the Redis documentation suggests using Lua scripts where possible, as in most cases they will be faster than transactions. Use the `Redstruct::Utils::Scriptable` module and the `defscript` macro instead.
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/npepinpe/redstruct.
|
38
|
+
|
39
|
+
|
40
|
+
## License
|
41
|
+
|
42
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Bundler.require(:default, :development)
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << 'test'
|
8
|
+
t.libs << 'lib'
|
9
|
+
t.test_files = FileList['test/**/*_test.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
task :default => :test
|
13
|
+
|
14
|
+
require 'redstruct/yard/defscript_handler'
|
15
|
+
YARD::Rake::YardocTask.new do |t|
|
16
|
+
t.files = ['lib/**/*.rb']
|
17
|
+
t.options = ['--output-dir=./docs']
|
18
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Redstruct
|
2
|
+
class Configuration
|
3
|
+
# @return [ConnectionPool] The Redis-rb connection pool to use
|
4
|
+
attr_accessor :connection_pool
|
5
|
+
|
6
|
+
# @return [::String] Default namespace for factories
|
7
|
+
attr_accessor :namespace
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@connection_pool = nil
|
11
|
+
@namespace = nil
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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
|
+
# While slower on load, defining all methods that we want to pipe to one of the connections results in
|
14
|
+
# faster calls at runtime, and gives us the convenience of not going through the pool.with everytime.
|
15
|
+
Redis.public_instance_methods(false).each do |method|
|
16
|
+
next if NON_COMMAND_METHODS.include?(method)
|
17
|
+
class_eval <<~METHOD, __FILE__, __LINE__ + 1
|
18
|
+
def #{method}(*args)
|
19
|
+
connection = Thread.current[:__redstruct_connection]
|
20
|
+
if connection.nil?
|
21
|
+
return @pool.with { |c| c.#{method}(*args) }
|
22
|
+
else
|
23
|
+
return connection.#{method}(*args)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
METHOD
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,95 @@
|
|
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::Types::Lock]
|
53
|
+
def lock(key, **options)
|
54
|
+
return create(Redstruct::Types::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::Types::Queue]
|
67
|
+
def queue(key)
|
68
|
+
return create(Redstruct::Types::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
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Redstruct
|
2
|
+
# Main interface of the gem; this class should be used to build all Redstruct
|
3
|
+
# objects, even when deserializing them.
|
4
|
+
class Factory
|
5
|
+
include Redstruct::Utils::Inspectable, Redstruct::Factory::Creation
|
6
|
+
extend Redstruct::Factory::Deserialization
|
7
|
+
|
8
|
+
# @return [Connection] The connection proxy to use when executing commands. Shared by all factory produced objects.
|
9
|
+
attr_reader :connection
|
10
|
+
|
11
|
+
# @param [Redstruct::Connection] connection connection to use for all objects built by the factory
|
12
|
+
# @param [ConnectionPool] pool pool to use to build a connection from if no connection param given
|
13
|
+
# @param [::String] namespace all objects build from the factory will have their keys namespaced under this one
|
14
|
+
# @return [Factory]
|
15
|
+
def initialize(connection: nil, pool: nil, namespace: nil)
|
16
|
+
namespace ||= Redstruct.config.namespace
|
17
|
+
|
18
|
+
if connection.nil?
|
19
|
+
pool ||= Redstruct.config.connection_pool
|
20
|
+
raise(Redstruct::Error, 'A connection pool is required to create a factory, but none was given') if pool.nil?
|
21
|
+
connection = Redstruct::Connection.new(pool)
|
22
|
+
end
|
23
|
+
|
24
|
+
@connection = connection
|
25
|
+
@namespace = namespace
|
26
|
+
@script_cache = {}.tap { |hash| hash.extend(MonitorMixin) }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns a namespaced version of the key (unless already namespaced)
|
30
|
+
# @param [String] key the key to isolate/namespace
|
31
|
+
# @return [String] namespaced version of the key (or the key itself if already namespaced)
|
32
|
+
def isolate(key)
|
33
|
+
return @namespace.nil? || key.start_with?(@namespace) ? key : "#{@namespace}:#{key}"
|
34
|
+
end
|
35
|
+
|
36
|
+
# :nocov:
|
37
|
+
|
38
|
+
# Helper method for serialization
|
39
|
+
def inspectable_attributes
|
40
|
+
return { namespace: @namespace, script_cache: @script_cache.keys }
|
41
|
+
end
|
42
|
+
|
43
|
+
# :nocov:
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,175 @@
|
|
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
|
@@ -0,0 +1,29 @@
|
|
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
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Redstruct
|
2
|
+
module Types
|
3
|
+
# Base class for all objects a factory can produce
|
4
|
+
class Base
|
5
|
+
include Redstruct::Utils::Inspectable
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@factory, :connection, :connection
|
9
|
+
|
10
|
+
# @return [String] The key used to identify the struct on redis
|
11
|
+
attr_reader :key
|
12
|
+
|
13
|
+
def initialize(key:, factory:)
|
14
|
+
@factory = factory
|
15
|
+
@key = key
|
16
|
+
end
|
17
|
+
|
18
|
+
def with
|
19
|
+
self.connection.pool.with do |c|
|
20
|
+
begin
|
21
|
+
Thread.current[:__redstruct_connection] = c
|
22
|
+
yield(c)
|
23
|
+
ensure
|
24
|
+
Thread.current[:__redstruct_connection] = nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_h
|
30
|
+
return { key: @key }
|
31
|
+
end
|
32
|
+
|
33
|
+
def create
|
34
|
+
return unless block_given?
|
35
|
+
subfactory = @factory.factory(@key)
|
36
|
+
yield(subfactory)
|
37
|
+
end
|
38
|
+
protected :create
|
39
|
+
|
40
|
+
# :nocov:
|
41
|
+
def inspectable_attributes
|
42
|
+
{ key: @key, factory: @factory }
|
43
|
+
end
|
44
|
+
# :nocov:
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Redstruct
|
2
|
+
module Types
|
3
|
+
class Counter < Redstruct::Types::String
|
4
|
+
include Redstruct::Utils::Scriptable
|
5
|
+
|
6
|
+
def initialize(increment: 1, max: nil, **options)
|
7
|
+
super(**options)
|
8
|
+
@increment = increment
|
9
|
+
@max = max
|
10
|
+
end
|
11
|
+
|
12
|
+
def get
|
13
|
+
super.to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def set(value)
|
17
|
+
super(value.to_i)
|
18
|
+
end
|
19
|
+
|
20
|
+
def increment(by: nil, max: nil)
|
21
|
+
by ||= @increment
|
22
|
+
max ||= @max
|
23
|
+
|
24
|
+
value = if max.nil?
|
25
|
+
self.connection.incrby(@key, by.to_i).to_i
|
26
|
+
else
|
27
|
+
ring_increment_script(keys: @key, argv: [by.to_i, max.to_i]).to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
return value
|
31
|
+
end
|
32
|
+
|
33
|
+
def decrement(by: nil, max: nil)
|
34
|
+
by ||= @increment
|
35
|
+
by = -by.to_i
|
36
|
+
return increment(by: by, max: max)
|
37
|
+
end
|
38
|
+
|
39
|
+
def getset(value)
|
40
|
+
return super(value.to_i).to_i
|
41
|
+
end
|
42
|
+
|
43
|
+
# @!group Lua Scripts
|
44
|
+
|
45
|
+
defscript :ring_increment_script, <<~LUA
|
46
|
+
local by = tonumber(ARGV[1])
|
47
|
+
local max = tonumber(ARGV[2])
|
48
|
+
local current = redis.call('get', KEYS[1])
|
49
|
+
local value = current and tonumber(current) or 0
|
50
|
+
|
51
|
+
value = (value + by) % max
|
52
|
+
redis.call('set', KEYS[1], value)
|
53
|
+
|
54
|
+
return value
|
55
|
+
LUA
|
56
|
+
|
57
|
+
# @!endgroup
|
58
|
+
|
59
|
+
# Helper method for easy inspection
|
60
|
+
def inspectable_attributes
|
61
|
+
super.merge(max: @max, increment: @increment)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Redstruct
|
2
|
+
module Types
|
3
|
+
class Hash < Redstruct::Types::Struct
|
4
|
+
include Redstruct::Utils::Coercion
|
5
|
+
|
6
|
+
def [](key)
|
7
|
+
return self.connection.hget(@key, key)
|
8
|
+
end
|
9
|
+
|
10
|
+
def []=(key, value)
|
11
|
+
self.connection.hset(@key, key, value)
|
12
|
+
end
|
13
|
+
|
14
|
+
def set(key, value, overwrite: true)
|
15
|
+
if overwrite
|
16
|
+
self[key] = value
|
17
|
+
else
|
18
|
+
self.connection.hsetnx(@key, key, value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(*keys)
|
23
|
+
return self[keys.first] if keys.size == 1
|
24
|
+
return self.connection.mapped_hmget(@key, *keys)
|
25
|
+
end
|
26
|
+
|
27
|
+
def update(hash)
|
28
|
+
self.connection.mapped_hmset(@key, hash)
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete(*keys)
|
32
|
+
return self.connection.hdel(@key, keys)
|
33
|
+
end
|
34
|
+
|
35
|
+
def key?(key)
|
36
|
+
return coerce_bool(self.connection.hexists(@key, key))
|
37
|
+
end
|
38
|
+
|
39
|
+
def incr(key, increment: 1)
|
40
|
+
if increment.is_a?(Float)
|
41
|
+
self.connection.hincrbyfloat(@key, key, increment.to_f)
|
42
|
+
else
|
43
|
+
self.connection.hincrby(@key, key, increment)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def decr(key, increment: 1)
|
48
|
+
return incr(key, (-increment))
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_h
|
52
|
+
return self.connection.hgetall(@key)
|
53
|
+
end
|
54
|
+
|
55
|
+
def keys
|
56
|
+
return self.connection.hkeys(@key)
|
57
|
+
end
|
58
|
+
|
59
|
+
def values
|
60
|
+
return self.connection.hvals(@key)
|
61
|
+
end
|
62
|
+
|
63
|
+
def size
|
64
|
+
return self.connection.hlen(@key)
|
65
|
+
end
|
66
|
+
|
67
|
+
def each(options = {}, &block)
|
68
|
+
return self.connection.hscan_each(@key, options, &block)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|