scripts/bash/orchestrate_reprocess.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
#
# Orchestrates reprocess_missing_feature_usage.sh across many customers
# with tiered time-window chunking, adaptive sleeps, and resume support.
#
# Prerequisites: same as reprocess_missing_feature_usage.sh + the CSV file
#
# Usage:
# source .env.backfill && ./orchestrate_reprocess.sh
#
set -euo pipefail
###############################################################################
# Configuration
###############################################################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPROCESS_SCRIPT="${SCRIPT_DIR}/reprocess_missing_feature_usage.sh"
CSV_FILE="${CSV_FILE:-/Users/nikhilmishra/Downloads/vapi-top-1000-cust-by-events-feb.csv}"
PROGRESS_FILE="${PROGRESS_FILE:-${SCRIPT_DIR}/reprocess_progress.log}"
# February 2026
MONTH_START="2026-02-01T00:00:00Z"
MONTH_END="2026-03-01T00:00:00Z"
# Reduced concurrency for safety — tuned to keep CH memory under 80GB
# KEY INSIGHT: Each API call creates 1 Temporal workflow. Workflows run concurrently
# on ClickHouse. With 200+ batches, we need to throttle enqueue rate so only ~5-10
# workflows are active at any time.
export BATCH_SIZE="${BATCH_SIZE:-10000}"
export API_CHUNK_SIZE="${API_CHUNK_SIZE:-2000}"
export API_PARALLEL="${API_PARALLEL:-2}"
export DRY_RUN="${DRY_RUN:-false}"
export SLEEP_BETWEEN_BATCHES="${SLEEP_BETWEEN_BATCHES:-10}"
# Sleep between time-window chunks within a customer (let Temporal drain)
CHUNK_SLEEP="${CHUNK_SLEEP:-60}"
# Abort thresholds
MAX_TEMPORAL_FAILURES="${MAX_TEMPORAL_FAILURES:-20}" # abort if >20 NEW failures in this run
# Customers to skip
SKIP_CUSTOMERS="${SKIP_CUSTOMERS:-UNKNOWN ORG}"
###############################################################################
# Helpers
###############################################################################
log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
# Graceful shutdown
SHUTDOWN=false
trap 'log "SIGINT received — finishing current customer then exiting."; SHUTDOWN=true' INT
trap 'log "SIGTERM received — finishing current customer then exiting."; SHUTDOWN=true' TERM
# Check if a customer+window is already completed in progress file
is_completed() {
local cust="$1" ws="$2" we="$3"
if [[ -f "$PROGRESS_FILE" ]]; then
grep -qF "${cust}|${ws}|${we}|OK" "$PROGRESS_FILE" 2>/dev/null
else
return 1
fi
}
# Record progress
record_progress() {
local cust="$1" ws="$2" we="$3" status="$4" info="$5"
printf '%s|%s|%s|%s|%s|%s\n' "$cust" "$ws" "$we" "$status" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$info" >> "$PROGRESS_FILE"
}
# Determine tier for a customer based on event count
# Returns: chunk_days|sleep_after
get_tier() {
local events="$1"
if (( events > 10000000 )); then echo "2|120"
elif (( events > 2000000 )); then echo "7|60"
elif (( events > 500000 )); then echo "14|20"
else echo "28|5"
fi
}
# Generate ISO-8601 date windows for February 2026
# Args: chunk_days
# Outputs: lines of "start_date|end_date"
generate_windows() {
local chunk_days="$1"
# February 2026 has 28 days
local day=1
while (( day <= 28 )); do
local end_day=$((day + chunk_days))
if (( end_day > 28 )); then
end_day=29 # March 1
fi
local ws we
ws=$(printf '2026-02-%02dT00:00:00Z' "$day")
if (( end_day <= 28 )); then
we=$(printf '2026-02-%02dT00:00:00Z' "$end_day")
else
we="2026-03-01T00:00:00Z"
fi
echo "${ws}|${we}"
day=$end_day
done
}
###############################################################################
# Validation
###############################################################################
if [[ ! -f "$CSV_FILE" ]]; then
log "ERROR: CSV file not found: $CSV_FILE"
exit 1
fi
if [[ ! -f "$REPROCESS_SCRIPT" ]]; then
log "ERROR: Reprocess script not found: $REPROCESS_SCRIPT"
exit 1
fi
if [[ ! -x "$REPROCESS_SCRIPT" ]]; then
chmod +x "$REPROCESS_SCRIPT"
fi
for cmd in curl jq; do
if ! command -v "$cmd" &>/dev/null; then
log "ERROR: '$cmd' is required but not installed."
exit 1
fi
done
###############################################################################
# Load customers from CSV
###############################################################################
CUSTOMERS=()
CUSTOMER_EVENTS=()
while IFS=',' read -r cust_id events; do
# Skip header
[[ "$cust_id" == "external_customer_id" ]] && continue
# Skip empty lines
[[ -z "$cust_id" ]] && continue
# Skip configured customers
if echo "$SKIP_CUSTOMERS" | grep -qF "$cust_id"; then
log "Skipping customer: $cust_id (in SKIP_CUSTOMERS)"
continue
fi
CUSTOMERS+=("$cust_id")
CUSTOMER_EVENTS+=("$events")
done < "$CSV_FILE"
total_customers=${#CUSTOMERS[@]}
log "Loaded ${total_customers} customers from CSV (already sorted largest-first)"
###############################################################################
# Banner
###############################################################################
log "================================================================"
log " Orchestrated Reprocess — Missing Feature Usage"
log "================================================================"
log " Customers: ${total_customers}"
log " Date range: ${MONTH_START} -> ${MONTH_END}"
log " Batch size: ${BATCH_SIZE}"
log " API chunk: ${API_CHUNK_SIZE} (x${API_PARALLEL} parallel)"
log " Dry run: ${DRY_RUN}"
log " Progress file: ${PROGRESS_FILE}"
log "================================================================"
###############################################################################
# Temporal failure tracking
###############################################################################
# Record the baseline failure count at start (from prior runs)
TEMPORAL_BASELINE_FAILURES=0
if command -v temporal &>/dev/null && [[ -n "${FLEXPRICE_TEMPORAL_ADDRESS:-}" ]]; then
TEMPORAL_BASELINE_FAILURES=$(
TEMPORAL_ADDRESS="$FLEXPRICE_TEMPORAL_ADDRESS" \
TEMPORAL_NAMESPACE="$FLEXPRICE_TEMPORAL_NAMESPACE" \
TEMPORAL_API_KEY="$FLEXPRICE_TEMPORAL_API_KEY" \
TEMPORAL_TLS=true \
temporal workflow count \
--query "WorkflowType='ReprocessRawEventsWorkflow' AND ExecutionStatus='Failed'" \
2>/dev/null | sed -n 's/Total: //p' || echo "0"
)
log "Temporal baseline failures: ${TEMPORAL_BASELINE_FAILURES}"
fi
check_temporal_failures() {
if ! command -v temporal &>/dev/null || [[ -z "${FLEXPRICE_TEMPORAL_ADDRESS:-}" ]]; then
return 0 # skip check if no temporal CLI
fi
local current_failures
current_failures=$(
TEMPORAL_ADDRESS="$FLEXPRICE_TEMPORAL_ADDRESS" \
TEMPORAL_NAMESPACE="$FLEXPRICE_TEMPORAL_NAMESPACE" \
TEMPORAL_API_KEY="$FLEXPRICE_TEMPORAL_API_KEY" \
TEMPORAL_TLS=true \
temporal workflow count \
--query "WorkflowType='ReprocessRawEventsWorkflow' AND ExecutionStatus='Failed'" \
2>/dev/null | sed -n 's/Total: //p' || echo "0"
)
local new_failures=$((current_failures - TEMPORAL_BASELINE_FAILURES))
if (( new_failures > MAX_TEMPORAL_FAILURES )); then
log "ABORT: ${new_failures} new Temporal failures (threshold: ${MAX_TEMPORAL_FAILURES})"
log " Total failures: ${current_failures} (baseline was ${TEMPORAL_BASELINE_FAILURES})"
return 1
fi
if (( new_failures > 0 )); then
log " Temporal: ${new_failures} new failures so far (threshold: ${MAX_TEMPORAL_FAILURES})"
fi
return 0
}
###############################################################################
# Main loop
###############################################################################
global_start=$(date +%s)
customers_done=0
customers_skipped=0
customers_failed=0
total_windows_done=0
for cust_idx in "${!CUSTOMERS[@]}"; do
cust_id="${CUSTOMERS[$cust_idx]}"
if [[ "$SHUTDOWN" == "true" ]]; then
log "Shutdown requested. Stopping after ${customers_done} customers."
break
fi
events="${CUSTOMER_EVENTS[$cust_idx]}"
tier_info=$(get_tier "$events")
chunk_days="${tier_info%%|*}"
sleep_after="${tier_info##*|}"
customers_done=$((customers_done + 1))
log ""
log "============================================================"
log " Customer ${customers_done}/${total_customers}: ${cust_id}"
log " Events: ${events} | Chunk: ${chunk_days}-day | Sleep after: ${sleep_after}s"
log "============================================================"
# Generate time windows for this customer
windows=()
while IFS= read -r w; do
windows+=("$w")
done < <(generate_windows "$chunk_days")
all_windows_ok=true
windows_completed=0
windows_skipped=0
for window in "${windows[@]}"; do
if [[ "$SHUTDOWN" == "true" ]]; then
log "Shutdown requested during customer ${cust_id}. Stopping."
break 2
fi
ws="${window%%|*}"
we="${window##*|}"
# Check resume
if is_completed "$cust_id" "$ws" "$we"; then
windows_skipped=$((windows_skipped + 1))
continue
fi
log " Window: ${ws} -> ${we}"
# Run the reprocess script (no pipe — avoids SIGPIPE / exit 141)
window_log=$(mktemp)
set +e
EXTERNAL_CUSTOMER_ID="$cust_id" \
START_DATE="$ws" \
END_DATE="$we" \
BATCH_SIZE="$BATCH_SIZE" \
API_CHUNK_SIZE="$API_CHUNK_SIZE" \
API_PARALLEL="$API_PARALLEL" \
DRY_RUN="$DRY_RUN" \
SLEEP_BETWEEN_BATCHES="$SLEEP_BETWEEN_BATCHES" \
"$REPROCESS_SCRIPT" > "$window_log" 2>&1
exit_code=$?
set -e
# Show key lines from the log (summary only)
if [[ -f "$window_log" ]]; then
grep -E '(missing event IDs found|Sending|Done!|Total events|API successes|API failures|Nothing to do|Speed:.*API:)' "$window_log" \
| while IFS= read -r line; do printf ' %s\n' "$line"; done
fi
rm -f "$window_log"
if [[ "$exit_code" -eq 0 ]]; then
record_progress "$cust_id" "$ws" "$we" "OK" "exit=0"
windows_completed=$((windows_completed + 1))
total_windows_done=$((total_windows_done + 1))
else
record_progress "$cust_id" "$ws" "$we" "FAIL" "exit=${exit_code}"
log " WARNING: Window ${ws}-${we} failed (exit=${exit_code}). Continuing."
all_windows_ok=false
fi
# Sleep between chunks (skip if last window)
last_window="${windows[${#windows[@]}-1]}"
if [[ "$window" != "$last_window" ]]; then
sleep "$CHUNK_SLEEP"
# Check Temporal failures between windows
if ! check_temporal_failures; then
log "Aborting due to excessive Temporal failures."
SHUTDOWN=true
break 2
fi
fi
done
if [[ "$windows_skipped" -gt 0 ]]; then
log " Skipped ${windows_skipped} already-completed windows"
fi
if [[ "$all_windows_ok" == "true" ]]; then
log " Customer ${cust_id}: ALL windows OK (${windows_completed} completed, ${windows_skipped} skipped)"
else
log " Customer ${cust_id}: Some windows FAILED"
customers_failed=$((customers_failed + 1))
fi
# Progress + ETA
now=$(date +%s)
elapsed=$((now - global_start))
if (( elapsed > 0 && customers_done > 0 )); then
remaining_customers=$((total_customers - customers_done))
avg_per_customer=$((elapsed / customers_done))
eta_seconds=$((remaining_customers * avg_per_customer))
eta_hours=$((eta_seconds / 3600))
eta_min=$(( (eta_seconds % 3600) / 60 ))
log " Progress: ${customers_done}/${total_customers} | Elapsed: $((elapsed/3600))h$(( (elapsed%3600)/60 ))m | ETA: ~${eta_hours}h${eta_min}m"
fi
# Adaptive sleep between customers
if [[ "$customers_done" -lt "$total_customers" && "$SHUTDOWN" != "true" ]]; then
log " Sleeping ${sleep_after}s before next customer ..."
sleep "$sleep_after"
# Check Temporal failures between customers
if ! check_temporal_failures; then
log "Aborting due to excessive Temporal failures."
SHUTDOWN=true
fi
fi
done
###############################################################################
# Summary
###############################################################################
end_epoch=$(date +%s)
total_elapsed=$((end_epoch - global_start))
total_hours=$((total_elapsed / 3600))
total_min=$(( (total_elapsed % 3600) / 60 ))
total_sec=$((total_elapsed % 60))
log ""
log "================================================================"
log " ORCHESTRATION COMPLETE"
log "================================================================"
log " Customers processed: ${customers_done} / ${total_customers}"
log " Customers failed: ${customers_failed}"
log " Total windows: ${total_windows_done}"
log " Total time: ${total_hours}h ${total_min}m ${total_sec}s"
log " Progress file: ${PROGRESS_FILE}"
if [[ "$SHUTDOWN" == "true" ]]; then
log " NOTE: Stopped early due to signal. Re-run to resume."
fi
log "================================================================"