Skip to content

Commit

Permalink
Enabled/disable jobs feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Cristian Dotta committed Oct 7, 2016
1 parent 695a3ad commit 9bc9af6
Show file tree
Hide file tree
Showing 12 changed files with 246 additions and 7 deletions.
4 changes: 4 additions & 0 deletions lib/sidekiq-scheduler/job_presenter.rb
Expand Up @@ -45,6 +45,10 @@ def [](key)
@attributes[key]
end

def enabled?
Sidekiq::Scheduler.job_enabled?(@name)
end

# Builds the presenter instances for the schedule hash
#
# @param schedule_hash [Hash] with the redis schedule
Expand Down
5 changes: 5 additions & 0 deletions lib/sidekiq-scheduler/web.rb
Expand Up @@ -18,6 +18,11 @@ def self.registered(app)
Sidekiq::Scheduler.enqueue_job(schedule)
redirect "#{root_path}recurring-jobs"
end

app.get '/recurring-jobs/:name/toggle' do
Sidekiq::Scheduler.toggle_job_enabled(params[:name])
redirect "#{root_path}recurring-jobs"
end
end
end
end
Expand Down
41 changes: 39 additions & 2 deletions lib/sidekiq/scheduler.rb
Expand Up @@ -8,7 +8,7 @@ class Scheduler
extend Sidekiq::Util

REGISTERED_JOBS_THRESHOLD_IN_SECONDS = 24 * 60 * 60
RUFUS_METADATA_KEYS = %w(description at cron every in interval)
RUFUS_METADATA_KEYS = %w(description at cron every in interval enabled)

# We expect rufus jobs to have #params
Rufus::Scheduler::Job.module_eval do
Expand Down Expand Up @@ -335,10 +335,29 @@ def pushed_job_key(job_name)

# Returns the key of the Redis hash for job's execution times hash
#
# @return [String] with the key
def next_times_key
'sidekiq-scheduler:next_times'
end

# Returns the Redis's key for saving schedule states.
#
# @return [String] with the key
def schedules_state_key
'sidekiq-scheduler:states'
end

def job_enabled?(name)
job = Sidekiq.schedule[name]
schedule_state(name).fetch('enabled', job.fetch('enabled', true)) if job
end

def toggle_job_enabled(name)
state = schedule_state(name)
state['enabled'] = !job_enabled?(name)
set_schedule_state(name, state)
end

private

def new_rufus_scheduler
Expand All @@ -353,14 +372,32 @@ def new_job(name, interval_type, config, args)
opts = { :job => true, :tags => [name] }

rufus_scheduler.send(interval_type, *args, opts) do |job, time|
idempotent_job_enqueue(name, time, sanitize_job_config(config))
idempotent_job_enqueue(name, time, sanitize_job_config(config)) if job_enabled?(name)
end
end

def sanitize_job_config(config)
config.reject { |k, _| RUFUS_METADATA_KEYS.include?(k) }
end

# Retrieves a schedule state
#
# @param name [String] with the schedule's name
# @return [Hash] with the schedule's state
def schedule_state(name)
state = Sidekiq.redis { |r| r.hget(schedules_state_key, name) }

state ? MultiJson.decode(state) : {}
end

# Saves a schedule state
#
# @param name [String] with the schedule's name
# @param name [Hash] with the schedule's state
def set_schedule_state(name, state)
Sidekiq.redis { |r| r.hset(schedules_state_key, name, MultiJson.encode(state)) }
end

end
end
end
1 change: 1 addition & 0 deletions sidekiq-scheduler.gemspec
Expand Up @@ -31,6 +31,7 @@ Gem::Specification.new do |s|
s.add_development_dependency 'rspec'
s.add_development_dependency 'mock_redis', '~> 0'
s.add_development_dependency 'simplecov', '~> 0'
s.add_development_dependency 'byebug'

if RUBY_VERSION >= '2.2.2'
s.add_development_dependency 'activejob'
Expand Down
17 changes: 17 additions & 0 deletions spec/sidekiq-scheduler/job_presenter_spec.rb
Expand Up @@ -2,6 +2,7 @@

describe SidekiqScheduler::JobPresenter do
let(:job_name) { "job_name" }
let(:job_config) { { 'cron' => '* * * * *', 'class' => 'SomeIvarJob', 'args' => '/tmp' } }
let(:attributes) { {} }

subject { described_class.new(job_name, attributes) }
Expand Down Expand Up @@ -73,6 +74,22 @@
end
end

describe "#enabled?" do
before { Sidekiq.schedule = { job_name => job_config } }

subject { described_class.new(job_name, attributes).enabled? }

context "when the job is enabled" do
it { is_expected.to be }
end

context "when the job is disabled" do
before { Sidekiq::Scheduler.toggle_job_enabled(job_name) }

it { is_expected.not_to be }
end
end

describe "#[]" do
let(:params) { "params" }
it "delegates the method to the attriutes" do
Expand Down
30 changes: 26 additions & 4 deletions spec/sidekiq-scheduler/web_spec.rb
Expand Up @@ -9,20 +9,26 @@ def app
Sidekiq::Web
end

let(:enabled_job_name) { 'Foo Job' }

let(:disabled_job_name) { 'Bar Job' }

let(:jobs) do
{
'Foo Job' => {
enabled_job_name => {
'class' => 'FooClass',
'cron' => '0 * * * * US/Eastern',
'args' => [42],
'description' => 'Does foo things.'
'description' => 'Does foo things.',
'enabled' => true
},

'Bar Job' => {
disabled_job_name => {
'class' => 'BarClass',
'every' => '1h',
'args' => ['foo', 'bar'],
'queue' => 'special'
'queue' => 'special',
'enabled' => false
}
}
end
Expand Down Expand Up @@ -64,6 +70,22 @@ def app
end
end

describe '/recurring-jobs/:name/toggle' do
context 'when the job is enabled' do
it 'disables the job' do
expect { get "/recurring-jobs/#{URI.escape(enabled_job_name)}/toggle" }
.to change { Sidekiq::Scheduler.job_enabled?(enabled_job_name) }.from(true).to(false)
end
end

context 'when the job is disabled' do
it 'enables the job' do
expect { get "/recurring-jobs/#{URI.escape(disabled_job_name)}/toggle" }
.to change { Sidekiq::Scheduler.job_enabled?(disabled_job_name) }.from(false).to(true)
end
end
end


it 'enqueues particular job' do
job_name = jobs.keys.first
Expand Down
140 changes: 140 additions & 0 deletions spec/sidekiq/scheduler_spec.rb
Expand Up @@ -782,4 +782,144 @@ def enqueued_jobs_registry
end
end
end

describe '.job_enabled?' do
let(:job_name) { 'job_name' }
let(:job_schedule) { { job_name => job_config } }

subject { Sidekiq::Scheduler.job_enabled?(job_name) }

before do
Sidekiq::Scheduler.enabled = false
Sidekiq.schedule = job_schedule
Sidekiq::Scheduler.load_schedule!
end

context 'when the job have no schedule state' do
context 'when the enabled base config is not set' do
let(:job_config) do
{
'cron' => '* * * * *',
'class' => 'SomeIvarJob',
'args' => '/tmp'
}
end

it 'returns true by default' do
expect(subject).to be
end
end

context 'when the enabled base config is set' do
let(:enabled) { false }
let(:job_config) do
{
'cron' => '* * * * *',
'class' => 'SomeIvarJob',
'args' => '/tmp',
'enabled' => enabled
}
end

it 'returns the value' do
expect(subject).to eq(enabled)
end
end
end

context 'when the job has schedule state' do
let(:state) { { 'enabled' => enabled } }

before do
Sidekiq.redis do |r|
r.hset(described_class.schedules_state_key, job_name, MultiJson.encode(state))
end
end


context 'when the enabled base config is not set' do
let(:enabled) { false }
let(:job_config) do
{
'cron' => '* * * * *',
'class' => 'SomeIvarJob',
'args' => '/tmp'
}
end

it 'returns the state value' do
expect(subject).to eq(enabled)
end
end

context 'when the enabled base config is set' do
let(:enabled) { true }
let(:job_config) do
{
'cron' => '* * * * *',
'class' => 'SomeIvarJob',
'args' => '/tmp',
'enabled' => false
}
end

it 'returns the state value' do
expect(subject).to eq(enabled)
end
end
end
end

describe '.toggle_job_enabled' do
let(:job_name) { 'job_name' }
let(:job_schedule) { { job_name => job_config } }

subject { Sidekiq::Scheduler.toggle_job_enabled(job_name) }

before do
Sidekiq::Scheduler.enabled = false
Sidekiq.schedule = job_schedule
Sidekiq::Scheduler.load_schedule!
end

context 'when the job has schedule state' do
let(:enabled) { false }
let(:state) { { 'enabled' => enabled } }
let(:job_config) do
{
'cron' => '* * * * *',
'class' => 'SomeIvarJob',
'args' => '/tmp'
}
end

before do
Sidekiq.redis do |r|
r.hset(described_class.schedules_state_key, job_name, MultiJson.encode(state))
end
end

it 'toggles the value' do
expect { subject }.to change { described_class.job_enabled?(job_name) }
.from(enabled).to(!enabled)
end
end

context 'when the job have no schedule state' do
let(:enabled) { false }
let(:job_config) do
{
'cron' => '* * * * *',
'class' => 'SomeIvarJob',
'args' => '/tmp',
'enabled' => enabled
}
end

it 'saves as state the toggled base config' do
expect { subject }.to change { described_class.job_enabled?(job_name) }
.from(enabled).to(!enabled)
end
end
end
end
2 changes: 2 additions & 0 deletions web/locales/cs.yml
Expand Up @@ -9,3 +9,5 @@ cs:
enqueue_now: Zařadit nyní
next_time: Příště
no_next_time: no next execution for this job
disable: Zakázat
enable: Povolit
2 changes: 2 additions & 0 deletions web/locales/en.yml
Expand Up @@ -9,3 +9,5 @@ en:
enqueue_now: Enqueue now
next_time: Next Time
no_next_time: no next execution for this job
disable: Disable
enable: Enable
2 changes: 2 additions & 0 deletions web/locales/es.yml
Expand Up @@ -9,3 +9,5 @@ es:
enqueue_now: Encolar ahora
next_time: Próxima ejecución
no_next_time: esta tarea no se volverá a ejecutar
disable: Inhabilitar
enable: Habilitar
2 changes: 2 additions & 0 deletions web/locales/zh-cn.yml
Expand Up @@ -9,3 +9,5 @@ zh-cn:
enqueue_now: 立即执行
next_time: 下次执行时间
no_next_time: 已无后续作业
disable: 禁用
enable: 启用
7 changes: 6 additions & 1 deletion web/views/recurring_jobs.erb
Expand Up @@ -26,11 +26,16 @@
<a href="<%= root_path %>queues/<%= job.queue %>"><%= job.queue %></a>
</td>
<td><%= job['args'] %></td>
<td><%= job.next_time || t('no_next_time') %></td>
<td><span style="<%= 'text-decoration:line-through' unless job.enabled? %>">
<%= job.next_time || t('no_next_time') %>
</span></td>
<td class="text-center">
<a class="btn btn-warn btn-xs" href="<%= root_path %>recurring-jobs/<%= URI.escape(job.name) %>/enqueue">
<%= t('enqueue_now') %>
</a>
<a class="btn <%= job.enabled? ? "btn-primary" : "btn-warn"%> btn-xs" href="<%= root_path %>recurring-jobs/<%= URI.escape(job.name) %>/toggle">
<%= job.enabled? ? t('disable') : t('enable') %>
</a>
</td>
</tr>
<% end %>
Expand Down

0 comments on commit 9bc9af6

Please sign in to comment.