PERF: limit anonymization to 1 per cluster (#21992)
Anonymization is among the most expensive operations we can perform with extreme potential to impact the database. To mitigate risk we only allow a single anonymization across the entire cluster concurrently. This commit introduces support for `cluster_concurrency 1`. When you set that on a Job it will only allow 1 concurrent execution per cluster.
This commit is contained in:
parent
1a27e715ee
commit
eb603b246b
|
@ -161,6 +161,15 @@ module Jobs
|
||||||
|
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
def self.cluster_concurrency(val)
|
||||||
|
raise ArgumentError, "cluster_concurrency must be 1 or nil" if val != 1 && val != nil
|
||||||
|
@cluster_concurrency = val
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get_cluster_concurrency
|
||||||
|
@cluster_concurrency
|
||||||
|
end
|
||||||
|
|
||||||
def log(*args)
|
def log(*args)
|
||||||
args.each do |arg|
|
args.each do |arg|
|
||||||
Rails.logger.info "#{Time.now.to_formatted_s(:db)}: [#{self.class.name.upcase}] #{arg}"
|
Rails.logger.info "#{Time.now.to_formatted_s(:db)}: [#{self.class.name.upcase}] #{arg}"
|
||||||
|
@ -216,7 +225,41 @@ module Jobs
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.cluster_concurrency_redis_key
|
||||||
|
"cluster_concurrency:#{self}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.clear_cluster_concurrency_lock!
|
||||||
|
Discourse.redis.without_namespace.del(cluster_concurrency_redis_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.acquire_cluster_concurrency_lock!
|
||||||
|
!!Discourse.redis.without_namespace.set(cluster_concurrency_redis_key, 0, nx: true, ex: 120)
|
||||||
|
end
|
||||||
|
|
||||||
def perform(*args)
|
def perform(*args)
|
||||||
|
requeued = false
|
||||||
|
keepalive_thread = nil
|
||||||
|
finished = false
|
||||||
|
|
||||||
|
if self.class.get_cluster_concurrency
|
||||||
|
if !self.class.acquire_cluster_concurrency_lock!
|
||||||
|
self.class.perform_in(10.seconds, *args)
|
||||||
|
requeued = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
parent_thread = Thread.current
|
||||||
|
cluster_concurrency_redis_key = self.class.cluster_concurrency_redis_key
|
||||||
|
|
||||||
|
keepalive_thread =
|
||||||
|
Thread.new do
|
||||||
|
while parent_thread.alive? && !finished
|
||||||
|
Discourse.redis.without_namespace.expire(cluster_concurrency_redis_key, 120)
|
||||||
|
sleep 60
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
opts = args.extract_options!.with_indifferent_access
|
opts = args.extract_options!.with_indifferent_access
|
||||||
|
|
||||||
Sidekiq.redis { |r| r.set("last_job_perform_at", Time.now.to_i) } if ::Jobs.run_later?
|
Sidekiq.redis { |r| r.set("last_job_perform_at", Time.now.to_i) } if ::Jobs.run_later?
|
||||||
|
@ -278,6 +321,12 @@ module Jobs
|
||||||
|
|
||||||
nil
|
nil
|
||||||
ensure
|
ensure
|
||||||
|
if self.class.get_cluster_concurrency && !requeued
|
||||||
|
finished = true
|
||||||
|
keepalive_thread.wakeup
|
||||||
|
keepalive_thread.join
|
||||||
|
self.class.clear_cluster_concurrency_lock!
|
||||||
|
end
|
||||||
ActiveRecord::Base.connection_handler.clear_active_connections!
|
ActiveRecord::Base.connection_handler.clear_active_connections!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
module Jobs
|
module Jobs
|
||||||
class AnonymizeUser < ::Jobs::Base
|
class AnonymizeUser < ::Jobs::Base
|
||||||
sidekiq_options queue: "low"
|
sidekiq_options queue: "low"
|
||||||
|
# this is an extremely expensive job
|
||||||
|
# we are limiting it so only 1 per cluster runs
|
||||||
|
cluster_concurrency 1
|
||||||
|
|
||||||
def execute(args)
|
def execute(args)
|
||||||
@user_id = args[:user_id]
|
@user_id = args[:user_id]
|
||||||
|
|
|
@ -22,6 +22,46 @@ RSpec.describe ::Jobs::Base do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class ConcurrentJob < ::Jobs::Base
|
||||||
|
cluster_concurrency 1
|
||||||
|
|
||||||
|
def self.running?
|
||||||
|
@running
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.running=(val)
|
||||||
|
@running = val
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
|
self.class.running = true
|
||||||
|
sleep 20
|
||||||
|
ensure
|
||||||
|
self.class.running = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles job concurrency" do
|
||||||
|
ConcurrentJob.clear_cluster_concurrency_lock!
|
||||||
|
|
||||||
|
expect(ConcurrentJob.get_cluster_concurrency).to eq(1)
|
||||||
|
expect(BadJob.get_cluster_concurrency).to eq(nil)
|
||||||
|
|
||||||
|
expect(Sidekiq::Queues["default"].size).to eq(0)
|
||||||
|
|
||||||
|
thread = Thread.new { ConcurrentJob.new.perform({ "test" => 100 }) }
|
||||||
|
|
||||||
|
wait_for { ConcurrentJob.running? }
|
||||||
|
|
||||||
|
ConcurrentJob.new.perform({ "test" => 100 })
|
||||||
|
expect(Sidekiq::Queues["default"].size).to eq(1)
|
||||||
|
|
||||||
|
expect(Sidekiq::Queues["default"][0]["args"][0]).to eq("test" => 100)
|
||||||
|
|
||||||
|
thread.wakeup
|
||||||
|
thread.join
|
||||||
|
end
|
||||||
|
|
||||||
it "handles correct jobs" do
|
it "handles correct jobs" do
|
||||||
job = GoodJob.new
|
job = GoodJob.new
|
||||||
job.perform({})
|
job.perform({})
|
||||||
|
|
Loading…
Reference in New Issue