Skip to content

Commit 2e88152

Browse files
committed
DEV: Add compatibility with Pitchfork
This patch introduces a web server adapter for the upgrader class. This allows the upgrader to work with both Unicorn and Pitchfork. The user experience when watching the upgrade process from the admin page should stay the same.
1 parent 638dc23 commit 2e88152

File tree

9 files changed

+635
-70
lines changed

9 files changed

+635
-70
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
module DockerManager
4+
class PitchforkAdapter < WebServerAdapter
5+
def server_name
6+
"Pitchfork"
7+
end
8+
9+
def launcher_pid
10+
`pgrep -f unicorn_launcher`.strip.to_i
11+
end
12+
13+
def master_pid
14+
`pgrep -f "pitchfork monitor"`.strip.to_i
15+
end
16+
end
17+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
module DockerManager
4+
class UnicornAdapter < WebServerAdapter
5+
def server_name
6+
"Unicorn"
7+
end
8+
9+
def launcher_pid
10+
`pgrep -f unicorn_launcher`.strip.to_i
11+
end
12+
13+
def master_pid
14+
`pgrep -f "unicorn master -E"`.strip.to_i
15+
end
16+
end
17+
end

lib/docker_manager/upgrader.rb

Lines changed: 28 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
# frozen_string_literal: true
22

3+
require_relative "web_server_adapter"
4+
require_relative "unicorn_adapter"
5+
require_relative "pitchfork_adapter"
6+
37
class DockerManager::Upgrader
8+
attr_reader :web_server
9+
10+
delegate :min_workers, :server_name, :launcher_pid, :master_pid, :workers, to: :web_server
11+
412
def initialize(user_id, repos, from_version)
513
@user_id = user_id
614
@user = User.find(user_id)
715
@repos = repos.is_a?(Array) ? repos : [repos]
816
@from_version = from_version
17+
@web_server = web_server_adapter
918
end
1019

1120
def reset!
@@ -15,10 +24,6 @@ def reset!
1524
status(nil)
1625
end
1726

18-
def min_workers
19-
1
20-
end
21-
2227
def upgrade
2328
return if @repos.any? { |repo| !repo.start_upgrading }
2429

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

34-
launcher_pid = unicorn_launcher_pid
35-
master_pid = unicorn_master_pid
36-
workers = unicorn_workers(master_pid).size
37-
38-
if workers < 2
39-
log("ABORTING, you do not have enough unicorn workers running")
39+
if workers.size <= min_workers
40+
log("ABORTING, you do not have enough #{server_name} workers running")
4041
raise "Not enough workers"
4142
end
4243

4344
if launcher_pid <= 0 || master_pid <= 0
44-
log("ABORTING, missing unicorn launcher or unicorn master")
45-
raise "No unicorn master or launcher"
45+
log("ABORTING, missing #{server_name} launcher or master/monitor")
46+
raise "No #{server_name} master or launcher"
4647
end
4748

4849
percent(5)
4950

50-
log("Cycling Unicorn, to free up memory")
51-
reload_unicorn(launcher_pid)
51+
log("Cycling #{server_name}, to free up memory")
52+
web_server.reload
5253

5354
percent(10)
5455
reloaded = false
55-
num_workers_spun_down = workers - min_workers
56+
num_workers_spun_down = workers.size - min_workers
5657

5758
if num_workers_spun_down.positive?
58-
log "Stopping #{workers - min_workers} Unicorn worker(s), to free up memory"
59-
num_workers_spun_down.times { Process.kill("TTOU", unicorn_master_pid) }
60-
end
61-
62-
if ENV["UNICORN_SIDEKIQS"].to_i > 0
63-
log "Stopping job queue to reclaim memory, master pid is #{master_pid}"
64-
Process.kill("TSTP", unicorn_master_pid)
65-
sleep 1
66-
# older versions do not have support, so quickly send a cont so master process is not hung
67-
Process.kill("CONT", unicorn_master_pid)
59+
log "Stopping #{num_workers_spun_down} #{server_name} worker(s), to free up memory"
60+
web_server.scale_down_workers(num_workers_spun_down)
6861
end
6962

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

119112
percent(80)
120-
reload_unicorn(launcher_pid)
113+
web_server.reload
121114
reloaded = true
122115

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

149142
if num_workers_spun_down.to_i.positive? && !reloaded
150-
log "Spinning up #{num_workers_spun_down} Unicorn worker(s) that were stopped initially"
151-
num_workers_spun_down.times { Process.kill("TTIN", unicorn_master_pid) }
143+
log "Spinning up #{num_workers_spun_down} #{server_name} worker(s) that were stopped initially"
144+
web_server.scale_up_workers(num_workers_spun_down)
152145
end
153146

154147
raise ex
155148
ensure
156149
@repos.each(&:stop_upgrading)
150+
web_server.clear_restart_flag
157151
end
158152

159153
def publish(type, value)
@@ -269,47 +263,11 @@ def log_version_upgrade
269263

270264
private
271265

272-
def pid_exists?(pid)
273-
Process.getpgid(pid)
274-
rescue Errno::ESRCH
275-
false
276-
end
277-
278-
def unicorn_launcher_pid
279-
`ps aux | grep unicorn_launcher | grep -v sudo | grep -v grep | awk '{ print $2 }'`.strip.to_i
280-
end
281-
282-
def unicorn_master_pid
283-
`ps aux | grep "unicorn master -E" | grep -v "grep" | awk '{print $2}'`.strip.to_i
284-
end
285-
286-
def unicorn_workers(master_pid)
287-
`ps -f --ppid #{master_pid} | grep worker | awk '{ print $2 }'`.split("\n").map(&:to_i)
288-
end
289-
290-
def local_web_url
291-
"http://127.0.0.1:#{ENV["UNICORN_PORT"] || 3000}/srv/status"
292-
end
293-
294-
def reload_unicorn(launcher_pid)
295-
log("Restarting unicorn pid: #{launcher_pid}")
296-
original_master_pid = unicorn_master_pid
297-
Process.kill("USR2", launcher_pid)
298-
299-
iterations = 0
300-
while pid_exists?(original_master_pid)
301-
iterations += 1
302-
break if iterations >= 60
303-
log("Waiting for Unicorn to reload#{"." * iterations}")
304-
sleep 2
305-
end
306-
307-
iterations = 0
308-
while `curl -s #{local_web_url}` != "ok"
309-
iterations += 1
310-
break if iterations >= 60
311-
log("Waiting for Unicorn workers to start up#{"." * iterations}")
312-
sleep 2
313-
end
266+
def web_server_adapter
267+
if `pgrep -f '^unicorn[^_]'`.present?
268+
DockerManager::UnicornAdapter
269+
else
270+
DockerManager::PitchforkAdapter
271+
end.new(self)
314272
end
315273
end
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
module DockerManager
4+
class WebServerAdapter
5+
RESTART_FLAG_KEY = "docker_manager:upgrade:server_restarting"
6+
7+
attr_reader :upgrader
8+
9+
delegate :log, to: :upgrader
10+
11+
def initialize(upgrader)
12+
@upgrader = upgrader
13+
end
14+
15+
def workers
16+
`pgrep -f -P #{master_pid} worker`.split("\n").map(&:to_i)
17+
end
18+
19+
def local_web_url
20+
"http://127.0.0.1:#{ENV["UNICORN_PORT"] || 3000}/srv/status"
21+
end
22+
23+
def scale_down_workers(count)
24+
count.times { Process.kill("TTOU", master_pid) }
25+
end
26+
27+
def scale_up_workers(count)
28+
count.times { Process.kill("TTIN", master_pid) }
29+
end
30+
31+
def min_workers
32+
1
33+
end
34+
35+
def reload
36+
set_restart_flag
37+
log("Restarting #{server_name} pid: #{launcher_pid}")
38+
original_master_pid = master_pid
39+
Process.kill("USR2", launcher_pid)
40+
41+
# Wait for the original master/monitor to exit (it will spawn a new one)
42+
iterations = 0
43+
while pid_exists?(original_master_pid)
44+
iterations += 1
45+
break if iterations >= 60
46+
log("Waiting for #{server_name} to reload#{"." * iterations}")
47+
sleep 2
48+
end
49+
50+
# Wait for workers to be ready
51+
iterations = 0
52+
while `curl -s #{local_web_url}` != "ok"
53+
iterations += 1
54+
break if iterations >= 60
55+
log("Waiting for #{server_name} workers to start up#{"." * iterations}")
56+
sleep 2
57+
end
58+
clear_restart_flag
59+
end
60+
61+
def set_restart_flag
62+
Discourse.redis.setex(RESTART_FLAG_KEY, 2.minutes.to_i, 1)
63+
end
64+
65+
def clear_restart_flag
66+
Discourse.redis.del(RESTART_FLAG_KEY)
67+
end
68+
69+
private
70+
71+
def pid_exists?(pid)
72+
Process.getpgid(pid)
73+
rescue Errno::ESRCH
74+
false
75+
end
76+
end
77+
end
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
require "docker_manager/upgrader"
4+
5+
RSpec.describe DockerManager::PitchforkAdapter do
6+
subject(:adapter) { described_class.new(upgrader) }
7+
8+
let(:upgrader) { instance_double(DockerManager::Upgrader, log: nil) }
9+
10+
before { allow_any_instance_of(Kernel).to receive(:`) }
11+
12+
it_behaves_like "a web server adapter"
13+
14+
describe "#server_name" do
15+
it "returns 'Pitchfork'" do
16+
expect(adapter.server_name).to eq("Pitchfork")
17+
end
18+
end
19+
20+
describe "#launcher_pid" do
21+
before do
22+
allow_any_instance_of(Kernel).to receive(:`).with("pgrep -f unicorn_launcher").and_return(
23+
"1234\n",
24+
)
25+
end
26+
27+
it "returns the pid of the 'unicorn_launcher' process" do
28+
expect(adapter.launcher_pid).to eq(1234)
29+
end
30+
end
31+
32+
describe "#master_pid" do
33+
before do
34+
allow_any_instance_of(Kernel).to receive(:`).with('pgrep -f "pitchfork monitor"').and_return(
35+
"5678\n",
36+
)
37+
end
38+
39+
it "returns the pid of the Pitchfork monitor process" do
40+
expect(adapter.master_pid).to eq(5678)
41+
end
42+
end
43+
end
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
require "docker_manager/upgrader"
4+
5+
RSpec.describe DockerManager::UnicornAdapter do
6+
subject(:adapter) { described_class.new(upgrader) }
7+
8+
let(:upgrader) { instance_double(DockerManager::Upgrader, log: nil) }
9+
10+
before { allow_any_instance_of(Kernel).to receive(:`) }
11+
12+
it_behaves_like "a web server adapter"
13+
14+
describe "#server_name" do
15+
it "returns 'Unicorn'" do
16+
expect(adapter.server_name).to eq("Unicorn")
17+
end
18+
end
19+
20+
describe "#launcher_pid" do
21+
before do
22+
allow_any_instance_of(Kernel).to receive(:`).with("pgrep -f unicorn_launcher").and_return(
23+
"1234\n",
24+
)
25+
end
26+
27+
it "returns the pid of the 'unicorn_launcher' process" do
28+
expect(adapter.launcher_pid).to eq(1234)
29+
end
30+
end
31+
32+
describe "#master_pid" do
33+
before do
34+
allow_any_instance_of(Kernel).to receive(:`).with('pgrep -f "unicorn master -E"').and_return(
35+
"5678\n",
36+
)
37+
end
38+
39+
it "returns the pid of the Unicorn master process" do
40+
expect(adapter.master_pid).to eq(5678)
41+
end
42+
end
43+
end

0 commit comments

Comments
 (0)