discourse/lib/demon/sidekiq.rb
Loïc Guitaut 6b243ffdfd
DEV: Remove Unicorn web server in favor of Pitchfork (#39032)
Pitchfork has been the default web server for some time now. This
removes Unicorn entirely to simplify the codebase and unblock future
improvements (like Rack 3).

Notable changes beyond the straightforward removal:

- `Discourse.after_unicorn_worker_fork` →
`Discourse.apply_worker_db_variables_overrides`: renamed and wired into
pitchfork.conf.rb's `after_worker_fork`. This actually *fixes*
per-worker DB variable overrides (`unicorn_worker_db_variables_*`) which
were never called under Pitchfork.
- `bin/ember-cli`: `--unicorn` flag renamed to `--server` (`-u` kept).
- `lib/demon/sidekiq.rb`: removed Unicorn-specific USR1/USR2 signal
handlers and `reopen_logs` (called `Unicorn::Util.reopen_logs`), which
were already dead code under Pitchfork.

Intentionally kept unchanged:
- `config/unicorn_launcher` (used by Docker images, separate effort)
- `docker_manager` plugin (separate repo)
- `UNICORN_*` env vars (renaming deferred)
- Rack < 3 constraint (separate PR)
2026-04-01 15:04:59 +02:00

146 lines
4.1 KiB
Ruby
Vendored

# frozen_string_literal: true
require "demon/base"
class Demon::Sidekiq < ::Demon::Base
def self.prefix
"sidekiq"
end
def self.after_fork(&blk)
blk ? (@blk = blk) : @blk
end
# Number of seconds Sidekiq waits for jobs to finish before forcefully
# terminating them. See the "-t" CLI option.
SIDEKIQ_SHUTDOWN_TIMEOUT_SECONDS = 5
# By default Sidekiq does a heartbeat check every 5 seconds. If the processes misses 20 heartbeat checks, we consider it
# dead and kill the process.
SIDEKIQ_HEARTBEAT_CHECK_MISS_THRESHOLD_SECONDS = 5.seconds * 20
def self.heartbeat_check
sidekiq_processes_for_current_hostname = {}
Sidekiq::ProcessSet.new.each do |process|
if process["hostname"] == HOSTNAME
sidekiq_processes_for_current_hostname[process["pid"]] = process
end
end
Demon::Sidekiq.demons.values.each do |daemon|
next if !daemon.already_running?
running_sidekiq_process = sidekiq_processes_for_current_hostname[daemon.pid]
if !running_sidekiq_process ||
(Time.now.to_i - running_sidekiq_process["beat"]) >
SIDEKIQ_HEARTBEAT_CHECK_MISS_THRESHOLD_SECONDS
Rails.logger.warn("Sidekiq heartbeat test failed for #{daemon.pid}, restarting")
daemon.restart
end
end
end
SIDEKIQ_RSS_MEMORY_CHECK_INTERVAL_SECONDS = 30.minutes
def self.rss_memory_check
if defined?(@@last_sidekiq_rss_memory_check) && @@last_sidekiq_rss_memory_check &&
@@last_sidekiq_rss_memory_check > Time.now.to_i - SIDEKIQ_RSS_MEMORY_CHECK_INTERVAL_SECONDS
return @@last_sidekiq_rss_memory_check
end
Demon::Sidekiq.demons.values.each do |daemon|
next if !daemon.already_running?
daemon_rss_bytes = (`ps -o rss= -p #{daemon.pid}`.chomp.to_i || 0) * 1024
if daemon_rss_bytes > max_allowed_sidekiq_rss_bytes
Rails.logger.warn(
"Sidekiq is consuming too much memory (using: %0.2fM) for '%s', restarting" %
[(daemon_rss_bytes.to_f / 1.megabyte), HOSTNAME],
)
daemon.restart
end
end
@@last_sidekiq_rss_memory_check = Time.now.to_i
end
# Given Discourse AI a steady state during streaming is closer
# to 700MB given tokenization overhead. Set the default a bit higher
# so only outliers get restarted.
DEFAULT_MAX_ALLOWED_SIDEKIQ_RSS_MEGABYTES = 1000
def self.max_allowed_sidekiq_rss_bytes
[ENV["UNICORN_SIDEKIQ_MAX_RSS"].to_i, DEFAULT_MAX_ALLOWED_SIDEKIQ_RSS_MEGABYTES].max.megabytes
end
def stop_signal
"TERM"
end
def stop_timeout
# Official documentation says that Sidekiq should be given shutdown timeout
# plus 5 seconds to shutdown cleanly.
#
# See https://github.com/sidekiq/sidekiq/wiki/Deployment.
SIDEKIQ_SHUTDOWN_TIMEOUT_SECONDS + 5
end
private
def suppress_stdout
false
end
def suppress_stderr
false
end
def log_in_trap(message, level: :info)
SignalTrapLogger.instance.log(@logger, message, level: level)
end
def after_fork
Demon::Sidekiq.after_fork&.call
SignalTrapLogger.instance.after_fork
log("Loading Sidekiq in process id #{Process.pid}")
require "sidekiq/cli"
cli = Sidekiq::CLI.instance
options = [
"-c",
GlobalSetting.sidekiq_workers.to_s,
"-t",
SIDEKIQ_SHUTDOWN_TIMEOUT_SECONDS.to_s,
]
[["critical", 8], ["default", 4], ["low", 2], ["ultra_low", 1]].each do |queue_name, weight|
custom_queue_hostname = ENV["UNICORN_SIDEKIQ_#{queue_name.upcase}_QUEUE_HOSTNAME"]
if !custom_queue_hostname || custom_queue_hostname.split(",").include?(Discourse.os_hostname)
options << "-q"
options << "#{queue_name},#{weight}"
end
end
# Sidekiq not as high priority as web, in this environment it is forked so a web is very
# likely running
Discourse::Utils.execute_command("renice", "-n", "5", "-p", Process.pid.to_s)
cli.parse(options)
load Rails.root + "config/initializers/100-sidekiq.rb"
cli.run
rescue => error
log(
"Error encountered while starting Sidekiq: [#{error.class}] #{error.message}\n#{error.backtrace.join("\n")}",
level: :error,
)
exit 1
end
end