#!/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