brpoplpush-redis_script 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/CHANGELOG.md +0 -0
- data/LICENSE +21 -0
- data/README.md +84 -0
- data/lib/brpoplpush/redis_script.rb +76 -0
- data/lib/brpoplpush/redis_script/client.rb +94 -0
- data/lib/brpoplpush/redis_script/config.rb +69 -0
- data/lib/brpoplpush/redis_script/dsl.rb +60 -0
- data/lib/brpoplpush/redis_script/logging.rb +95 -0
- data/lib/brpoplpush/redis_script/lua_error.rb +95 -0
- data/lib/brpoplpush/redis_script/script.rb +75 -0
- data/lib/brpoplpush/redis_script/scripts.rb +119 -0
- data/lib/brpoplpush/redis_script/template.rb +41 -0
- data/lib/brpoplpush/redis_script/timing.rb +35 -0
- data/lib/brpoplpush/redis_script/version.rb +7 -0
- data/lib/tasks/changelog.rake +23 -0
- metadata +202 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 37ccff484d1570a0306da629f9dd37f2da81dc0c33ad44725b2ec0a1e417a48a
|
4
|
+
data.tar.gz: 7e2dfe274df20f602aadca5035cbd15eb82dc823e539e7ec18fbd2e6f9755520
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1231b92745e69ed64ffc0e24c93650f5ea92f33fbc0c8b72c6218ed2402d524ab3b00b05d53138f605b9c787b03a1fb3075c4c580288dcd9faef46500bb1ead3
|
7
|
+
data.tar.gz: c3938006e1cc0c922a6464125868ca9c5945a538a4d82c83d210e929972535efe32c961b81687c09bc667e027a386d7f2196979a5f98d1ed108ccd8dd34f8c62
|
data/CHANGELOG.md
ADDED
File without changes
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 brpoplpush
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# brpoplpush-redis_script
|
2
|
+
|
3
|
+
Bring your own LUA scripts into redis
|
4
|
+
|
5
|
+
[](https://travis-ci.com/brpoplpush/brpoplpush-redis_script) [](https://codeclimate.com/github/brpoplpush/brpoplpush-redis_script/maintainability) [](https://codeclimate.com/github/brpoplpush/brpoplpush-redis_script/test_coverage)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'brpoplpush-redis_script'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
`bundle`
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
`gem install brpoplpush-redis_script`
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
If you want to avoid global state in your project/gem the recommended way to use `RedisScript` is the following way.
|
26
|
+
|
27
|
+
Include the DSL module from the gem and configure with a path. We don't believe it is a good idea to put all your lua files in a single directory. We rather believe that these scripts should be placed and organized by feature.
|
28
|
+
|
29
|
+
Let's take sidekiq-unique-jobs for example. It uses `brpoplpush-redis_script` like follows:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
# lib/my_redis_scripts.rb
|
33
|
+
require "brpoplpush-redis_script"
|
34
|
+
|
35
|
+
module SidekiqUniqueJobs::Scripts
|
36
|
+
include Brpoplpush::RedisScript::DSL
|
37
|
+
|
38
|
+
configure do |config|
|
39
|
+
config.scripts_path = Rails.root.join("app", "lua")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
SidekiqUniqueJobs::Scripts.execute(:lock, Redis.new, keys: ["key1", "key2"] argv: ["bogus"])
|
44
|
+
# => 1
|
45
|
+
|
46
|
+
SidekiqUniqueJobs::Scripts.execute(:lock, Redis.new, keys: ["key1", "key1"] argv: ["bogus"])
|
47
|
+
# => -1
|
48
|
+
```
|
49
|
+
|
50
|
+
```lua
|
51
|
+
-- app/lua/lock.lua
|
52
|
+
|
53
|
+
local key_one = KEYS[1]
|
54
|
+
local key_two = KEYS[2]
|
55
|
+
|
56
|
+
local locked_val = ARGV[1]
|
57
|
+
|
58
|
+
if not key_one == key_two then
|
59
|
+
redis.call("SET", key_two, )
|
60
|
+
return 1
|
61
|
+
end
|
62
|
+
|
63
|
+
return -1
|
64
|
+
```
|
65
|
+
|
66
|
+
This is a very simplified version of course.
|
67
|
+
|
68
|
+
## Development
|
69
|
+
|
70
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
71
|
+
|
72
|
+
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).
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/bropoplpush/brpoplpush-redis_script](https://github.com/bropoplpush/brpoplpush-redis_script). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
77
|
+
|
78
|
+
## License
|
79
|
+
|
80
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
81
|
+
|
82
|
+
## Code of Conduct
|
83
|
+
|
84
|
+
Everyone interacting in the Brpoplpush::RedisScript project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/brpoplpush-redis_script/blob/master/CODE_OF_CONDUCT.md).
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent/map"
|
4
|
+
require "concurrent/mutable_struct"
|
5
|
+
require "digest/sha1"
|
6
|
+
require "logger"
|
7
|
+
require "pathname"
|
8
|
+
require "redis"
|
9
|
+
|
10
|
+
require "brpoplpush/redis_script/version"
|
11
|
+
require "brpoplpush/redis_script/template"
|
12
|
+
require "brpoplpush/redis_script/lua_error"
|
13
|
+
require "brpoplpush/redis_script/script"
|
14
|
+
require "brpoplpush/redis_script/scripts"
|
15
|
+
require "brpoplpush/redis_script/config"
|
16
|
+
require "brpoplpush/redis_script/timing"
|
17
|
+
require "brpoplpush/redis_script/logging"
|
18
|
+
require "brpoplpush/redis_script/dsl"
|
19
|
+
require "brpoplpush/redis_script/client"
|
20
|
+
|
21
|
+
module Brpoplpush
|
22
|
+
# Interface to dealing with .lua files
|
23
|
+
#
|
24
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
25
|
+
module RedisScript
|
26
|
+
module_function
|
27
|
+
|
28
|
+
include Brpoplpush::RedisScript::DSL
|
29
|
+
|
30
|
+
#
|
31
|
+
# The current gem version
|
32
|
+
#
|
33
|
+
#
|
34
|
+
# @return [String] the current gem version
|
35
|
+
#
|
36
|
+
def version
|
37
|
+
VERSION
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# The current logger
|
42
|
+
#
|
43
|
+
#
|
44
|
+
# @return [Logger] the configured logger
|
45
|
+
#
|
46
|
+
def logger
|
47
|
+
config.logger
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Set a new logger
|
52
|
+
#
|
53
|
+
# @param [Logger] other another logger
|
54
|
+
#
|
55
|
+
# @return [Logger] the new logger
|
56
|
+
#
|
57
|
+
def logger=(other)
|
58
|
+
config.logger = other
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Execute the given script_name
|
63
|
+
#
|
64
|
+
#
|
65
|
+
# @param [Symbol] script_name the name of the lua script
|
66
|
+
# @param [Array<String>] keys script keys
|
67
|
+
# @param [Array<Object>] argv script arguments
|
68
|
+
# @param [Redis] conn the redis connection to use
|
69
|
+
#
|
70
|
+
# @return value from script
|
71
|
+
#
|
72
|
+
def execute(script_name, conn, keys: [], argv: [])
|
73
|
+
Client.execute(script_name, conn, keys: keys, argv: argv)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brpoplpush
|
4
|
+
module RedisScript
|
5
|
+
# Interface to dealing with .lua files
|
6
|
+
#
|
7
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
8
|
+
class Client
|
9
|
+
include Brpoplpush::RedisScript::Timing
|
10
|
+
|
11
|
+
#
|
12
|
+
# @!attribute [r] logger
|
13
|
+
# @return [Logger] an instance of a logger
|
14
|
+
attr_reader :logger
|
15
|
+
#
|
16
|
+
# @!attribute [r] file_name
|
17
|
+
# @return [String] The name of the file to execute
|
18
|
+
attr_reader :config
|
19
|
+
#
|
20
|
+
# @!attribute [r] scripts
|
21
|
+
# @return [Scripts] the collection with loaded scripts
|
22
|
+
attr_reader :scripts
|
23
|
+
|
24
|
+
def initialize(config)
|
25
|
+
@config = config
|
26
|
+
@logger = config.logger
|
27
|
+
@scripts = Scripts.fetch(config.scripts_path)
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Execute a lua script with the provided script_name
|
32
|
+
#
|
33
|
+
# @note this method is recursive if we need to load a lua script
|
34
|
+
# that wasn't previously loaded.
|
35
|
+
#
|
36
|
+
# @param [Symbol] script_name the name of the script to execute
|
37
|
+
# @param [Redis] conn the redis connection to use for execution
|
38
|
+
# @param [Array<String>] keys script keys
|
39
|
+
# @param [Array<Object>] argv script arguments
|
40
|
+
#
|
41
|
+
# @return value from script
|
42
|
+
#
|
43
|
+
def execute(script_name, conn, keys: [], argv: [])
|
44
|
+
result, elapsed = timed do
|
45
|
+
scripts.execute(script_name, conn, keys: keys, argv: argv)
|
46
|
+
end
|
47
|
+
|
48
|
+
logger.debug("Executed #{script_name}.lua in #{elapsed}ms")
|
49
|
+
result
|
50
|
+
rescue ::Redis::CommandError => ex
|
51
|
+
handle_error(script_name, conn, ex) do
|
52
|
+
execute(script_name, conn, keys: keys, argv: argv)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
#
|
59
|
+
# Handle errors to allow retrying errors that need retrying
|
60
|
+
#
|
61
|
+
# @param [Redis::CommandError] ex exception to handle
|
62
|
+
#
|
63
|
+
# @return [void]
|
64
|
+
#
|
65
|
+
# @yieldreturn [void] yields back to the caller when NOSCRIPT is raised
|
66
|
+
def handle_error(script_name, conn, ex)
|
67
|
+
case ex.message
|
68
|
+
when /NOSCRIPT/
|
69
|
+
handle_noscript(script_name) { return yield }
|
70
|
+
when /BUSY/
|
71
|
+
handle_busy(conn) { return yield }
|
72
|
+
end
|
73
|
+
|
74
|
+
raise unless LuaError.intercepts?(ex)
|
75
|
+
|
76
|
+
script = scripts.fetch(script_name, conn)
|
77
|
+
raise LuaError.new(ex, script)
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_noscript(script_name)
|
81
|
+
scripts.delete(script_name)
|
82
|
+
yield
|
83
|
+
end
|
84
|
+
|
85
|
+
def handle_busy(conn)
|
86
|
+
scripts.kill(conn)
|
87
|
+
rescue ::Redis::CommandError => ex
|
88
|
+
logger.warn(ex)
|
89
|
+
ensure
|
90
|
+
yield
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brpoplpush
|
4
|
+
module RedisScript
|
5
|
+
#
|
6
|
+
# Class holding gem configuration
|
7
|
+
#
|
8
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
9
|
+
class Config
|
10
|
+
#
|
11
|
+
# @!attribute [r] logger
|
12
|
+
# @return [Logger] a logger to use for debugging
|
13
|
+
attr_reader :logger
|
14
|
+
#
|
15
|
+
# @!attribute [r] scripts_path
|
16
|
+
# @return [Pathname] a directory with lua scripts
|
17
|
+
attr_reader :scripts_path
|
18
|
+
|
19
|
+
#
|
20
|
+
# Initialize a new instance of {Config}
|
21
|
+
#
|
22
|
+
#
|
23
|
+
def initialize
|
24
|
+
@conn = Redis.new
|
25
|
+
@logger = Logger.new(STDOUT)
|
26
|
+
@scripts_path = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Sets a value for scripts_path
|
31
|
+
#
|
32
|
+
# @param [String, Pathname] obj <description>
|
33
|
+
#
|
34
|
+
# @raise [ArgumentError] when directory does not exist
|
35
|
+
# @raise [ArgumentError] when argument isn't supported
|
36
|
+
#
|
37
|
+
# @return [Pathname]
|
38
|
+
#
|
39
|
+
def scripts_path=(obj)
|
40
|
+
raise ArgumentError "#{obj} does not exist" unless Dir.exist?(obj.to_s)
|
41
|
+
|
42
|
+
@scripts_path =
|
43
|
+
case obj
|
44
|
+
when String
|
45
|
+
Pathname.new(obj)
|
46
|
+
when Pathname
|
47
|
+
obj
|
48
|
+
else
|
49
|
+
raise ArgumentError, "#{obj} should be a Pathname or String"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Sets a value for logger
|
55
|
+
#
|
56
|
+
# @param [Logger] obj a logger to use
|
57
|
+
#
|
58
|
+
# @raise [ArgumentError] when given argument isn't a Logger
|
59
|
+
#
|
60
|
+
# @return [Logger]
|
61
|
+
#
|
62
|
+
def logger=(obj)
|
63
|
+
raise ArgumentError, "#{obj} should be a Logger" unless obj.is_a?(Logger)
|
64
|
+
|
65
|
+
@logger = obj
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "digest/sha1"
|
5
|
+
require "concurrent/map"
|
6
|
+
|
7
|
+
module Brpoplpush
|
8
|
+
module RedisScript
|
9
|
+
# Interface to dealing with .lua files
|
10
|
+
#
|
11
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
12
|
+
module DSL
|
13
|
+
def self.included(base)
|
14
|
+
base.class_eval do
|
15
|
+
extend ClassMethods
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Module ClassMethods extends the base class with necessary methods
|
21
|
+
#
|
22
|
+
# @author Mikael Henriksson <mikael@zoolutions.se>
|
23
|
+
#
|
24
|
+
module ClassMethods
|
25
|
+
def execute(file_name, conn, keys: [], argv: [])
|
26
|
+
Brpoplpush::RedisScript::Client
|
27
|
+
.new(config)
|
28
|
+
.execute(file_name, conn, keys: keys, argv: argv)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Configure the gem
|
32
|
+
#
|
33
|
+
# This is usually called once at startup of an application
|
34
|
+
# @param [Hash] options global gem options
|
35
|
+
# @option options [String, Pathname] :path
|
36
|
+
# @option options [Logger] :logger (default is Logger.new(STDOUT))
|
37
|
+
# @yield control to the caller when given block
|
38
|
+
def configure(options = {})
|
39
|
+
if block_given?
|
40
|
+
yield config
|
41
|
+
else
|
42
|
+
options.each do |key, val|
|
43
|
+
config.send("#{key}=", val)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# The current configuration (See: {.configure} on how to configure)
|
50
|
+
#
|
51
|
+
#
|
52
|
+
# @return [RedisScript::Config] the gem configuration
|
53
|
+
#
|
54
|
+
def config
|
55
|
+
@config ||= Config.new
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brpoplpush
|
4
|
+
module RedisScript
|
5
|
+
# Utility module for reducing the number of uses of logger.
|
6
|
+
#
|
7
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
8
|
+
module Logging
|
9
|
+
def self.included(base)
|
10
|
+
base.send(:extend, self)
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# A convenience method for using the configured gem logger
|
15
|
+
#
|
16
|
+
# @see RedisScript#.logger
|
17
|
+
#
|
18
|
+
# @return [Logger]
|
19
|
+
#
|
20
|
+
def logger
|
21
|
+
Brpoplpush::RedisScript.logger
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Logs a message at debug level
|
26
|
+
#
|
27
|
+
# @param [String, Exception] message_or_exception the message or exception to log
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
#
|
31
|
+
# @yield [String, Exception] the message or exception to use for log message
|
32
|
+
#
|
33
|
+
def log_debug(message_or_exception = nil, &block)
|
34
|
+
logger.debug(message_or_exception, &block)
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Logs a message at info level
|
40
|
+
#
|
41
|
+
# @param [String, Exception] message_or_exception the message or exception to log
|
42
|
+
#
|
43
|
+
# @return [void]
|
44
|
+
#
|
45
|
+
# @yield [String, Exception] the message or exception to use for log message
|
46
|
+
#
|
47
|
+
def log_info(message_or_exception = nil, &block)
|
48
|
+
logger.info(message_or_exception, &block)
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Logs a message at warn level
|
54
|
+
#
|
55
|
+
# @param [String, Exception] message_or_exception the message or exception to log
|
56
|
+
#
|
57
|
+
# @return [void]
|
58
|
+
#
|
59
|
+
# @yield [String, Exception] the message or exception to use for log message
|
60
|
+
#
|
61
|
+
def log_warn(message_or_exception = nil, &block)
|
62
|
+
logger.warn(message_or_exception, &block)
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Logs a message at error level
|
68
|
+
#
|
69
|
+
# @param [String, Exception] message_or_exception the message or exception to log
|
70
|
+
#
|
71
|
+
# @return [void]
|
72
|
+
#
|
73
|
+
# @yield [String, Exception] the message or exception to use for log message
|
74
|
+
#
|
75
|
+
def log_error(message_or_exception = nil, &block)
|
76
|
+
logger.error(message_or_exception, &block)
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Logs a message at fatal level
|
82
|
+
#
|
83
|
+
# @param [String, Exception] message_or_exception the message or exception to log
|
84
|
+
#
|
85
|
+
# @return [void]
|
86
|
+
#
|
87
|
+
# @yield [String, Exception] the message or exception to use for log message
|
88
|
+
#
|
89
|
+
def log_fatal(message_or_exception = nil, &block)
|
90
|
+
logger.fatal(message_or_exception, &block)
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brpoplpush
|
4
|
+
module RedisScript
|
5
|
+
#
|
6
|
+
# Misconfiguration is raised when gem is misconfigured
|
7
|
+
#
|
8
|
+
# @author Mikael Henriksson <mikael@zoolutions.se>
|
9
|
+
#
|
10
|
+
class Misconfiguration < RuntimeError
|
11
|
+
end
|
12
|
+
# LuaError raised on errors in Lua scripts
|
13
|
+
#
|
14
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
15
|
+
class LuaError < RuntimeError
|
16
|
+
# Reformats errors raised by redis representing failures while executing
|
17
|
+
# a lua script. The default errors have confusing messages and backtraces,
|
18
|
+
# and a type of +RuntimeError+. This class improves the message and
|
19
|
+
# modifies the backtrace to include the lua script itself in a reasonable
|
20
|
+
# way.
|
21
|
+
|
22
|
+
PATTERN = /ERR Error (compiling|running) script \(.*?\): .*?:(\d+): (.*)/.freeze
|
23
|
+
LIB_PATH = File.expand_path("..", __dir__).freeze
|
24
|
+
CONTEXT_LINE_NUMBER = 2
|
25
|
+
|
26
|
+
attr_reader :error, :file, :content
|
27
|
+
|
28
|
+
# Is this error one that should be reformatted?
|
29
|
+
#
|
30
|
+
# @param error [StandardError] the original error raised by redis
|
31
|
+
# @return [Boolean] is this an error that should be reformatted?
|
32
|
+
def self.intercepts?(error)
|
33
|
+
PATTERN.match?(error.message)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Initialize a new {LuaError} from an existing redis error, adjusting
|
37
|
+
# the message and backtrace in the process.
|
38
|
+
#
|
39
|
+
# @param error [StandardError] the original error raised by redis
|
40
|
+
# @param script [Script] a DTO with information about the script
|
41
|
+
#
|
42
|
+
def initialize(error, script)
|
43
|
+
@error = error
|
44
|
+
@file = script.path
|
45
|
+
@content = script.source
|
46
|
+
@backtrace = @error.backtrace
|
47
|
+
|
48
|
+
@error.message.match(PATTERN) do |regexp_match|
|
49
|
+
line_number = regexp_match[2].to_i
|
50
|
+
message = regexp_match[3]
|
51
|
+
error_context = generate_error_context(content, line_number)
|
52
|
+
|
53
|
+
super("#{message}\n\n#{error_context}\n\n")
|
54
|
+
set_backtrace(generate_backtrace(file, line_number))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# :nocov:
|
61
|
+
def generate_error_context(content, line_number)
|
62
|
+
lines = content.lines.to_a
|
63
|
+
beginning_line_number = [1, line_number - CONTEXT_LINE_NUMBER].max
|
64
|
+
ending_line_number = [lines.count, line_number + CONTEXT_LINE_NUMBER].min
|
65
|
+
line_number_width = ending_line_number.to_s.length
|
66
|
+
|
67
|
+
(beginning_line_number..ending_line_number).map do |number|
|
68
|
+
indicator = (number == line_number) ? "=>" : " "
|
69
|
+
formatted_number = format("%#{line_number_width}d", number)
|
70
|
+
" #{indicator} #{formatted_number}: #{lines[number - 1]}"
|
71
|
+
end.join.chomp
|
72
|
+
end
|
73
|
+
|
74
|
+
# :nocov:
|
75
|
+
def generate_backtrace(file, line_number)
|
76
|
+
pre_gem = backtrace_before_entering_gem(@backtrace)
|
77
|
+
index_of_first_gem_line = (@backtrace.size - pre_gem.size - 1)
|
78
|
+
|
79
|
+
pre_gem.unshift(@backtrace[index_of_first_gem_line])
|
80
|
+
pre_gem.unshift("#{file}:#{line_number}")
|
81
|
+
pre_gem
|
82
|
+
end
|
83
|
+
|
84
|
+
# :nocov:
|
85
|
+
def backtrace_before_entering_gem(backtrace)
|
86
|
+
backtrace.reverse.take_while { |line| !line_from_gem(line) }.reverse
|
87
|
+
end
|
88
|
+
|
89
|
+
# :nocov:
|
90
|
+
def line_from_gem(line)
|
91
|
+
line.split(":").first.include?(LIB_PATH)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brpoplpush
|
4
|
+
module RedisScript
|
5
|
+
# Interface to dealing with .lua files
|
6
|
+
#
|
7
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
8
|
+
class Script
|
9
|
+
def self.load(name, root_path, conn)
|
10
|
+
script = new(name: name, root_path: root_path)
|
11
|
+
script.load(conn)
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# @!attribute [r] script_name
|
16
|
+
# @return [Symbol, String] the name of the script without extension
|
17
|
+
attr_reader :name
|
18
|
+
#
|
19
|
+
# @!attribute [r] script_path
|
20
|
+
# @return [String] the path to the script on disk
|
21
|
+
attr_reader :path
|
22
|
+
#
|
23
|
+
# @!attribute [r] root_path
|
24
|
+
# @return [Pathname]
|
25
|
+
attr_reader :root_path
|
26
|
+
#
|
27
|
+
# @!attribute [r] source
|
28
|
+
# @return [String] the source code of the lua script
|
29
|
+
attr_reader :source
|
30
|
+
#
|
31
|
+
# @!attribute [rw] sha
|
32
|
+
# @return [String] the sha of the script
|
33
|
+
attr_reader :sha
|
34
|
+
#
|
35
|
+
# @!attribute [rw] call_count
|
36
|
+
# @return [Integer] the number of times the script was called/executed
|
37
|
+
attr_reader :call_count
|
38
|
+
|
39
|
+
def initialize(name:, root_path:)
|
40
|
+
@name = name
|
41
|
+
@root_path = root_path
|
42
|
+
@path = root_path.join("#{name}.lua").to_s
|
43
|
+
@source = render_file
|
44
|
+
@sha = compiled_sha
|
45
|
+
@call_count = 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def ==(other)
|
49
|
+
sha == compiled_sha && compiled_sha == other.sha
|
50
|
+
end
|
51
|
+
|
52
|
+
def increment_call_count
|
53
|
+
@call_count += 1
|
54
|
+
end
|
55
|
+
|
56
|
+
def changed?
|
57
|
+
compiled_sha != sha
|
58
|
+
end
|
59
|
+
|
60
|
+
def render_file
|
61
|
+
Template.new(root_path).render(path)
|
62
|
+
end
|
63
|
+
|
64
|
+
def compiled_sha
|
65
|
+
Digest::SHA1.hexdigest(source)
|
66
|
+
end
|
67
|
+
|
68
|
+
def load(conn)
|
69
|
+
@sha = conn.script(:load, source)
|
70
|
+
|
71
|
+
self
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brpoplpush
|
4
|
+
module RedisScript
|
5
|
+
# Interface to dealing with .lua files
|
6
|
+
#
|
7
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
8
|
+
class Scripts
|
9
|
+
#
|
10
|
+
# @return [Concurrent::Map] a map with configured script paths
|
11
|
+
SCRIPT_PATHS = Concurrent::Map.new
|
12
|
+
|
13
|
+
#
|
14
|
+
# Fetch a scripts configuration for path
|
15
|
+
#
|
16
|
+
# @param [Pathname] root_path the path to scripts
|
17
|
+
#
|
18
|
+
# @return [Scripts] a collection of scripts
|
19
|
+
#
|
20
|
+
def self.fetch(root_path)
|
21
|
+
if (scripts = SCRIPT_PATHS.get(root_path))
|
22
|
+
return scripts
|
23
|
+
end
|
24
|
+
|
25
|
+
create(root_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Create a new scripts collection based on path
|
30
|
+
#
|
31
|
+
# @param [Pathname] root_path the path to scripts
|
32
|
+
#
|
33
|
+
# @return [Scripts] a collection of scripts
|
34
|
+
#
|
35
|
+
def self.create(root_path)
|
36
|
+
scripts = new(root_path)
|
37
|
+
store(scripts)
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Store the scripts collection in memory
|
42
|
+
#
|
43
|
+
# @param [Scripts] scripts the path to scripts
|
44
|
+
#
|
45
|
+
# @return [Scripts] the scripts instance that was stored
|
46
|
+
#
|
47
|
+
def self.store(scripts)
|
48
|
+
SCRIPT_PATHS.put(scripts.root_path, scripts)
|
49
|
+
scripts
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# @!attribute [r] scripts
|
54
|
+
# @return [Concurrent::Map] a collection of loaded scripts
|
55
|
+
attr_reader :scripts
|
56
|
+
|
57
|
+
#
|
58
|
+
# @!attribute [r] root_path
|
59
|
+
# @return [Pathname] the path to the directory with lua scripts
|
60
|
+
attr_reader :root_path
|
61
|
+
|
62
|
+
def initialize(path)
|
63
|
+
raise ArgumentError, "path needs to be a Pathname" unless path.is_a?(Pathname)
|
64
|
+
|
65
|
+
@scripts = Concurrent::Map.new
|
66
|
+
@root_path = path
|
67
|
+
end
|
68
|
+
|
69
|
+
def fetch(name, conn)
|
70
|
+
if (script = scripts.get(name.to_sym))
|
71
|
+
return script
|
72
|
+
end
|
73
|
+
|
74
|
+
load(name, conn)
|
75
|
+
end
|
76
|
+
|
77
|
+
def load(name, conn)
|
78
|
+
script = Script.load(name, root_path, conn)
|
79
|
+
scripts.put(name.to_sym, script)
|
80
|
+
|
81
|
+
script
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete(script)
|
85
|
+
if script.is_a?(Script)
|
86
|
+
scripts.delete(script.name)
|
87
|
+
else
|
88
|
+
scripts.delete(script.to_sym)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def kill(conn)
|
93
|
+
conn.script(:kill)
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# Execute a lua script with given name
|
98
|
+
#
|
99
|
+
# @note this method is recursive if we need to load a lua script
|
100
|
+
# that wasn't previously loaded.
|
101
|
+
#
|
102
|
+
# @param [Symbol] name the name of the script to execute
|
103
|
+
# @param [Redis] conn the redis connection to use for execution
|
104
|
+
# @param [Array<String>] keys script keys
|
105
|
+
# @param [Array<Object>] argv script arguments
|
106
|
+
#
|
107
|
+
# @return value from script
|
108
|
+
#
|
109
|
+
def execute(name, conn, keys: [], argv: [])
|
110
|
+
script = fetch(name, conn)
|
111
|
+
conn.evalsha(script.sha, keys: keys, argv: argv)
|
112
|
+
end
|
113
|
+
|
114
|
+
def count
|
115
|
+
scripts.keys.size
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brpoplpush
|
4
|
+
# Interface to dealing with .lua files
|
5
|
+
#
|
6
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
7
|
+
module RedisScript
|
8
|
+
#
|
9
|
+
# Class Template provides LUA script partial template rendering
|
10
|
+
#
|
11
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
12
|
+
#
|
13
|
+
class Template
|
14
|
+
def initialize(script_path)
|
15
|
+
@script_path = script_path
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Renders a Lua script and includes any partials in that file
|
20
|
+
# all `<%= include_partial '' %>` replaced with the actual contents of the partial
|
21
|
+
#
|
22
|
+
# @param [Pathname] pathname the path to the
|
23
|
+
#
|
24
|
+
# @return [String] the rendered Luascript
|
25
|
+
#
|
26
|
+
def render(pathname)
|
27
|
+
@partial_templates ||= {}
|
28
|
+
ERB.new(File.read(pathname)).result(binding)
|
29
|
+
end
|
30
|
+
|
31
|
+
# helper method to include a lua partial within another lua script
|
32
|
+
#
|
33
|
+
def include_partial(relative_path)
|
34
|
+
return if @partial_templates.key?(relative_path)
|
35
|
+
|
36
|
+
@partial_templates[relative_path] = nil
|
37
|
+
render(Pathname.new("#{@script_path}/#{relative_path}"))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brpoplpush
|
4
|
+
module RedisScript
|
5
|
+
# Handles timing> of things
|
6
|
+
#
|
7
|
+
# @author Mikael Henriksson <mikael@mhenrixon.com>
|
8
|
+
module Timing
|
9
|
+
module_function
|
10
|
+
|
11
|
+
#
|
12
|
+
# Used for timing method calls
|
13
|
+
#
|
14
|
+
#
|
15
|
+
# @return [yield return, Float]
|
16
|
+
#
|
17
|
+
def timed
|
18
|
+
start_time = now
|
19
|
+
|
20
|
+
[yield, now - start_time]
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Returns a float representation of the current time.
|
25
|
+
# Either from Process or Time
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# @return [Float]
|
29
|
+
#
|
30
|
+
def now
|
31
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
desc "Generate a Changelog"
|
4
|
+
task :changelog do
|
5
|
+
# rubocop:disable Style/MutableConstant
|
6
|
+
CHANGELOG_CMD ||= %w[
|
7
|
+
github_changelog_generator
|
8
|
+
-u
|
9
|
+
brpoplpush
|
10
|
+
-p
|
11
|
+
brpoplpush-redis_script
|
12
|
+
--no-verbose
|
13
|
+
--token
|
14
|
+
]
|
15
|
+
ADD_CHANGELOG_CMD ||= "git add --all"
|
16
|
+
COMMIT_CHANGELOG_CMD ||= "git commit -a -m 'Update changelog'"
|
17
|
+
# rubocop:enable Style/MutableConstant
|
18
|
+
|
19
|
+
sh("git checkout master")
|
20
|
+
sh(*CHANGELOG_CMD.push(ENV["CHANGELOG_GITHUB_TOKEN"]))
|
21
|
+
sh(ADD_CHANGELOG_CMD)
|
22
|
+
sh(COMMIT_CHANGELOG_CMD)
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: brpoplpush-redis_script
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mikael Henriksson
|
8
|
+
- Mauro Berlanda
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2019-10-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: concurrent-ruby
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.0'
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.0.5
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - "~>"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '1.0'
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.0.5
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: redis
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
- - "<="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '5.0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '1.0'
|
51
|
+
- - "<="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '5.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: bundler
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '2.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rake
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '12.3'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '12.3'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rspec
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.7'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.7'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: github-markup
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '3.0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '3.0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: github_changelog_generator
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '1.14'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '1.14'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: yard
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 0.9.18
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: 0.9.18
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: gem-release
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '2.0'
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '2.0'
|
152
|
+
description: Bring your own LUA scripts into redis.
|
153
|
+
email:
|
154
|
+
- mikael@mhenrixon.com
|
155
|
+
- mauro.berlanda@gmail.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- CHANGELOG.md
|
161
|
+
- LICENSE
|
162
|
+
- README.md
|
163
|
+
- lib/brpoplpush/redis_script.rb
|
164
|
+
- lib/brpoplpush/redis_script/client.rb
|
165
|
+
- lib/brpoplpush/redis_script/config.rb
|
166
|
+
- lib/brpoplpush/redis_script/dsl.rb
|
167
|
+
- lib/brpoplpush/redis_script/logging.rb
|
168
|
+
- lib/brpoplpush/redis_script/lua_error.rb
|
169
|
+
- lib/brpoplpush/redis_script/script.rb
|
170
|
+
- lib/brpoplpush/redis_script/scripts.rb
|
171
|
+
- lib/brpoplpush/redis_script/template.rb
|
172
|
+
- lib/brpoplpush/redis_script/timing.rb
|
173
|
+
- lib/brpoplpush/redis_script/version.rb
|
174
|
+
- lib/tasks/changelog.rake
|
175
|
+
homepage: https://github.com/brpoplpush/brpoplpush-redis_script
|
176
|
+
licenses:
|
177
|
+
- MIT
|
178
|
+
metadata:
|
179
|
+
allowed_push_host: https://rubygems.org
|
180
|
+
homepage_uri: https://github.com/brpoplpush/brpoplpush-redis_script
|
181
|
+
source_code_uri: https://github.com/brpoplpush/brpoplpush-redis_script
|
182
|
+
changelog_uri: https://github.com/brpoplpush/brpoplpush-redis_script/CHANGELOG.md
|
183
|
+
post_install_message:
|
184
|
+
rdoc_options: []
|
185
|
+
require_paths:
|
186
|
+
- lib
|
187
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
188
|
+
requirements:
|
189
|
+
- - ">="
|
190
|
+
- !ruby/object:Gem::Version
|
191
|
+
version: 2.5.0
|
192
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0'
|
197
|
+
requirements: []
|
198
|
+
rubygems_version: 3.0.6
|
199
|
+
signing_key:
|
200
|
+
specification_version: 4
|
201
|
+
summary: Bring your own LUA scripts into redis.
|
202
|
+
test_files: []
|