Files
flyer-crawler.projectium.com/scripts/migrate-pm2-namespaces.sh
Torben Sorensen 82a38b4e2a feat: implement PM2 namespace isolation for improved process management
- Updated TypeScript configuration to include test files.
- Modified Vitest configuration to include test files from both src and tests directories.
- Added ADR-063 documenting the decision and implementation of PM2 namespaces.
- Created implementation report detailing the migration to PM2 namespaces.
- Developed migration script for transitioning to namespaced PM2 processes.
- Updated ecosystem configuration files to define namespaces for production, test, and development environments.
- Enhanced workflow files to include namespace flags in all PM2 commands.
- Verified migration with comprehensive tests ensuring all processes are correctly namespaced.
2026-02-19 14:51:17 -08:00

530 lines
16 KiB
Bash

#!/bin/bash
# migrate-pm2-namespaces.sh
# Migration script for PM2 namespaces
# Transitions from non-namespaced to namespaced PM2 processes
#
# This script is IDEMPOTENT - safe to run multiple times.
# Uses zero-downtime migration approach by starting new processes before stopping old ones.
#
# Usage:
# ./scripts/migrate-pm2-namespaces.sh [--dry-run] [--test-only] [--prod-only]
#
# Options:
# --dry-run Show what would be done without making changes
# --test-only Only migrate test environment
# --prod-only Only migrate production environment
#
# IMPORTANT: The ecosystem config files (ecosystem.config.cjs, ecosystem-test.config.cjs)
# already define the namespace at the top level. PM2 will automatically use the namespace
# when starting with these config files.
set -euo pipefail
# Configuration
PROD_DIR="/var/www/flyer-crawler.projectium.com"
TEST_DIR="/var/www/flyer-crawler-test.projectium.com"
PROD_NAMESPACE="flyer-crawler-prod"
TEST_NAMESPACE="flyer-crawler-test"
PROD_PORT=3001
TEST_PORT=3002
HEALTH_CHECK_RETRIES=10
HEALTH_CHECK_DELAY=2
# Process names (must match ecosystem config files)
PROD_PROCESSES=("flyer-crawler-api" "flyer-crawler-worker" "flyer-crawler-analytics-worker")
TEST_PROCESSES=("flyer-crawler-api-test" "flyer-crawler-worker-test" "flyer-crawler-analytics-worker-test")
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Parse arguments
DRY_RUN=false
TEST_ONLY=false
PROD_ONLY=false
for arg in "$@"; do
case $arg in
--dry-run)
DRY_RUN=true
;;
--test-only)
TEST_ONLY=true
;;
--prod-only)
PROD_ONLY=true
;;
--help|-h)
echo "Usage: $0 [--dry-run] [--test-only] [--prod-only]"
echo ""
echo "Options:"
echo " --dry-run Show what would be done without making changes"
echo " --test-only Only migrate test environment"
echo " --prod-only Only migrate production environment"
echo ""
echo "This script migrates PM2 processes from non-namespaced to namespaced."
echo "It is idempotent and safe to run multiple times."
echo ""
echo "Namespaces:"
echo " Production: $PROD_NAMESPACE"
echo " Test: $TEST_NAMESPACE"
exit 0
;;
*)
echo "Unknown option: $arg"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[OK]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo ""
echo -e "${CYAN}===${NC} $1 ${CYAN}===${NC}"
}
log_progress() {
echo -e "${BLUE}[>>>]${NC} $1"
}
# Check if a process exists in PM2 (non-namespaced)
process_exists_global() {
local name="$1"
pm2 jlist 2>/dev/null | grep -q "\"name\":\"$name\""
}
# Check if a namespace has processes
namespace_has_processes() {
local namespace="$1"
local count
count=$(pm2 list --namespace "$namespace" 2>/dev/null | grep -c "online\|stopped\|errored" || echo "0")
[ "$count" -gt 0 ]
}
# Get process count in namespace
get_namespace_process_count() {
local namespace="$1"
pm2 list --namespace "$namespace" 2>/dev/null | grep -c "online\|stopped\|errored" || echo "0"
}
# Check health endpoint
check_health() {
local port="$1"
local retries="${2:-$HEALTH_CHECK_RETRIES}"
local delay="${3:-$HEALTH_CHECK_DELAY}"
for ((i=1; i<=retries; i++)); do
if curl -sf "http://localhost:$port/api/health" > /dev/null 2>&1; then
return 0
fi
if [ "$i" -lt "$retries" ]; then
sleep "$delay"
fi
done
return 1
}
# Show rollback instructions
show_rollback_instructions() {
local env_name="$1"
local namespace="$2"
local app_dir="$3"
local config_file="$4"
shift 4
local processes=("$@")
echo ""
log_error "Migration failed for $env_name environment."
echo ""
echo -e "${YELLOW}ROLLBACK INSTRUCTIONS:${NC}"
echo ""
echo "1. Stop any partially started namespaced processes:"
echo " pm2 delete all --namespace $namespace 2>/dev/null || true"
echo ""
echo "2. If old processes were stopped, restart them manually:"
for proc in "${processes[@]}"; do
echo " pm2 start $app_dir/$config_file --only $proc"
done
echo ""
echo "3. Save PM2 state:"
echo " pm2 save"
echo ""
echo "4. Verify processes are running:"
echo " pm2 list"
echo ""
echo "5. Check application health:"
if [[ "$env_name" == "Production" ]]; then
echo " curl http://localhost:$PROD_PORT/api/health"
else
echo " curl http://localhost:$TEST_PORT/api/health"
fi
echo ""
}
# Migrate a single environment
migrate_environment() {
local env_name="$1"
local namespace="$2"
local app_dir="$3"
local config_file="$4"
local api_port="$5"
shift 5
local processes=("$@")
log_step "Migrating $env_name Environment"
# Check if directory exists
if [ ! -d "$app_dir" ]; then
log_warn "$env_name directory not found: $app_dir"
log_warn "Skipping $env_name migration"
return 0
fi
# Check if config file exists
if [ ! -f "$app_dir/$config_file" ]; then
log_warn "Config file not found: $app_dir/$config_file"
log_warn "Skipping $env_name migration"
return 0
fi
log_info "Config file: $app_dir/$config_file"
log_info "Namespace: $namespace"
log_info "API Port: $api_port"
# Check current state
local has_global_processes=false
local has_namespaced_processes=false
local global_process_list=""
for proc in "${processes[@]}"; do
if process_exists_global "$proc"; then
has_global_processes=true
global_process_list="$global_process_list $proc"
fi
done
if namespace_has_processes "$namespace"; then
has_namespaced_processes=true
fi
log_info "Current state:"
log_info " - Non-namespaced processes:$global_process_list"
log_info " - Namespace '$namespace' has processes: $has_namespaced_processes"
# Determine migration action
if [ "$has_namespaced_processes" = true ] && [ "$has_global_processes" = false ]; then
log_success "$env_name is already using namespace '$namespace' - no migration needed"
echo ""
log_info "Current $env_name processes:"
pm2 list --namespace "$namespace"
return 0
fi
if [ "$has_global_processes" = false ] && [ "$has_namespaced_processes" = false ]; then
log_info "$env_name has no running processes - starting fresh with namespace"
if [ "$DRY_RUN" = true ]; then
log_info "[DRY-RUN] Would run:"
echo " cd $app_dir"
echo " pm2 start $config_file"
echo " pm2 save --namespace $namespace"
return 0
fi
cd "$app_dir"
log_progress "Starting processes with namespace..."
if pm2 start "$config_file"; then
log_progress "Saving PM2 state..."
pm2 save --namespace "$namespace"
log_success "$env_name started with namespace '$namespace'"
echo ""
log_info "Started processes:"
pm2 list --namespace "$namespace"
else
log_error "Failed to start $env_name processes"
show_rollback_instructions "$env_name" "$namespace" "$app_dir" "$config_file" "${processes[@]}"
return 1
fi
return 0
fi
# Migration required: global processes exist
log_info "Found non-namespaced processes - proceeding with migration"
if [ "$DRY_RUN" = true ]; then
log_info "[DRY-RUN] Migration steps for $env_name:"
echo ""
echo " Step 1: Stop old non-namespaced processes"
for proc in "${processes[@]}"; do
echo " pm2 stop $proc"
done
echo ""
echo " Step 2: Delete old non-namespaced processes"
for proc in "${processes[@]}"; do
echo " pm2 delete $proc"
done
echo ""
echo " Step 3: Save PM2 state to clear dump.pm2"
echo " pm2 save"
echo ""
echo " Step 4: Start with namespace (namespace defined in config file)"
echo " cd $app_dir && pm2 start $config_file"
echo ""
echo " Step 5: Wait for health check on port $api_port"
echo " curl http://localhost:$api_port/api/health"
echo ""
echo " Step 6: Save namespace state"
echo " pm2 save --namespace $namespace"
echo ""
return 0
fi
# Step 1: Stop old non-namespaced processes
log_progress "Step 1/5: Stopping old non-namespaced processes..."
for proc in "${processes[@]}"; do
if process_exists_global "$proc"; then
log_info " Stopping $proc..."
pm2 stop "$proc" 2>/dev/null || true
fi
done
# Brief pause to allow ports to be released
sleep 2
# Step 2: Delete old non-namespaced processes
log_progress "Step 2/5: Deleting old non-namespaced processes..."
for proc in "${processes[@]}"; do
if process_exists_global "$proc"; then
log_info " Deleting $proc..."
pm2 delete "$proc" 2>/dev/null || true
fi
done
# Step 3: Save to clear from dump.pm2
log_progress "Step 3/5: Saving PM2 state to clear dump.pm2..."
pm2 save
# Step 4: Start new namespaced processes
log_progress "Step 4/5: Starting processes with namespace..."
cd "$app_dir"
if ! pm2 start "$config_file"; then
log_error "Failed to start processes with namespace"
show_rollback_instructions "$env_name" "$namespace" "$app_dir" "$config_file" "${processes[@]}"
return 1
fi
# Step 5: Health check
log_progress "Step 5/5: Waiting for $env_name API to become healthy (port $api_port)..."
if check_health "$api_port"; then
log_success "$env_name API is healthy"
else
log_warn "$env_name API health check failed after ${HEALTH_CHECK_RETRIES} attempts"
log_warn "This may be expected if only worker processes are running"
log_warn "Continuing with migration..."
fi
# Save namespace state
log_progress "Saving PM2 state for namespace '$namespace'..."
pm2 save --namespace "$namespace"
log_success "$env_name migration complete"
echo ""
log_info "$env_name namespace processes:"
pm2 list --namespace "$namespace"
}
# Verify migration
verify_migration() {
local env_name="$1"
local namespace="$2"
local api_port="$3"
shift 3
local processes=("$@")
echo ""
log_info "Verifying $env_name migration..."
local all_ok=true
# Check namespace has processes
local process_count
process_count=$(get_namespace_process_count "$namespace")
if [ "$process_count" -eq 0 ]; then
log_warn " No processes found in namespace '$namespace'"
all_ok=false
else
log_success " Found $process_count processes in namespace '$namespace'"
fi
# Check no global processes remain
for proc in "${processes[@]}"; do
if process_exists_global "$proc"; then
log_warn " Non-namespaced process still exists: $proc"
all_ok=false
fi
done
if [ "$all_ok" = true ]; then
log_success " No orphaned non-namespaced processes"
fi
# Check health
echo -n " API health (http://localhost:$api_port/api/health): "
if curl -sf "http://localhost:$api_port/api/health" 2>/dev/null; then
echo ""
log_success " API is healthy"
else
echo -e "${YELLOW}UNAVAILABLE${NC}"
log_warn " API not responding (may be expected if not running)"
fi
return 0
}
# Main execution
main() {
echo "=============================================="
echo " PM2 Namespace Migration Script"
echo "=============================================="
echo ""
echo "This script migrates PM2 processes to use namespaces"
echo "for better isolation between test and production."
echo ""
if [ "$DRY_RUN" = true ]; then
log_warn "DRY-RUN MODE - No changes will be made"
echo ""
fi
# Check PM2 is available
if ! command -v pm2 &> /dev/null; then
log_error "PM2 is not installed or not in PATH"
exit 1
fi
# Show current PM2 state
log_step "Current PM2 State"
log_info "All PM2 processes:"
pm2 list 2>/dev/null || log_warn "No PM2 processes found"
# Check for existing namespaces
echo ""
log_info "Checking existing namespaces..."
if namespace_has_processes "$PROD_NAMESPACE"; then
local prod_count
prod_count=$(get_namespace_process_count "$PROD_NAMESPACE")
log_info " Production namespace ($PROD_NAMESPACE): $prod_count processes"
else
log_info " Production namespace ($PROD_NAMESPACE): no processes"
fi
if namespace_has_processes "$TEST_NAMESPACE"; then
local test_count
test_count=$(get_namespace_process_count "$TEST_NAMESPACE")
log_info " Test namespace ($TEST_NAMESPACE): $test_count processes"
else
log_info " Test namespace ($TEST_NAMESPACE): no processes"
fi
# Migrate test environment
if [ "$PROD_ONLY" = false ]; then
migrate_environment \
"Test" \
"$TEST_NAMESPACE" \
"$TEST_DIR" \
"ecosystem-test.config.cjs" \
"$TEST_PORT" \
"${TEST_PROCESSES[@]}"
fi
# Migrate production environment
if [ "$TEST_ONLY" = false ]; then
migrate_environment \
"Production" \
"$PROD_NAMESPACE" \
"$PROD_DIR" \
"ecosystem.config.cjs" \
"$PROD_PORT" \
"${PROD_PROCESSES[@]}"
fi
# Final verification
if [ "$DRY_RUN" = false ]; then
log_step "Final Verification"
if [ "$PROD_ONLY" = false ]; then
verify_migration "Test" "$TEST_NAMESPACE" "$TEST_PORT" "${TEST_PROCESSES[@]}"
fi
if [ "$TEST_ONLY" = false ]; then
verify_migration "Production" "$PROD_NAMESPACE" "$PROD_PORT" "${PROD_PROCESSES[@]}"
fi
fi
# Summary
log_step "Migration Summary"
if [ "$DRY_RUN" = true ]; then
log_info "Dry-run complete. Run without --dry-run to apply changes."
echo ""
echo "To execute the migration:"
echo " $0"
echo ""
echo "To migrate only test:"
echo " $0 --test-only"
echo ""
echo "To migrate only production:"
echo " $0 --prod-only"
else
log_success "Migration complete!"
echo ""
echo -e "${CYAN}Namespace Management Commands:${NC}"
echo ""
echo "List processes by namespace:"
echo " Production: pm2 list --namespace $PROD_NAMESPACE"
echo " Test: pm2 list --namespace $TEST_NAMESPACE"
echo ""
echo "Restart by namespace:"
echo " Production: pm2 restart all --namespace $PROD_NAMESPACE"
echo " Test: pm2 restart all --namespace $TEST_NAMESPACE"
echo ""
echo "Stop by namespace:"
echo " Production: pm2 stop all --namespace $PROD_NAMESPACE"
echo " Test: pm2 stop all --namespace $TEST_NAMESPACE"
echo ""
echo "View logs by namespace:"
echo " Production: pm2 logs --namespace $PROD_NAMESPACE"
echo " Test: pm2 logs --namespace $TEST_NAMESPACE"
echo ""
echo "Save state by namespace (IMPORTANT after any changes):"
echo " Production: pm2 save --namespace $PROD_NAMESPACE"
echo " Test: pm2 save --namespace $TEST_NAMESPACE"
fi
echo ""
echo "=============================================="
}
# Run main function
main