resque-kalashnikov 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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