brpoplpush-redis_script 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ [![Build Status](https://travis-ci.com/brpoplpush/brpoplpush-redis_script.svg?branch=master)](https://travis-ci.com/brpoplpush/brpoplpush-redis_script) [![Maintainability](https://api.codeclimate.com/v1/badges/3770a079b380d50c3d50/maintainability)](https://codeclimate.com/github/brpoplpush/brpoplpush-redis_script/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/3770a079b380d50c3d50/test_coverage)](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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brpoplpush
4
+ module RedisScript
5
+ VERSION = "0.1.0"
6
+ end
7
+ 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: []