diff --git a/.gitignore b/.gitignore
index b814b61..e45e3aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,4 @@ doc
test/dump.rdb
bin/
.bundle
+*.iml
diff --git a/app/assets/javascripts/resque_status/application.js.coffee b/app/assets/javascripts/resque_status/application.js.coffee
new file mode 100644
index 0000000..f773dd6
--- /dev/null
+++ b/app/assets/javascripts/resque_status/application.js.coffee
@@ -0,0 +1,110 @@
+jQuery ($) ->
+ $('#main').attr('id', '')
+ $('#resque-status').attr('id', 'main')
+
+ $('#main').on 'click', "a.status-kill", (e) ->
+ return false unless confirm("Are you sure you want to kill this job? There is no undo.")
+ $link = $(this)
+ $link.animate opacity: 0.5
+ $.post $link.attr("href"), ->
+ $link.remove()
+ return false
+
+
+ kill_all = $('.status-kill-all')
+ kill_all.on 'click', ->
+ $link = $(this)
+ return false if $link.is('.disabled')
+ return false unless confirm("Are you sure you want to kill this job? There is no undo.")
+ data = {
+ id: []
+ }
+ checked = $('.status-jobs-option').filter(':checked')
+ return false if checked.length == 0
+ checked.each (index, value)->
+ data['id'].push $(value).val()
+
+ $.post($link.attr('href'), data, ->
+ checked.remove()
+ updateKillAll()
+ )
+ return false;
+
+ updateKillAll = ->
+ jobs_option = $('.status-jobs-option')
+ if jobs_option.length == 0
+ kill_all.remove()
+ $('input.status-check-all').remove()
+ return
+ if jobs_option.is(':checked')
+ kill_all.removeClass('disabled')
+ else
+ kill_all.addClass('disabled')
+
+ $('#main').on 'click', 'input.status-check-all', (e) ->
+ $('.status-jobs-option').prop('checked', $(this).prop('checked'))
+ updateKillAll()
+
+ $('.status-jobs-option').on 'click', (e) ->
+ updateKillAll()
+
+ updateKillAll()
+
+ $('#main').on 'click', 'a.status-clear', (e) ->
+ return false unless confirm("Are you absolutely sure? This cannot be undone.");
+ $.ajax
+ type: "DELETE"
+ url: $(this).attr('href')
+ success: ->
+ window.location.reload()
+ return
+ false
+
+ status_map = {
+ completed: 'progress-success',
+ failed: 'progress-danger',
+ working: 'progress-info',
+ queued: 'progress-info',
+ killed: 'progress-warning'
+ }
+ # itterate over the holders
+ checkStatus = ($status) ->
+ status_path = $status.data 'url'
+ $.getJSON status_path, (json) ->
+ if json
+ pct = "0%"
+ pct = json.pct_complete + "%" if json.pct_complete
+
+ $status.find(".progress .bar").animate width: pct
+ $status.find(".progress .progress-pct").text pct
+ $status.find(".status-message").html json.message if json.message
+ $status.data('status', json.status)
+ $status.find(".progress").attr("class", "progress "+status_map[json.status]) if json.status
+ $status.find(".status-time").text new Date(json.time * 1000).toString() if json.time
+
+ $details = $status.find(".status-details-body")
+ $details.empty()
+ for key of json
+ $row = $("
").appendTo($details)
+ $("| ").text(key).appendTo $row
+ $(" | ").text(printValue(key, json[key])).appendTo $row
+
+ status = $status.data("status")
+ if status is "working" or status is "queued" or status is ""
+ setTimeout (->
+ checkStatus($status)
+ ), 1500
+ return
+
+ return
+ printValue = (key, value) ->
+ if /(^|_)time$/.test(key) and typeof value is "number"
+ time = new Date()
+ time.setTime value * 1000
+ time.toUTCString()
+ else
+ JSON.stringify value
+
+ $(".status-holder").each ->
+ checkStatus $(this)
+ return
diff --git a/app/assets/stylesheets/resque_status/application.css b/app/assets/stylesheets/resque_status/application.css
new file mode 100644
index 0000000..7fed727
--- /dev/null
+++ b/app/assets/stylesheets/resque_status/application.css
@@ -0,0 +1,71 @@
+
+th.progress {
+ width: 100px;
+}
+td.status {
+ font-weight: bold;
+}
+/*td.progress .progress-bar {*/
+ /*position: absolute;*/
+ /*top: 0px;*/
+ /*left: 0px;*/
+ /*background: #999;*/
+ /*display:block;*/
+ /*height: 100%;*/
+ /*z-index: 0;*/
+ /*opacity: 0.5;*/
+ /*-moz-opacity: 0.5;*/
+ /*-webkit-opacity: 0.5;*/
+/*}*/
+.resque-status .progress {
+ margin: 0;
+ position: relative;
+}
+.resque-status .progress .progress-pct {
+ z-index: 10;
+ /*color: #333;*/
+ color: #ffffff;
+ text-shadow: 1px 1px 2px black;
+ text-align: center;
+ position: absolute;
+ width: 100%;
+}
+
+.status-completed {
+ color:#61BF55;
+}
+.status-failed {
+ color: #E47E74;
+}
+.status-working {
+ color: #528499;
+}
+.status-killed {
+ color: #B84F16;
+}
+
+.status-holder {
+ background: #F7F7F7;
+ border: 1px solid #E5E5E5;
+ padding: 20px;
+ font-size: 110%;
+ margin-bottom: 40px;
+}
+.status-message {
+ font-weight: bold;
+}
+
+.status-time {
+ font-size: 70%;
+ padding: 10px 0px;
+ color: #999;
+}
+
+#main a.status-kill:link, #main a.status-kill:visited {
+ color: #B84F16;
+ font-weight: bold;
+}
+
+.status-clear {
+ margin-left:5px
+}
diff --git a/app/controllers/resque_status/application_controller.rb b/app/controllers/resque_status/application_controller.rb
new file mode 100644
index 0000000..4841afd
--- /dev/null
+++ b/app/controllers/resque_status/application_controller.rb
@@ -0,0 +1,5 @@
+module ResqueStatus
+ class ApplicationController < ResqueWeb::ApplicationController
+ helper :all
+ end
+end
diff --git a/app/controllers/resque_status/clear_controller.rb b/app/controllers/resque_status/clear_controller.rb
new file mode 100644
index 0000000..5cea3e7
--- /dev/null
+++ b/app/controllers/resque_status/clear_controller.rb
@@ -0,0 +1,9 @@
+module ResqueStatus
+ class ClearController < ResqueStatus::ApplicationController
+ def destroy
+ params[:id] = nil if params[:id] == 'all'
+ Resque::Plugins::Status::Hash.clear(params[:id])
+ head :no_content
+ end
+ end
+end
diff --git a/app/controllers/resque_status/statuses_controller.rb b/app/controllers/resque_status/statuses_controller.rb
new file mode 100644
index 0000000..de02599
--- /dev/null
+++ b/app/controllers/resque_status/statuses_controller.rb
@@ -0,0 +1,33 @@
+module ResqueStatus
+ class StatusesController < ResqueStatus::ApplicationController
+ layout 'resque_web/application'
+ def show
+ @status = Resque::Plugins::Status::Hash.get(params[:id])
+ @polling = request.xhr?
+ respond_to do |format|
+ format.html
+ format.json { render :json => @status.json }
+ end
+ end
+
+ def index
+ @start = params[:start].to_i
+ @end = @start + (params[:per_page] || 20)-1
+ @statuses = Resque::Plugins::Status::Hash.statuses(@start, @end)
+ @size = Resque::Plugins::Status::Hash.count
+ @polling = request.xhr?
+ @has_killable = @statuses.select(&:killable?).size > 0
+ respond_to do |format|
+ format.html {render :layout => !request.xhr? }
+ end
+ end
+
+ def kill
+ ids = params[:ids] || Array(params[:id])
+ ids.each do |id|
+ Resque::Plugins::Status::Hash.kill(params[:id])
+ end
+ head :no_content
+ end
+ end
+end
diff --git a/app/helpers/resque_status/status_helper.rb b/app/helpers/resque_status/status_helper.rb
new file mode 100644
index 0000000..8c4a235
--- /dev/null
+++ b/app/helpers/resque_status/status_helper.rb
@@ -0,0 +1,12 @@
+module ResqueStatus
+ module StatusHelper
+ def status_poll(start)
+ if @polling
+ text = "Last Updated: #{Time.now.strftime("%H:%M:%S")}"
+ else
+ text = "Live Poll"
+ end
+ " #{text} "
+ end
+ end
+end
diff --git a/app/views/resque_status/statuses/index.erb b/app/views/resque_status/statuses/index.erb
new file mode 100644
index 0000000..3b97034
--- /dev/null
+++ b/app/views/resque_status/statuses/index.erb
@@ -0,0 +1,88 @@
+<% unless @polling %>
+
+<% end %>
+
+ Statuses
+ These are recent jobs created with the Resque::Plugins::Status class
+
+ <% unless @statuses.empty? %>
+
+
+ <% if @has_killable && !@polling %>
+ Kill sellected
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+ <% unless @statuses.empty? %>
+ <%= pagination :start => @start, :total => @size, :per_page => 20 %>
+ <% end %>
+
+ <%= status_poll(@start).html_safe%>
+
+
+<% unless @polling %>
+
+
+ <%= javascript_include_tag "resque_status/application" %>
+ <%= stylesheet_link_tag "resque_status/application", :media => "all" %>
+<% end %>
diff --git a/app/views/resque_status/statuses/show.erb b/app/views/resque_status/statuses/show.erb
new file mode 100644
index 0000000..b67b081
--- /dev/null
+++ b/app/views/resque_status/statuses/show.erb
@@ -0,0 +1,31 @@
+
+ Statuses: <%= @status.uuid %>/<%= @status.name %>
+
+ Viewing a specific job created with Resque::Plugins::Status.
+ Return to the list of statuses
+
+
+
+
+ <%= @status.pct_complete %>%
+
+ <%= @status.message %>
+ <%= @status.time? ? @status.time : 'Not started' %>
+ Details
+
+
+
+
+
+ | Key |
+ Value |
+
+
+
+
+
+
+
+
+<%= javascript_include_tag "resque_status/application" %>
+<%= stylesheet_link_tag "resque_status/application", :media => "all" %>
diff --git a/config/initializers/resque_config.rb b/config/initializers/resque_config.rb
new file mode 100644
index 0000000..3878226
--- /dev/null
+++ b/config/initializers/resque_config.rb
@@ -0,0 +1,2 @@
+config = ENV.fetch("RAILS_RESQUE_REDIS", "127.0.0.1:6379")
+Resque.redis = config
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000..a1884f7
--- /dev/null
+++ b/config/routes.rb
@@ -0,0 +1,15 @@
+# Match IDs with dots in them
+id_pattern = /[^\/]+/
+ResqueWeb::Plugins::ResqueStatus::Engine.routes.draw do
+ resources :statuses, :only => [:show,:index] do
+ member do
+ post :kill
+ end
+ collection do
+ resources :clear, :only => [:destroy]
+ end
+ end
+ root 'statuses#index'
+
+
+end
diff --git a/lib/resque-status.rb b/lib/resque-status.rb
index defb135..1feb61a 100644
--- a/lib/resque-status.rb
+++ b/lib/resque-status.rb
@@ -1 +1,4 @@
require "#{File.dirname(__FILE__)}/resque/status"
+require 'resque_status'
+require 'resque_web/plugins/resque_status/engine' if defined?(Rails)
+
diff --git a/lib/resque/plugins/status/hash.rb b/lib/resque/plugins/status/hash.rb
index bb5ae89..733a0c7 100644
--- a/lib/resque/plugins/status/hash.rb
+++ b/lib/resque/plugins/status/hash.rb
@@ -8,170 +8,162 @@ module Status
# the common status attributes. It also has a number of class methods for
# creating/updating/retrieving status objects from Redis
class Hash < ::Hash
+ class << self
+ # Create a status, generating a new UUID, passing the message to the status
+ # Returns the UUID of the new status.
+ def create(uuid, *messages)
+ set(uuid, *messages)
+ redis.zadd(set_key, Time.now.to_i, uuid)
+ redis.zremrangebyscore(set_key, 0, Time.now.to_i - @expire_in) if @expire_in
+ uuid
+ end
- # Create a status, generating a new UUID, passing the message to the status
- # Returns the UUID of the new status.
- def self.create(uuid, *messages)
- set(uuid, *messages)
- redis.zadd(set_key, Time.now.to_i, uuid)
- redis.zremrangebyscore(set_key, 0, Time.now.to_i - @expire_in) if @expire_in
- uuid
- end
-
- # Get a status by UUID. Returns a Resque::Plugins::Status::Hash
- def self.get(uuid)
- val = redis.get(status_key(uuid))
- val ? Resque::Plugins::Status::Hash.new(uuid, decode(val)) : nil
- end
-
- # Get multiple statuses by UUID. Returns array of Resque::Plugins::Status::Hash
- def self.mget(uuids)
- return [] if uuids.empty?
- status_keys = uuids.map{|u| status_key(u)}
- vals = redis.mget(*status_keys)
-
- uuids.zip(vals).map do |uuid, val|
+ # Get a status by UUID. Returns a Resque::Plugins::Status::Hash
+ def get(uuid)
+ val = redis.get(status_key(uuid))
val ? Resque::Plugins::Status::Hash.new(uuid, decode(val)) : nil
end
- end
- # set a status by UUID. messages can be any number of strings or hashes
- # that are merged in order to create a single status.
- def self.set(uuid, *messages)
- val = Resque::Plugins::Status::Hash.new(uuid, *messages)
- redis.set(status_key(uuid), encode(val))
- if expire_in
- redis.expire(status_key(uuid), expire_in)
+ # Get multiple statuses by UUID. Returns array of Resque::Plugins::Status::Hash
+ def mget(uuids)
+ return [] if uuids.empty?
+ status_keys = uuids.map { |u| status_key(u) }
+ vals = redis.mget(*status_keys)
+
+ uuids.zip(vals).map do |uuid, val|
+ val ? Resque::Plugins::Status::Hash.new(uuid, decode(val)) : nil
+ end
end
- val
- end
- # clear statuses from redis passing an optional range. See `statuses` for info
- # about ranges
- def self.clear(range_start = nil, range_end = nil)
- status_ids(range_start, range_end).each do |id|
- remove(id)
+ # set a status by UUID. messages can be any number of strings or hashes
+ # that are merged in order to create a single status.
+ def set(uuid, *messages)
+ val = Resque::Plugins::Status::Hash.new(uuid, *messages)
+ redis.set(status_key(uuid), encode(val))
+ if expire_in
+ redis.expire(status_key(uuid), expire_in)
+ end
+ val
end
- end
- def self.clear_completed(range_start = nil, range_end = nil)
- status_ids(range_start, range_end).select do |id|
- get(id).completed?
- end.each do |id|
- remove(id)
+ # clear statuses from redis passing an optional range. See `statuses` for info
+ # about ranges
+ def clear(status = nil, range_start = nil, range_end = nil)
+ ids = status_ids(range_start, range_end)
+ ids = ids.select do |id|
+ get(id)['status'] === status.to_s
+ end if status
+ ids.each { |id| remove(id) }
end
- end
- def self.clear_failed(range_start = nil, range_end = nil)
- status_ids(range_start, range_end).select do |id|
- get(id).failed?
- end.each do |id|
- remove(id)
+ Resque::Plugins::Status::STATUSES.each do |status|
+ define_method("clear_#{status}") do
+ clear("#{status}")
+ end
end
- end
- def self.remove(uuid)
- redis.del(status_key(uuid))
- redis.zrem(set_key, uuid)
- end
+ def remove(uuid)
+ redis.del(status_key(uuid))
+ redis.zrem(set_key, uuid)
+ end
- def self.count
- redis.zcard(set_key)
- end
+ def count
+ redis.zcard(set_key)
+ end
- # Return num Resque::Plugins::Status::Hash objects in reverse chronological order.
- # By default returns the entire set.
- # @param [Numeric] range_start The optional starting range
- # @param [Numeric] range_end The optional ending range
- # @example retuning the last 20 statuses
- # Resque::Plugins::Status::Hash.statuses(0, 20)
- def self.statuses(range_start = nil, range_end = nil)
- ids = status_ids(range_start, range_end)
- mget(ids).compact || []
- end
+ # Return num Resque::Plugins::Status::Hash objects in reverse chronological order.
+ # By default returns the entire set.
+ # @param [Numeric] range_start The optional starting range
+ # @param [Numeric] range_end The optional ending range
+ # @example retuning the last 20 statuses
+ # Resque::Plugins::Status::Hash.statuses(0, 20)
+ def statuses(range_start = nil, range_end = nil)
+ ids = status_ids(range_start, range_end)
+ mget(ids).compact || []
+ end
- # Return the num most recent status/job UUIDs in reverse chronological order.
- def self.status_ids(range_start = nil, range_end = nil)
- if range_end && range_start
- # Because we want a reverse chronological order, we need to get a range starting
- # by the higest negative number. The ordering is transparent from the API user's
- # perspective so we need to convert the passed params
- (redis.zrevrange(set_key, (range_start.abs), ((range_end || 1).abs)) || [])
- else
- # Because we want a reverse chronological order, we need to get a range starting
- # by the higest negative number.
- redis.zrevrange(set_key, 0, -1) || []
+ # Return the num most recent status/job UUIDs in reverse chronological order.
+ def status_ids(range_start = nil, range_end = nil)
+ if range_end && range_start
+ # Because we want a reverse chronological order, we need to get a range starting
+ # by the higest negative number. The ordering is transparent from the API user's
+ # perspective so we need to convert the passed params
+ (redis.zrevrange(set_key, (range_start.abs), ((range_end || 1).abs)) || [])
+ else
+ # Because we want a reverse chronological order, we need to get a range starting
+ # by the higest negative number.
+ redis.zrevrange(set_key, 0, -1) || []
+ end
end
- end
- # Kill the job at UUID on its next iteration this works by adding the UUID to a
- # kill list (a.k.a. a list of jobs to be killed. Each iteration the job checks
- # if it _should_ be killed by calling tick or at. If so, it raises
- # a Resque::Plugins::Status::Killed error and sets the status to 'killed'.
- def self.kill(uuid)
- redis.sadd(kill_key, uuid)
- end
+ # Kill the job at UUID on its next iteration this works by adding the UUID to a
+ # kill list (a.k.a. a list of jobs to be killed. Each iteration the job checks
+ # if it _should_ be killed by calling tick or at. If so, it raises
+ # a Resque::Plugins::Status::Killed error and sets the status to 'killed'.
+ def kill(uuid)
+ redis.sadd(kill_key, uuid)
+ end
- # Remove the job at UUID from the kill list
- def self.killed(uuid)
- redis.srem(kill_key, uuid)
- end
+ # Remove the job at UUID from the kill list
+ def killed(uuid)
+ redis.srem(kill_key, uuid)
+ end
- # Return the UUIDs of the jobs on the kill list
- def self.kill_ids
- redis.smembers(kill_key)
- end
+ # Return the UUIDs of the jobs on the kill list
+ def kill_ids
+ redis.smembers(kill_key)
+ end
- # Kills num jobs within range starting with the most recent first.
- # By default kills all jobs.
- # Note that the same conditions apply as kill, i.e. only jobs that check
- # on each iteration by calling tick or at are eligible to killed.
- # @param [Numeric] range_start The optional starting range
- # @param [Numeric] range_end The optional ending range
- # @example killing the last 20 submitted jobs
- # Resque::Plugins::Status::Hash.killall(0, 20)
- def self.killall(range_start = nil, range_end = nil)
- status_ids(range_start, range_end).collect do |id|
- kill(id)
+ # Kills num jobs within range starting with the most recent first.
+ # By default kills all jobs.
+ # Note that the same conditions apply as kill, i.e. only jobs that check
+ # on each iteration by calling tick or at are eligible to killed.
+ # @param [Numeric] range_start The optional starting range
+ # @param [Numeric] range_end The optional ending range
+ # @example killing the last 20 submitted jobs
+ # Resque::Plugins::Status::Hash.killall(0, 20)
+ def killall(range_start = nil, range_end = nil)
+ status_ids(range_start, range_end).collect do |id|
+ kill(id)
+ end
end
- end
- # Check whether a job with UUID is on the kill list
- def self.should_kill?(uuid)
- redis.sismember(kill_key, uuid)
- end
+ # Check whether a job with UUID is on the kill list
+ def should_kill?(uuid)
+ redis.sismember(kill_key, uuid)
+ end
- # The time in seconds that jobs and statuses should expire from Redis (after
- # the last time they are touched/updated)
- def self.expire_in
- @expire_in
- end
+ # The time in seconds that jobs and statuses should expire from Redis (after
+ # the last time they are touched/updated)
+ def expire_in
+ @expire_in
+ end
- # Set the expire_in time in seconds
- def self.expire_in=(seconds)
- @expire_in = seconds.nil? ? nil : seconds.to_i
- end
+ # Set the expire_in time in seconds
+ def expire_in=(seconds)
+ @expire_in = seconds.nil? ? nil : seconds.to_i
+ end
- def self.status_key(uuid)
- "status:#{uuid}"
- end
+ def status_key(uuid)
+ "status:#{uuid}"
+ end
- def self.set_key
- "_statuses"
- end
+ def set_key
+ "_statuses"
+ end
- def self.kill_key
- "_kill"
- end
+ def kill_key
+ "_kill"
+ end
- def self.generate_uuid
- SecureRandom.hex.to_s
- end
+ def generate_uuid
+ SecureRandom.hex.to_s
+ end
- def self.hash_accessor(name, options = {})
- options[:default] ||= nil
- coerce = options[:coerce] ? ".#{options[:coerce]}" : ""
- module_eval <<-EOT
+ def hash_accessor(name, options = {})
+ options[:default] ||= nil
+ coerce = options[:coerce] ? ".#{options[:coerce]}" : ""
+ module_eval <<-EOT
def #{name}
value = (self['#{name}'] ? self['#{name}']#{coerce} : #{options[:default].inspect})
yield value if block_given?
@@ -185,11 +177,11 @@ def #{name}=(value)
def #{name}?
!!self['#{name}']
end
- EOT
- end
+ EOT
+ end
+
+ # Proxy deprecated methods directly back to Resque itself.
- # Proxy deprecated methods directly back to Resque itself.
- class << self
[:redis, :encode, :decode].each do |method|
define_method(method) { |*args| Resque.send(method, *args) }
end
diff --git a/lib/resque_status.rb b/lib/resque_status.rb
new file mode 100644
index 0000000..a10505a
--- /dev/null
+++ b/lib/resque_status.rb
@@ -0,0 +1,2 @@
+module ResqueStatus
+end
diff --git a/lib/resque_web/plugins/resque_status/engine.rb b/lib/resque_web/plugins/resque_status/engine.rb
new file mode 100644
index 0000000..86fba92
--- /dev/null
+++ b/lib/resque_web/plugins/resque_status/engine.rb
@@ -0,0 +1,21 @@
+module ResqueWeb
+ module Plugins
+ module ResqueStatus
+ class Engine < ::Rails::Engine
+ # isolate or not?
+ isolate_namespace ::ResqueStatus
+ initializer "resque_status.assets.precompile" do |app|
+ app.config.assets.precompile += %w(resque_status/application.css resque_status/application.js.coffee)
+ end
+ end
+
+ def self.engine_path
+ '/statuses'
+ end
+
+ def self.tabs
+ [{'Status' => Engine.app.url_helpers.statuses_path}]
+ end
+ end
+ end
+end
diff --git a/resque-status.gemspec b/resque-status.gemspec
index cbb623a..34770a4 100644
--- a/resque-status.gemspec
+++ b/resque-status.gemspec
@@ -18,30 +18,7 @@ Gem::Specification.new do |s|
"LICENSE",
"README.rdoc"
]
- s.files = [
- ".document",
- "Gemfile",
- "Gemfile.lock",
- "LICENSE",
- "README.rdoc",
- "Rakefile",
- "examples/sleep_job.rb",
- "init.rb",
- "lib/resque-status.rb",
- "lib/resque/job_with_status.rb",
- "lib/resque/plugins/status.rb",
- "lib/resque/plugins/status/hash.rb",
- "lib/resque/server/views/status.erb",
- "lib/resque/server/views/status_styles.erb",
- "lib/resque/server/views/statuses.erb",
- "lib/resque/status.rb",
- "lib/resque/status_server.rb",
- "resque-status.gemspec",
- "test/redis-test.conf",
- "test/test_helper.rb",
- "test/test_resque_plugins_status.rb",
- "test/test_resque_plugins_status_hash.rb"
- ]
+ s.files = Dir['{lib,config,app}/**/*']
s.homepage = "http://github.com/quirkey/resque-status"
s.rubyforge_project = "quirkey"
s.rubygems_version = "2.2.1"
|