redis_scripts 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ /Gemfile.lock
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ == 0.0.1 2013-01-21
2
+
3
+ * Hi.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source :rubygems
2
+ gemspec
3
+
4
+ group :test do
5
+ gem 'debugger'
6
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) George Ogata
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,49 @@
1
+ ## Redis Scripts
2
+
3
+ Elegant redis scripting for ruby.
4
+
5
+ ## Show me
6
+
7
+ require 'redis_scripts'
8
+
9
+ # Grab a redis handle.
10
+ redis = Redis.new
11
+
12
+ # Configure the scripts location. Can also be done globally via
13
+ # RedisScripts.load_path.
14
+ redis.scripts.load_path = '/path/to/scripts'
15
+
16
+ # Run the script at /path/to/scripts/foo.lua .
17
+ redis.scripts.run :foo, keys, values
18
+
19
+ The call to `run` intuitively translates to a call to `EVALSHA`. If the SHA is
20
+ not in the redis script cache, it will be loaded (with `SCRIPT LOAD`), and then
21
+ re-executed via `EVALSHA`, ensuring future calls are optimal.
22
+
23
+ ## Priming the cache
24
+
25
+ You can load all scripts into cache ahead of time like this:
26
+
27
+ redis.scripts.load_all
28
+
29
+ You could do this, for example, each time you deploy updates to your
30
+ application.
31
+
32
+ ## Emptying the cache
33
+
34
+ Redis does not offer a way to list or delete individual scripts. To clear out
35
+ the script cache, you'll need to call `redis.script 'flush'` to empty the cache
36
+ entirely. You can then call `load_all` to reload your scripts, or just let
37
+ subsequent calls to `run` restore them as needed.
38
+
39
+ ## Contributing
40
+
41
+ * [Bug reports](https://github.com/oggy/redis_scripts/issues)
42
+ * [Source](https://github.com/oggy/redis_scripts)
43
+ * Patches: Fork on Github, send pull request.
44
+ * Include tests where practical.
45
+ * Leave the version alone, or bump it in a separate commit.
46
+
47
+ ## Copyright
48
+
49
+ Copyright (c) George Ogata. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'ritual'
@@ -0,0 +1,11 @@
1
+ class RedisScripts
2
+ VERSION = [0, 0, 1]
3
+
4
+ class << VERSION
5
+ include Comparable
6
+
7
+ def to_s
8
+ join('.')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,165 @@
1
+ require 'digest/sha1'
2
+ require 'shellwords'
3
+ require 'redis'
4
+
5
+ # Adapter for elegant Redis scripting.
6
+ #
7
+ # This is usually accessed as +redis.scripts+, although can also be instantiated
8
+ # as +RedisScripts.new(redis)+.
9
+ class RedisScripts
10
+ autoload :VERSION, 'redis_scripts/version'
11
+
12
+ class << self
13
+ # Global load path for redis scripts.
14
+ #
15
+ # redis.scripts.load_path defaults to this value for all redis clients.
16
+ attr_accessor :load_path
17
+ end
18
+
19
+ # Create a RedisScripts adapter for the given +redis+ handle.
20
+ def initialize(redis)
21
+ @redis = redis
22
+ @load_path = RedisScripts.load_path
23
+ end
24
+
25
+ # The adapter's redis handle.
26
+ attr_reader :redis
27
+
28
+ # Paths to look for Redis scripts.
29
+ #
30
+ # These directories are searched recursively for all .lua files. Defaults to
31
+ # RedisScripts.load_path, which itself has no default, so one of these needs
32
+ # to be set.
33
+ #
34
+ # Like the ruby load path, earlier directories shadow later directories in the
35
+ # event two directories contain scripts with the same name.
36
+ attr_accessor :load_path
37
+
38
+ # Run the script named +name+ with the given +args+.
39
+ #
40
+ # +name+ is the path of the script relative to the +load_path+, minus the
41
+ # '.lua' extension. So if the load_path contains 'scripts', and the script is
42
+ # at 'scripts/foo/bar.lua', then +name+ should be 'foo/bar'. +name+ may be a
43
+ # string or symbol.
44
+ #
45
+ # +args+ are passed to Redis#evalsha (see documentation for the Redis gem). If
46
+ # the script is not yet loaded in the redis script cache, it is loaded and
47
+ # called again. Note that this means this should not be called inside a MULTI
48
+ # transaction - this is usually not a problem, since the purpose of scripting
49
+ # is to perform a sequence of atomic operations in a single command.
50
+ #
51
+ # Raises ArgumentError if no such script exists.
52
+ def run(name, *args)
53
+ script = script(name)
54
+ begin
55
+ redis.evalsha script.sha, *args
56
+ rescue Redis::CommandError => error
57
+ error.message.include?('NOSCRIPT') or
58
+ raise
59
+ sha, value = redis.pipelined do
60
+ redis.script 'load', script.content
61
+ redis.evalsha(script.sha, *args)
62
+ end
63
+ sha == script.sha or
64
+ raise SHAMismatch, "SHA mismatch for #{name}: expected #{script.sha}, got #{sha}"
65
+ value
66
+ end
67
+ end
68
+
69
+ # Call EVAL for the named script.
70
+ #
71
+ # Raises ArgumentError if no such script exists.
72
+ def eval(name, *args)
73
+ redis.eval script(name).content, *args
74
+ end
75
+
76
+ # Call SCRIPT LOAD for the named script.
77
+ #
78
+ # Raises ArgumentError if no such script exists.
79
+ def load(name)
80
+ redis.script 'load', script(name).content
81
+ end
82
+
83
+ # Call SCRIPT LOAD for all scripts.
84
+ #
85
+ # This effectively primes the script cache with all your scripts. It does not
86
+ # remove any scripts - use +redis.script('flush')+ to empty the script cache
87
+ # first if that is required.
88
+ def load_all
89
+ scripts.each do |name, script|
90
+ redis.script 'load', script.content
91
+ end
92
+ end
93
+
94
+ # Call SCRIPT EXISTS for the named script.
95
+ #
96
+ # Raises ArgumentError if no such script exists.
97
+ def exists(name)
98
+ redis.script 'exists', script(name).sha
99
+ end
100
+
101
+ # Call EVALSHA for the named script.
102
+ #
103
+ # Raises ArgumentError if no such script exists.
104
+ def evalsha(name, *args)
105
+ redis.evalsha script(name).sha, *args
106
+ end
107
+
108
+ # Return the named script, as a Script object.
109
+ #
110
+ # Raises ArgumentError if no such script exists.
111
+ def script(name)
112
+ scripts[name.to_s] or
113
+ raise ArgumentError, "no such script: #{name}"
114
+ end
115
+
116
+ # Represents a script in a lua file under the load path.
117
+ Script = Struct.new(:name, :path) do
118
+ # The SHA1 of the content of the script.
119
+ def sha
120
+ @sha ||= Digest::SHA1.file(path).to_s
121
+ end
122
+
123
+ # The content of the script.
124
+ def content
125
+ @content = File.read(path)
126
+ end
127
+ end
128
+
129
+ # Raised when Redis returns an unexpected SHA when loading a script into the
130
+ # redis script cache.
131
+ #
132
+ # Should never happen.
133
+ SHAMismatch = Class.new(RuntimeError)
134
+
135
+ private
136
+
137
+ def scripts
138
+ @scripts ||= find_scripts
139
+ end
140
+
141
+ def find_scripts
142
+ scripts = {}
143
+ @load_path.each do |path|
144
+ command = ['find', path, '-name', '*.lua'].shelljoin
145
+ prefix = /^#{Regexp.escape(path)}#{File::SEPARATOR}/
146
+ `#{command} 2> /dev/null`.lines.each do |path|
147
+ path.chomp!
148
+ name = path.sub(prefix, '').sub(/\.lua\z/, '')
149
+ scripts[name] ||= Script.new(name, path)
150
+ end
151
+ end
152
+ scripts
153
+ end
154
+
155
+ module Mixin
156
+ # Return a RedisScripts adapter for this redis handle.
157
+ #
158
+ # See RedisScripts for details.
159
+ def scripts
160
+ @scripts ||= RedisScripts.new(self)
161
+ end
162
+ end
163
+
164
+ Redis.__send__ :include, Mixin
165
+ end
@@ -0,0 +1,20 @@
1
+ $:.unshift File.expand_path('lib', File.dirname(__FILE__))
2
+ require 'redis_scripts/version'
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'redis_scripts'
6
+ gem.version = RedisScripts::VERSION
7
+ gem.authors = ['George Ogata']
8
+ gem.email = ['george.ogata@gmail.com']
9
+ gem.description = "Elegant redis scripting for ruby."
10
+ gem.summary = "Elegant redis scripting for ruby."
11
+ gem.homepage = 'https://github.com/oggy/redis_scripts'
12
+
13
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+
17
+ gem.add_runtime_dependency 'redis', '~> 3.0.0'
18
+ gem.add_development_dependency 'temporaries', '~> 0.3.0'
19
+ gem.add_development_dependency 'ritual', '~> 0.4.1'
20
+ end
@@ -0,0 +1,15 @@
1
+ ROOT = File.expand_path('..', File.dirname(__FILE__))
2
+ Bundler.require(:test)
3
+
4
+ $:.unshift "#{ROOT}/lib"
5
+ require 'redis_scripts'
6
+ require 'minitest/spec'
7
+ require 'temporaries'
8
+
9
+ require 'yaml'
10
+ config_path = "#{ROOT}/redis.yml"
11
+ if File.exist?(config_path)
12
+ REDIS_CONFIG = YAML.load_file(config_path)
13
+ else
14
+ REDIS_CONFIG = {url: 'redis://localhost:6379'}
15
+ end
@@ -0,0 +1,147 @@
1
+ require_relative 'test_helper'
2
+
3
+ describe RedisScripts do
4
+ use_temporary_directory "#{ROOT}/test/tmp"
5
+
6
+ let(:redis) { Redis.new(REDIS_CONFIG) }
7
+ let(:scripts) { redis.scripts }
8
+
9
+ before do
10
+ redis.flushall
11
+ redis.script 'flush'
12
+ end
13
+
14
+ after do
15
+ redis.quit
16
+ end
17
+
18
+ describe "#initialize" do
19
+ it "defaults the load path to the global one, if available" do
20
+ RedisScripts.load_path = ['global']
21
+ scripts.load_path.must_equal ['global']
22
+ end
23
+
24
+ it "finds scripts in all load path directories" do
25
+ write_file "#{tmp}/1/a.lua", "return 'a'"
26
+ write_file "#{tmp}/2/b.lua", "return 'b'"
27
+ scripts.load_path = ["#{tmp}/1", "#{tmp}/2"]
28
+ scripts.script('a').name.must_equal 'a'
29
+ scripts.script('b').name.must_equal 'b'
30
+ end
31
+
32
+ it "finds scripts in nested directories" do
33
+ write_file "#{tmp}/a/b.lua", "return 'a/b'"
34
+ scripts.load_path = [tmp]
35
+ scripts.script('a/b').name.must_equal 'a/b'
36
+ end
37
+
38
+ it "favors scripts that come earlier in the load path" do
39
+ write_file "#{tmp}/1/a.lua", "return 'a'"
40
+ write_file "#{tmp}/2/a.lua", "return 'b'"
41
+ scripts.load_path = ["#{tmp}/1", "#{tmp}/2"]
42
+ scripts.eval('a', [], []).must_equal 'a'
43
+ end
44
+ end
45
+
46
+ describe "#script" do
47
+ it "returns the named script if it exists" do
48
+ write_file "#{tmp}/a.lua", "return 'a'"
49
+ scripts.load_path = [tmp]
50
+ scripts.script('a').must_be_instance_of(RedisScripts::Script)
51
+ end
52
+
53
+ it "raises an ArgumentError if no such script exists" do
54
+ scripts.load_path = [tmp]
55
+ -> { scripts.script('a') }.must_raise(ArgumentError)
56
+ end
57
+
58
+ it "supports a Symbol argument" do
59
+ write_file "#{tmp}/a.lua", "return 'a'"
60
+ scripts.load_path = [tmp]
61
+ scripts.script(:a).must_be_instance_of(RedisScripts::Script)
62
+ end
63
+ end
64
+
65
+ describe "#load" do
66
+ it "loads the named script into the script cache" do
67
+ write_file "#{tmp}/a.lua", "return 'a'"
68
+ scripts.load_path = [tmp]
69
+ scripts.load 'a'
70
+ redis.script('exists', Digest::SHA1.hexdigest("return 'a'")).must_equal true
71
+ end
72
+
73
+ it "raises ArgumentError if there is no such script" do
74
+ scripts.load_path = [tmp]
75
+ -> { scripts.load('a', [], []) }.must_raise(ArgumentError)
76
+ end
77
+ end
78
+
79
+ describe "#load_all" do
80
+ it "loads all scripts into the script cache" do
81
+ write_file "#{tmp}/a.lua", "return 'a'"
82
+ write_file "#{tmp}/b.lua", "return 'b'"
83
+ scripts.load_path = [tmp]
84
+ scripts.load_all
85
+ redis.script('exists', Digest::SHA1.hexdigest("return 'a'")).must_equal true
86
+ redis.script('exists', Digest::SHA1.hexdigest("return 'b'")).must_equal true
87
+ end
88
+ end
89
+
90
+ describe "#exists" do
91
+ it "runs SCRIPT EXISTS for the named script" do
92
+ write_file "#{tmp}/a.lua", "return 'a'"
93
+ write_file "#{tmp}/b.lua", "return 'b'"
94
+ scripts.load_path = [tmp]
95
+ scripts.load 'a'
96
+ scripts.exists('a').must_equal true
97
+ scripts.exists('b').must_equal false
98
+ end
99
+
100
+ it "raises ArgumentError if there is no such script" do
101
+ scripts.load_path = [tmp]
102
+ -> { scripts.exists('a', [], []) }.must_raise(ArgumentError)
103
+ end
104
+ end
105
+
106
+ describe "#evalsha" do
107
+ it "runs EVALSHA for the named script" do
108
+ write_file "#{tmp}/a.lua", "return 'a'"
109
+ write_file "#{tmp}/b.lua", "return 'b'"
110
+ scripts.load_path = [tmp]
111
+ scripts.load 'a'
112
+ scripts.evalsha('a', [], []).must_equal 'a'
113
+ -> { scripts.evalsha('b', [], []) }.must_raise(Redis::CommandError)
114
+ end
115
+
116
+ it "raises ArgumentError if there is no such script" do
117
+ scripts.load_path = [tmp]
118
+ -> { scripts.evalsha('a', [], []) }.must_raise(ArgumentError)
119
+ end
120
+ end
121
+
122
+ describe "#run" do
123
+ it "evaluates the named script" do
124
+ write_file "#{tmp}/a.lua", "return 'a'"
125
+ scripts.load_path = [tmp]
126
+ scripts.run('a', [], []).must_equal 'a'
127
+ scripts.run('a', [], []).must_equal 'a' # cached case
128
+ end
129
+
130
+ it "loads the script into cache for fast subsequent execution" do
131
+ write_file "#{tmp}/a.lua", "return 'a'"
132
+ scripts.load_path = [tmp]
133
+ scripts.run('a', [], [])
134
+ redis.script('exists', Digest::SHA1.hexdigest("return 'a'")).must_equal true
135
+ end
136
+
137
+ it "raises ArgumentError if there is no such script" do
138
+ scripts.load_path = [tmp]
139
+ -> { scripts.run('a', [], []) }.must_raise(ArgumentError)
140
+ end
141
+ end
142
+
143
+ def write_file(path, content)
144
+ FileUtils.mkdir_p File.dirname(path)
145
+ open(path, 'w') { |f| f.print content }
146
+ end
147
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis_scripts
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - George Ogata
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: temporaries
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.3.0
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.3.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: ritual
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 0.4.1
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.4.1
62
+ description: Elegant redis scripting for ruby.
63
+ email:
64
+ - george.ogata@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - CHANGELOG
71
+ - Gemfile
72
+ - LICENSE
73
+ - README.markdown
74
+ - Rakefile
75
+ - lib/redis_scripts.rb
76
+ - lib/redis_scripts/version.rb
77
+ - redis_scripts.gemspec
78
+ - test/test_helper.rb
79
+ - test/test_redis_scripts.rb
80
+ homepage: https://github.com/oggy/redis_scripts
81
+ licenses: []
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ! '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ! '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 1.8.23
101
+ signing_key:
102
+ specification_version: 3
103
+ summary: Elegant redis scripting for ruby.
104
+ test_files:
105
+ - test/test_helper.rb
106
+ - test/test_redis_scripts.rb
107
+ has_rdoc: