FEATURE: per host regular jobs

These are jobs that will run on every host that is running discourse.

If you have multiple hosts running the same site you get independent
schedules
This commit is contained in:
Sam 2015-06-26 13:32:08 +10:00
parent ba1cf44d0f
commit d6d9a7fa09
4 changed files with 134 additions and 38 deletions

View file

@ -121,13 +121,15 @@ module Scheduler
self.new(redis, true) self.new(redis, true)
end end
def initialize(redis = nil, skip_runner = false) def initialize(redis = nil, options=nil)
@redis = $redis || redis @redis = $redis || redis
@random_ratio = 0.1 @random_ratio = 0.1
unless skip_runner unless options && options[:skip_runner]
@runner = Runner.new(self) @runner = Runner.new(self)
self.class.current = self self.class.current = self
end end
@hostname = options && options[:hostname]
@manager_id = SecureRandom.hex @manager_id = SecureRandom.hex
end end
@ -139,6 +141,10 @@ module Scheduler
@current = manager @current = manager
end end
def hostname
@hostname ||= `hostname`.strip
end
def schedule_info(klass) def schedule_info(klass)
ScheduleInfo.new(klass, self) ScheduleInfo.new(klass, self)
end end
@ -162,17 +168,22 @@ module Scheduler
def reschedule_orphans! def reschedule_orphans!
lock do lock do
redis.zrange(Manager.queue_key, 0, -1).each do |key| reschedule_orphans_on!
klass = get_klass(key) reschedule_orphans_on!(hostname)
next unless klass end
info = schedule_info(klass) end
if ['QUEUED', 'RUNNING'].include?(info.prev_result) && def reschedule_orphans_on!(hostname=nil)
(info.current_owner.blank? || !redis.get(info.current_owner)) redis.zrange(Manager.queue_key(hostname), 0, -1).each do |key|
info.prev_result = 'ORPHAN' klass = get_klass(key)
info.next_run = Time.now.to_i next unless klass
info.write! info = schedule_info(klass)
end
if ['QUEUED', 'RUNNING'].include?(info.prev_result) &&
(info.current_owner.blank? || !redis.get(info.current_owner))
info.prev_result = 'ORPHAN'
info.next_run = Time.now.to_i
info.write!
end end
end end
end end
@ -185,24 +196,30 @@ module Scheduler
def tick def tick
lock do lock do
(key, due), _ = redis.zrange Manager.queue_key, 0, 0, withscores: true schedule_next_job
return unless key schedule_next_job(hostname)
if due.to_i <= Time.now.to_i end
klass = get_klass(key) end
unless klass
# corrupt key, nuke it (renamed job or something) def schedule_next_job(hostname=nil)
redis.zrem Manager.queue_key, key (key, due), _ = redis.zrange Manager.queue_key(hostname), 0, 0, withscores: true
return
end return unless key
info = schedule_info(klass) if due.to_i <= Time.now.to_i
info.prev_run = Time.now.to_i klass = get_klass(key)
info.prev_result = "QUEUED" unless klass
info.prev_duration = -1 # corrupt key, nuke it (renamed job or something)
info.next_run = nil redis.zrem Manager.queue_key(hostname), key
info.current_owner = identity_key return
info.schedule!
@runner.enq(klass)
end end
info = schedule_info(klass)
info.prev_run = Time.now.to_i
info.prev_result = "QUEUED"
info.prev_duration = -1
info.next_run = nil
info.current_owner = identity_key
info.schedule!
@runner.enq(klass)
end end
end end
@ -256,19 +273,27 @@ module Scheduler
end end
def identity_key def identity_key
@identity_key ||= "_scheduler_#{`hostname`}:#{Process.pid}:#{self.class.seq}:#{SecureRandom.hex}" @identity_key ||= "_scheduler_#{hostname}:#{Process.pid}:#{self.class.seq}:#{SecureRandom.hex}"
end end
def self.lock_key def self.lock_key
"_scheduler_lock_" "_scheduler_lock_"
end end
def self.queue_key def self.queue_key(hostname=nil)
"_scheduler_queue_" if hostname
"_scheduler_queue_#{hostname}_"
else
"_scheduler_queue_"
end
end end
def self.schedule_key(klass) def self.schedule_key(klass,hostname=nil)
"_scheduler_#{klass}" if hostname
"_scheduler_#{klass}_#{hostname}"
else
"_scheduler_#{klass}"
end
end end
end end
end end

View file

@ -17,6 +17,15 @@ module Scheduler::Schedule
@every @every
end end
# schedule job indepndently on each host (looking at hostname)
def per_host
@per_host = true
end
def is_per_host
@per_host
end
def schedule_info def schedule_info
manager = Scheduler::Manager.without_runner manager = Scheduler::Manager.without_runner
manager.schedule_info self manager.schedule_info self

View file

@ -86,6 +86,7 @@ module Scheduler
end end
def write! def write!
clear! clear!
redis.set key, { redis.set key, {
next_run: @next_run, next_run: @next_run,
@ -95,7 +96,7 @@ module Scheduler
current_owner: @current_owner current_owner: @current_owner
}.to_json }.to_json
redis.zadd Manager.queue_key, @next_run , @klass redis.zadd queue_key, @next_run , @klass
end end
def del! def del!
@ -104,7 +105,19 @@ module Scheduler
end end
def key def key
Manager.schedule_key(@klass) if @klass.is_per_host
Manager.schedule_key(@klass, @manager.hostname)
else
Manager.schedule_key(@klass)
end
end
def queue_key
if @klass.is_per_host
Manager.queue_key(@manager.hostname)
else
Manager.queue_key
end
end end
def redis def redis
@ -114,7 +127,7 @@ module Scheduler
private private
def clear! def clear!
redis.del key redis.del key
redis.zrem Manager.queue_key, @klass redis.zrem queue_key, @klass
end end
end end

View file

@ -33,6 +33,25 @@ describe Scheduler::Manager do
sleep 1000 sleep 1000
end end
end end
class PerHostJob
extend ::Scheduler::Schedule
per_host
every 10.minutes
def self.runs=(val)
@runs = val
end
def self.runs
@runs ||= 0
end
def perform
self.class.runs += 1
end
end
end end
let(:manager) { Scheduler::Manager.new(DiscourseRedis.new) } let(:manager) { Scheduler::Manager.new(DiscourseRedis.new) }
@ -42,12 +61,43 @@ describe Scheduler::Manager do
$redis.del manager.class.queue_key $redis.del manager.class.queue_key
manager.remove(Testing::RandomJob) manager.remove(Testing::RandomJob)
manager.remove(Testing::SuperLongJob) manager.remove(Testing::SuperLongJob)
manager.remove(Testing::PerHostJob)
end end
after do after do
manager.stop! manager.stop!
manager.remove(Testing::RandomJob) manager.remove(Testing::RandomJob)
manager.remove(Testing::SuperLongJob) manager.remove(Testing::SuperLongJob)
manager.remove(Testing::PerHostJob)
end
describe 'per host jobs' do
it "correctly schedules on multiple hosts" do
Testing::PerHostJob.runs = 0
hosts = ['a','b','c']
hosts.map do |host|
manager = Scheduler::Manager.new(DiscourseRedis.new, hostname: host)
manager.ensure_schedule!(Testing::PerHostJob)
info = manager.schedule_info(Testing::PerHostJob)
info.next_run = Time.now.to_i - 1
info.write!
manager
end.each do |manager|
manager.blocking_tick
manager.stop!
end
expect(Testing::PerHostJob.runs).to eq(3)
end
end end
describe '#sync' do describe '#sync' do
@ -63,7 +113,6 @@ describe Scheduler::Manager do
$redis.zadd Scheduler::Manager.queue_key, Time.now.to_i - 1000, "BLABLA" $redis.zadd Scheduler::Manager.queue_key, Time.now.to_i - 1000, "BLABLA"
manager.tick manager.tick
expect($redis.zcard(Scheduler::Manager.queue_key)).to eq(0) expect($redis.zcard(Scheduler::Manager.queue_key)).to eq(0)
end end
it 'should recover from crashed manager' do it 'should recover from crashed manager' do