grockit-resque 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/.gitignore +2 -0
  2. data/.kick +26 -0
  3. data/CONTRIBUTORS +14 -0
  4. data/HISTORY.md +78 -0
  5. data/LICENSE +20 -0
  6. data/README.markdown +762 -0
  7. data/Rakefile +67 -0
  8. data/bin/resque +57 -0
  9. data/bin/resque-web +18 -0
  10. data/config.ru +14 -0
  11. data/deps.rip +6 -0
  12. data/examples/async_helper.rb +31 -0
  13. data/examples/demo/README.markdown +71 -0
  14. data/examples/demo/Rakefile +3 -0
  15. data/examples/demo/app.rb +38 -0
  16. data/examples/demo/config.ru +19 -0
  17. data/examples/demo/job.rb +22 -0
  18. data/examples/god/resque.god +53 -0
  19. data/examples/god/stale.god +26 -0
  20. data/examples/instance.rb +11 -0
  21. data/examples/simple.rb +30 -0
  22. data/init.rb +1 -0
  23. data/lib/resque.rb +247 -0
  24. data/lib/resque/errors.rb +7 -0
  25. data/lib/resque/failure.rb +63 -0
  26. data/lib/resque/failure/base.rb +58 -0
  27. data/lib/resque/failure/hoptoad.rb +122 -0
  28. data/lib/resque/failure/multiple.rb +44 -0
  29. data/lib/resque/failure/redis.rb +33 -0
  30. data/lib/resque/helpers.rb +57 -0
  31. data/lib/resque/job.rb +158 -0
  32. data/lib/resque/server.rb +182 -0
  33. data/lib/resque/server/public/idle.png +0 -0
  34. data/lib/resque/server/public/jquery-1.3.2.min.js +19 -0
  35. data/lib/resque/server/public/jquery.relatize_date.js +95 -0
  36. data/lib/resque/server/public/poll.png +0 -0
  37. data/lib/resque/server/public/ranger.js +24 -0
  38. data/lib/resque/server/public/reset.css +48 -0
  39. data/lib/resque/server/public/style.css +75 -0
  40. data/lib/resque/server/public/working.png +0 -0
  41. data/lib/resque/server/views/error.erb +1 -0
  42. data/lib/resque/server/views/failed.erb +35 -0
  43. data/lib/resque/server/views/key.erb +17 -0
  44. data/lib/resque/server/views/layout.erb +38 -0
  45. data/lib/resque/server/views/next_more.erb +10 -0
  46. data/lib/resque/server/views/overview.erb +4 -0
  47. data/lib/resque/server/views/queues.erb +46 -0
  48. data/lib/resque/server/views/stats.erb +62 -0
  49. data/lib/resque/server/views/workers.erb +78 -0
  50. data/lib/resque/server/views/working.erb +69 -0
  51. data/lib/resque/stat.rb +53 -0
  52. data/lib/resque/tasks.rb +39 -0
  53. data/lib/resque/version.rb +3 -0
  54. data/lib/resque/worker.rb +442 -0
  55. data/tasks/redis.rake +135 -0
  56. data/tasks/resque.rake +2 -0
  57. data/test/redis-test.conf +132 -0
  58. data/test/resque_test.rb +227 -0
  59. data/test/test_helper.rb +96 -0
  60. data/test/worker_test.rb +243 -0
  61. metadata +172 -0
@@ -0,0 +1,30 @@
1
+ # This is a simple Resque job.
2
+ class Archive
3
+ @queue = :file_serve
4
+
5
+ def self.perform(repo_id, branch = 'master')
6
+ repo = Repository.find(repo_id)
7
+ repo.create_archive(branch)
8
+ end
9
+ end
10
+
11
+ # This is in our app code
12
+ class Repository < Model
13
+ # ... stuff ...
14
+
15
+ def async_create_archive(branch)
16
+ Resque.enqueue(Archive, self.id, branch)
17
+ end
18
+
19
+ # ... more stuff ...
20
+ end
21
+
22
+ # Calling this code:
23
+ repo = Repository.find(22)
24
+ repo.async_create_archive('homebrew')
25
+
26
+ # Will return immediately and create a Resque job which is later
27
+ # processed.
28
+
29
+ # Essentially, this code is run by the worker when processing:
30
+ Archive.perform(22, 'homebrew')
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'resque'
@@ -0,0 +1,247 @@
1
+ require 'redis/namespace'
2
+
3
+ begin
4
+ require 'yajl'
5
+ rescue LoadError
6
+ require 'json'
7
+ end
8
+
9
+ require 'resque/version'
10
+
11
+ require 'resque/errors'
12
+
13
+ require 'resque/failure'
14
+ require 'resque/failure/base'
15
+
16
+ require 'resque/helpers'
17
+ require 'resque/stat'
18
+ require 'resque/job'
19
+ require 'resque/worker'
20
+
21
+ module Resque
22
+ include Helpers
23
+ extend self
24
+
25
+ # Accepts:
26
+ # 1. A 'hostname:port' string
27
+ # 2. A 'hostname:port:db' string (to select the Redis db)
28
+ # 3. An instance of `Redis`
29
+ def redis=(server)
30
+ case server
31
+ when String
32
+ host, port, db = server.split(':')
33
+ redis = Redis.new(:host => host, :port => port,
34
+ :thread_safe => true, :db => db)
35
+ @redis = Redis::Namespace.new(:resque, :redis => redis)
36
+ when Redis
37
+ @redis = Redis::Namespace.new(:resque, :redis => server)
38
+ else
39
+ raise "I don't know what to do with #{server.inspect}"
40
+ end
41
+ end
42
+
43
+ # Returns the current Redis connection. If none has been created, will
44
+ # create a new one.
45
+ def redis
46
+ return @redis if @redis
47
+ self.redis = 'localhost:6379'
48
+ self.redis
49
+ end
50
+
51
+ def to_s
52
+ "Resque Client connected to #{redis.server}"
53
+ end
54
+
55
+
56
+ #
57
+ # queue manipulation
58
+ #
59
+
60
+ # Pushes a job onto a queue. Queue name should be a string and the
61
+ # item should be any JSON-able Ruby object.
62
+ def push(queue, item)
63
+ watch_queue(queue)
64
+ queue_key = "queue:#{queue}"
65
+ if item[:unique]
66
+ unless redis.lrange(queue_key, 0, -1).include?(encode(item))
67
+ redis.rpush queue_key, encode(item)
68
+ end
69
+ else
70
+ redis.rpush queue_key, encode(item)
71
+ end
72
+ end
73
+
74
+ # Pops a job off a queue. Queue name should be a string.
75
+ #
76
+ # Returns a Ruby object.
77
+ def pop(queue)
78
+ decode redis.lpop("queue:#{queue}")
79
+ end
80
+
81
+ # Returns an int representing the size of a queue.
82
+ # Queue name should be a string.
83
+ def size(queue)
84
+ redis.llen("queue:#{queue}").to_i
85
+ end
86
+
87
+ # Returns an array of items currently queued. Queue name should be
88
+ # a string.
89
+ #
90
+ # start and count should be integer and can be used for pagination.
91
+ # start is the item to begin, count is how many items to return.
92
+ #
93
+ # To get the 3rd page of a 30 item, paginatied list one would use:
94
+ # Resque.peek('my_list', 59, 30)
95
+ def peek(queue, start = 0, count = 1)
96
+ list_range("queue:#{queue}", start, count)
97
+ end
98
+
99
+ # Does the dirty work of fetching a range of items from a Redis list
100
+ # and converting them into Ruby objects.
101
+ def list_range(key, start = 0, count = 1)
102
+ if count == 1
103
+ decode redis.lindex(key, start)
104
+ else
105
+ Array(redis.lrange(key, start, start+count-1)).map do |item|
106
+ decode item
107
+ end
108
+ end
109
+ end
110
+
111
+ # Returns an array of all known Resque queues as strings.
112
+ def queues
113
+ redis.smembers(:queues)
114
+ end
115
+
116
+ # Given a queue name, completely deletes the queue.
117
+ def remove_queue(queue)
118
+ redis.srem(:queues, queue.to_s)
119
+ redis.del("queue:#{queue}")
120
+ end
121
+
122
+ # Used internally to keep track of which queues we've created.
123
+ # Don't call this directly.
124
+ def watch_queue(queue)
125
+ redis.sadd(:queues, queue.to_s)
126
+ end
127
+
128
+
129
+ #
130
+ # job shortcuts
131
+ #
132
+
133
+ # This method can be used to conveniently add a job to a queue.
134
+ # It assumes the class you're passing it is a real Ruby class (not
135
+ # a string or reference) which either:
136
+ #
137
+ # a) has a @queue ivar set
138
+ # b) responds to `queue`
139
+ #
140
+ # If either of those conditions are met, it will use the value obtained
141
+ # from performing one of the above operations to determine the queue.
142
+ #
143
+ # If no queue can be inferred this method will raise a `Resque::NoQueueError`
144
+ #
145
+ # This method is considered part of the `stable` API.
146
+ def enqueue(klass, *args)
147
+ Job.create(queue_from_class(klass), klass, *args)
148
+ end
149
+
150
+ def enqueue_unique(klass, *args)
151
+ Job.create_unique(queue_from_class(klass), klass, *args)
152
+ end
153
+
154
+ # This method can be used to conveniently remove a job from a queue.
155
+ # It assumes the class you're passing it is a real Ruby class (not
156
+ # a string or reference) which either:
157
+ #
158
+ # a) has a @queue ivar set
159
+ # b) responds to `queue`
160
+ #
161
+ # If either of those conditions are met, it will use the value obtained
162
+ # from performing one of the above operations to determine the queue.
163
+ #
164
+ # If no queue can be inferred this method will raise a `Resque::NoQueueError`
165
+ #
166
+ # If no args are given, this method will dequeue *all* jobs matching
167
+ # the provided class. See `Resque::Job.destroy` for more
168
+ # information.
169
+ #
170
+ # Returns the number of jobs destroyed.
171
+ #
172
+ # Example:
173
+ #
174
+ # # Removes all jobs of class `UpdateNetworkGraph`
175
+ # Resque.dequeue(GitHub::Jobs::UpdateNetworkGraph)
176
+ #
177
+ # # Removes all jobs of class `UpdateNetworkGraph` with matching args.
178
+ # Resque.dequeue(GitHub::Jobs::UpdateNetworkGraph, 'repo:135325')
179
+ #
180
+ # This method is considered part of the `stable` API.
181
+ def dequeue(klass, *args)
182
+ Job.destroy(queue_from_class(klass), klass, *args)
183
+ end
184
+
185
+ # Given a class, try to extrapolate an appropriate queue based on a
186
+ # class instance variable or `queue` method.
187
+ def queue_from_class(klass)
188
+ klass.instance_variable_get(:@queue) ||
189
+ (klass.respond_to?(:queue) and klass.queue)
190
+ end
191
+
192
+ # This method will return a `Resque::Job` object or a non-true value
193
+ # depending on whether a job can be obtained. You should pass it the
194
+ # precise name of a queue: case matters.
195
+ #
196
+ # This method is considered part of the `stable` API.
197
+ def reserve(queue)
198
+ Job.reserve(queue)
199
+ end
200
+
201
+
202
+ #
203
+ # worker shortcuts
204
+ #
205
+
206
+ # A shortcut to Worker.all
207
+ def workers
208
+ Worker.all
209
+ end
210
+
211
+ # A shortcut to Worker.working
212
+ def working
213
+ Worker.working
214
+ end
215
+
216
+ # A shortcut to unregister_worker
217
+ # useful for command line tool
218
+ def remove_worker(worker_id)
219
+ worker = Resque::Worker.find(worker_id)
220
+ worker.unregister_worker
221
+ end
222
+
223
+ #
224
+ # stats
225
+ #
226
+
227
+ # Returns a hash, similar to redis-rb's #info, of interesting stats.
228
+ def info
229
+ return {
230
+ :pending => queues.inject(0) { |m,k| m + size(k) },
231
+ :processed => Stat[:processed],
232
+ :queues => queues.size,
233
+ :workers => workers.size.to_i,
234
+ :working => working.size,
235
+ :failed => Stat[:failed],
236
+ :servers => [redis.server]
237
+ }
238
+ end
239
+
240
+ # Returns an array of all known Resque keys in Redis. Redis' KEYS operation
241
+ # is O(N) for the keyspace, so be careful - this can be slow for big databases.
242
+ def keys
243
+ redis.keys("*").map do |key|
244
+ key.sub('resque:', '')
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,7 @@
1
+ module Resque
2
+ # Raised whenever we need a queue but none is provided.
3
+ class NoQueueError < RuntimeError; end
4
+
5
+ # Raised when trying to create a job without a class
6
+ class NoClassError < RuntimeError; end
7
+ end
@@ -0,0 +1,63 @@
1
+ module Resque
2
+ # The Failure module provides an interface for working with different
3
+ # failure backends.
4
+ #
5
+ # You can use it to query the failure backend without knowing which specific
6
+ # backend is being used. For instance, the Resque web app uses it to display
7
+ # stats and other information.
8
+ module Failure
9
+ # Creates a new failure, which is delegated to the appropriate backend.
10
+ #
11
+ # Expects a hash with the following keys:
12
+ # :exception - The Exception object
13
+ # :worker - The Worker object who is reporting the failure
14
+ # :queue - The string name of the queue from which the job was pulled
15
+ # :payload - The job's payload
16
+ def self.create(options = {})
17
+ backend.new(*options.values_at(:exception, :worker, :queue, :payload)).save
18
+ end
19
+
20
+ #
21
+ # Sets the current backend. Expects a class descendent of
22
+ # `Resque::Failure::Base`.
23
+ #
24
+ # Example use:
25
+ # require 'resque/failure/hoptoad'
26
+ # Resque::Failure.backend = Resque::Failure::Hoptoad
27
+ def self.backend=(backend)
28
+ @backend = backend
29
+ end
30
+
31
+ # Returns the current backend class. If none has been set, falls
32
+ # back to `Resque::Failure::Redis`
33
+ def self.backend
34
+ return @backend if @backend
35
+ require 'resque/failure/redis'
36
+ @backend = Failure::Redis
37
+ end
38
+
39
+ # Returns the int count of how many failures we have seen.
40
+ def self.count
41
+ backend.count
42
+ end
43
+
44
+ # Returns an array of all the failures, paginated.
45
+ #
46
+ # `start` is the int of the first item in the page, `count` is the
47
+ # number of items to return.
48
+ def self.all(start = 0, count = 1)
49
+ backend.all(start, count)
50
+ end
51
+
52
+ # The string url of the backend's web interface, if any.
53
+ def self.url
54
+ backend.url
55
+ end
56
+
57
+ # Clear all failure jobs
58
+ def self.clear
59
+ backend.clear
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,58 @@
1
+ module Resque
2
+ module Failure
3
+ # All Failure classes are expected to subclass Base.
4
+ #
5
+ # When a job fails, a new instance of your Failure backend is created
6
+ # and #save is called.
7
+ class Base
8
+ # The exception object raised by the failed job
9
+ attr_accessor :exception
10
+
11
+ # The worker object who detected the failure
12
+ attr_accessor :worker
13
+
14
+ # The string name of the queue from which the failed job was pulled
15
+ attr_accessor :queue
16
+
17
+ # The payload object associated with the failed job
18
+ attr_accessor :payload
19
+
20
+ def initialize(exception, worker, queue, payload)
21
+ @exception = exception
22
+ @worker = worker
23
+ @queue = queue
24
+ @payload = payload
25
+ end
26
+
27
+ # When a job fails, a new instance of your Failure backend is created
28
+ # and #save is called.
29
+ #
30
+ # This is where you POST or PUT or whatever to your Failure service.
31
+ def save
32
+ end
33
+
34
+ # The number of failures.
35
+ def self.count
36
+ 0
37
+ end
38
+
39
+ # Returns a paginated array of failure objects.
40
+ def self.all(start = 0, count = 1)
41
+ []
42
+ end
43
+
44
+ # A URL where someone can go to view failures.
45
+ def self.url
46
+ end
47
+
48
+ # Clear all failure objects
49
+ def self.clear
50
+ end
51
+
52
+ # Logging!
53
+ def log(message)
54
+ @worker.log(message)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,122 @@
1
+ require 'net/http'
2
+ require 'builder'
3
+
4
+ module Resque
5
+ module Failure
6
+ # A Failure backend that sends exceptions raised by jobs to Hoptoad.
7
+ #
8
+ # To use it, put this code in an initializer, Rake task, or wherever:
9
+ #
10
+ # Resque::Failure::Hoptoad.configure do |config|
11
+ # config.api_key = 'blah'
12
+ # config.secure = true
13
+ # config.subdomain = 'your_hoptoad_subdomain'
14
+ # end
15
+ class Hoptoad < Base
16
+ #from the hoptoad plugin
17
+ INPUT_FORMAT = %r{^([^:]+):(\d+)(?::in `([^']+)')?$}.freeze
18
+
19
+ class << self
20
+ attr_accessor :secure, :api_key, :subdomain
21
+ end
22
+
23
+ def self.url
24
+ "http://#{subdomain}.hoptoadapp.com/" if subdomain
25
+ end
26
+
27
+ def self.count
28
+ # We can't get the total # of errors from Hoptoad so we fake it
29
+ # by asking Resque how many errors it has seen.
30
+ Stat[:failed]
31
+ end
32
+
33
+ def self.configure
34
+ yield self
35
+ Resque::Failure.backend = self
36
+ end
37
+
38
+
39
+
40
+ def save
41
+ http = use_ssl? ? :https : :http
42
+ url = URI.parse("#{http}://hoptoadapp.com/notifier_api/v2/notices")
43
+
44
+ http = Net::HTTP.new(url.host, url.port)
45
+ headers = {
46
+ 'Content-type' => 'text/xml',
47
+ 'Accept' => 'text/xml, application/xml'
48
+ }
49
+
50
+ http.read_timeout = 5 # seconds
51
+ http.open_timeout = 2 # seconds
52
+
53
+ http.use_ssl = use_ssl?
54
+
55
+ begin
56
+ response = http.post(url.path, xml, headers)
57
+ rescue TimeoutError => e
58
+ log "Timeout while contacting the Hoptoad server."
59
+ end
60
+
61
+ case response
62
+ when Net::HTTPSuccess then
63
+ log "Hoptoad Success: #{response.class}"
64
+ else
65
+ body = response.body if response.respond_to? :body
66
+ log "Hoptoad Failure: #{response.class}\n#{body}"
67
+ end
68
+ end
69
+
70
+ def xml
71
+ x = Builder::XmlMarkup.new
72
+ x.instruct!
73
+ x.notice :version=>"2.0" do
74
+ x.tag! "api-key", api_key
75
+ x.notifier do
76
+ x.name "Resqueue"
77
+ x.version "0.1"
78
+ x.url "http://github.com/defunkt/resque"
79
+ end
80
+ x.error do
81
+ x.class exception.class.name
82
+ x.message "#{exception.class.name}: #{exception.message}"
83
+ x.backtrace do
84
+ fill_in_backtrace_lines(x)
85
+ end
86
+ end
87
+ x.request do
88
+ x.url queue.to_s
89
+ x.component worker.to_s
90
+ x.params do
91
+ x.var :key=>"payload_class" do
92
+ x.text! payload["class"].to_s
93
+ end
94
+ x.var :key=>"payload_args" do
95
+ x.text! payload["args"].to_s
96
+ end
97
+ end
98
+ end
99
+ x.tag!("server-environment") do
100
+ x.tag!("environment-name",RAILS_ENV)
101
+ end
102
+
103
+ end
104
+ end
105
+
106
+ def fill_in_backtrace_lines(x)
107
+ exception.backtrace.each do |unparsed_line|
108
+ _, file, number, method = unparsed_line.match(INPUT_FORMAT).to_a
109
+ x.line :file=>file,:number=>number
110
+ end
111
+ end
112
+
113
+ def use_ssl?
114
+ self.class.secure
115
+ end
116
+
117
+ def api_key
118
+ self.class.api_key
119
+ end
120
+ end
121
+ end
122
+ end