mirror of
https://github.com/mperham/sidekiq.git
synced 2022-11-09 13:52:34 -05:00
merge master
This commit is contained in:
commit
0f166de976
20 changed files with 731 additions and 239 deletions
11
Changes.md
11
Changes.md
|
@ -1,10 +1,15 @@
|
|||
# Sidekiq Changes
|
||||
|
||||
HEAD
|
||||
4.2.0
|
||||
-----------
|
||||
|
||||
- Enable development-mode code reloading. With Rails 5.0+, you don't need
|
||||
to restart Sidekiq to pick up your Sidekiq::Worker changes anymore! [#2457]
|
||||
- Enable development-mode code reloading. **With Rails 5.0+, you don't need
|
||||
to restart Sidekiq to pick up your Sidekiq::Worker changes anymore!** [#2457]
|
||||
- **Remove Sinatra dependency**. Sidekiq's Web UI now uses Rack directly.
|
||||
Thank you to Sidekiq's newest committer, **badosu**, for writing the code
|
||||
and doing a lot of testing to ensure compatibility with many different
|
||||
3rd party plugins. If your Web UI works with 4.1.4 but fails with
|
||||
4.2.0, please open an issue. [#3075]
|
||||
- Allow tuning of concurrency with the `RAILS_MAX_THREADS` env var. [#2985]
|
||||
This is the same var used by Puma so you can tune all of your systems
|
||||
the same way:
|
||||
|
|
5
examples/web/Gemfile
Normal file
5
examples/web/Gemfile
Normal file
|
@ -0,0 +1,5 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'sidekiq', path: '../../'
|
||||
gem 'thin'
|
||||
gem 'pry'
|
14
examples/web/config.ru
Normal file
14
examples/web/config.ru
Normal file
|
@ -0,0 +1,14 @@
|
|||
require 'sidekiq/web'
|
||||
require 'redis'
|
||||
|
||||
$redis = Redis.new
|
||||
|
||||
class SinatraWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(msg="lulz you forgot a msg!")
|
||||
$redis.lpush("sinkiq-example-messages", msg)
|
||||
end
|
||||
end
|
||||
|
||||
run Sidekiq::Web
|
|
@ -1,26 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
require 'erb'
|
||||
require 'yaml'
|
||||
require 'sinatra/base'
|
||||
|
||||
require 'sidekiq'
|
||||
require 'sidekiq/api'
|
||||
require 'sidekiq/paginator'
|
||||
require 'sidekiq/web_helpers'
|
||||
require 'sidekiq/web/helpers'
|
||||
|
||||
require 'sidekiq/web/router'
|
||||
require 'sidekiq/web/action'
|
||||
require 'sidekiq/web/application'
|
||||
|
||||
require 'rack/protection'
|
||||
|
||||
require 'rack/builder'
|
||||
require 'rack/file'
|
||||
require 'rack/session/cookie'
|
||||
|
||||
module Sidekiq
|
||||
class Web < Sinatra::Base
|
||||
include Sidekiq::Paginator
|
||||
|
||||
enable :sessions
|
||||
use ::Rack::Protection, :use => :authenticity_token unless ENV['RACK_ENV'] == 'test'
|
||||
|
||||
set :root, File.expand_path(File.dirname(__FILE__) + "/../../web")
|
||||
set :public_folder, proc { "#{root}/assets" }
|
||||
set :views, proc { "#{root}/views" }
|
||||
set :locales, ["#{root}/locales"]
|
||||
|
||||
helpers WebHelpers
|
||||
class Web
|
||||
ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../web")
|
||||
VIEWS = "#{ROOT}/views".freeze
|
||||
LOCALES = ["#{ROOT}/locales".freeze]
|
||||
LAYOUT = "#{VIEWS}/layout.erb".freeze
|
||||
ASSETS = "#{ROOT}/assets".freeze
|
||||
|
||||
DEFAULT_TABS = {
|
||||
"Dashboard" => '',
|
||||
|
@ -32,6 +34,18 @@ module Sidekiq
|
|||
}
|
||||
|
||||
class << self
|
||||
def settings
|
||||
self
|
||||
end
|
||||
|
||||
def middlewares
|
||||
@middlewares ||= []
|
||||
end
|
||||
|
||||
def use(*middleware_args, &block)
|
||||
middlewares << [middleware_args, block]
|
||||
end
|
||||
|
||||
def default_tabs
|
||||
DEFAULT_TABS
|
||||
end
|
||||
|
@ -41,227 +55,94 @@ module Sidekiq
|
|||
end
|
||||
alias_method :tabs, :custom_tabs
|
||||
|
||||
attr_accessor :app_url
|
||||
end
|
||||
|
||||
get "/busy" do
|
||||
erb :busy
|
||||
end
|
||||
|
||||
post "/busy" do
|
||||
if params['identity']
|
||||
p = Sidekiq::Process.new('identity' => params['identity'])
|
||||
p.quiet! if params[:quiet]
|
||||
p.stop! if params[:stop]
|
||||
else
|
||||
processes.each do |pro|
|
||||
pro.quiet! if params[:quiet]
|
||||
pro.stop! if params[:stop]
|
||||
end
|
||||
def locales
|
||||
@locales ||= LOCALES
|
||||
end
|
||||
redirect "#{root_path}busy"
|
||||
end
|
||||
|
||||
get "/queues" do
|
||||
@queues = Sidekiq::Queue.all
|
||||
erb :queues
|
||||
end
|
||||
|
||||
get "/queues/:name" do
|
||||
halt 404 unless params[:name]
|
||||
@count = (params[:count] || 25).to_i
|
||||
@name = params[:name]
|
||||
@queue = Sidekiq::Queue.new(@name)
|
||||
(@current_page, @total_size, @messages) = page("queue:#{@name}", params[:page], @count)
|
||||
@messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
|
||||
erb :queue
|
||||
end
|
||||
|
||||
post "/queues/:name" do
|
||||
Sidekiq::Queue.new(params[:name]).clear
|
||||
redirect "#{root_path}queues"
|
||||
end
|
||||
|
||||
post "/queues/:name/delete" do
|
||||
Sidekiq::Job.new(params[:key_val], params[:name]).delete
|
||||
redirect_with_query("#{root_path}queues/#{params[:name]}")
|
||||
end
|
||||
|
||||
get '/morgue' do
|
||||
@count = (params[:count] || 25).to_i
|
||||
(@current_page, @total_size, @dead) = page("dead", params[:page], @count, reverse: true)
|
||||
@dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
||||
erb :morgue
|
||||
end
|
||||
|
||||
get "/morgue/:key" do
|
||||
halt 404 unless params['key']
|
||||
@dead = Sidekiq::DeadSet.new.fetch(*parse_params(params['key'])).first
|
||||
redirect "#{root_path}morgue" if @dead.nil?
|
||||
erb :dead
|
||||
end
|
||||
|
||||
post '/morgue' do
|
||||
redirect request.path unless params['key']
|
||||
|
||||
params['key'].each do |key|
|
||||
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
||||
retry_or_delete_or_kill job, params if job
|
||||
def views
|
||||
@views ||= VIEWS
|
||||
end
|
||||
redirect_with_query("#{root_path}morgue")
|
||||
end
|
||||
|
||||
post "/morgue/all/delete" do
|
||||
Sidekiq::DeadSet.new.clear
|
||||
redirect "#{root_path}morgue"
|
||||
end
|
||||
|
||||
post "/morgue/all/retry" do
|
||||
Sidekiq::DeadSet.new.retry_all
|
||||
redirect "#{root_path}morgue"
|
||||
end
|
||||
|
||||
post "/morgue/:key" do
|
||||
halt 404 unless params['key']
|
||||
job = Sidekiq::DeadSet.new.fetch(*parse_params(params['key'])).first
|
||||
retry_or_delete_or_kill job, params if job
|
||||
redirect_with_query("#{root_path}morgue")
|
||||
end
|
||||
|
||||
|
||||
get '/retries' do
|
||||
@count = (params[:count] || 25).to_i
|
||||
(@current_page, @total_size, @retries) = page("retry", params[:page], @count)
|
||||
@retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
||||
erb :retries
|
||||
end
|
||||
|
||||
get "/retries/:key" do
|
||||
@retry = Sidekiq::RetrySet.new.fetch(*parse_params(params['key'])).first
|
||||
redirect "#{root_path}retries" if @retry.nil?
|
||||
erb :retry
|
||||
end
|
||||
|
||||
post '/retries' do
|
||||
redirect request.path unless params['key']
|
||||
|
||||
params['key'].each do |key|
|
||||
job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
|
||||
retry_or_delete_or_kill job, params if job
|
||||
def session_secret=(secret)
|
||||
@session_secret = secret
|
||||
end
|
||||
redirect_with_query("#{root_path}retries")
|
||||
|
||||
attr_accessor :app_url, :session_secret, :redis_pool
|
||||
attr_writer :locales, :views
|
||||
end
|
||||
|
||||
post "/retries/all/delete" do
|
||||
Sidekiq::RetrySet.new.clear
|
||||
redirect "#{root_path}retries"
|
||||
def settings
|
||||
self.class.settings
|
||||
end
|
||||
|
||||
post "/retries/all/retry" do
|
||||
Sidekiq::RetrySet.new.retry_all
|
||||
redirect "#{root_path}retries"
|
||||
def use(*middleware_args, &block)
|
||||
middlewares << [middleware_args, block]
|
||||
end
|
||||
|
||||
post "/retries/:key" do
|
||||
job = Sidekiq::RetrySet.new.fetch(*parse_params(params['key'])).first
|
||||
retry_or_delete_or_kill job, params if job
|
||||
redirect_with_query("#{root_path}retries")
|
||||
def middlewares
|
||||
@middlewares ||= Web.middlewares.dup
|
||||
end
|
||||
|
||||
get '/scheduled' do
|
||||
@count = (params[:count] || 25).to_i
|
||||
(@current_page, @total_size, @scheduled) = page("schedule", params[:page], @count)
|
||||
@scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
||||
erb :scheduled
|
||||
def call(env)
|
||||
app.call(env)
|
||||
end
|
||||
|
||||
get "/scheduled/:key" do
|
||||
@job = Sidekiq::ScheduledSet.new.fetch(*parse_params(params['key'])).first
|
||||
redirect "#{root_path}scheduled" if @job.nil?
|
||||
erb :scheduled_job_info
|
||||
def self.call(env)
|
||||
@app ||= new
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
post '/scheduled' do
|
||||
redirect request.path unless params['key']
|
||||
|
||||
params['key'].each do |key|
|
||||
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
||||
delete_or_add_queue job, params if job
|
||||
end
|
||||
redirect_with_query("#{root_path}scheduled")
|
||||
def app
|
||||
@app ||= build
|
||||
end
|
||||
|
||||
post "/scheduled/:key" do
|
||||
halt 404 unless params['key']
|
||||
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(params['key'])).first
|
||||
delete_or_add_queue job, params if job
|
||||
redirect_with_query("#{root_path}scheduled")
|
||||
end
|
||||
|
||||
get '/' do
|
||||
@redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
|
||||
stats_history = Sidekiq::Stats::History.new((params[:days] || 30).to_i)
|
||||
@processed_history = stats_history.processed
|
||||
@failed_history = stats_history.failed
|
||||
erb :dashboard
|
||||
end
|
||||
|
||||
REDIS_KEYS = %w(redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human)
|
||||
|
||||
get '/dashboard/stats' do
|
||||
redirect "#{root_path}stats"
|
||||
end
|
||||
|
||||
get '/stats' do
|
||||
sidekiq_stats = Sidekiq::Stats.new
|
||||
redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
||||
|
||||
content_type :json
|
||||
Sidekiq.dump_json(
|
||||
sidekiq: {
|
||||
processed: sidekiq_stats.processed,
|
||||
failed: sidekiq_stats.failed,
|
||||
busy: sidekiq_stats.workers_size,
|
||||
processes: sidekiq_stats.processes_size,
|
||||
enqueued: sidekiq_stats.enqueued,
|
||||
scheduled: sidekiq_stats.scheduled_size,
|
||||
retries: sidekiq_stats.retry_size,
|
||||
dead: sidekiq_stats.dead_size,
|
||||
default_latency: sidekiq_stats.default_queue_latency
|
||||
},
|
||||
redis: redis_stats
|
||||
)
|
||||
end
|
||||
|
||||
get '/stats/queues' do
|
||||
queue_stats = Sidekiq::Stats::Queues.new
|
||||
|
||||
content_type :json
|
||||
Sidekiq.dump_json(
|
||||
queue_stats.lengths
|
||||
)
|
||||
def self.register(extension)
|
||||
extension.registered(WebApplication)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def retry_or_delete_or_kill job, params
|
||||
if params['retry']
|
||||
job.retry
|
||||
elsif params['delete']
|
||||
job.delete
|
||||
elsif params['kill']
|
||||
job.kill
|
||||
def using?(middleware)
|
||||
middlewares.any? do |(m,_)|
|
||||
m.kind_of?(Array) && (m[0] == middleware || m[0].kind_of?(middleware))
|
||||
end
|
||||
end
|
||||
|
||||
def delete_or_add_queue job, params
|
||||
if params['delete']
|
||||
job.delete
|
||||
elsif params['add_to_queue']
|
||||
job.add_to_queue
|
||||
def build
|
||||
middlewares = self.middlewares
|
||||
klass = self.class
|
||||
|
||||
unless using?(::Rack::Protection) || ENV['RACK_ENV'] == 'test'
|
||||
middlewares.unshift [[::Rack::Protection, { use: :authenticity_token }], nil]
|
||||
end
|
||||
|
||||
unless using? ::Rack::Session::Cookie
|
||||
unless secret = Web.session_secret
|
||||
require 'securerandom'
|
||||
secret = SecureRandom.hex(64)
|
||||
end
|
||||
|
||||
middlewares.unshift [[::Rack::Session::Cookie, { secret: secret }], nil]
|
||||
end
|
||||
|
||||
::Rack::Builder.new do
|
||||
%w(stylesheets javascripts images).each do |asset_dir|
|
||||
map "/#{asset_dir}" do
|
||||
run ::Rack::File.new("#{ASSETS}/#{asset_dir}")
|
||||
end
|
||||
end
|
||||
|
||||
middlewares.each {|middleware, block| use *middleware, &block }
|
||||
|
||||
run WebApplication.new(klass)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Sidekiq::WebApplication.helpers WebHelpers
|
||||
Sidekiq::WebApplication.helpers Sidekiq::Paginator
|
||||
|
||||
Sidekiq::WebAction.class_eval "def _render\n#{ERB.new(File.read(Web::LAYOUT)).src}\nend"
|
||||
end
|
||||
|
||||
if defined?(::ActionDispatch::Request::Session) &&
|
||||
|
|
98
lib/sidekiq/web/action.rb
Normal file
98
lib/sidekiq/web/action.rb
Normal file
|
@ -0,0 +1,98 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidekiq
|
||||
class WebAction
|
||||
RACK_SESSION = 'rack.session'.freeze
|
||||
|
||||
LOCATION = "Location".freeze
|
||||
|
||||
CONTENT_TYPE = "Content-Type".freeze
|
||||
TEXT_HTML = { CONTENT_TYPE => "text/html".freeze }
|
||||
APPLICATION_JSON = { CONTENT_TYPE => "application/json".freeze }
|
||||
|
||||
attr_accessor :env, :block, :type
|
||||
|
||||
def settings
|
||||
Web.settings
|
||||
end
|
||||
|
||||
def request
|
||||
@request ||= ::Rack::Request.new(env)
|
||||
end
|
||||
|
||||
def halt(res)
|
||||
throw :halt, res
|
||||
end
|
||||
|
||||
def redirect(location)
|
||||
throw :halt, [302, { LOCATION => "#{request.base_url}#{location}" }, []]
|
||||
end
|
||||
|
||||
def params
|
||||
indifferent_hash = Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
||||
|
||||
indifferent_hash.merge! request.params
|
||||
route_params.each {|k,v| indifferent_hash[k.to_s] = v }
|
||||
|
||||
indifferent_hash
|
||||
end
|
||||
|
||||
def route_params
|
||||
env[WebRouter::ROUTE_PARAMS]
|
||||
end
|
||||
|
||||
def session
|
||||
env[RACK_SESSION]
|
||||
end
|
||||
|
||||
def content_type(type)
|
||||
@type = type
|
||||
end
|
||||
|
||||
def erb(content, options = {})
|
||||
if content.kind_of? Symbol
|
||||
unless respond_to?(:"_erb_#{content}")
|
||||
src = ERB.new(File.read("#{Web.settings.views}/#{content}.erb")).src
|
||||
WebAction.class_eval("def _erb_#{content}\n#{src}\n end")
|
||||
end
|
||||
end
|
||||
|
||||
if @_erb
|
||||
_erb(content, options[:locals])
|
||||
else
|
||||
@_erb = true
|
||||
content = _erb(content, options[:locals])
|
||||
|
||||
_render { content }
|
||||
end
|
||||
end
|
||||
|
||||
def render(engine, content, options = {})
|
||||
raise "Only erb templates are supported" if engine != :erb
|
||||
|
||||
erb(content, options)
|
||||
end
|
||||
|
||||
def json(payload)
|
||||
[200, APPLICATION_JSON, [Sidekiq.dump_json(payload)]]
|
||||
end
|
||||
|
||||
def initialize(env, block)
|
||||
@env = env
|
||||
@block = block
|
||||
@@files ||= {}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def _erb(file, locals)
|
||||
locals.each {|k, v| define_singleton_method(k){ v } } if locals
|
||||
|
||||
if file.kind_of?(String)
|
||||
ERB.new(file).result(binding)
|
||||
else
|
||||
send(:"_erb_#{file}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
335
lib/sidekiq/web/application.rb
Normal file
335
lib/sidekiq/web/application.rb
Normal file
|
@ -0,0 +1,335 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidekiq
|
||||
class WebApplication
|
||||
extend WebRouter
|
||||
|
||||
CONTENT_LENGTH = "Content-Length".freeze
|
||||
CONTENT_TYPE = "Content-Type".freeze
|
||||
REDIS_KEYS = %w(redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human)
|
||||
NOT_FOUND = [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not Found"]]
|
||||
|
||||
def initialize(klass)
|
||||
@klass = klass
|
||||
end
|
||||
|
||||
def settings
|
||||
@klass.settings
|
||||
end
|
||||
|
||||
def self.settings
|
||||
Sidekiq::Web.settings
|
||||
end
|
||||
|
||||
def self.tabs
|
||||
Sidekiq::Web.tabs
|
||||
end
|
||||
|
||||
def self.set(key, val)
|
||||
# nothing, backwards compatibility
|
||||
end
|
||||
|
||||
get "/" do
|
||||
@redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
|
||||
stats_history = Sidekiq::Stats::History.new((params['days'] || 30).to_i)
|
||||
@processed_history = stats_history.processed
|
||||
@failed_history = stats_history.failed
|
||||
|
||||
erb(:dashboard)
|
||||
end
|
||||
|
||||
get "/busy" do
|
||||
erb(:busy)
|
||||
end
|
||||
|
||||
post "/busy" do
|
||||
if params['identity']
|
||||
p = Sidekiq::Process.new('identity' => params['identity'])
|
||||
p.quiet! if params['quiet']
|
||||
p.stop! if params['stop']
|
||||
else
|
||||
processes.each do |pro|
|
||||
pro.quiet! if params['quiet']
|
||||
pro.stop! if params['stop']
|
||||
end
|
||||
end
|
||||
|
||||
redirect "#{root_path}busy"
|
||||
end
|
||||
|
||||
get "/queues" do
|
||||
@queues = Sidekiq::Queue.all
|
||||
|
||||
erb(:queues)
|
||||
end
|
||||
|
||||
get "/queues/:name" do
|
||||
@name = route_params[:name]
|
||||
|
||||
halt(404) unless @name
|
||||
|
||||
@count = (params['count'] || 25).to_i
|
||||
@queue = Sidekiq::Queue.new(@name)
|
||||
(@current_page, @total_size, @messages) = page("queue:#{@name}", params['page'], @count)
|
||||
@messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
|
||||
|
||||
erb(:queue)
|
||||
end
|
||||
|
||||
post "/queues/:name" do
|
||||
Sidekiq::Queue.new(route_params[:name]).clear
|
||||
|
||||
redirect "#{root_path}queues"
|
||||
end
|
||||
|
||||
post "/queues/:name/delete" do
|
||||
name = route_params[:name]
|
||||
Sidekiq::Job.new(params['key_val'], name).delete
|
||||
|
||||
redirect_with_query("#{root_path}queues/#{name}")
|
||||
end
|
||||
|
||||
get '/morgue' do
|
||||
@count = (params['count'] || 25).to_i
|
||||
(@current_page, @total_size, @dead) = page("dead", params['page'], @count, reverse: true)
|
||||
@dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
||||
|
||||
erb(:morgue)
|
||||
end
|
||||
|
||||
get "/morgue/:key" do
|
||||
halt(404) unless key = route_params[:key]
|
||||
|
||||
@dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
||||
|
||||
if @dead.nil?
|
||||
redirect "#{root_path}morgue"
|
||||
else
|
||||
erb(:dead)
|
||||
end
|
||||
end
|
||||
|
||||
post '/morgue' do
|
||||
redirect(request.path) unless params['key']
|
||||
|
||||
params['key'].each do |key|
|
||||
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
||||
retry_or_delete_or_kill job, params if job
|
||||
end
|
||||
|
||||
redirect_with_query("#{root_path}morgue")
|
||||
end
|
||||
|
||||
post "/morgue/all/delete" do
|
||||
Sidekiq::DeadSet.new.clear
|
||||
|
||||
redirect "#{root_path}morgue"
|
||||
end
|
||||
|
||||
post "/morgue/all/retry" do
|
||||
Sidekiq::DeadSet.new.retry_all
|
||||
|
||||
redirect "#{root_path}morgue"
|
||||
end
|
||||
|
||||
post "/morgue/:key" do
|
||||
halt(404) unless key = route_params[:key]
|
||||
|
||||
job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
|
||||
retry_or_delete_or_kill job, params if job
|
||||
|
||||
redirect_with_query("#{root_path}morgue")
|
||||
end
|
||||
|
||||
get '/retries' do
|
||||
@count = (params['count'] || 25).to_i
|
||||
(@current_page, @total_size, @retries) = page("retry", params['page'], @count)
|
||||
@retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
||||
|
||||
erb(:retries)
|
||||
end
|
||||
|
||||
get "/retries/:key" do
|
||||
@retry = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
|
||||
|
||||
if @retry.nil?
|
||||
redirect "#{root_path}retries"
|
||||
else
|
||||
erb(:retry)
|
||||
end
|
||||
end
|
||||
|
||||
post '/retries' do
|
||||
redirect(request.path) unless params['key']
|
||||
|
||||
params['key'].each do |key|
|
||||
job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
|
||||
retry_or_delete_or_kill job, params if job
|
||||
end
|
||||
|
||||
redirect_with_query("#{root_path}retries")
|
||||
end
|
||||
|
||||
post "/retries/all/delete" do
|
||||
Sidekiq::RetrySet.new.clear
|
||||
|
||||
redirect "#{root_path}retries"
|
||||
end
|
||||
|
||||
post "/retries/all/retry" do
|
||||
Sidekiq::RetrySet.new.retry_all
|
||||
|
||||
redirect "#{root_path}retries"
|
||||
end
|
||||
|
||||
post "/retries/:key" do
|
||||
job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
|
||||
|
||||
retry_or_delete_or_kill job, params if job
|
||||
|
||||
redirect_with_query("#{root_path}retries")
|
||||
end
|
||||
|
||||
get '/scheduled' do
|
||||
@count = (params['count'] || 25).to_i
|
||||
(@current_page, @total_size, @scheduled) = page("schedule", params['page'], @count)
|
||||
@scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
||||
|
||||
erb(:scheduled)
|
||||
end
|
||||
|
||||
get "/scheduled/:key" do
|
||||
@job = Sidekiq::ScheduledSet.new.fetch(*parse_params(route_params[:key])).first
|
||||
|
||||
if @job.nil?
|
||||
redirect "#{root_path}scheduled"
|
||||
else
|
||||
erb(:scheduled_job_info)
|
||||
end
|
||||
end
|
||||
|
||||
post '/scheduled' do
|
||||
redirect(request.path) unless params['key']
|
||||
|
||||
params['key'].each do |key|
|
||||
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
||||
delete_or_add_queue job, params if job
|
||||
end
|
||||
|
||||
redirect_with_query("#{root_path}scheduled")
|
||||
end
|
||||
|
||||
post "/scheduled/:key" do
|
||||
halt(404) unless key = route_params[:key]
|
||||
|
||||
job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
|
||||
delete_or_add_queue job, params if job
|
||||
|
||||
redirect_with_query("#{root_path}scheduled")
|
||||
end
|
||||
|
||||
get '/dashboard/stats' do
|
||||
redirect "#{root_path}stats"
|
||||
end
|
||||
|
||||
get '/stats' do
|
||||
sidekiq_stats = Sidekiq::Stats.new
|
||||
redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
|
||||
|
||||
json(
|
||||
sidekiq: {
|
||||
processed: sidekiq_stats.processed,
|
||||
failed: sidekiq_stats.failed,
|
||||
busy: sidekiq_stats.workers_size,
|
||||
processes: sidekiq_stats.processes_size,
|
||||
enqueued: sidekiq_stats.enqueued,
|
||||
scheduled: sidekiq_stats.scheduled_size,
|
||||
retries: sidekiq_stats.retry_size,
|
||||
dead: sidekiq_stats.dead_size,
|
||||
default_latency: sidekiq_stats.default_queue_latency
|
||||
},
|
||||
redis: redis_stats
|
||||
)
|
||||
end
|
||||
|
||||
get '/stats/queues' do
|
||||
json Sidekiq::Stats::Queues.new.lengths
|
||||
end
|
||||
|
||||
def call(env)
|
||||
action = self.class.match(env)
|
||||
return NOT_FOUND unless action
|
||||
|
||||
resp = catch(:halt) do
|
||||
app = @klass
|
||||
self.class.run_befores(app, action)
|
||||
begin
|
||||
resp = action.instance_exec env, &action.block
|
||||
ensure
|
||||
self.class.run_afters(app, action)
|
||||
end
|
||||
|
||||
resp
|
||||
end
|
||||
|
||||
resp = case resp
|
||||
when Array
|
||||
resp
|
||||
when Fixnum
|
||||
[resp, {}, []]
|
||||
else
|
||||
type_header = case action.type
|
||||
when :json
|
||||
WebAction::APPLICATION_JSON
|
||||
when String
|
||||
{ WebAction::CONTENT_TYPE => action.type }
|
||||
else
|
||||
WebAction::TEXT_HTML
|
||||
end
|
||||
|
||||
[200, type_header, [resp]]
|
||||
end
|
||||
|
||||
resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
|
||||
|
||||
resp
|
||||
end
|
||||
|
||||
def self.helpers(mod=nil, &block)
|
||||
if block_given?
|
||||
WebAction.class_eval(&block)
|
||||
else
|
||||
WebAction.send(:include, mod)
|
||||
end
|
||||
end
|
||||
|
||||
def self.before(path=nil, &block)
|
||||
befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
||||
end
|
||||
|
||||
def self.after(path=nil, &block)
|
||||
afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
|
||||
end
|
||||
|
||||
def self.run_befores(app, action)
|
||||
run_hooks(befores, app, action)
|
||||
end
|
||||
|
||||
def self.run_afters(app, action)
|
||||
run_hooks(afters, app, action)
|
||||
end
|
||||
|
||||
def self.run_hooks(hooks, app, action)
|
||||
hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }.
|
||||
each {|_,b| action.instance_exec(action.env, app, &b) }
|
||||
end
|
||||
|
||||
def self.befores
|
||||
@befores ||= []
|
||||
end
|
||||
|
||||
def self.afters
|
||||
@afters ||= []
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
require 'uri'
|
||||
require 'yaml'
|
||||
|
||||
module Sidekiq
|
||||
# This is not a public API
|
||||
|
@ -45,21 +46,13 @@ module Sidekiq
|
|||
# <meta .../>
|
||||
# <% end %>
|
||||
#
|
||||
def add_to_head(&block)
|
||||
def add_to_head
|
||||
@head_html ||= []
|
||||
@head_html << block if block_given?
|
||||
@head_html << yield.dup if block_given?
|
||||
end
|
||||
|
||||
def display_custom_head
|
||||
return unless defined?(@head_html)
|
||||
@head_html.map { |block| capture(&block) }.join
|
||||
end
|
||||
|
||||
# Simple capture method for erb templates. The origin was
|
||||
# capture method from sinatra-contrib library.
|
||||
def capture(&block)
|
||||
block.call
|
||||
eval('', block.binding)
|
||||
@head_html.join if @head_html
|
||||
end
|
||||
|
||||
# Given a browser request Accept-Language header like
|
||||
|
@ -69,7 +62,7 @@ module Sidekiq
|
|||
def locale
|
||||
@locale ||= begin
|
||||
locale = 'en'.freeze
|
||||
languages = request.env['HTTP_ACCEPT_LANGUAGE'.freeze] || 'en'.freeze
|
||||
languages = env['HTTP_ACCEPT_LANGUAGE'.freeze] || 'en'.freeze
|
||||
languages.downcase.split(','.freeze).each do |lang|
|
||||
next if lang == '*'.freeze
|
||||
lang = lang.split(';'.freeze)[0]
|
||||
|
@ -251,5 +244,23 @@ module Sidekiq
|
|||
"#{redis_connection}#{namespace_suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
def retry_or_delete_or_kill(job, params)
|
||||
if params['retry']
|
||||
job.retry
|
||||
elsif params['delete']
|
||||
job.delete
|
||||
elsif params['kill']
|
||||
job.kill
|
||||
end
|
||||
end
|
||||
|
||||
def delete_or_add_queue(job, params)
|
||||
if params['delete']
|
||||
job.delete
|
||||
elsif params['add_to_queue']
|
||||
job.add_to_queue
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
96
lib/sidekiq/web/router.rb
Normal file
96
lib/sidekiq/web/router.rb
Normal file
|
@ -0,0 +1,96 @@
|
|||
# frozen_string_literal: true
|
||||
require 'rack'
|
||||
|
||||
module Sidekiq
|
||||
module WebRouter
|
||||
GET = 'GET'.freeze
|
||||
DELETE = 'DELETE'.freeze
|
||||
POST = 'POST'.freeze
|
||||
PUT = 'PUT'.freeze
|
||||
PATCH = 'PATCH'.freeze
|
||||
HEAD = 'HEAD'.freeze
|
||||
|
||||
ROUTE_PARAMS = 'rack.route_params'.freeze
|
||||
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
||||
PATH_INFO = 'PATH_INFO'.freeze
|
||||
|
||||
def get(path, &block)
|
||||
route(GET, path, &block)
|
||||
end
|
||||
|
||||
def post(path, &block)
|
||||
route(POST, path, &block)
|
||||
end
|
||||
|
||||
def put(path, &block)
|
||||
route(PUT, path, &block)
|
||||
end
|
||||
|
||||
def patch(path, &block)
|
||||
route(PATCH, path, &block)
|
||||
end
|
||||
|
||||
def delete(path, &block)
|
||||
route(DELETE, path, &block)
|
||||
end
|
||||
|
||||
def route(method, path, &block)
|
||||
@routes ||= { GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => [] }
|
||||
|
||||
@routes[method] << WebRoute.new(method, path, block)
|
||||
@routes[HEAD] << WebRoute.new(method, path, block) if method == GET
|
||||
end
|
||||
|
||||
def match(env)
|
||||
request_method = env[REQUEST_METHOD]
|
||||
path_info = ::Rack::Utils.unescape env[PATH_INFO]
|
||||
|
||||
@routes[request_method].each do |route|
|
||||
if params = route.match(request_method, path_info)
|
||||
env[ROUTE_PARAMS] = params
|
||||
|
||||
return WebAction.new(env, route.block)
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
class WebRoute
|
||||
attr_accessor :request_method, :pattern, :block, :name
|
||||
|
||||
NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^\.:$\/]+)/.freeze
|
||||
|
||||
def initialize(request_method, pattern, block)
|
||||
@request_method = request_method
|
||||
@pattern = pattern
|
||||
@block = block
|
||||
end
|
||||
|
||||
def matcher
|
||||
@matcher ||= compile
|
||||
end
|
||||
|
||||
def compile
|
||||
if pattern.match(NAMED_SEGMENTS_PATTERN)
|
||||
p = pattern.gsub(NAMED_SEGMENTS_PATTERN, '/\1(?<\2>[^$/]+)')
|
||||
|
||||
Regexp.new("\\A#{p}\\Z")
|
||||
else
|
||||
pattern
|
||||
end
|
||||
end
|
||||
|
||||
def match(request_method, path)
|
||||
case matcher
|
||||
when String
|
||||
{} if path == matcher
|
||||
else
|
||||
if path_match = path.match(matcher)
|
||||
params = Hash[path_match.names.map(&:to_sym).zip(path_match.captures)]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,6 @@ source 'https://rubygems.org'
|
|||
gem 'pry'
|
||||
gem 'sidekiq', :path => '..'
|
||||
gem 'rails', '5.0.0'
|
||||
gem 'sinatra', github: 'sinatra/sinatra'
|
||||
|
||||
platforms :ruby do
|
||||
gem 'sqlite3'
|
||||
|
|
|
@ -18,7 +18,7 @@ Gem::Specification.new do |gem|
|
|||
gem.add_dependency 'redis', '~> 3.2', '>= 3.2.1'
|
||||
gem.add_dependency 'connection_pool', '~> 2.2', '>= 2.2.0'
|
||||
gem.add_dependency 'concurrent-ruby', '~> 1.0'
|
||||
gem.add_dependency 'sinatra', '>= 1.4.7'
|
||||
gem.add_dependency 'rack-protection', '~> 1.5'
|
||||
gem.add_development_dependency 'redis-namespace', '~> 1.5', '>= 1.5.2'
|
||||
gem.add_development_dependency 'minitest', '~> 5.7', '>= 5.7.0'
|
||||
gem.add_development_dependency 'rake', '~> 10.0'
|
||||
|
|
|
@ -3,10 +3,8 @@
|
|||
require_relative 'helper'
|
||||
require 'sidekiq/web'
|
||||
require 'rack/test'
|
||||
require 'tilt/erubis'
|
||||
|
||||
class TestWeb < Sidekiq::Test
|
||||
|
||||
describe 'sidekiq web' do
|
||||
include Rack::Test::Methods
|
||||
|
||||
|
@ -341,7 +339,6 @@ class TestWeb < Sidekiq::Test
|
|||
assert last_response.body.include?( "<a>hello</a>" )
|
||||
assert !last_response.body.include?( "<a>hello</a>" )
|
||||
|
||||
|
||||
# on /queues page
|
||||
params = add_xss_retry # sorry, don't know how to easily make this show up on queues page otherwise.
|
||||
post "/retries/#{job_params(*params)}", 'retry' => 'Retry'
|
||||
|
@ -374,7 +371,7 @@ class TestWeb < Sidekiq::Test
|
|||
before do
|
||||
Sidekiq::Web.settings.locales << File.join(File.dirname(__FILE__), "fixtures")
|
||||
Sidekiq::Web.tabs['Custom Tab'] = '/custom'
|
||||
Sidekiq::Web.get('/custom') do
|
||||
Sidekiq::WebApplication.get('/custom') do
|
||||
clear_caches # ugly hack since I can't figure out how to access WebHelpers outside of this context
|
||||
t('translated_text')
|
||||
end
|
||||
|
@ -387,6 +384,7 @@ class TestWeb < Sidekiq::Test
|
|||
|
||||
it 'can show user defined tab with custom locales' do
|
||||
get '/custom'
|
||||
|
||||
assert_match(/Changed text/, last_response.body)
|
||||
end
|
||||
end
|
||||
|
@ -564,6 +562,7 @@ class TestWeb < Sidekiq::Test
|
|||
Sidekiq.redis do |conn|
|
||||
conn.zadd('retry', score, Sidekiq.dump_json(msg))
|
||||
end
|
||||
|
||||
[msg, score]
|
||||
end
|
||||
|
||||
|
@ -596,6 +595,7 @@ class TestWeb < Sidekiq::Test
|
|||
Sidekiq.redis do |conn|
|
||||
conn.zadd('retry', score, Sidekiq.dump_json(msg))
|
||||
end
|
||||
|
||||
[msg, score]
|
||||
end
|
||||
|
||||
|
@ -611,4 +611,51 @@ class TestWeb < Sidekiq::Test
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'sidekiq web with basic auth' do
|
||||
include Rack::Test::Methods
|
||||
|
||||
def app
|
||||
app = Sidekiq::Web.new
|
||||
app.use(Rack::Auth::Basic) { |user, pass| user == "a" && pass == "b" }
|
||||
|
||||
app
|
||||
end
|
||||
|
||||
it 'requires basic authentication' do
|
||||
get '/'
|
||||
|
||||
assert_equal 401, last_response.status
|
||||
refute_nil last_response.header["WWW-Authenticate"]
|
||||
end
|
||||
|
||||
it 'authenticates successfuly' do
|
||||
basic_authorize 'a', 'b'
|
||||
|
||||
get '/'
|
||||
|
||||
assert_equal 200, last_response.status
|
||||
end
|
||||
end
|
||||
|
||||
describe 'sidekiq web with custom session' do
|
||||
include Rack::Test::Methods
|
||||
|
||||
def app
|
||||
app = Sidekiq::Web.new
|
||||
|
||||
app.use Rack::Session::Cookie, secret: 'v3rys3cr31', host: 'nicehost.org'
|
||||
|
||||
app
|
||||
end
|
||||
|
||||
it 'requires basic authentication' do
|
||||
get '/'
|
||||
|
||||
session_options = last_request.env['rack.session'].options
|
||||
|
||||
assert_equal 'v3rys3cr31', session_options[:secret]
|
||||
assert_equal 'nicehost.org', session_options[:host]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -58,14 +58,14 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @redis_info.fetch("used_memory_human", nil) %>
|
||||
<% if @redis_info.fetch("used_memory_human", nil) %>
|
||||
<div class="stat">
|
||||
<h3 class="used_memory_human"><%= @redis_info.fetch("used_memory_human") %></h3>
|
||||
<p><%= t('MemoryUsage') %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @redis_info.fetch("used_memory_peak_human", nil) %>
|
||||
<% if @redis_info.fetch("used_memory_peak_human", nil) %>
|
||||
<div class="stat">
|
||||
<h3 class="used_memory_peak_human"><%= @redis_info.fetch("used_memory_peak_human") %></h3>
|
||||
<p><%= t('PeakMemoryUsage') %></p>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%= erb :_job_info, :locals => {:job => @dead, :type => :dead} %>
|
||||
<%= erb :_job_info, locals: { job: @dead, type: :dead } %>
|
||||
|
||||
<h3><%= t('Error') %></h3>
|
||||
<div class="table_container">
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<title><%= environment_title_prefix %><%= Sidekiq::NAME %></title>
|
||||
<meta charset="utf8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link href="<%= root_path %>stylesheets/bootstrap.css" media="screen" rel="stylesheet" type="text/css" />
|
||||
<link href="<%= root_path %>stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
|
||||
|
@ -26,7 +27,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= erb :_footer %>
|
||||
<%= erb :_footer %>
|
||||
<%= erb :_poll_js %>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
</div>
|
||||
<% if @dead.size > 0 && @total_size > @count %>
|
||||
<div class="col-sm-4">
|
||||
<%= erb :_paging, :locals => { :url => "#{root_path}morgue" } %>
|
||||
<%= erb :_paging, locals: { url: "#{root_path}morgue" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= filtering('dead') %>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</h3>
|
||||
</div>
|
||||
<div class="col-sm-4 pull-right">
|
||||
<%= erb :_paging, :locals => { :url => "#{root_path}queues/#{@name}" } %>
|
||||
<%= erb :_paging, locals: { url: "#{root_path}queues/#{@name}" } %>
|
||||
</div>
|
||||
</header>
|
||||
<div class="table_container">
|
||||
|
@ -42,4 +42,4 @@
|
|||
<% end %>
|
||||
</table>
|
||||
</div>
|
||||
<%= erb :_paging, :locals => { :url => "#{root_path}queues/#{@name}" } %>
|
||||
<%= erb :_paging, locals: { url: "#{root_path}queues/#{@name}" } %>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
</div>
|
||||
<% if @retries.size > 0 && @total_size > @count %>
|
||||
<div class="col-sm-4">
|
||||
<%= erb :_paging, :locals => { :url => "#{root_path}retries" } %>
|
||||
<%= erb :_paging, locals: { url: "#{root_path}retries" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= filtering('retries') %>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%= erb :_job_info, :locals => {:job => @retry, :type => :retry} %>
|
||||
<%= erb :_job_info, locals: { job: @retry, type: :retry } %>
|
||||
|
||||
<h3><%= t('Error') %></h3>
|
||||
<div class="table_container">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
</div>
|
||||
<% if @scheduled.size > 0 && @total_size > @count %>
|
||||
<div class="col-sm-4">
|
||||
<%= erb :_paging, :locals => { :url => "#{root_path}scheduled" } %>
|
||||
<%= erb :_paging, locals: { url: "#{root_path}scheduled" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= filtering('scheduled') %>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%= erb :_job_info, :locals => {:job => @job, :type => :scheduled} %>
|
||||
<%= erb :_job_info, locals: { job: @job, type: :scheduled } %>
|
||||
|
||||
<form class="form-horizontal" action="<%= root_path %>scheduled/<%= job_params(@job, @job.score) %>" method="post">
|
||||
<%= csrf_tag %>
|
||||
|
|
Loading…
Reference in a new issue