Skip to content

Commit 25dff96

Browse files
Luis MetzgerLuis Metzger
authored andcommitted
SRCH-6443: Harden CodeDeploy hooks for crawler Resque
- Source optional /home/search/.config/searchgov-codedeploy.env (Ansible-managed on crawlers). - When REQUIRE_RESQUE_SERVICES=true: fail ApplicationStop if units missing; require active resque-worker/resque-scheduler in ValidateService. - After stop, terminate orphan resque processes (optional SKIP_ORPHAN_RESQUE_SIGTERM). - Add verify_resque_cwd.sh for REQUIRE_RESQUE_CWD_CHECK post-deploy validation.
1 parent 4a5661f commit 25dff96

4 files changed

Lines changed: 453 additions & 0 deletions

File tree

cicd-scripts/application_start.sh

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
if [ -f /home/search/.config/searchgov-codedeploy.env ]; then
5+
set -a
6+
# shellcheck disable=SC1090
7+
source /home/search/.config/searchgov-codedeploy.env
8+
set +a
9+
fi
10+
11+
log() {
12+
echo "[CODEDEPLOY][APPLICATION_START] $*"
13+
}
14+
15+
warn() {
16+
echo "[CODEDEPLOY][APPLICATION_START][WARN] $*"
17+
}
18+
19+
error() {
20+
echo "[CODEDEPLOY][APPLICATION_START][ERROR] $*"
21+
}
22+
23+
service_exists() {
24+
local service_name="$1"
25+
systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}' | grep -Fxq "${service_name}.service" || \
26+
systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}' | grep -Fxq "$service_name"
27+
}
28+
29+
restart_or_start_service() {
30+
local service_name="$1"
31+
32+
if service_exists "$service_name"; then
33+
log "Restarting service: $service_name"
34+
systemctl restart "$service_name"
35+
else
36+
log "Service not found, skipping: $service_name"
37+
fi
38+
}
39+
40+
wait_for_pid_exit() {
41+
local pid="$1"
42+
local timeout_seconds="${2:-10}"
43+
local elapsed=0
44+
45+
while kill -0 "$pid" >/dev/null 2>&1; do
46+
if [ "$elapsed" -ge "$timeout_seconds" ]; then
47+
warn "PID $pid did not exit after ${timeout_seconds}s"
48+
return 1
49+
fi
50+
sleep 1
51+
elapsed=$((elapsed + 1))
52+
done
53+
54+
return 0
55+
}
56+
57+
start_puma_fallback() {
58+
local app_root="$1"
59+
local current_path="${app_root}/current"
60+
local puma_pidfile="${PUMA_PIDFILE:-${current_path}/tmp/pids/server.pid}"
61+
local puma_stdout_log="${PUMA_STDOUT_LOG:-${current_path}/log/puma_stdout.log}"
62+
local puma_stderr_log="${PUMA_STDERR_LOG:-${current_path}/log/puma_stderr.log}"
63+
64+
if [ ! -d "$current_path" ]; then
65+
log "Current release path not found, skipping fallback puma start: $current_path"
66+
return 0
67+
fi
68+
69+
70+
# CodeDeploy hooks run in non-login shells; make rbenv shims available if present.
71+
if [ -d "/home/search/.rbenv" ]; then
72+
export RBENV_ROOT="/home/search/.rbenv"
73+
export PATH="$RBENV_ROOT/bin:$RBENV_ROOT/shims:$PATH"
74+
if command -v rbenv >/dev/null 2>&1; then
75+
eval "$(rbenv init - bash)" || warn "rbenv init failed; continuing"
76+
fi
77+
fi
78+
79+
# Configure shared Bundler environment (same as after_install.sh)
80+
log "Configuring shared Bundler environment for Puma"
81+
export BUNDLE_WITHOUT="development:test"
82+
export BUNDLE_PATH="${app_root}/shared/bundle"
83+
export BUNDLE_APP_CONFIG="${app_root}/shared/.bundle"
84+
export BUNDLE_DEPLOYMENT="false"
85+
export BUNDLE_FROZEN="false"
86+
87+
# Stop existing Puma process from pidfile (preferred method)
88+
if [ -f "$puma_pidfile" ]; then
89+
local existing_pid
90+
existing_pid="$(cat "$puma_pidfile" 2>/dev/null || true)"
91+
if [[ -n "$existing_pid" && "$existing_pid" =~ ^[0-9]+$ ]]; then
92+
if kill -0 "$existing_pid" >/dev/null 2>&1; then
93+
log "Stopping existing puma process from pidfile: $existing_pid"
94+
kill "$existing_pid" || true
95+
wait_for_pid_exit "$existing_pid" 10 || kill -9 "$existing_pid" || true
96+
fi
97+
fi
98+
rm -f "$puma_pidfile"
99+
else
100+
# Fallback: If no pidfile exists, check if port 3000 is occupied by Puma
101+
log "No pidfile found, checking for processes using port 3000..."
102+
if command -v lsof >/dev/null 2>&1; then
103+
PORT_3000_PID=$(lsof -ti:3000 2>/dev/null || true)
104+
if [ -n "$PORT_3000_PID" ]; then
105+
# Verify it's a Puma process before killing
106+
if ps -p "$PORT_3000_PID" -o comm= 2>/dev/null | grep -qi puma; then
107+
log "Found Puma process on port 3000: $PORT_3000_PID"
108+
log "Killing Puma process on port 3000..."
109+
kill -TERM "$PORT_3000_PID" 2>/dev/null || true
110+
sleep 3
111+
# Force kill if still running
112+
if lsof -ti:3000 >/dev/null 2>&1; then
113+
log "Force killing stubborn Puma process on port 3000..."
114+
kill -KILL $(lsof -ti:3000 2>/dev/null) 2>/dev/null || true
115+
sleep 1
116+
fi
117+
log "Port 3000 cleared"
118+
else
119+
warn "Process $PORT_3000_PID on port 3000 is not Puma - leaving it alone"
120+
fi
121+
else
122+
log "Port 3000 is available"
123+
fi
124+
else
125+
warn "lsof command not found, cannot check port 3000"
126+
fi
127+
fi
128+
129+
if ! command -v bundle >/dev/null 2>&1; then
130+
log "ERROR: bundle command not found in PATH=$PATH"
131+
return 127
132+
fi
133+
134+
cd "$current_path"
135+
mkdir -p "$(dirname "$puma_pidfile")" "$(dirname "$puma_stdout_log")" "$(dirname "$puma_stderr_log")"
136+
137+
# NOTE: Puma 6+ can reject `-d` daemon mode depending on config/plugins.
138+
# Start it in background from the shell and capture logs explicitly.
139+
log "Starting puma in background (fallback, no systemd service found)"
140+
RAILS_ENV="${RAILS_ENV:-production}" bundle exec puma -C config/puma.rb >>"$puma_stdout_log" 2>>"$puma_stderr_log" &
141+
142+
# Brief pause to surface immediate boot failures before health polling starts.
143+
sleep 2
144+
}
145+
146+
wait_for_http_healthy() {
147+
local url="$1"
148+
local attempts="${2:-12}"
149+
local sleep_seconds="${3:-5}"
150+
local try=1
151+
152+
while [ "$try" -le "$attempts" ]; do
153+
if curl --fail --silent --show-error --max-time 10 "$url" >/dev/null; then
154+
log "HTTP endpoint became healthy on attempt ${try}/${attempts}: $url"
155+
return 0
156+
fi
157+
158+
if [ "$try" -lt "$attempts" ]; then
159+
warn "HTTP endpoint not ready yet (attempt ${try}/${attempts}): $url"
160+
sleep "$sleep_seconds"
161+
fi
162+
163+
try=$((try + 1))
164+
done
165+
166+
return 1
167+
}
168+
169+
dump_startup_diagnostics() {
170+
local app_root="$1"
171+
local current_path="${app_root}/current"
172+
173+
warn "Collecting startup diagnostics"
174+
warn "PATH=$PATH"
175+
warn "pwd=$(pwd)"
176+
177+
if command -v ss >/dev/null 2>&1; then
178+
warn "Listening sockets on :3000 (ss)"
179+
ss -ltnp 2>/dev/null | grep ':3000' || true
180+
elif command -v netstat >/dev/null 2>&1; then
181+
warn "Listening sockets on :3000 (netstat)"
182+
netstat -lntp 2>/dev/null | grep ':3000' || true
183+
fi
184+
185+
warn "Running puma-related processes"
186+
ps -ef | grep -E 'puma|rails server' | grep -v grep || true
187+
188+
if [ -f "${current_path}/log/puma_error.log" ]; then
189+
warn "Tail of ${current_path}/log/puma_error.log"
190+
tail -n 80 "${current_path}/log/puma_error.log" || true
191+
fi
192+
193+
if [ -f "${current_path}/log/puma_stdout.log" ]; then
194+
warn "Tail of ${current_path}/log/puma_stdout.log"
195+
tail -n 80 "${current_path}/log/puma_stdout.log" || true
196+
fi
197+
198+
if [ -f "${current_path}/log/puma_stderr.log" ]; then
199+
warn "Tail of ${current_path}/log/puma_stderr.log"
200+
tail -n 80 "${current_path}/log/puma_stderr.log" || true
201+
fi
202+
203+
if [ -f "${current_path}/log/production.log" ]; then
204+
warn "Tail of ${current_path}/log/production.log"
205+
tail -n 80 "${current_path}/log/production.log" || true
206+
fi
207+
}
208+
209+
PUMA_SERVICE="${PUMA_SERVICE:-puma}"
210+
RESQUE_WORKER_SERVICE="${RESQUE_WORKER_SERVICE:-resque-worker}"
211+
RESQUE_SCHEDULER_SERVICE="${RESQUE_SCHEDULER_SERVICE:-resque-scheduler}"
212+
SEARCHGOV_ROOT="${SEARCHGOV_ROOT:-/home/search/searchgov}"
213+
APP_HEALTHCHECK_URL="${APP_HEALTHCHECK_URL:-http://127.0.0.1:3000/}"
214+
215+
log "Starting ApplicationStart hook"
216+
log "Host: $(hostname) | User: $(whoami)"
217+
218+
restart_or_start_service "$PUMA_SERVICE"
219+
restart_or_start_service "$RESQUE_WORKER_SERVICE"
220+
restart_or_start_service "$RESQUE_SCHEDULER_SERVICE"
221+
222+
if ! service_exists "$PUMA_SERVICE"; then
223+
start_puma_fallback "$SEARCHGOV_ROOT"
224+
fi
225+
226+
if ! wait_for_http_healthy "$APP_HEALTHCHECK_URL" "${HEALTHCHECK_ATTEMPTS:-12}" "${HEALTHCHECK_SLEEP_SECONDS:-5}"; then
227+
error "ApplicationStart could not bring app online at $APP_HEALTHCHECK_URL"
228+
dump_startup_diagnostics "$SEARCHGOV_ROOT"
229+
exit 1
230+
fi
231+
232+
log "ApplicationStart hook completed"

cicd-scripts/application_stop.sh

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
if [ -f /home/search/.config/searchgov-codedeploy.env ]; then
5+
set -a
6+
# shellcheck disable=SC1090
7+
source /home/search/.config/searchgov-codedeploy.env
8+
set +a
9+
fi
10+
11+
log() {
12+
echo "[CODEDEPLOY][APPLICATION_STOP] $*"
13+
}
14+
15+
error() {
16+
echo "[CODEDEPLOY][APPLICATION_STOP][ERROR] $*" >&2
17+
}
18+
19+
service_exists() {
20+
local service_name="$1"
21+
systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}' | grep -Fxq "${service_name}.service" || \
22+
systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}' | grep -Fxq "$service_name"
23+
}
24+
25+
stop_service_if_present() {
26+
local service_name="$1"
27+
28+
if service_exists "$service_name"; then
29+
log "Stopping service: $service_name"
30+
systemctl stop "$service_name"
31+
else
32+
log "Service not found, skipping: $service_name"
33+
fi
34+
}
35+
36+
# These defaults are intentionally overridable per environment.
37+
PUMA_SERVICE="${PUMA_SERVICE:-puma}"
38+
RESQUE_WORKER_SERVICE="${RESQUE_WORKER_SERVICE:-resque-worker}"
39+
RESQUE_SCHEDULER_SERVICE="${RESQUE_SCHEDULER_SERVICE:-resque-scheduler}"
40+
41+
log "Starting ApplicationStop hook"
42+
log "Host: $(hostname) | User: $(whoami)"
43+
44+
if [ "${REQUIRE_RESQUE_SERVICES:-false}" = "true" ]; then
45+
for required in "$RESQUE_WORKER_SERVICE" "$RESQUE_SCHEDULER_SERVICE"; do
46+
if ! service_exists "$required"; then
47+
error "REQUIRE_RESQUE_SERVICES is true but unit not installed: $required (run crawl Ansible resque_systemd role)"
48+
exit 1
49+
fi
50+
done
51+
fi
52+
53+
stop_service_if_present "$PUMA_SERVICE"
54+
stop_service_if_present "$RESQUE_WORKER_SERVICE"
55+
stop_service_if_present "$RESQUE_SCHEDULER_SERVICE"
56+
57+
if [ "${REQUIRE_RESQUE_SERVICES:-false}" = "true" ] && [ "${SKIP_ORPHAN_RESQUE_SIGTERM:-false}" != "true" ]; then
58+
log "Sending SIGTERM to leftover search-user Resque processes (non-systemd orphans)"
59+
pkill -u search -TERM -f '[r]esque-' || true
60+
sleep 3
61+
pkill -u search -KILL -f '[r]esque-' || true
62+
fi
63+
64+
log "ApplicationStop hook completed"

0 commit comments

Comments
 (0)