- 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.
530 lines
16 KiB
Bash
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
|