resque-kalashnikov 1.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.
Files changed (72) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +99 -0
  7. data/Rakefile +7 -0
  8. data/demo/.gitignore +15 -0
  9. data/demo/Gemfile +31 -0
  10. data/demo/README.rdoc +9 -0
  11. data/demo/Rakefile +7 -0
  12. data/demo/app/assets/images/rails.png +0 -0
  13. data/demo/app/assets/javascripts/application.js +13 -0
  14. data/demo/app/assets/stylesheets/application.css +13 -0
  15. data/demo/app/controllers/application_controller.rb +3 -0
  16. data/demo/app/controllers/test_controller.rb +15 -0
  17. data/demo/app/helpers/application_helper.rb +2 -0
  18. data/demo/app/mailers/.gitkeep +0 -0
  19. data/demo/app/models/.gitkeep +0 -0
  20. data/demo/app/workers/slow_http_request.rb +3 -0
  21. data/demo/config.ru +4 -0
  22. data/demo/config/application.rb +65 -0
  23. data/demo/config/boot.rb +6 -0
  24. data/demo/config/environment.rb +5 -0
  25. data/demo/config/environments/development.rb +26 -0
  26. data/demo/config/environments/production.rb +51 -0
  27. data/demo/config/environments/test.rb +35 -0
  28. data/demo/config/initializers/backtrace_silencers.rb +7 -0
  29. data/demo/config/initializers/inflections.rb +15 -0
  30. data/demo/config/initializers/mime_types.rb +5 -0
  31. data/demo/config/initializers/secret_token.rb +7 -0
  32. data/demo/config/initializers/session_store.rb +8 -0
  33. data/demo/config/initializers/wrap_parameters.rb +10 -0
  34. data/demo/config/locales/en.yml +5 -0
  35. data/demo/config/routes.rb +11 -0
  36. data/demo/db/seeds.rb +7 -0
  37. data/demo/lib/assets/.gitkeep +0 -0
  38. data/demo/lib/tasks/.gitkeep +0 -0
  39. data/demo/log/.gitkeep +0 -0
  40. data/demo/public/404.html +26 -0
  41. data/demo/public/422.html +26 -0
  42. data/demo/public/500.html +25 -0
  43. data/demo/public/favicon.ico +0 -0
  44. data/demo/public/index.html +12 -0
  45. data/demo/public/robots.txt +5 -0
  46. data/demo/script/rails +6 -0
  47. data/demo/script/resque_async.rb +29 -0
  48. data/demo/vendor/assets/javascripts/.gitkeep +0 -0
  49. data/demo/vendor/assets/stylesheets/.gitkeep +0 -0
  50. data/demo/vendor/plugins/.gitkeep +0 -0
  51. data/demo/zeus.json +22 -0
  52. data/lib/event_machine/forced_stop.rb +3 -0
  53. data/lib/resque/catridge.rb +87 -0
  54. data/lib/resque/plugins/resque_kalashnikov/resque_kalashnikov.rb +130 -0
  55. data/lib/resque_kalashnikov.rb +17 -0
  56. data/lib/resque_kalashnikov/delegation.rb +153 -0
  57. data/lib/resque_kalashnikov/http_request.rb +64 -0
  58. data/lib/resque_kalashnikov/railtie.rb +10 -0
  59. data/lib/resque_kalashnikov/server.rb +50 -0
  60. data/lib/resque_kalashnikov/server/views/catridges.erb +57 -0
  61. data/lib/resque_kalashnikov/version.rb +3 -0
  62. data/lib/tasks.rb +34 -0
  63. data/resque-kalashnikov.gemspec +30 -0
  64. data/screenshot.png +0 -0
  65. data/spec/catridge_spec.rb +42 -0
  66. data/spec/http_request_spec.rb +84 -0
  67. data/spec/server_spec.rb +46 -0
  68. data/spec/spec_helper.rb +56 -0
  69. data/spec/support/stub_server.rb +46 -0
  70. data/spec/worker_spec.rb +113 -0
  71. data/tasks/resque_kalashnikov.rake +2 -0
  72. metadata +267 -0
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'em-synchrony'
3
+
4
+ require "resque_kalashnikov/delegation"
5
+ require 'resque/worker'
6
+ require 'resque/catridge'
7
+ require "resque/plugins/resque_kalashnikov/resque_kalashnikov"
8
+ require "event_machine/forced_stop"
9
+ require "resque_kalashnikov/http_request"
10
+ require "resque_kalashnikov/railtie" if defined?(Rails)
11
+
12
+ module ResqueKalashnikov
13
+ delegate :stats, :misfire_codes, :misfire_stats, :misfire_stats_reset, :reset_stats, to: Resque::Catridge, prefix: 'kalashnikov'
14
+ end
15
+
16
+ Resque.extend ResqueKalashnikov
17
+ Resque::Worker.send(:include, Resque::Plugins::ResqueKalashnikov)
@@ -0,0 +1,153 @@
1
+ class Module
2
+ # Provides a delegate class method to easily expose contained objects' methods
3
+ # as your own. Pass one or more methods (specified as symbols or strings)
4
+ # and the name of the target object via the <tt>:to</tt> option (also a symbol
5
+ # or string). At least one method and the <tt>:to</tt> option are required.
6
+ #
7
+ # Delegation is particularly useful with Active Record associations:
8
+ #
9
+ # class Greeter < ActiveRecord::Base
10
+ # def hello
11
+ # "hello"
12
+ # end
13
+ #
14
+ # def goodbye
15
+ # "goodbye"
16
+ # end
17
+ # end
18
+ #
19
+ # class Foo < ActiveRecord::Base
20
+ # belongs_to :greeter
21
+ # delegate :hello, :to => :greeter
22
+ # end
23
+ #
24
+ # Foo.new.hello # => "hello"
25
+ # Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
26
+ #
27
+ # Multiple delegates to the same target are allowed:
28
+ #
29
+ # class Foo < ActiveRecord::Base
30
+ # belongs_to :greeter
31
+ # delegate :hello, :goodbye, :to => :greeter
32
+ # end
33
+ #
34
+ # Foo.new.goodbye # => "goodbye"
35
+ #
36
+ # Methods can be delegated to instance variables, class variables, or constants
37
+ # by providing them as a symbols:
38
+ #
39
+ # class Foo
40
+ # CONSTANT_ARRAY = [0,1,2,3]
41
+ # @@class_array = [4,5,6,7]
42
+ #
43
+ # def initialize
44
+ # @instance_array = [8,9,10,11]
45
+ # end
46
+ # delegate :sum, :to => :CONSTANT_ARRAY
47
+ # delegate :min, :to => :@@class_array
48
+ # delegate :max, :to => :@instance_array
49
+ # end
50
+ #
51
+ # Foo.new.sum # => 6
52
+ # Foo.new.min # => 4
53
+ # Foo.new.max # => 11
54
+ #
55
+ # Delegates can optionally be prefixed using the <tt>:prefix</tt> option. If the value
56
+ # is <tt>true</tt>, the delegate methods are prefixed with the name of the object being
57
+ # delegated to.
58
+ #
59
+ # Person = Struct.new(:name, :address)
60
+ #
61
+ # class Invoice < Struct.new(:client)
62
+ # delegate :name, :address, :to => :client, :prefix => true
63
+ # end
64
+ #
65
+ # john_doe = Person.new("John Doe", "Vimmersvej 13")
66
+ # invoice = Invoice.new(john_doe)
67
+ # invoice.client_name # => "John Doe"
68
+ # invoice.client_address # => "Vimmersvej 13"
69
+ #
70
+ # It is also possible to supply a custom prefix.
71
+ #
72
+ # class Invoice < Struct.new(:client)
73
+ # delegate :name, :address, :to => :client, :prefix => :customer
74
+ # end
75
+ #
76
+ # invoice = Invoice.new(john_doe)
77
+ # invoice.customer_name # => "John Doe"
78
+ # invoice.customer_address # => "Vimmersvej 13"
79
+ #
80
+ # If the delegate object is +nil+ an exception is raised, and that happens
81
+ # no matter whether +nil+ responds to the delegated method. You can get a
82
+ # +nil+ instead with the +:allow_nil+ option.
83
+ #
84
+ # class Foo
85
+ # attr_accessor :bar
86
+ # def initialize(bar = nil)
87
+ # @bar = bar
88
+ # end
89
+ # delegate :zoo, :to => :bar
90
+ # end
91
+ #
92
+ # Foo.new.zoo # raises NoMethodError exception (you called nil.zoo)
93
+ #
94
+ # class Foo
95
+ # attr_accessor :bar
96
+ # def initialize(bar = nil)
97
+ # @bar = bar
98
+ # end
99
+ # delegate :zoo, :to => :bar, :allow_nil => true
100
+ # end
101
+ #
102
+ # Foo.new.zoo # returns nil
103
+ #
104
+ def delegate(*methods)
105
+ options = methods.pop
106
+ unless options.is_a?(Hash) && to = options[:to]
107
+ raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)."
108
+ end
109
+ prefix, to, allow_nil = options[:prefix], options[:to], options[:allow_nil]
110
+
111
+ if prefix == true && to.to_s =~ /^[^a-z_]/
112
+ raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
113
+ end
114
+
115
+ method_prefix =
116
+ if prefix
117
+ "#{prefix == true ? to : prefix}_"
118
+ else
119
+ ''
120
+ end
121
+
122
+ file, line = caller.first.split(':', 2)
123
+ line = line.to_i
124
+
125
+ methods.each do |method|
126
+ method = method.to_s
127
+
128
+ if allow_nil
129
+ module_eval(<<-EOS, file, line - 2)
130
+ def #{method_prefix}#{method}(*args, &block) # def customer_name(*args, &block)
131
+ if #{to} || #{to}.respond_to?(:#{method}) # if client || client.respond_to?(:name)
132
+ #{to}.__send__(:#{method}, *args, &block) # client.__send__(:name, *args, &block)
133
+ end # end
134
+ end # end
135
+ EOS
136
+ else
137
+ exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
138
+
139
+ module_eval(<<-EOS, file, line - 1)
140
+ def #{method_prefix}#{method}(*args, &block) # def customer_name(*args, &block)
141
+ #{to}.__send__(:#{method}, *args, &block) # client.__send__(:name, *args, &block)
142
+ rescue NoMethodError # rescue NoMethodError
143
+ if #{to}.nil? # if client.nil?
144
+ #{exception} # # add helpful message to the exception
145
+ else # else
146
+ raise # raise
147
+ end # end
148
+ end # end
149
+ EOS
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,64 @@
1
+ require "em-synchrony/em-http"
2
+
3
+ module ResqueKalashnikov
4
+ class HttpRequest
5
+ attr_accessor :url, :http_method, :opts
6
+
7
+ def initialize(*args)
8
+ case args.size
9
+ when 1 then @url = args[0]
10
+ when 2 then @url, @opts = args
11
+ when 3 then @url, @http_method, @opts = args
12
+ else
13
+ raise "insufficient params in #{self.class}: args=#{args}"
14
+ end
15
+ @http_method ||= 'get'
16
+ @opts ||= {}
17
+ @http_method.downcase!
18
+ end
19
+
20
+ # This method is invoked inside EM
21
+ # no blocking calls, please
22
+ def handle http
23
+ Resque::Catridge.new(self, http)
24
+ end
25
+
26
+ def retry_limit
27
+ instance_variable_get(:@retry_limit) || 2
28
+ end
29
+
30
+ def perform
31
+ catrige = handle http_request
32
+ reload if catrige.reload? && catrige.retries < retry_limit
33
+ http_request.response
34
+ end
35
+
36
+ def reload_opts
37
+ opts
38
+ end
39
+
40
+ def http_method
41
+ valid_methods.include?(@http_method) ? @http_method : 'get'
42
+ end
43
+
44
+ private
45
+
46
+ def reload
47
+ Resque.enqueue self.class, url, http_method, reload_opts
48
+ end
49
+
50
+ def http_request
51
+ EM::HttpRequest.new(url).send http_method, query: opts
52
+ end
53
+
54
+ def valid_methods
55
+ ['get', 'post', 'head', 'delete', 'put', 'options', 'patch']
56
+ end
57
+
58
+ class << self
59
+ def perform(*args)
60
+ new(*args).perform
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,10 @@
1
+ require 'resque_kalashnikov'
2
+ require 'rails'
3
+
4
+ module ResqueKalashnikov
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ require 'resque_kalashnikov/../tasks'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,50 @@
1
+ require 'resque_kalashnikov'
2
+ require 'resque/server'
3
+
4
+ module ResqueKalashnikov
5
+
6
+ module Server
7
+
8
+ def render_erb(view)
9
+ erb File.read(File.join(File.dirname(__FILE__), view))
10
+ end
11
+
12
+ def self.included(base)
13
+ base.class_eval do
14
+ get "/kalashnikov" do
15
+ render_erb 'server/views/catridges.erb'
16
+ end
17
+
18
+ post "/kalashnikov/retry/:status" do
19
+ status = params[:status]
20
+ klass_name, args = Resque.decode Base64.decode64 params[:request_key]
21
+ klass = Resque::Job.constantize klass_name
22
+ queue = Resque.queue_from_class(klass)
23
+ redis = Redis.connect
24
+ redis.rpush "resque:queue:#{queue}", Resque.encode(:class => klass_name, :args => args)
25
+ sleep 3 # FIXME: if refreshed immediately - no effect seen - instead, can be polled
26
+ redirect u('/kalashnikov')
27
+ end
28
+
29
+ post "/kalashnikov/reset/:status" do
30
+ status = params[:status]
31
+ request_key = Base64.decode64 params[:request_key]
32
+ redis = Redis.connect
33
+ redis.hset "resque:kalashnikov:misfires:#{status}", request_key, 0
34
+ redirect u('/kalashnikov')
35
+ end
36
+
37
+ get "/kalashnikov/reset_stats" do
38
+ Resque::Catridge.reset_stats
39
+ redirect u('/kalashnikov')
40
+ end
41
+ end
42
+ end
43
+
44
+ Resque::Server.tabs << 'Kalashnikov'
45
+ end
46
+ end
47
+
48
+ Resque::Server.class_eval do
49
+ include ResqueKalashnikov::Server
50
+ end
@@ -0,0 +1,57 @@
1
+ <% statuses = resque.kalashnikov_stats %>
2
+
3
+ <h1 class='wi'>Kalashnikov</h1>
4
+ <p class='intro'>The list below contains statistics of Kalashnikov</p>
5
+ <table>
6
+ <tr>
7
+ <th>HTTP code</th>
8
+ <th>Jobs done</th>
9
+ </tr>
10
+ <% statuses.sort.each do |status, count| %>
11
+ <tr>
12
+ <td class='status'><%= status %></td>
13
+ <td class='size'><%= count %></td>
14
+ </tr>
15
+ <% end %>
16
+ </table>
17
+
18
+ <% resque.kalashnikov_misfire_codes.each do |misfire_stats_key| %>
19
+ <% status = misfire_stats_key.split(':').last %>
20
+ <h1 class='wi'>Error code: <%= status %></h1>
21
+ <table>
22
+ <tr>
23
+ <th>Job</th>
24
+ <th>HTTP</th>
25
+ <th>URL</th>
26
+ <th>Options</th>
27
+ <th>Retries</th>
28
+ <th>Retry</th>
29
+ <th>Reset retries</th>
30
+ </tr>
31
+ <% resque.kalashnikov_misfire_stats(misfire_stats_key).sort_by { |k,v| v } .reverse.each do |misfire_json, count| %>
32
+ <tr>
33
+ <% job, payload = resque.decode misfire_json %>
34
+ <% url, http_method, options = payload %>
35
+ <td class='job'><%= job %></td>
36
+ <td class='http_method'><%= http_method.upcase %></td>
37
+ <td class='url'><a href="<%= url %>"><%= url %></a></td>
38
+ <td class='options'><%= options %></td>
39
+ <td class='size'><%= count %></td>
40
+ <td class='url'>
41
+ <form method="POST" action="<%= u %Q{kalashnikov/retry/#{status}} %>" >
42
+ <input name="request_key" type="hidden" value="<%= Base64.encode64 misfire_json %>" />
43
+ <input type='submit' name='' value='Retry' />
44
+ </form>
45
+ </td>
46
+ <td class='url'>
47
+ <form method="POST" action="<%= u %Q{kalashnikov/reset/#{status}} %>" >
48
+ <input name="request_key" type="hidden" value="<%= Base64.encode64 misfire_json %>" />
49
+ <input type='submit' name='' value='Reset' />
50
+ </form>
51
+ </td>
52
+ </tr>
53
+ <% end %>
54
+ </table>
55
+ <% end %>
56
+
57
+ <a href="<%= u %Q{kalashnikov/reset_stats} %>">Reset Kalashnikov statistics</a>
@@ -0,0 +1,3 @@
1
+ module ResqueKalashnikov
2
+ VERSION = '1.1.0'
3
+ end
data/lib/tasks.rb ADDED
@@ -0,0 +1,34 @@
1
+ namespace :resque do
2
+
3
+ desc "Fire Kalashnikov"
4
+ task :fire do
5
+
6
+ require 'resque'
7
+ require 'em-synchrony'
8
+ require 'em-synchrony/em-hiredis'
9
+ require 'resque_kalashnikov'
10
+
11
+ queues = (ENV['QUEUES'] || ENV['QUEUE']).to_s.split(',')
12
+
13
+ # FIXME: cannot start with clean redis
14
+ abort "QUEUE env var cannot be '*', please, list your queues" if queues.include? '*'
15
+ redis = Redis.connect
16
+ queues.each { |queue| redis.sadd "resque:queues", queue }
17
+
18
+
19
+ if defined?(Rails) && Rails.respond_to?(:application)
20
+ Rails.application.eager_load!
21
+ end
22
+
23
+ worker = Resque::Worker.new(*queues)
24
+ #worker.verbose = true
25
+
26
+ EM.synchrony do
27
+ Resque.redis = EM::Synchrony::ConnectionPool.new(size: 100) do
28
+ EM::Hiredis.connect
29
+ end
30
+ ['TERM', 'INT', 'QUIT'].each { |signal| trap(signal) { EM.stop } }
31
+ worker.work(0)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resque_kalashnikov/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "resque-kalashnikov"
8
+ gem.version = ResqueKalashnikov::VERSION
9
+ gem.authors = ["Vlad Bokov"]
10
+ gem.email = ["bokov.vlad@gmail.com"]
11
+ gem.summary = %q{This is awesome}
12
+ gem.description = %q{Handles your HTTP requests in background in non-blocking way using Resque worker}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency('rake')
21
+ gem.add_dependency('resque', '~> 1.23.0')
22
+ gem.add_dependency('redis', '> 3.0.0')
23
+ gem.add_dependency('em-http-request')
24
+ gem.add_dependency('em-synchrony')
25
+ gem.add_dependency('em-hiredis')
26
+
27
+ gem.add_development_dependency('rack-test')
28
+ gem.add_development_dependency('webmock')
29
+ gem.add_development_dependency('rspec')
30
+ end
data/screenshot.png ADDED
Binary file
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe Resque::Catridge do
4
+ before do
5
+ @fake_redis = double
6
+ @fake_redis.stub(:hget)
7
+ @fake_redis.stub(:rpush)
8
+ @fake_redis.stub(:hincrby)
9
+ @fake_redis.stub(:keys)
10
+ Resque::Catridge.stub(:redis).and_return(@fake_redis)
11
+ end
12
+
13
+ def fake_response_with_status(status)
14
+ response = double
15
+ response.stub_chain(:response_header, :status).and_return(status)
16
+ response
17
+ end
18
+
19
+ def fake_request
20
+ request = double
21
+ request.stub(:url).and_return 'some-url'
22
+ request.stub(:http_method).and_return 'get'
23
+ request.stub(:reload_opts).and_return 'some-opts'
24
+ request
25
+ end
26
+
27
+ def build(response)
28
+ Resque::Catridge.new fake_request, response
29
+ end
30
+
31
+ it 'forces no reload on 200' do
32
+ build(fake_response_with_status 200).reload?.should be_false
33
+ end
34
+
35
+ it 'forces 404 for reload' do
36
+ build(fake_response_with_status 404).reload?.should be_true
37
+ end
38
+
39
+ it 'forces 500 for reload' do
40
+ build(fake_response_with_status 500).reload?.should be_true
41
+ end
42
+ end