FIX: Critical subshell scope bug - CRON_OFFSET not persisting across iterations

The staggered cron scheduling was completely broken due to bash subshell scope
issue. The pattern was:
    cron_time=$(generate_staggered_cron)  # Creates subshell!

This caused CRON_OFFSET to increment in the subshell but not persist to the
parent shell, resulting in ALL 200 sites getting cron time 0 * * * *.

BEFORE (broken):
    All 200 sites → 0 * * * * (massive load spikes!)

AFTER (fixed):
    Sites distributed as: 0, 3, 6, 9, 12, ... 57 (repeats)
    200 sites: 10 sites per time slot (perfect distribution)

Solution: Changed from command substitution to global variable approach:
    - generate_staggered_cron now sets LAST_CRON_TIME instead of echo
    - Callers read $LAST_CRON_TIME after function call
    - CRON_OFFSET increments now properly persist across loop iterations

Fixed three locations:
    - Option 2: disable for domain
    - Option 3: disable for user
    - Option 4: disable server-wide

All 200 sites will now run with proper load distribution across the hour.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
cschantz
2026-03-02 20:54:12 -05:00
parent b66f40446e
commit 7034f7b797
@@ -201,6 +201,9 @@ BATCH_MODE=false
# Global counter for staggering cron times
CRON_OFFSET=0
# Global variable to hold generated cron time (avoids subshell scope issues)
declare -g LAST_CRON_TIME=""
# PERFORMANCE OPTIMIZATION: Global cache for find results
# Instead of running find 23 times, run once and reuse results
declare -g WP_SITES_CACHE=""
@@ -1421,6 +1424,7 @@ generate_staggered_cron() {
# CRITICAL: Stagger cron times to prevent all WordPress sites from running at once
# This prevents server load spikes from concurrent execution
# Each site gets a different minute within the hour, spread across 60 minutes
# NOTE: Uses global variable LAST_CRON_TIME instead of echo to avoid subshell issues
# Calculate staggered minute (0-59) - each site gets a different minute
local minute=$((CRON_OFFSET % 60))
@@ -1431,12 +1435,12 @@ generate_staggered_cron() {
local stagger_minute=$((minute * 3))
stagger_minute=$((stagger_minute % 60))
# Increment offset for next site
# Increment offset for next site (THIS PERSISTS in parent shell, not in subshell)
CRON_OFFSET=$((CRON_OFFSET + 1))
# Return cron schedule: run every hour at the staggered minute
# This ensures each WordPress site runs at a different time to prevent load spikes
echo "$stagger_minute * * * *"
# Set global variable instead of using echo (avoids subshell scope issue)
# This ensures CRON_OFFSET increments persist across loop iterations
LAST_CRON_TIME="$stagger_minute * * * *"
}
# Function to extract user from WordPress site path
@@ -1968,7 +1972,8 @@ case "$choice" in
fi
# Generate staggered cron time and add to crontab
cron_time=$(generate_staggered_cron)
generate_staggered_cron # Sets LAST_CRON_TIME globally (avoids subshell)
cron_time="$LAST_CRON_TIME"
if safe_add_cron_job "$user" "$cron_time" "$cron_cmd"; then
echo -e "${GREEN}${NC} Added cron job ($cron_time)"
else
@@ -2091,7 +2096,8 @@ case "$choice" in
fi
if ! cron_job_exists "$user" "$site_path"; then
cron_time=$(generate_staggered_cron)
generate_staggered_cron # Sets LAST_CRON_TIME globally (avoids subshell)
cron_time="$LAST_CRON_TIME"
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN] Would add cron job ($cron_time)"
else
@@ -2212,7 +2218,8 @@ case "$choice" in
cron_cmd=$(build_cron_command "$site_path")
if ! cron_job_exists "$user" "$site_path"; then
cron_time=$(generate_staggered_cron)
generate_staggered_cron # Sets LAST_CRON_TIME globally (avoids subshell)
cron_time="$LAST_CRON_TIME"
if [ "$DRY_RUN" = "true" ]; then
echo " [DRY-RUN] Would add cron job ($cron_time)"
else