1
0
Fork 0
mirror of https://github.com/mperham/sidekiq.git synced 2022-11-09 13:52:34 -05:00
mperham--sidekiq/lib/sidekiq/api.rb

534 lines
12 KiB
Ruby
Raw Normal View History

2014-03-07 00:53:16 -05:00
# encoding: utf-8
require 'sidekiq'
module Sidekiq
2012-12-04 08:11:25 -05:00
class Stats
def processed
Sidekiq.redis { |conn| conn.get("stat:processed") }.to_i
end
2012-12-04 08:11:25 -05:00
def failed
Sidekiq.redis { |conn| conn.get("stat:failed") }.to_i
end
2012-12-04 08:11:25 -05:00
def reset(*stats)
all = %w(failed processed)
stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
2013-04-30 18:16:21 -04:00
Sidekiq.redis do |conn|
stats.each { |stat| conn.set("stat:#{stat}", 0) }
2013-04-30 18:16:21 -04:00
end
end
def queues
Sidekiq.redis do |conn|
queues = conn.smembers('queues')
2012-12-04 08:11:25 -05:00
2012-12-04 17:05:47 -05:00
array_of_arrays = queues.inject({}) do |memo, queue|
memo[queue] = conn.llen("queue:#{queue}")
memo
2012-12-04 17:05:47 -05:00
end.sort_by { |_, size| size }
Hash[array_of_arrays.reverse]
2012-12-04 08:11:25 -05:00
end
end
2012-12-04 08:11:25 -05:00
def enqueued
queues.values.inject(&:+) || 0
2012-12-04 08:11:25 -05:00
end
2012-12-05 20:35:49 -05:00
2012-12-29 23:10:36 -05:00
def scheduled_size
Sidekiq.redis {|c| c.zcard('schedule') }
end
def retry_size
Sidekiq.redis {|c| c.zcard('retry') }
end
2014-02-09 17:56:01 -05:00
def dead_size
Sidekiq.redis {|c| c.zcard('dead') }
end
2012-12-05 20:35:49 -05:00
class History
def initialize(days_previous, start_date = nil)
@days_previous = days_previous
@start_date = start_date || Time.now.utc.to_date
end
def processed
date_stat_hash("processed")
end
def failed
date_stat_hash("failed")
end
private
def date_stat_hash(stat)
i = 0
stat_hash = {}
Sidekiq.redis do |conn|
while i < @days_previous
date = @start_date - i
value = conn.get("stat:#{stat}:#{date}")
stat_hash[date.to_s] = value ? value.to_i : 0
i += 1
end
end
stat_hash
end
end
2012-12-04 08:11:25 -05:00
end
##
# Encapsulates a queue within Sidekiq.
# Allows enumeration of all jobs within the queue
# and deletion of jobs.
#
# queue = Sidekiq::Queue.new("mailer")
# queue.each do |job|
# job.klass # => 'MyWorker'
# job.args # => [1, 2, 3]
# job.delete if job.jid == 'abcdef1234567890'
# end
#
class Queue
include Enumerable
def self.all
Sidekiq.redis {|c| c.smembers('queues') }.map {|q| Sidekiq::Queue.new(q) }
end
attr_reader :name
def initialize(name="default")
@name = name
@rname = "queue:#{name}"
end
def size
Sidekiq.redis { |con| con.llen(@rname) }
end
2013-05-24 01:58:06 -04:00
def latency
entry = Sidekiq.redis do |conn|
conn.lrange(@rname, -1, -1)
end.first
return 0 unless entry
Time.now.to_f - Sidekiq.load_json(entry)['enqueued_at']
end
def each(&block)
initial_size = size
deleted_size = 0
page = 0
page_size = 50
loop do
range_start = page * page_size - deleted_size
range_end = page * page_size - deleted_size + (page_size - 1)
entries = Sidekiq.redis do |conn|
conn.lrange @rname, range_start, range_end
end
break if entries.empty?
page += 1
entries.each do |entry|
block.call Job.new(entry, @name)
end
deleted_size = initial_size - size
end
end
def find_job(jid)
self.detect { |j| j.jid == jid }
end
def clear
Sidekiq.redis do |conn|
conn.multi do
conn.del(@rname)
conn.srem("queues", name)
end
end
end
2014-03-07 00:53:16 -05:00
alias_method :💣, :clear
end
##
# Encapsulates a pending job within a Sidekiq queue or
# sorted set.
#
# The job should be considered immutable but may be
# removed from the queue via Job#delete.
#
class Job
attr_reader :item
def initialize(item, queue_name=nil)
@value = item
@item = Sidekiq.load_json(item)
@queue = queue_name || @item['queue']
end
def klass
@item['class']
end
def args
@item['args']
end
def jid
@item['jid']
end
2013-05-23 07:31:41 -04:00
def enqueued_at
2013-08-15 23:06:16 -04:00
Time.at(@item['enqueued_at'] || 0).utc
2013-05-23 07:31:41 -04:00
end
def queue
@queue
end
2013-05-24 22:59:40 -04:00
def latency
Time.now.to_f - @item['enqueued_at']
end
##
# Remove this job from the queue.
def delete
count = Sidekiq.redis do |conn|
conn.lrem("queue:#{@queue}", 1, @value)
end
count != 0
end
def [](name)
@item.__send__(:[], name)
end
end
class SortedEntry < Job
attr_reader :score
2014-02-10 00:17:05 -05:00
attr_reader :parent
def initialize(parent, score, item)
super(item)
@score = score
@parent = parent
end
def at
2013-08-15 23:06:16 -04:00
Time.at(score).utc
2012-10-20 17:09:27 -04:00
end
def delete
@parent.delete(score, jid)
end
def reschedule(at)
@parent.delete(score, jid)
@parent.schedule(at, item)
end
def add_to_queue
Sidekiq.redis do |conn|
results = conn.multi do
conn.zrangebyscore('schedule', score, score)
conn.zremrangebyscore('schedule', score, score)
end.first
results.map do |message|
msg = Sidekiq.load_json(message)
Sidekiq::Client.push(msg)
end
end
end
def retry
2014-02-10 00:17:05 -05:00
raise "Retry not available on jobs which have not failed" unless item["failed_at"]
Sidekiq.redis do |conn|
results = conn.multi do
2014-02-21 21:02:28 -05:00
conn.zrangebyscore(parent.name, score, score)
conn.zremrangebyscore(parent.name, score, score)
end.first
results.map do |message|
msg = Sidekiq.load_json(message)
msg['retry_count'] = msg['retry_count'] - 1
Sidekiq::Client.push(msg)
end
end
end
end
class SortedSet
include Enumerable
2014-02-10 00:17:05 -05:00
attr_reader :name
def initialize(name)
2014-02-10 00:17:05 -05:00
@name = name
@_size = size
end
def size
2014-02-10 00:17:05 -05:00
Sidekiq.redis {|c| c.zcard(name) }
end
def clear
Sidekiq.redis do |conn|
conn.del(name)
end
end
2014-03-07 00:56:59 -05:00
alias_method :💣, :clear
end
class JobSet < SortedSet
def schedule(timestamp, message)
Sidekiq.redis do |conn|
2014-02-10 00:17:05 -05:00
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
end
end
def each(&block)
initial_size = @_size
offset_size = 0
page = -1
page_size = 50
loop do
range_start = page * page_size + offset_size
range_end = page * page_size + offset_size + (page_size - 1)
elements = Sidekiq.redis do |conn|
2014-02-10 00:17:05 -05:00
conn.zrange name, range_start, range_end, :with_scores => true
end
break if elements.empty?
page -= 1
elements.each do |element, score|
block.call SortedEntry.new(self, score, element)
end
offset_size = initial_size - @_size
end
end
def fetch(score, jid = nil)
elements = Sidekiq.redis do |conn|
2014-02-10 00:17:05 -05:00
conn.zrangebyscore(name, score, score)
end
elements.inject([]) do |result, element|
entry = SortedEntry.new(self, score, element)
if jid
result << entry if entry.jid == jid
else
result << entry
end
2012-12-02 23:32:16 -05:00
result
end
end
def find_job(jid)
self.detect { |j| j.jid == jid }
end
def delete(score, jid = nil)
if jid
elements = Sidekiq.redis do |conn|
2014-02-10 00:17:05 -05:00
conn.zrangebyscore(name, score, score)
end
elements_with_jid = elements.map do |element|
message = Sidekiq.load_json(element)
if message["jid"] == jid
_, @_size = Sidekiq.redis do |conn|
conn.multi do
2014-02-10 00:17:05 -05:00
conn.zrem(name, element)
conn.zcard name
end
end
end
end
elements_with_jid.count != 0
else
count, @_size = Sidekiq.redis do |conn|
conn.multi do
2014-02-10 00:17:05 -05:00
conn.zremrangebyscore(name, score, score)
conn.zcard name
end
end
count != 0
end
end
end
##
# Allows enumeration of scheduled jobs within Sidekiq.
# Based on this, you can search/filter for jobs. Here's an
# example where I'm selecting all jobs of a certain type
# and deleting them from the retry queue.
#
# r = Sidekiq::ScheduledSet.new
# r.select do |retri|
# retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
# retri.args[0] == 'User' &&
# retri.args[1] == 'setup_new_subscriber'
# end.map(&:delete)
class ScheduledSet < JobSet
def initialize
super 'schedule'
end
end
##
# Allows enumeration of retries within Sidekiq.
# Based on this, you can search/filter for jobs. Here's an
# example where I'm selecting all jobs of a certain type
# and deleting them from the retry queue.
#
# r = Sidekiq::RetrySet.new
# r.select do |retri|
# retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
# retri.args[0] == 'User' &&
# retri.args[1] == 'setup_new_subscriber'
# end.map(&:delete)
class RetrySet < JobSet
def initialize
super 'retry'
end
2013-02-19 23:36:59 -05:00
2014-02-09 17:56:01 -05:00
def retry_all
while size > 0
each(&:retry)
end
end
end
class DeadSet < JobSet
2014-02-09 17:56:01 -05:00
def initialize
super 'dead'
end
2013-02-19 23:36:59 -05:00
def retry_all
while size > 0
each(&:retry)
end
end
end
##
# Enumerates the set of Sidekiq processes which are actively working
# right now. Each process send a heartbeat to Redis every 5 seconds
# so this set should be relatively accurate, barring network partitions.
#
# Yields a hash of data which looks something like this:
#
# {
# 'hostname' => 'app-1.example.com',
# 'started_at' => <process start time>,
# 'pid' => 12345,
# 'tag' => 'myapp'
# 'concurrency' => 25,
# 'queues' => ['default', 'low'],
# 'busy' => 10,
# 'beat' => <last heartbeat>,
# }
class ProcessSet
include Enumerable
def each(&block)
procs = Sidekiq.redis { |conn| conn.smembers('processes') }
to_prune = []
Sidekiq.redis do |conn|
procs.sort.each do |key|
info, busy, at_s = conn.hmget(key, 'info', 'busy', 'beat')
2014-03-09 17:32:27 -04:00
# the hash named key has an expiry of 60 seconds.
# if it's not found, that means the process has not reported
# in to Redis and probably died.
(to_prune << key; next) if info.nil?
hash = Sidekiq.load_json(info)
yield hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f)
end
end
2014-03-09 17:32:27 -04:00
Sidekiq.redis {|conn| conn.srem('processes', to_prune) } unless to_prune.empty?
nil
end
2014-03-09 17:32:27 -04:00
# This method is not guaranteed accurate since it does not prune the set
# based on current heartbeat. #each does that and ensures the set only
# contains Sidekiq processes which have sent a heartbeat within the last
# 60 seconds.
def size
Sidekiq.redis { |conn| conn.scard('processes') }
end
end
2013-01-24 12:50:30 -05:00
##
# Programmatic access to the current active worker set.
#
# WARNING WARNING WARNING
#
# This is live data that can change every millisecond.
# If you call #size => 5 and then expect #each to be
2013-01-24 12:50:30 -05:00
# called 5 times, you're going to have a bad time.
#
# workers = Sidekiq::Workers.new
# workers.size => 2
2014-03-08 17:21:52 -05:00
# workers.each do |process_id, thread_id, work|
# # process_id is a unique identifier per Sidekiq process
# # thread_id is a unique identifier per thread
2013-01-24 12:50:30 -05:00
# # work is a Hash which looks like:
# # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
2014-02-01 23:48:44 -05:00
# # run_at is an epoch Integer.
2013-01-24 12:50:30 -05:00
# end
2014-03-08 17:21:52 -05:00
#
2013-01-24 12:50:30 -05:00
class Workers
include Enumerable
def each(&block)
Sidekiq.redis do |conn|
procs = conn.smembers('processes')
procs.sort.each do |key|
valid, workers = conn.multi do
conn.exists(key)
conn.hgetall("#{key}:workers")
end
next unless valid
workers.each_pair do |tid, json|
2014-03-08 17:21:52 -05:00
yield key, tid, Sidekiq.load_json(json)
end
end
2013-01-24 12:50:30 -05:00
end
end
2014-03-08 17:21:52 -05:00
# Note that #size is only as accurate as Sidekiq's heartbeat,
# which happens every 5 seconds. It is NOT real-time.
#
# Not very efficient if you have lots of Sidekiq
# processes but the alternative is a global counter
# which can easily get out of sync with crashy processes.
2013-01-24 12:50:30 -05:00
def size
Sidekiq.redis do |conn|
procs = conn.smembers('processes')
return 0 if procs.empty?
conn.multi do
procs.each do |key|
conn.hget(key, 'busy')
end
end.map(&:to_i).inject(:+)
end
2013-01-24 12:50:30 -05:00
end
end
end