redis-prescription 1.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4c70dac256ca079b9af76b03cefc189db30507860c0a6816ddd519da7bdd75a
4
- data.tar.gz: a1c5e2fe3645bd592f26a00a16accf2be1d0340468c4f824e06c7da51f1d29eb
3
+ metadata.gz: 617a2c55e0d763f97983c56f3e7c7fbea30b4c999736a0eb8eaa4f38dab3a684
4
+ data.tar.gz: 16ab341290e6ee88abe15d0c0ee5523b1aaa1ab7b0228e05c9650ff4b861cf52
5
5
  SHA512:
6
- metadata.gz: a1f3988eb35db89865af75881b7bd0e33cb4a922fa468589f8f9de3396ac88118405b400ac8999995648ed92a224f768f2fc88a19d3a75d6c54145c9f01488b1
7
- data.tar.gz: 19a72258d2adf1bef91b436965ebcea477a886cc4c9e269e81f5cffe489b406811f9fc7c46088bfc02071c720fecc016c819b48e297bd8b602e6ce11d21d31fd
6
+ metadata.gz: 654c70a9fda3e076291c0c5339cde768c430c4d960d434fedf9e3bf7e8aaab5d4ea280a1be3a8d554015b09fd8adec26a4f2349ee169cd43e3139ce2b9b6badd
7
+ data.tar.gz: ce8112a9aa153b41e05e4e327dbcb6b6bf62abd120a96dad1eacbf818e747899abd7615bf975484850cad540cd4669764e320ce7a07e07d447a371397fe33c55
data/LICENSE.txt CHANGED
@@ -1,6 +1,7 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018 Alexey Zapparov
3
+ Copyright (c) 2020-2023 Alexey Zapparov
4
+ Copyright (c) 2018 SensorTower Inc.
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
@@ -1,48 +1,54 @@
1
- # Redis::Prescription
1
+ = Redis::Prescription
2
2
 
3
3
  Redis LUA stored procedure runner. Preloads (and reloads when needed, e.g. when
4
4
  scripts were flushed away) script and then runs it with `EVALSHA`.
5
5
 
6
6
 
7
- ## Installation
7
+ == Installation
8
8
 
9
9
  Add this line to your application's Gemfile:
10
10
 
11
- ```ruby
12
- gem "redis-prescription"
13
- ```
14
-
15
- And then execute:
16
-
17
- $ bundle
11
+ $ bundle add redis-prescription
18
12
 
19
13
  Or install it yourself as:
20
14
 
21
15
  $ gem install redis-prescription
22
16
 
23
17
 
24
- ## Usage
18
+ == Usage
25
19
 
26
- ``` ruby
27
- script = Redis::Prescription.new <<~LUA
20
+ [source,ruby]
21
+ ----
22
+ script = RedisPrescription.new <<~LUA
28
23
  return tonumber(redis.call('GET', KEYS[1]) or 42)
29
24
  LUA
30
25
 
31
- script.eval(Redis.current, :xxx) # => 42
26
+ redis = Redis.new
27
+ script.call(redis, keys: [:xxx]) # => 42
32
28
 
33
- Redis.current.set(:xxx, 123)
34
- script.eval(Redis.current, :xxx) # => 123
35
- ```
29
+ redis.set(:xxx, 123)
30
+ script.call(redis, keys: [:xxx]) # => 123
31
+ ----
36
32
 
37
33
 
38
- ## Supported Ruby Versions
34
+ == Supported Ruby Versions
39
35
 
40
- This library aims to support and is [tested against][1] the following Ruby
41
- versions:
36
+ This library aims to support and is tested against:
42
37
 
43
- * Ruby 2.3.x
44
- * Ruby 2.4.x
45
- * Ruby 2.5.x
38
+ * https://www.ruby-lang.org[Ruby]
39
+ ** MRI 3.0.x
40
+ ** MRI 3.1.x
41
+ * https://redis.io[Redis Server]
42
+ ** 6.2.x
43
+ ** 7.0.x
44
+ * https://github.com/redis/redis-rb[redis-rb]
45
+ ** 4.7.x
46
+ ** 4.8.x
47
+ ** 5.0.x
48
+ * https://github.com/redis-rb/redis-client[redis-client]
49
+ ** 0.12.x
50
+ ** 0.13.x
51
+ ** 0.14.x
46
52
 
47
53
  If something doesn't work on one of these versions, it's a bug.
48
54
 
@@ -57,33 +63,26 @@ patches in a timely fashion. If critical issues for a particular implementation
57
63
  exist at the time of a major release, support for that Ruby version may be
58
64
  dropped.
59
65
 
66
+ Same rules apply to *Redis Server*, *redis-rb*, and *redis-client* support.
67
+
68
+
69
+ == Similar Projects
70
+
71
+ * https://github.com/Shopify/wolverine
60
72
 
61
- ## Development
62
73
 
63
- After checking out the repo, run `bundle install` to install dependencies.
64
- Then, run `bundle exec rake spec` to run the tests with ruby-rb client.
74
+ == Development
65
75
 
66
- To install this gem onto your local machine, run `bundle exec rake install`.
67
- To release a new version, update the version number in `version.rb`, and then
68
- run `bundle exec rake release`, which will create a git tag for the version,
69
- push git commits and tags, and push the `.gem` file to [rubygems.org][2].
76
+ scripts/update-gemfiles
77
+ scripts/run-rspec
78
+ bundle exec rubocop
70
79
 
71
80
 
72
- ## Contributing
81
+ == Contributing
73
82
 
74
- * Fork sidekiq-throttled on GitHub
83
+ * Fork redis-prescription
75
84
  * Make your changes
76
85
  * Ensure all tests pass (`bundle exec rake`)
77
- * Send a pull request
86
+ * Send a merge request
78
87
  * If we like them we'll merge them
79
88
  * If we've accepted a patch, feel free to ask for commit access!
80
-
81
-
82
- ## Copyright
83
-
84
- Copyright (c) 2018 SensorTower Inc.
85
- See LICENSE.md for further details.
86
-
87
-
88
- [1]: http://travis-ci.org/sensortower/redis-prescription
89
- [2]: https://rubygems.org
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "redis_prescription"
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ class RedisPrescription
6
+ module Adapters
7
+ # redis-rb adapter
8
+ class Redis
9
+ def self.adapts?(redis)
10
+ defined?(::Redis) && redis.is_a?(::Redis)
11
+ end
12
+
13
+ def initialize(redis)
14
+ @redis = redis
15
+ end
16
+
17
+ def eval(script, keys, argv)
18
+ @redis.eval(script, keys, argv)
19
+ rescue ::Redis::CommandError => e
20
+ raise CommandError, e.message
21
+ end
22
+
23
+ def evalsha(digest, keys, argv)
24
+ @redis.evalsha(digest, keys, argv)
25
+ rescue ::Redis::CommandError => e
26
+ raise CommandError, e.message
27
+ end
28
+
29
+ def purge!
30
+ @redis.script("flush")
31
+ @redis.flushdb
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ class RedisPrescription
6
+ module Adapters
7
+ # redis-client adapter
8
+ class RedisClient
9
+ def self.adapts?(redis)
10
+ return true if defined?(::RedisClient) && redis.is_a?(::RedisClient)
11
+ return true if defined?(::RedisClient::Decorator::Client) && redis.is_a?(::RedisClient::Decorator::Client)
12
+
13
+ false
14
+ end
15
+
16
+ def initialize(redis)
17
+ @redis = redis
18
+ end
19
+
20
+ def eval(script, keys, argv)
21
+ @redis.call("EVAL", script, keys.size, *keys, *argv)
22
+ rescue ::RedisClient::CommandError => e
23
+ raise CommandError, e.message
24
+ end
25
+
26
+ def evalsha(digest, keys, argv)
27
+ @redis.call("EVALSHA", digest, keys.size, *keys, *argv)
28
+ rescue ::RedisClient::CommandError => e
29
+ raise CommandError, e.message
30
+ end
31
+
32
+ def purge!
33
+ @redis.call("SCRIPT", "FLUSH")
34
+ @redis.call("FLUSHDB")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./adapters/redis"
4
+ require_relative "./adapters/redis_client"
5
+
6
+ class RedisPrescription
7
+ # @api internal
8
+ module Adapters
9
+ class << self
10
+ def [](redis)
11
+ return Adapters::Redis.new(redis) if Adapters::Redis.adapts?(redis)
12
+ return Adapters::RedisClient.new(redis) if Adapters::RedisClient.adapts?(redis)
13
+
14
+ raise TypeError, "Unsupported redis client: #{redis.class}"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisPrescription
4
+ # Top-level error class for all RedisPrescription errors
5
+ class Error < StandardError; end
6
+
7
+ # Redis command error wrapper, `#cause` will be `Redis::CommandError` or
8
+ # `RedisClient::CommandError`.
9
+ class CommandError < Error; end
10
+
11
+ # Lua script eval/evalsha failure
12
+ class ScriptError < Error
13
+ # rubocop:disable Layout/LineLength
14
+ LUA_ERROR_MESSAGE = %r{
15
+ # * Error compiling script:
16
+ # ** Redis 6.0, 6.2, 7.0
17
+ # *** ERR Error compiling script (new function): user_script:7: unexpected symbol near '!'
18
+ # * Error running script:
19
+ # ** Redis 6.0, 6.2
20
+ # *** ERR Error running script (call to f_64203334c42d5690c2d008a78aa7789f5b83e5bb): @user_script:4: user_script:4: attempt to perform arithmetic on a string value
21
+ # ** Redis 7.0
22
+ # *** ERR user_script:4: attempt to perform arithmetic on a string value script: 64203334c42d5690c2d008a78aa7789f5b83e5bb, on @user_script:4.
23
+ \A
24
+ ERR\s
25
+ (?:
26
+ Error\scompiling\sscript\s\([^)]+\):\s # Redis 6.0, 6.2, 7.0
27
+ .+:(?<loc>\d+):\s # Redis 6.0, 6.2, 7.0
28
+ (?<message>.+) # Redis 6.0, 6.2, 7.0
29
+ |
30
+ (?:Error\srunning\sscript\s\([^)]+\):\s@\S+\s)? # Redis 6.0, 6.2
31
+ .+:(?<loc>\d+):\s # Redis 6.0, 6.2, 7.0
32
+ (?<message>.+?) # Redis 6.0, 6.2, 7.0
33
+ (?::\s\h+,\son\s@[^:]+:\d+\.)? # Redis 7.0
34
+ )
35
+ \z
36
+ }x
37
+ private_constant :LUA_ERROR_MESSAGE
38
+ # rubocop:enable Layout/LineLength
39
+
40
+ # Lua script source
41
+ #
42
+ # @return [String]
43
+ attr_reader :source
44
+
45
+ # Line of code where error was encountered
46
+ #
47
+ # @return [Integer?]
48
+ attr_reader :loc
49
+
50
+ # @param message [String]
51
+ # @param source [#to_s]
52
+ def initialize(message, source)
53
+ @source = -source.to_s
54
+
55
+ if (parsed = LUA_ERROR_MESSAGE.match(message))
56
+ @loc = parsed[:loc].to_i
57
+ message = [parsed[:message], excerpt(@source, @loc)].compact.join("\n\n")
58
+ end
59
+
60
+ super(message)
61
+ end
62
+
63
+ private
64
+
65
+ def excerpt(source, loc)
66
+ lines = excerpt_lines(source, loc)
67
+ gutter = lines.map(&:first).max.to_s.length
68
+
69
+ lines.map! do |(pos, line)|
70
+ format(pos == loc ? "\t%#{gutter}d > %s" : "\t%#{gutter}d | %s", pos, line).rstrip
71
+ end
72
+
73
+ lines.join("\n")
74
+ rescue => e
75
+ warn "Failed extracting source excerpt: #{e.message}"
76
+ nil
77
+ end
78
+
79
+ def excerpt_lines(source, loc)
80
+ lines = source.lines
81
+ pos = loc - 1 # reported line of code is 1-offset
82
+ min = pos - 2 # 2 lines of head context
83
+ max = pos + 2 # 2 lines of tail context
84
+
85
+ min = 0 if min.negative?
86
+ max = lines.size - 1 if lines.size <= max
87
+
88
+ (min..max).map { |i| [i.succ, lines[i]] }
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisPrescription
4
+ # Gem version.
5
+ VERSION = "2.1.0"
6
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./redis_prescription/adapters"
4
+ require_relative "./redis_prescription/errors"
5
+ require_relative "./redis_prescription/version"
6
+
7
+ # Lua script executor for redis.
8
+ #
9
+ # Instead of executing script with `EVAL` everytime - loads script once
10
+ # and then runs it with `EVALSHA`.
11
+ #
12
+ # @example Usage
13
+ #
14
+ # redis = Redis.new
15
+ # script = RedisPrescription.new("return ARGV[1] + ARGV[2]")
16
+ # script.call(redis, argv: [2, 2]) # => 4
17
+ class RedisPrescription
18
+ # Redis error fired when script ID is unkown.
19
+ NOSCRIPT = "NOSCRIPT"
20
+ private_constant :NOSCRIPT
21
+
22
+ EMPTY_LIST = [].freeze
23
+ private_constant :EMPTY_LIST
24
+
25
+ # Lua script source.
26
+ # @return [String]
27
+ attr_reader :source
28
+
29
+ # Lua script SHA1 digest.
30
+ # @return [String]
31
+ attr_reader :digest
32
+
33
+ # @param source [#to_s] Lua script
34
+ def initialize(source)
35
+ @source = -source.to_s
36
+ @digest = Digest::SHA1.hexdigest(@source).freeze
37
+ end
38
+
39
+ # Executes script and return result of execution.
40
+ # @param redis [Redis, RedisClient]
41
+ # @param keys [Array] keys to pass to the script
42
+ # @param argv [Array] arguments to pass to the script
43
+ # @raise [TypeError] if given redis client is not supported
44
+ # @raise [ScriptError] if script execution failed
45
+ # @return depends on the script
46
+ def call(redis, keys: EMPTY_LIST, argv: EMPTY_LIST)
47
+ unpool(redis) { |r| evalsha_with_fallback(r, keys, argv) }
48
+ rescue CommandError => e
49
+ raise ScriptError.new(e.message, @source)
50
+ end
51
+
52
+ private
53
+
54
+ def unpool(redis)
55
+ if redis.respond_to?(:with)
56
+ redis.with { |r| yield Adapters[r] }
57
+ else
58
+ yield Adapters[redis]
59
+ end
60
+ end
61
+
62
+ def evalsha_with_fallback(redis, keys, argv)
63
+ redis.evalsha(@digest, keys, argv)
64
+ rescue CommandError => e
65
+ raise unless e.message.include?(NOSCRIPT)
66
+
67
+ redis.eval(@source, keys, argv)
68
+ end
69
+ end
metadata CHANGED
@@ -1,56 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-prescription
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Zapparov
8
- autorequire:
9
- bindir: bin
8
+ autorequire:
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2018-02-11 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.16'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.16'
11
+ date: 2023-04-05 00:00:00.000000000 Z
12
+ dependencies: []
27
13
  description: |
28
- Preloads (and reloads when needed, e.g. when scripts
29
- were flushed away) script and then runs it with `EVALSHA`.
14
+ Preloads (and reloads when needed, e.g. when scripts were flushed away)
15
+ script and then runs it with `EVALSHA`.
30
16
  email:
31
- - ixti@member.fsf.org
17
+ - alexey@zapparov.com
32
18
  executables: []
33
19
  extensions: []
34
20
  extra_rdoc_files: []
35
21
  files:
36
- - ".gitignore"
37
- - ".rspec"
38
- - ".rubocop.yml"
39
- - ".travis.yml"
40
- - ".yardopts"
41
- - Gemfile
42
- - Guardfile
43
22
  - LICENSE.txt
44
- - README.md
45
- - Rakefile
46
- - lib/redis/prescription.rb
47
- - lib/redis/prescription/version.rb
48
- - redis-prescription.gemspec
49
- homepage: https://github.com/ixti/redis-prescription
23
+ - README.adoc
24
+ - lib/redis-prescription.rb
25
+ - lib/redis_prescription.rb
26
+ - lib/redis_prescription/adapters.rb
27
+ - lib/redis_prescription/adapters/redis.rb
28
+ - lib/redis_prescription/adapters/redis_client.rb
29
+ - lib/redis_prescription/errors.rb
30
+ - lib/redis_prescription/version.rb
31
+ homepage: https://gitlab.com/ixti/redis-prescription
50
32
  licenses:
51
33
  - MIT
52
- metadata: {}
53
- post_install_message:
34
+ metadata:
35
+ homepage_uri: https://gitlab.com/ixti/redis-prescription
36
+ source_code_uri: https://gitlab.com/ixti/redis-prescription
37
+ bug_tracker_uri: https://gitlab.com/ixti/redis-prescription/issues
38
+ changelog_uri: https://gitlab.com/ixti/redis-prescription/blob/v2.1.0/CHANGES.md
39
+ rubygems_mfa_required: 'true'
40
+ post_install_message:
54
41
  rdoc_options: []
55
42
  require_paths:
56
43
  - lib
@@ -58,16 +45,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
58
45
  requirements:
59
46
  - - ">="
60
47
  - !ruby/object:Gem::Version
61
- version: '0'
48
+ version: '3.0'
62
49
  required_rubygems_version: !ruby/object:Gem::Requirement
63
50
  requirements:
64
51
  - - ">="
65
52
  - !ruby/object:Gem::Version
66
53
  version: '0'
67
54
  requirements: []
68
- rubyforge_project:
69
- rubygems_version: 2.7.3
70
- signing_key:
55
+ rubygems_version: 3.3.26
56
+ signing_key:
71
57
  specification_version: 4
72
58
  summary: Redis LUA stored procedure runner.
73
59
  test_files: []
data/.gitignore DELETED
@@ -1,10 +0,0 @@
1
- /Gemfile.lock
2
-
3
- /.autoenv.zsh
4
- /.bundle
5
- /.yardoc
6
- /_yardoc
7
- /coverage
8
- /doc
9
- /pkg
10
- /tmp
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,40 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.3
3
- DisplayCopNames: true
4
-
5
-
6
- ## Layout ######################################################################
7
-
8
- Layout/DotPosition:
9
- EnforcedStyle: trailing
10
-
11
- Layout/IndentArray:
12
- EnforcedStyle: consistent
13
-
14
-
15
- ## Metrics #####################################################################
16
-
17
- Metrics/BlockLength:
18
- Exclude:
19
- - "spec/**/*"
20
-
21
-
22
- ## Style #######################################################################
23
-
24
- Style/HashSyntax:
25
- EnforcedStyle: hash_rockets
26
-
27
- Style/RegexpLiteral:
28
- EnforcedStyle: percent_r
29
-
30
- Style/RescueStandardError:
31
- EnforcedStyle: implicit
32
-
33
- Style/SafeNavigation:
34
- Enabled: false
35
-
36
- Style/StringLiterals:
37
- EnforcedStyle: double_quotes
38
-
39
- Style/YodaCondition:
40
- Enabled: false
data/.travis.yml DELETED
@@ -1,28 +0,0 @@
1
- language: ruby
2
- sudo: false
3
-
4
- services:
5
- - redis-server
6
-
7
- cache: bundler
8
-
9
- rvm:
10
- - 2.3
11
- - 2.4
12
- - 2.5
13
-
14
- matrix:
15
- fast_finish: true
16
- include:
17
- - rvm: 2.4
18
- env: TEST_SUITE="rubocop"
19
-
20
- before_install:
21
- - gem update --system
22
- - gem --version
23
- - gem install bundler --no-rdoc --no-ri
24
- - bundle --version
25
-
26
- install: bundle install --without development doc
27
-
28
- script: bundle exec rake $TEST_SUITE
data/.yardopts DELETED
@@ -1,2 +0,0 @@
1
- --markup-provider=redcarpet
2
- --markup=markdown
data/Gemfile DELETED
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
- ruby RUBY_VERSION
5
-
6
- gem "rake"
7
- gem "rspec"
8
- gem "rubocop", "~> 0.52.0", :require => false
9
-
10
- gem "redis"
11
- gem "redis-namespace"
12
-
13
- group :development do
14
- gem "guard", :require => false
15
- gem "guard-rspec", :require => false
16
- gem "guard-rubocop", :require => false
17
- gem "pry", :require => false
18
- end
19
-
20
- group :test do
21
- gem "codecov", :require => false
22
- gem "simplecov", :require => false
23
- end
24
-
25
- group :doc do
26
- gem "redcarpet"
27
- gem "yard"
28
- end
29
-
30
- # Specify your gem's dependencies in redis-prescription.gemspec
31
- gemspec
data/Guardfile DELETED
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- guard :rspec, :cmd => "bundle exec rspec" do
4
- require "guard/rspec/dsl"
5
- dsl = Guard::RSpec::Dsl.new(self)
6
-
7
- # RSpec files
8
- rspec = dsl.rspec
9
- watch(rspec.spec_helper) { rspec.spec_dir }
10
- watch(rspec.spec_support) { rspec.spec_dir }
11
- watch(rspec.spec_files)
12
-
13
- # Ruby files
14
- ruby = dsl.ruby
15
- dsl.watch_spec_files_for(ruby.lib_files)
16
- end
17
-
18
- guard :rubocop do
19
- watch(%r{.+\.rb$})
20
- watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
21
- end
data/Rakefile DELETED
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
-
5
- require "rspec/core/rake_task"
6
- RSpec::Core::RakeTask.new
7
-
8
- require "rubocop/rake_task"
9
- RuboCop::RakeTask.new
10
-
11
- if ENV["CI"]
12
- task :default => :spec
13
- else
14
- task :default => %i[rubocop spec]
15
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Redis
4
- class Prescription
5
- # Gem version.
6
- VERSION = "1.0.0"
7
- end
8
- end
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "prescription/version"
4
-
5
- # @see https://github.com/redis/redis-rb
6
- class Redis
7
- # Lua script executor for redis.
8
- #
9
- # Instead of executing script with `EVAL` everytime - loads script once
10
- # and then runs it with `EVALSHA`.
11
- #
12
- # @example Usage
13
- #
14
- # script = Redis::Prescription.new("return ARGV[1] + ARGV[2]")
15
- # script.eval(Redis.current, :argv => [2, 2]) # => 2
16
- class Prescription
17
- # Script load command.
18
- LOAD = "load"
19
- private_constant :LOAD
20
-
21
- # Redis error fired when script ID is unkown.
22
- NOSCRIPT = "NOSCRIPT"
23
- private_constant :NOSCRIPT
24
-
25
- # LUA script source.
26
- # @return [String]
27
- attr_reader :source
28
-
29
- # LUA script SHA1 digest.
30
- # @return [String]
31
- attr_reader :digest
32
-
33
- # @param source [#to_s] Lua script.
34
- def initialize(source)
35
- @source = source.to_s.strip.freeze
36
- @digest = Digest::SHA1.hexdigest(@source).freeze
37
- end
38
-
39
- # Loads script to redis.
40
- # @param redis (see #namespaceless)
41
- # @return [void]
42
- def bootstrap!(redis)
43
- digest = namespaceless(redis).script(LOAD, @source)
44
- return if @digest == digest
45
-
46
- # XXX: this may happen **ONLY** if script digesting will be
47
- # changed in redis, which is not likely gonna happen.
48
- warn "[#{self.class}] Unexpected digest: " \
49
- "#{digest.inspect} (expected: #{@digest.inspect})"
50
-
51
- @digest = digest.freeze
52
- end
53
-
54
- # Executes script and returns result of execution.
55
- # @param redis (see #namespaceless)
56
- # @param keys [Array] keys to pass to the script
57
- # @param argv [Array] arguments to pass to the script
58
- # @return depends on the script
59
- def eval(redis, keys: [], argv: [])
60
- redis.evalsha(@digest, keys, argv)
61
- rescue => e
62
- raise unless e.message.include? NOSCRIPT
63
-
64
- bootstrap!(redis)
65
- redis.evalsha(@digest, keys, argv)
66
- end
67
-
68
- # Reads given file and returns new {Prescription} with its contents.
69
- # @param file [String]
70
- # @return [Prescription]
71
- def self.read(file)
72
- new File.read file
73
- end
74
-
75
- private
76
-
77
- # Yields real namespace-less redis client.
78
- # @param redis [Redis, Redis::Namespace]
79
- # @return [Redis]
80
- def namespaceless(redis)
81
- if redis.is_a?(Redis)
82
- redis
83
- elsif defined?(Redis::Namespace) && redis.is_a?(Redis::Namespace)
84
- redis.redis
85
- else
86
- raise TypeError, "Unsupported redis client type: #{redis.class}"
87
- end
88
- end
89
- end
90
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- lib = File.expand_path("../lib", __FILE__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
-
6
- require "redis/prescription/version"
7
-
8
- Gem::Specification.new do |spec|
9
- spec.name = "redis-prescription"
10
- spec.version = Redis::Prescription::VERSION
11
- spec.authors = ["Alexey Zapparov"]
12
- spec.email = ["ixti@member.fsf.org"]
13
-
14
- spec.summary = "Redis LUA stored procedure runner."
15
- spec.description = <<~DESCRIPTION
16
- Preloads (and reloads when needed, e.g. when scripts
17
- were flushed away) script and then runs it with `EVALSHA`.
18
- DESCRIPTION
19
-
20
- spec.homepage = "https://github.com/ixti/redis-prescription"
21
- spec.license = "MIT"
22
-
23
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
24
- f.match(%r{^(test|spec|features)/})
25
- end
26
-
27
- spec.require_paths = ["lib"]
28
-
29
- spec.add_development_dependency "bundler", "~> 1.16"
30
- end