scripts/bash/copy_raw_events_env.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------
# USER CONFIG
# ---------------------------
# Copy raw_events from one environment_id to another (same table).
# Source rows: environment_id = SRC_ENV_ID
# Inserted rows: same data but environment_id = DST_ENV_ID (for dual-write / migration).
CH_HOST="${CH_HOST:-127.0.0.1}"
CH_PORT="${CH_PORT:-9000}"
CH_USER="${CH_USER:-default}"
CH_PASSWORD="${CH_PASSWORD:-}"
CH_DB="${CH_DB:-flexprice}"
TABLE="${TABLE:-raw_events}"
TS_COL="${TS_COL:-timestamp}"
# Source environment (rows we read from)
SRC_ENV_ID="${SRC_ENV_ID:-env_01KF5GXB8X2TYQHSVE4YVZYCN8}"
# Destination environment (value we write into the new rows)
DST_ENV_ID="${DST_ENV_ID:-env_01KG4E6FR5YCNW0742N6CA1YD1}"
# Optional: restrict to one tenant (uses primary key, much more efficient)
TENANT_ID="${TENANT_ID:-}"
# Timezone for day boundaries (must match how timestamp is stored; default UTC)
TIMEZONE="${TIMEZONE:-UTC}"
# Brief sleep after INSERT before verification count (ReplacingMergeTree parts visibility)
VERIFY_SLEEP_SEC="${VERIFY_SLEEP_SEC:-5}"
START_DATE="${START_DATE:-2026-01-01}"
END_DATE_EXCL="${END_DATE_EXCL:-2026-01-29}"
PARALLEL="${PARALLEL:-10}"
MAX_RETRIES="${MAX_RETRIES:-8}"
BASE_BACKOFF_SEC="${BASE_BACKOFF_SEC:-5}"
CONNECT_TIMEOUT_SEC="${CONNECT_TIMEOUT_SEC:-10}"
SEND_TIMEOUT_SEC="${SEND_TIMEOUT_SEC:-30}"
RECEIVE_TIMEOUT_SEC="${RECEIVE_TIMEOUT_SEC:-30}"
MAX_EXEC_TIME="${MAX_EXEC_TIME:-0}"
LOG_DIR="${LOG_DIR:-./logs/copy_raw_events_env}"
mkdir -p "$LOG_DIR"
# ---------------------------
# CLICKHOUSE CLIENT WRAPPER
# ---------------------------
ch() {
clickhouse client \
--host "$CH_HOST" --port "$CH_PORT" \
--user "$CH_USER" --password "$CH_PASSWORD" \
--database "$CH_DB" \
--connect_timeout "$CONNECT_TIMEOUT_SEC" \
--send_timeout "$SEND_TIMEOUT_SEC" \
--receive_timeout "$RECEIVE_TIMEOUT_SEC" \
--multiquery \
--format=TSV \
"$@"
}
# ---------------------------
# HELPERS
# ---------------------------
# macOS: install coreutils and use gdate. Linux: can use date -d.
date_add() { gdate -d "$1 +1 day" +"%Y-%m-%d"; }
# Optional tenant filter fragment (uses primary key when set)
tenant_filter() {
if [[ -n "${TENANT_ID:-}" ]]; then
echo "AND tenant_id = '${TENANT_ID}'"
else
echo ""
fi
}
# Day bounds in configured timezone (consistent with how timestamp is stored)
day_where() {
local day="$1"
echo "${TS_COL} >= toDateTime64('${day} 00:00:00', 3, '${TIMEZONE}')
AND ${TS_COL} < toDateTime64('${day} 00:00:00', 3, '${TIMEZONE}') + INTERVAL 1 DAY"
}
# Check if destination env already has rows for this day (idempotency)
# Use FINAL to get deduplicated count from ReplacingMergeTree
exists_day_in_dst() {
local day="$1"
local tf
tf="$(tenant_filter)"
local dw
dw="$(day_where "$day")"
ch --query "
SELECT count()
FROM ${TABLE} FINAL
WHERE environment_id = '${DST_ENV_ID}'
AND ${dw}
${tf}
" 2>/dev/null | tr -d '\r'
}
copy_day() {
local day="$1"
local log="$LOG_DIR/${day}.log"
echo "[$(gdate -Iseconds)] Starting ${day} (${SRC_ENV_ID} -> ${DST_ENV_ID})" | tee -a "$log"
local already
already="$(exists_day_in_dst "$day" || echo "0")"
already="$(echo "$already" | tr -d '\r\n ')"
echo "[$(gdate -Iseconds)] DEBUG: already='$already' for ${day}" | tee -a "$log"
if [[ -n "$already" && "$already" != "0" ]]; then
echo "[$(gdate -Iseconds)] SKIP ${day} (dst env already has ${already} rows)" | tee -a "$log"
return 0
fi
# Check how many rows exist in source env for this day (debug)
local dw
dw="$(day_where "$day")"
local tf
tf="$(tenant_filter)"
local src_count
src_count="$(ch --query "
SELECT count()
FROM ${TABLE}
WHERE environment_id = '${SRC_ENV_ID}'
AND ${dw}
${tf}
" 2>/dev/null | tr -d '\r\n ')"
echo "[$(gdate -Iseconds)] Source rows for ${day}: ${src_count}" | tee -a "$log"
if [[ "${src_count}" == "0" ]]; then
echo "[$(gdate -Iseconds)] SKIP ${day} (no source rows)" | tee -a "$log"
return 0
fi
# Test: verify basic SELECT works
echo "[$(gdate -Iseconds)] Testing: Can we select rows with LIMIT 10?" | tee -a "$log"
local test_basic
test_basic="$(ch --query "
SELECT count()
FROM ${TABLE}
WHERE environment_id = '${SRC_ENV_ID}'
AND ${dw}
${tf}
LIMIT 10
" 2>&1 | tee -a "$log" | tail -1 | tr -d '\r\n ')"
echo "[$(gdate -Iseconds)] Basic SELECT LIMIT 10 returned: ${test_basic}" | tee -a "$log"
# Test: verify we can select specific columns
echo "[$(gdate -Iseconds)] Testing: Can we select specific columns?" | tee -a "$log"
local test_columns
test_columns="$(ch --query "
SELECT count()
FROM (
SELECT id, tenant_id, timestamp
FROM ${TABLE}
WHERE environment_id = '${SRC_ENV_ID}'
AND ${dw}
${tf}
LIMIT 10
)
" 2>&1 | tee -a "$log" | tail -1 | tr -d '\r\n ')"
echo "[$(gdate -Iseconds)] Column SELECT returned: ${test_columns}" | tee -a "$log"
local attempt=1
while (( attempt <= MAX_RETRIES )); do
echo "[$(gdate -Iseconds)] Attempt ${attempt}/${MAX_RETRIES} for ${day}" | tee -a "$log"
# Insert into same table: select from source env, replace environment_id with dest env.
# Explicit column list to set environment_id to DST_ENV_ID.
# Build the query as a variable for debugging
# NOTE: We use a subquery to avoid ClickHouse optimization issues when overriding environment_id.
# We explicitly list all columns EXCEPT version, which will use DEFAULT value.
# This ensures copied rows get a new version timestamp and are treated as newer by ReplacingMergeTree.
local query="INSERT INTO ${TABLE} (
id,
tenant_id,
environment_id,
external_customer_id,
event_name,
source,
payload,
field1,
field2,
field3,
field4,
field5,
field6,
field7,
field8,
field9,
field10,
timestamp,
ingested_at,
sign
)
SELECT
id,
tenant_id,
'${DST_ENV_ID}' AS environment_id,
external_customer_id,
event_name,
source,
payload,
field1,
field2,
field3,
field4,
field5,
field6,
field7,
field8,
field9,
field10,
timestamp,
ingested_at,
sign
FROM (
SELECT
id,
tenant_id,
environment_id,
external_customer_id,
event_name,
source,
payload,
field1,
field2,
field3,
field4,
field5,
field6,
field7,
field8,
field9,
field10,
timestamp,
ingested_at,
sign
FROM ${TABLE}
WHERE environment_id = '${SRC_ENV_ID}'
AND ${dw}
${tf}
)
SETTINGS
max_execution_time = ${MAX_EXEC_TIME},
max_threads = 0,
max_insert_threads = 0"
echo "[$(gdate -Iseconds)] Executing query:" | tee -a "$log"
echo "$query" >> "$log"
echo "---" >> "$log"
local insert_result
insert_result=$(echo "$query" | ch 2>&1)
local insert_exit=$?
echo "$insert_result" >> "$log"
echo "[$(gdate -Iseconds)] INSERT exit code: $insert_exit" | tee -a "$log"
if [[ $insert_exit -eq 0 ]]; then
[[ -n "${VERIFY_SLEEP_SEC:-}" && "${VERIFY_SLEEP_SEC:-0}" -gt 0 ]] && sleep "${VERIFY_SLEEP_SEC}"
local cnt
cnt="$(exists_day_in_dst "$day" || echo "0")"
echo "[$(gdate -Iseconds)] DONE ${day} (dst env rows=${cnt})" | tee -a "$log"
return 0
else
echo "[$(gdate -Iseconds)] INSERT failed with output above" | tee -a "$log"
fi
local sleep_for=$(( BASE_BACKOFF_SEC * attempt ))
echo "[$(gdate -Iseconds)] FAIL ${day} attempt ${attempt}. Sleeping ${sleep_for}s then retry..." | tee -a "$log"
sleep "$sleep_for"
attempt=$(( attempt + 1 ))
done
echo "[$(gdate -Iseconds)] ERROR: Giving up on ${day} after ${MAX_RETRIES} attempts" | tee -a "$log"
return 1
}
export -f copy_day
export -f exists_day_in_dst
export -f ch
export -f date_add
export -f tenant_filter
export -f day_where
# ---------------------------
# MAIN: build list of days and run parallel
# ---------------------------
days=()
d="$START_DATE"
while [[ "$d" != "$END_DATE_EXCL" ]]; do
days+=("$d")
d="$(date_add "$d")"
done
echo "Copy raw_events: ${SRC_ENV_ID} -> ${DST_ENV_ID}"
[[ -n "${TENANT_ID:-}" ]] && echo "Tenant filter: ${TENANT_ID}"
echo "Day bounds timezone: ${TIMEZONE}"
echo "Will process ${#days[@]} days from ${START_DATE} to $(gdate -d "${END_DATE_EXCL} -1 day" +%Y-%m-%d 2>/dev/null || echo "${END_DATE_EXCL}(exclusive)")"
echo "Parallelism: ${PARALLEL}"
echo "Logs: ${LOG_DIR}"
export PATH
export CH_HOST CH_PORT CH_USER CH_PASSWORD CH_DB
export TABLE TS_COL SRC_ENV_ID DST_ENV_ID
export TENANT_ID TIMEZONE VERIFY_SLEEP_SEC
export MAX_RETRIES BASE_BACKOFF_SEC
export CONNECT_TIMEOUT_SEC SEND_TIMEOUT_SEC RECEIVE_TIMEOUT_SEC MAX_EXEC_TIME
export LOG_DIR
if command -v parallel &> /dev/null; then
export -f ch exists_day_in_dst date_add copy_day tenant_filter day_where
printf "%s\n" "${days[@]}" | parallel -j "$PARALLEL" copy_day \
1> "$LOG_DIR/run.stdout.log" 2> "$LOG_DIR/run.stderr.log"
else
for day in "${days[@]}"; do
copy_day "$day"
done
fi
echo "All done. Check $LOG_DIR for per-day logs."
: '
==================================================
USAGE EXAMPLES
==================================================
Copy raw_events from env_01KF5GXB8X2TYQHSVE4YVZYCN8 to env_01KG4E6FR5YCNW0742N6CA1YD1
for a single day:
source .env.backfill && START_DATE=2026-01-29 END_DATE_EXCL=2026-01-30 ./copy_raw_events_env.sh
For a date range (per-day, parallel):
source .env.backfill && START_DATE=2026-01-01 END_DATE_EXCL=2026-01-30 ./copy_raw_events_env.sh
With tenant filter (uses primary key, more efficient):
source .env.backfill && TENANT_ID=tenant_xxx START_DATE=2026-01-29 END_DATE_EXCL=2026-01-30 ./copy_raw_events_env.sh
Custom env IDs:
source .env.backfill && SRC_ENV_ID=env_old DST_ENV_ID=env_new START_DATE=2026-01-01 END_DATE_EXCL=2026-02-01 ./copy_raw_events_env.sh
If timestamps are stored in a specific timezone (default UTC):
source .env.backfill && TIMEZONE=Asia/Kolkata START_DATE=2026-01-01 END_DATE_EXCL=2026-01-30 ./copy_raw_events_env.sh
Lower parallelism for very large days:
source .env.backfill && PARALLEL=4 START_DATE=2026-01-01 END_DATE_EXCL=2026-01-30 ./copy_raw_events_env.sh
==================================================
CONFIGURABLE ENVIRONMENT VARIABLES
==================================================
ClickHouse: CH_HOST, CH_PORT, CH_USER, CH_PASSWORD, CH_DB
Table: TABLE (default: raw_events), TS_COL (default: timestamp)
Envs: SRC_ENV_ID (source), DST_ENV_ID (destination)
Filter: TENANT_ID (optional; when set, uses primary key for efficiency)
Timezone: TIMEZONE (day boundaries; default UTC; set if timestamp is stored in another TZ)
Dates: START_DATE, END_DATE_EXCL (exclusive)
Execution: PARALLEL, MAX_RETRIES, BASE_BACKOFF_SEC, MAX_EXEC_TIME, LOG_DIR
VERIFY_SLEEP_SEC (seconds to wait after INSERT before count; default 2)
'