diff --git a/lib/sidekiq-scheduler/job_presenter.rb b/lib/sidekiq-scheduler/job_presenter.rb index ea9989b5..74fe7baa 100644 --- a/lib/sidekiq-scheduler/job_presenter.rb +++ b/lib/sidekiq-scheduler/job_presenter.rb @@ -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 diff --git a/lib/sidekiq-scheduler/web.rb b/lib/sidekiq-scheduler/web.rb index cd14cc0b..d137a8a0 100644 --- a/lib/sidekiq-scheduler/web.rb +++ b/lib/sidekiq-scheduler/web.rb @@ -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 diff --git a/lib/sidekiq/scheduler.rb b/lib/sidekiq/scheduler.rb index 106541b3..78e3d775 100644 --- a/lib/sidekiq/scheduler.rb +++ b/lib/sidekiq/scheduler.rb @@ -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 @@ -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 @@ -353,7 +372,7 @@ 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 @@ -361,6 +380,24 @@ 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 diff --git a/sidekiq-scheduler.gemspec b/sidekiq-scheduler.gemspec index 97f859bb..e01418c3 100644 --- a/sidekiq-scheduler.gemspec +++ b/sidekiq-scheduler.gemspec @@ -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' diff --git a/spec/sidekiq-scheduler/job_presenter_spec.rb b/spec/sidekiq-scheduler/job_presenter_spec.rb index 650443ff..732e28f8 100644 --- a/spec/sidekiq-scheduler/job_presenter_spec.rb +++ b/spec/sidekiq-scheduler/job_presenter_spec.rb @@ -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) } @@ -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 diff --git a/spec/sidekiq-scheduler/web_spec.rb b/spec/sidekiq-scheduler/web_spec.rb index 4e6e2210..94c44aff 100644 --- a/spec/sidekiq-scheduler/web_spec.rb +++ b/spec/sidekiq-scheduler/web_spec.rb @@ -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 @@ -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 diff --git a/spec/sidekiq/scheduler_spec.rb b/spec/sidekiq/scheduler_spec.rb index 4d2bdb20..9366459b 100644 --- a/spec/sidekiq/scheduler_spec.rb +++ b/spec/sidekiq/scheduler_spec.rb @@ -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 diff --git a/web/locales/cs.yml b/web/locales/cs.yml index 17088a63..2c5d8c9e 100644 --- a/web/locales/cs.yml +++ b/web/locales/cs.yml @@ -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 diff --git a/web/locales/en.yml b/web/locales/en.yml index 4a77b038..3fe3bf76 100644 --- a/web/locales/en.yml +++ b/web/locales/en.yml @@ -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 diff --git a/web/locales/es.yml b/web/locales/es.yml index ce0959e9..5e7e9b8c 100644 --- a/web/locales/es.yml +++ b/web/locales/es.yml @@ -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 diff --git a/web/locales/zh-cn.yml b/web/locales/zh-cn.yml index 421bccb3..b2f6d2eb 100644 --- a/web/locales/zh-cn.yml +++ b/web/locales/zh-cn.yml @@ -9,3 +9,5 @@ zh-cn: enqueue_now: 立即执行 next_time: 下次执行时间 no_next_time: 已无后续作业 + disable: 禁用 + enable: 启用 diff --git a/web/views/recurring_jobs.erb b/web/views/recurring_jobs.erb index 15399422..c691e356 100644 --- a/web/views/recurring_jobs.erb +++ b/web/views/recurring_jobs.erb @@ -26,11 +26,16 @@ <%= job.queue %> <%= job['args'] %> - <%= job.next_time || t('no_next_time') %> + + <%= job.next_time || t('no_next_time') %> + <%= t('enqueue_now') %> + btn-xs" href="<%= root_path %>recurring-jobs/<%= URI.escape(job.name) %>/toggle"> + <%= job.enabled? ? t('disable') : t('enable') %> + <% end %>