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 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: []