wolverine 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -2,3 +2,5 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ doc
6
+ .yardoc/*
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - jruby-18mode
7
+ - jruby-19mode
8
+ - rbx-18mode
9
+ - rbx-19mode
10
+
data/README.md CHANGED
@@ -1,12 +1,10 @@
1
- # Wolverine
1
+ # Wolverine [![Build Status](https://secure.travis-ci.org/burke/wolverine.png)](http://travis-ci.org/burke/wolverine) [![Dependency Status](https://gemnasium.com/Shopify/wolverine.png)](https://gemnasium.com/Shopify/wolverine)
2
2
 
3
3
  Wolverine is a simple library to allow you to manage and run redis server-side lua scripts from a rails app, or other ruby code.
4
4
 
5
- ## What are you talking about?
5
+ Redis versions 2.6 and up allow lua scripts to be run on the server that execute atomically and very quickly.
6
6
 
7
- Redis versions 2.6 and up allow lua scripts to be run on the server that execute atomically and very, very quickly.
8
-
9
- This is really, really cool.
7
+ This is *extremely* useful.
10
8
 
11
9
  Wolverine is a wrapper around that functionality, to package it up in a format more familiar to a Rails codebase.
12
10
 
@@ -53,8 +51,9 @@ Available configuration options:
53
51
 
54
52
  * `Wolverine.config.redis` (default `Redis.new`)
55
53
  * `Wolverine.config.script_path` (default `Rails.root + 'app/wolverine'`)
54
+ * `Wolverine.config.instrumentation` (default none)
56
55
 
57
- If you want to override one or both of these, doing so in an initializer is recommended but not required.
56
+ If you want to override one or more of these, doing so in an initializer is recommended but not required. See the [full documentation](http://shopify.github.com/wolverine/Wolverine/Configuration.html) for more details.
58
57
 
59
58
  ## More information
60
59
 
@@ -62,7 +61,7 @@ For more information on scripting redis with lua, refer to redis' excellent docu
62
61
 
63
62
  ## License
64
63
 
65
- Copyright (C) 2012 Shopify
64
+ Copyright (C) 2012 [Shopify](http://shopify.com) by [Burke Libbey](http://burkelibbey.org)
66
65
 
67
66
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
68
67
 
data/Rakefile CHANGED
@@ -5,4 +5,18 @@ Rake::TestTask.new do |t|
5
5
  t.pattern = "test/**/*_test.rb"
6
6
  end
7
7
 
8
- task default: :test
8
+ require 'yard'
9
+ require 'yard/rake/yardoc_task'
10
+ YARD::Rake::YardocTask.new do |yardoc|
11
+ yardoc.options = ['--verbose']
12
+ # yardoc.files = [
13
+ # 'lib/**/*.rb', 'README.md', 'CHANGELOG.md', 'LICENSE'
14
+ # ]
15
+ end
16
+
17
+ task :docs do
18
+ Rake::Task['yard'].invoke
19
+ system("cd doc && git add . && git commit -am 'Regenerated docs' && git push origin gh-pages")
20
+ end
21
+
22
+ task :default => :test
data/TODO CHANGED
@@ -1,5 +1,2 @@
1
- - be smarter about trimming and rearranging the backtrace
2
- - cache methods on Wolverine, rather than using mm all the time
3
- - clean up error handling method
4
- - Re-evaluate what actually needs to be exposed as public
5
- - Think of more TODO items
1
+ - cache methods on Wolverine, rather than using method_missing all the time
2
+ - better tests around error handling and stacktrace trimming
data/lib/wolverine.rb CHANGED
@@ -8,26 +8,46 @@ require 'wolverine/path_component'
8
8
  require 'wolverine/lua_error'
9
9
 
10
10
  module Wolverine
11
+ # Returns the configuration object for reading and writing
12
+ # configuration values.
13
+ #
14
+ # @return [Wolverine::Configuration] the configuration object
11
15
  def self.config
12
16
  @config ||= Configuration.new
13
17
  end
14
18
 
19
+ # Provides access to the redis connection currently in use by Wolverine.
20
+ #
21
+ # @return [Redis] the redis connection used by Wolverine
15
22
  def self.redis
16
23
  config.redis
17
24
  end
18
25
 
26
+ # Resets all the scripts cached by Wolverine. Scripts are lazy-loaded and
27
+ # cached in-memory, so if a file changes on disk, it will be necessary to
28
+ # manually reset the cache using +reset!+.
29
+ #
30
+ # @return [void]
19
31
  def self.reset!
20
32
  @root_directory = nil
21
33
  end
22
34
 
23
- def self.root_directory
24
- @root_directory ||= PathComponent.new(config.script_path)
25
- end
26
-
35
+ # Used to handle dynamic accesses to scripts. Successful lookups will be
36
+ # cached on the {PathComponent} object. See {PathComponent#method_missing}
37
+ # for more detail on how this works.
38
+ #
39
+ # @return [PathComponent, Object] a PathComponent if the method maps to a
40
+ # directory, or an execution result if the the method maps to a lua file.
27
41
  def self.method_missing sym, *args
28
- root_directory.send(sym, *args)
42
+ root_directory.send(sym, *args)
29
43
  rescue PathComponent::MissingTemplate
30
- super
44
+ super
45
+ end
46
+
47
+ private
48
+
49
+ def self.root_directory
50
+ @root_directory ||= PathComponent.new(config.script_path)
31
51
  end
32
52
 
33
53
  end
@@ -1,9 +1,30 @@
1
1
  module Wolverine
2
- class Configuration < Struct.new(:redis, :script_path)
2
+ class Configuration < Struct.new(:redis, :script_path, :instrumentation)
3
+
4
+ # @return [Redis] the redis connection actively in use by Wolverine
3
5
  def redis
4
6
  super || @redis ||= Redis.new
5
7
  end
6
8
 
9
+ # Wolverine.config.instrumentation can be used to specify a callback to
10
+ # fire with the runtime of each script. This can be useful for analyzing
11
+ # scripts to make sure they aren't running for an unreasonable amount of
12
+ # time.
13
+ #
14
+ # The proc will receive three parameters:
15
+ #
16
+ # * +script_name+: A unique identifier for the script, based on its
17
+ # location in the file system
18
+ # * +runtime+: A float, the total execution time of the script
19
+ # * +eval_type+: Either +eval+ or +evalsha+, the method used to run
20
+ # the script
21
+ # @return [#call] the proc or other callable to be triggered on completion
22
+ # of a script.
23
+ def instrumentation
24
+ super || @instrumentation ||= proc { |script_name, runtime, eval_type| nil }
25
+ end
26
+
27
+ # @return [Pathname] the path wolverine will check for scripts
7
28
  def script_path
8
29
  super || @script_path ||= Rails.root + 'app/wolverine'
9
30
  end
@@ -1,29 +1,43 @@
1
1
  module Wolverine
2
+ # Reformats errors raised by redis representing failures while executing
3
+ # a lua script. The default errors have confusing messages and backtraces,
4
+ # and a type of +RuntimeError+. This class improves the message and
5
+ # modifies the backtrace to include the lua script itself in a reasonable
6
+ # way.
2
7
  class LuaError < StandardError
3
8
  PATTERN = /ERR Error (compiling|running) script \(.*?\): \[.*?\]:(\d+): (.*)/
4
9
  WOLVERINE_LIB_PATH = File.expand_path('../../', __FILE__)
5
10
 
6
- def self.intercepts?(e)
7
- e.message =~ PATTERN
11
+ # Is this error one that should be reformatted?
12
+ #
13
+ # @param error [StandardError] the original error raised by redis
14
+ # @return [Boolean] is this an error that should be reformatted?
15
+ def self.intercepts? error
16
+ error.message =~ PATTERN
8
17
  end
9
18
 
10
- attr_reader :error, :file
11
- def initialize(error, file)
19
+ # Initialize a new {LuaError} from an existing redis error, adjusting
20
+ # the message and backtrace in the process.
21
+ #
22
+ # @param error [StandardError] the original error raised by redis
23
+ # @param file [Pathname] full path to the lua file the error ocurred in
24
+ def initialize error, file
12
25
  @error = error
13
26
  @file = file
14
27
 
15
- error.message =~ PATTERN
28
+ @error.message =~ PATTERN
16
29
  stage, line_number, message = $1, $2, $3
17
30
 
18
-
19
- super(message)
20
- set_backtrace generate_backtrace(file, line_number)
31
+ super message
32
+ set_backtrace generate_backtrace file, line_number
21
33
  end
22
34
 
35
+ private
36
+
23
37
  def generate_backtrace(file, line_number)
24
- pre_wolverine = backtrace_before_entering_wolverine(error.backtrace)
25
- index_of_first_wolverine_line = (error.backtrace.size - pre_wolverine.size - 1)
26
- pre_wolverine.unshift(error.backtrace[index_of_first_wolverine_line])
38
+ pre_wolverine = backtrace_before_entering_wolverine(@error.backtrace)
39
+ index_of_first_wolverine_line = (@error.backtrace.size - pre_wolverine.size - 1)
40
+ pre_wolverine.unshift(@error.backtrace[index_of_first_wolverine_line])
27
41
  pre_wolverine.unshift("#{file}:#{line_number}")
28
42
  pre_wolverine
29
43
  end
@@ -38,4 +52,4 @@ module Wolverine
38
52
 
39
53
  end
40
54
 
41
- end
55
+ end
@@ -1,11 +1,28 @@
1
1
  module Wolverine
2
+ # A {PathComponent} represents either the +Wolverine.config.script_path+
3
+ # directory, or a subdirectory of it. Calling (nearly) any method on it will
4
+ # cause it to look in the filesystem at the location it refers to for a file
5
+ # or directory matching the method name. These results are cached.
6
+ #
7
+ # Calling a method that maps to a directory will return a new {PathComponent}
8
+ # with a +path+ referring to that directory.
9
+ #
10
+ # Calling a method that maps to a file (with +'.lua'+ automatically appended
11
+ # to the name) will load the file via {Script} and call it with the
12
+ # arugments passed, returning the result ({method_missing}).
2
13
  class PathComponent
3
14
  class MissingTemplate < StandardError ; end
4
15
 
16
+ # @param path [Pathname] full path to the current file or directory
5
17
  def initialize path
6
18
  @path = path
7
19
  end
8
20
 
21
+ # @param sym [Symbol] the file or directory to look up and execute
22
+ # @param args [*Objects] arguments to pass to the {Script}, if +sym+ resolves to a lua file
23
+ # @return [PathComponent, Object] A new, nested {PathComponent} if +sym+ resolves to
24
+ # a directory, or an execution result if it resolves to a file.
25
+ # @raise [MissingTemplate] if +sym+ maps to neither a directory or a file
9
26
  def method_missing sym, *args
10
27
  create_method sym, *args
11
28
  send sym, *args
@@ -50,4 +67,4 @@ module Wolverine
50
67
 
51
68
  end
52
69
 
53
- end
70
+ end
@@ -1,15 +1,33 @@
1
+ require 'pathname'
2
+ require 'benchmark'
1
3
  require 'digest/sha1'
2
4
 
3
5
  module Wolverine
4
-
6
+ # {Script} represents a lua script in the filesystem. It loads the script
7
+ # from disk and handles talking to redis to execute it. Error handling
8
+ # is handled by {LuaError}.
5
9
  class Script
6
- attr_reader :content, :digest, :file
10
+
11
+ # Loads the script file from disk and calculates its +SHA1+ sum.
12
+ #
13
+ # @param file [Pathname] the full path to the indicated file
7
14
  def initialize file
8
- @file = file
15
+ @file = Pathname.new(file)
9
16
  @content = load_lua file
10
17
  @digest = Digest::SHA1.hexdigest @content
11
18
  end
12
19
 
20
+ # Passes the script and supplied arguments to redis for evaulation.
21
+ # It first attempts to use a script redis has already cached by using
22
+ # the +EVALSHA+ command, but falls back to providing the full script
23
+ # text via +EVAL+ if redis has not seen this script before. Future
24
+ # invocations will then use +EVALSHA+ without erroring.
25
+ #
26
+ # @param redis [Redis] the redis connection to run against
27
+ # @param args [*Objects] the arguments to the script
28
+ # @return [Object] the value passed back by redis after script execution
29
+ # @raise [LuaError] if the script failed to compile of encountered a
30
+ # runtime error
13
31
  def call redis, *args
14
32
  begin
15
33
  run_evalsha redis, *args
@@ -17,8 +35,8 @@ module Wolverine
17
35
  e.message =~ /NOSCRIPT/ ? run_eval(redis, *args) : raise
18
36
  end
19
37
  rescue => e
20
- if LuaError.intercepts?(e)
21
- raise LuaError.new(e, file)
38
+ if LuaError.intercepts?(e)
39
+ raise LuaError.new(e, @file)
22
40
  else
23
41
  raise
24
42
  end
@@ -27,11 +45,26 @@ module Wolverine
27
45
  private
28
46
 
29
47
  def run_evalsha redis, *args
30
- redis.evalsha digest, args.size, *args
48
+ instrument :evalsha do
49
+ redis.evalsha @digest, args.size, *args
50
+ end
31
51
  end
32
52
 
33
53
  def run_eval redis, *args
34
- redis.eval content, args.size, *args
54
+ instrument :eval do
55
+ redis.eval @content, args.size, *args
56
+ end
57
+ end
58
+
59
+ def instrument eval_type
60
+ ret = nil
61
+ runtime = Benchmark.realtime { ret = yield }
62
+ Wolverine.config.instrumentation.call relative_path.to_s, runtime, eval_type
63
+ ret
64
+ end
65
+
66
+ def relative_path
67
+ path = @file.relative_path_from(Wolverine.config.script_path)
35
68
  end
36
69
 
37
70
  def load_lua file
@@ -1,3 +1,3 @@
1
1
  module Wolverine
2
- VERSION = "0.2.2"
2
+ VERSION = "0.2.3"
3
3
  end
@@ -1,4 +1,4 @@
1
- require_relative '../test_helper'
1
+ require File.join(File.expand_path('../../test_helper', __FILE__))
2
2
 
3
3
  class WolverineIntegrationTest < MiniTest::Unit::TestCase
4
4
 
@@ -1,4 +1,4 @@
1
- require_relative '../test_helper'
1
+ require File.join(File.expand_path('../../test_helper', __FILE__))
2
2
  require 'pathname'
3
3
 
4
4
  module Rails
@@ -19,6 +19,11 @@ module Wolverine
19
19
  assert_equal Pathname.new('foo/app/wolverine'), actual
20
20
  end
21
21
 
22
+ def test_default_instrumentation
23
+ config = Wolverine::Configuration.new
24
+ assert_equal nil, config.instrumentation.call(1, 2, 3)
25
+ end
26
+
22
27
  def test_setting_redis
23
28
  config = Wolverine::Configuration.new
24
29
  config.redis = :foo
@@ -31,5 +36,11 @@ module Wolverine
31
36
  assert_equal :foo, config.script_path
32
37
  end
33
38
 
39
+ def test_setting_instrumentation
40
+ config = Wolverine::Configuration.new
41
+ config.instrumentation = proc { |a, b, c| :omg }
42
+ assert_equal :omg, config.instrumentation.call(1,2,3)
43
+ end
44
+
34
45
  end
35
46
  end
@@ -1,4 +1,4 @@
1
- require_relative '../test_helper'
1
+ require File.join(File.expand_path('../../test_helper', __FILE__))
2
2
 
3
3
  module Wolverine
4
4
  class PathComponentTest < MiniTest::Unit::TestCase
@@ -27,4 +27,4 @@ module Wolverine
27
27
  end
28
28
 
29
29
  end
30
- end
30
+ end
@@ -1,4 +1,4 @@
1
- require_relative '../test_helper'
1
+ require File.join(File.expand_path('../../test_helper', __FILE__))
2
2
  require 'digest/sha1'
3
3
 
4
4
  module Wolverine
@@ -7,49 +7,47 @@ module Wolverine
7
7
  DIGEST = Digest::SHA1.hexdigest(CONTENT)
8
8
 
9
9
  def setup
10
- Wolverine::Script.any_instance.stubs(load_lua: CONTENT)
10
+ base = Pathname.new('/a/b/c/d')
11
+ Wolverine.config.script_path = base
12
+ Wolverine::Script.any_instance.stubs(:load_lua => CONTENT)
11
13
  end
12
14
 
13
- def script
14
- @script ||= Wolverine::Script.new('file1')
15
+ def teardown
16
+ Wolverine.config.instrumentation = proc{}
15
17
  end
16
18
 
17
- def test_compilation_error
18
- base = Pathname.new('/a/b/c/d')
19
- file = Pathname.new('/a/b/c/d/e/file1.lua')
20
- Wolverine.config.script_path = base
21
- begin
22
- script = Wolverine::Script.new(file)
23
- script.instance_variable_set("@content", "asdfasdfasdf+31f")
24
- script.instance_variable_set("@digest", "79437f5edda13f9c1669b978dd7a9066dd2059f1")
25
- script.call(Redis.new)
26
- rescue Wolverine::LuaError => e
27
- assert_equal "'=' expected near '+'", e.message
28
- assert_equal "/a/b/c/d/e/file1.lua:1", e.backtrace.first
29
- assert_match /script.rb/, e.backtrace[1]
30
- end
19
+ def script
20
+ @script ||= Wolverine::Script.new('/a/b/c/d/e/file1.lua')
31
21
  end
32
22
 
33
- def test_runtime_error
34
- base = Pathname.new('/a/b/c/d')
35
- file = Pathname.new('/a/b/c/d/e/file1.lua')
36
- Wolverine.config.script_path = base
23
+ def test_error
24
+ redis = stub
25
+ redis.expects(:evalsha).raises(%q{ERR Error running script (call to f_178d75adaa46af3d8237cfd067c9fdff7b9d504f): [string "func definition"]:1: attempt to compare nil with number})
37
26
  begin
38
- script = Wolverine::Script.new(file)
39
- script.instance_variable_set("@content", "return nil > 3")
40
- script.instance_variable_set("@digest", "39437f5edda13f9c1669b978dd7a9066dd2059f1")
41
- script.call(Redis.new)
27
+ script.call(redis)
42
28
  rescue Wolverine::LuaError => e
43
- assert_equal "attempt to compare number with nil", e.message
29
+ assert_equal "attempt to compare nil with number", e.message
44
30
  assert_equal "/a/b/c/d/e/file1.lua:1", e.backtrace.first
45
31
  assert_match /script.rb/, e.backtrace[1]
46
32
  end
47
33
  end
48
34
 
49
- def test_digest_and_content
50
- content = "return 1"
51
- assert_equal CONTENT, script.content
52
- assert_equal DIGEST, script.digest
35
+ def test_instrumentation
36
+ callback = Object.new
37
+ tc = self
38
+ meta = class << callback ; self ; end
39
+ meta.send(:define_method, :call) { |a, b, c|
40
+ tc.assert_equal "e/file1.lua", a
41
+ tc.assert_operator b, :<, 1
42
+ tc.assert_equal :evalsha, c
43
+ }
44
+ Wolverine.config.instrumentation = callback
45
+ redis = Class.new do
46
+ define_method(:evalsha) do |digest, size, *args|
47
+ nil
48
+ end
49
+ end
50
+ script.call(redis.new, :a, :b)
53
51
  end
54
52
 
55
53
  def test_call_with_cache_hit
@@ -1,4 +1,4 @@
1
- require_relative 'test_helper'
1
+ require File.join(File.expand_path('../test_helper', __FILE__))
2
2
 
3
3
  class WolverineTest < MiniTest::Unit::TestCase
4
4
 
data/wolverine.gemspec CHANGED
@@ -18,7 +18,9 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- s.add_runtime_dependency 'redis'
22
- s.add_development_dependency 'mocha'
23
- s.add_development_dependency 'minitest'
21
+ s.add_runtime_dependency 'redis', '~> 2.2.2'
22
+ s.add_development_dependency 'mocha', '~> 0.10.5'
23
+ s.add_development_dependency 'minitest', '~> 2.11.3'
24
+ s.add_development_dependency 'rake'
25
+ s.add_development_dependency 'yard'
24
26
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wolverine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,22 +9,44 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-03 00:00:00.000000000 Z
12
+ date: 2012-03-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
16
- requirement: &70197029666560 !ruby/object:Gem::Requirement
16
+ requirement: &70155163744540 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
- - - ! '>='
19
+ - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: '0'
21
+ version: 2.2.2
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70197029666560
24
+ version_requirements: *70155163744540
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: mocha
27
- requirement: &70197029690560 !ruby/object:Gem::Requirement
27
+ requirement: &70155163743720 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.10.5
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70155163743720
36
+ - !ruby/object:Gem::Dependency
37
+ name: minitest
38
+ requirement: &70155163743080 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 2.11.3
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70155163743080
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: &70155163742680 !ruby/object:Gem::Requirement
28
50
  none: false
29
51
  requirements:
30
52
  - - ! '>='
@@ -32,10 +54,10 @@ dependencies:
32
54
  version: '0'
33
55
  type: :development
34
56
  prerelease: false
35
- version_requirements: *70197029690560
57
+ version_requirements: *70155163742680
36
58
  - !ruby/object:Gem::Dependency
37
- name: minitest
38
- requirement: &70197029689640 !ruby/object:Gem::Requirement
59
+ name: yard
60
+ requirement: &70155163742180 !ruby/object:Gem::Requirement
39
61
  none: false
40
62
  requirements:
41
63
  - - ! '>='
@@ -43,7 +65,7 @@ dependencies:
43
65
  version: '0'
44
66
  type: :development
45
67
  prerelease: false
46
- version_requirements: *70197029689640
68
+ version_requirements: *70155163742180
47
69
  description: Wolverine provides a simple way to run server-side redis scripts from
48
70
  a rails app
49
71
  email:
@@ -53,6 +75,7 @@ extensions: []
53
75
  extra_rdoc_files: []
54
76
  files:
55
77
  - .gitignore
78
+ - .travis.yml
56
79
  - Gemfile
57
80
  - LICENSE
58
81
  - README.md
@@ -109,3 +132,4 @@ test_files:
109
132
  - test/wolverine/path_component_test.rb
110
133
  - test/wolverine/script_test.rb
111
134
  - test/wolverine_test.rb
135
+ has_rdoc: