redstruct 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|