Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lib/docker_manager/pitchfork_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module DockerManager
class PitchforkAdapter < WebServerAdapter
def server_name
"Pitchfork"
end

def launcher_pid
`pgrep -f unicorn_launcher`.strip.to_i
end

def master_pid
`pgrep -f "pitchfork monitor"`.strip.to_i
end
end
end
17 changes: 17 additions & 0 deletions lib/docker_manager/unicorn_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module DockerManager
class UnicornAdapter < WebServerAdapter
def server_name
"Unicorn"
end

def launcher_pid
`pgrep -f unicorn_launcher`.strip.to_i
end

def master_pid
`pgrep -f "unicorn master -E"`.strip.to_i
end
end
end
98 changes: 28 additions & 70 deletions lib/docker_manager/upgrader.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# frozen_string_literal: true

require_relative "web_server_adapter"
require_relative "unicorn_adapter"
require_relative "pitchfork_adapter"

class DockerManager::Upgrader
attr_reader :web_server

delegate :min_workers, :server_name, :launcher_pid, :master_pid, :workers, to: :web_server

def initialize(user_id, repos, from_version)
@user_id = user_id
@user = User.find(user_id)
@repos = repos.is_a?(Array) ? repos : [repos]
@from_version = from_version
@web_server = web_server_adapter
end

def reset!
Expand All @@ -15,10 +24,6 @@ def reset!
status(nil)
end

def min_workers
1
end

def upgrade
return if @repos.any? { |repo| !repo.start_upgrading }

Expand All @@ -31,40 +36,28 @@ def upgrade
log("*** Please be patient, next steps might take a while ***")
log("********************************************************")

launcher_pid = unicorn_launcher_pid
master_pid = unicorn_master_pid
workers = unicorn_workers(master_pid).size

if workers < 2
log("ABORTING, you do not have enough unicorn workers running")
if workers.size <= min_workers
log("ABORTING, you do not have enough #{server_name} workers running")
raise "Not enough workers"
end

if launcher_pid <= 0 || master_pid <= 0
log("ABORTING, missing unicorn launcher or unicorn master")
raise "No unicorn master or launcher"
log("ABORTING, missing #{server_name} launcher or master/monitor")
raise "No #{server_name} master or launcher"
end

percent(5)

log("Cycling Unicorn, to free up memory")
reload_unicorn(launcher_pid)
log("Cycling #{server_name}, to free up memory")
web_server.reload

percent(10)
reloaded = false
num_workers_spun_down = workers - min_workers
num_workers_spun_down = workers.size - min_workers

if num_workers_spun_down.positive?
log "Stopping #{workers - min_workers} Unicorn worker(s), to free up memory"
num_workers_spun_down.times { Process.kill("TTOU", unicorn_master_pid) }
end

if ENV["UNICORN_SIDEKIQS"].to_i > 0
log "Stopping job queue to reclaim memory, master pid is #{master_pid}"
Process.kill("TSTP", unicorn_master_pid)
sleep 1
# older versions do not have support, so quickly send a cont so master process is not hung
Process.kill("CONT", unicorn_master_pid)
Comment on lines -62 to -67
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually doesn’t work anymore. It hasn’t for some time now, at least one year (see discourse/discourse#27257).

log "Stopping #{num_workers_spun_down} #{server_name} worker(s), to free up memory"
web_server.scale_down_workers(num_workers_spun_down)
end

# HEAD@{upstream} is just a fancy way how to say origin/main (in normal case)
Expand Down Expand Up @@ -117,7 +110,7 @@ def upgrade
run("bundle exec rake s3:upload_assets") if using_s3_assets

percent(80)
reload_unicorn(launcher_pid)
web_server.reload
reloaded = true

# Flush nginx cache here - this is not critical, and the rake task may not exist yet - ignore failures here.
Expand Down Expand Up @@ -147,13 +140,14 @@ def upgrade
end

if num_workers_spun_down.to_i.positive? && !reloaded
log "Spinning up #{num_workers_spun_down} Unicorn worker(s) that were stopped initially"
num_workers_spun_down.times { Process.kill("TTIN", unicorn_master_pid) }
log "Spinning up #{num_workers_spun_down} #{server_name} worker(s) that were stopped initially"
web_server.scale_up_workers(num_workers_spun_down)
end

raise ex
ensure
@repos.each(&:stop_upgrading)
web_server.clear_restart_flag
end

def publish(type, value)
Expand Down Expand Up @@ -269,47 +263,11 @@ def log_version_upgrade

private

def pid_exists?(pid)
Process.getpgid(pid)
rescue Errno::ESRCH
false
end

def unicorn_launcher_pid
`ps aux | grep unicorn_launcher | grep -v sudo | grep -v grep | awk '{ print $2 }'`.strip.to_i
end

def unicorn_master_pid
`ps aux | grep "unicorn master -E" | grep -v "grep" | awk '{print $2}'`.strip.to_i
end

def unicorn_workers(master_pid)
`ps -f --ppid #{master_pid} | grep worker | awk '{ print $2 }'`.split("\n").map(&:to_i)
end

def local_web_url
"http://127.0.0.1:#{ENV["UNICORN_PORT"] || 3000}/srv/status"
end

def reload_unicorn(launcher_pid)
log("Restarting unicorn pid: #{launcher_pid}")
original_master_pid = unicorn_master_pid
Process.kill("USR2", launcher_pid)

iterations = 0
while pid_exists?(original_master_pid)
iterations += 1
break if iterations >= 60
log("Waiting for Unicorn to reload#{"." * iterations}")
sleep 2
end

iterations = 0
while `curl -s #{local_web_url}` != "ok"
iterations += 1
break if iterations >= 60
log("Waiting for Unicorn workers to start up#{"." * iterations}")
sleep 2
end
def web_server_adapter
if `pgrep -f '^unicorn[^_]'`.present?
DockerManager::UnicornAdapter
else
DockerManager::PitchforkAdapter
end.new(self)
end
end
77 changes: 77 additions & 0 deletions lib/docker_manager/web_server_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

module DockerManager
class WebServerAdapter
RESTART_FLAG_KEY = "docker_manager:upgrade:server_restarting"

attr_reader :upgrader

delegate :log, to: :upgrader

def initialize(upgrader)
@upgrader = upgrader
end

def workers
`pgrep -f -P #{master_pid} worker`.split("\n").map(&:to_i)
end

def local_web_url
"http://127.0.0.1:#{ENV["UNICORN_PORT"] || 3000}/srv/status"
end

def scale_down_workers(count)
count.times { Process.kill("TTOU", master_pid) }
end

def scale_up_workers(count)
count.times { Process.kill("TTIN", master_pid) }
end

def min_workers
1
end

def reload
set_restart_flag
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With #set_restart_flag/#clear_restart_flag, we’re signaling MessageBus to allow its poll call to wait for the server response instead of its default behavior of killing the request after 100 ms.

log("Restarting #{server_name} pid: #{launcher_pid}")
original_master_pid = master_pid
Process.kill("USR2", launcher_pid)

# Wait for the original master/monitor to exit (it will spawn a new one)
iterations = 0
while pid_exists?(original_master_pid)
iterations += 1
break if iterations >= 60
log("Waiting for #{server_name} to reload#{"." * iterations}")
sleep 2
end

# Wait for workers to be ready
iterations = 0
while `curl -s #{local_web_url}` != "ok"
iterations += 1
break if iterations >= 60
log("Waiting for #{server_name} workers to start up#{"." * iterations}")
sleep 2
end
clear_restart_flag
end

def set_restart_flag
Discourse.redis.setex(RESTART_FLAG_KEY, 2.minutes.to_i, 1)
end

def clear_restart_flag
Discourse.redis.del(RESTART_FLAG_KEY)
end

private

def pid_exists?(pid)
Process.getpgid(pid)
rescue Errno::ESRCH
false
end
end
end
43 changes: 43 additions & 0 deletions spec/lib/docker_manager/pitchfork_adapter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require "docker_manager/upgrader"

RSpec.describe DockerManager::PitchforkAdapter do
subject(:adapter) { described_class.new(upgrader) }

let(:upgrader) { instance_double(DockerManager::Upgrader, log: nil) }

before { allow_any_instance_of(Kernel).to receive(:`) }

it_behaves_like "a web server adapter"

describe "#server_name" do
it "returns 'Pitchfork'" do
expect(adapter.server_name).to eq("Pitchfork")
end
end

describe "#launcher_pid" do
before do
allow_any_instance_of(Kernel).to receive(:`).with("pgrep -f unicorn_launcher").and_return(
"1234\n",
)
end

it "returns the pid of the 'unicorn_launcher' process" do
expect(adapter.launcher_pid).to eq(1234)
end
end

describe "#master_pid" do
before do
allow_any_instance_of(Kernel).to receive(:`).with('pgrep -f "pitchfork monitor"').and_return(
"5678\n",
)
end

it "returns the pid of the Pitchfork monitor process" do
expect(adapter.master_pid).to eq(5678)
end
end
end
43 changes: 43 additions & 0 deletions spec/lib/docker_manager/unicorn_adapter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require "docker_manager/upgrader"

RSpec.describe DockerManager::UnicornAdapter do
subject(:adapter) { described_class.new(upgrader) }

let(:upgrader) { instance_double(DockerManager::Upgrader, log: nil) }

before { allow_any_instance_of(Kernel).to receive(:`) }

it_behaves_like "a web server adapter"

describe "#server_name" do
it "returns 'Unicorn'" do
expect(adapter.server_name).to eq("Unicorn")
end
end

describe "#launcher_pid" do
before do
allow_any_instance_of(Kernel).to receive(:`).with("pgrep -f unicorn_launcher").and_return(
"1234\n",
)
end

it "returns the pid of the 'unicorn_launcher' process" do
expect(adapter.launcher_pid).to eq(1234)
end
end

describe "#master_pid" do
before do
allow_any_instance_of(Kernel).to receive(:`).with('pgrep -f "unicorn master -E"').and_return(
"5678\n",
)
end

it "returns the pid of the Unicorn master process" do
expect(adapter.master_pid).to eq(5678)
end
end
end
Loading