Skip to content

Commit af17958

Browse files
alexluongclaude
andauthored
fix: use redis pipeline in migration script for performance (#830)
The migration script was spawning a new redis-cli process per command, each requiring a full TLS handshake. On remote Redis instances this caused ~1s overhead per command, making the migration take minutes. Batch all writes into a single redis-cli --pipe call. Discovery and data gathering still use individual commands (needed for logic), but the actual migration executes in one round trip. Co-authored-by: Claude <noreply@anthropic.com>
1 parent c165a5a commit af17958

1 file changed

Lines changed: 56 additions & 68 deletions

File tree

scripts/issue-680/migrate.sh

Lines changed: 56 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
# 2. For each deployment ID:
1111
# a. Copies outpostrc hash → {id}:outpost:installation_id (string key)
1212
# b. Copies outpost:migration:{name} hashes → {id}:outpost:migration:{name}
13-
# c. Copies outpost:migration:{name}:run:* hashes → {id}:outpost:migration:{name}:run:*
1413
# 3. Optionally deletes the old shared keys (with --cleanup flag)
1514
#
15+
# All writes are batched into a single redis-cli --pipe call for performance.
16+
# This avoids per-command TLS handshake overhead on remote Redis instances.
17+
#
1618
# Usage:
1719
# # Dry run (default) - shows what would be done
1820
# ./migrate.sh
@@ -65,15 +67,21 @@ rcli() {
6567
redis-cli "${args[@]}" "$@"
6668
}
6769

70+
rcli_pipe() {
71+
local args=(-h "$REDIS_HOST" -p "$REDIS_PORT" --no-auth-warning --pipe)
72+
[[ -n "$REDIS_USER" ]] && args+=(--user "$REDIS_USER")
73+
[[ -n "$REDIS_PASS" ]] && args+=(--pass "$REDIS_PASS")
74+
[[ "$REDIS_TLS" == "1" ]] && args+=(--tls)
75+
redis-cli "${args[@]}"
76+
}
77+
6878
# --- Helpers ---
6979
log() { echo "[INFO] $*"; }
7080
warn() { echo "[WARN] $*" >&2; }
71-
dry() { if $DRY_RUN; then echo "[DRY] $*"; else echo "[EXEC] $*"; fi; }
7281

7382
# --- Step 1: Discover deployment IDs ---
7483
log "Discovering deployment IDs..."
7584

76-
# Scan for keys matching *:tenant:* and extract unique prefixes
7785
DEPLOYMENT_IDS=()
7886
cursor=0
7987
while true; do
@@ -82,7 +90,6 @@ while true; do
8290
keys=$(echo "$result" | tail -n +2)
8391

8492
for key in $keys; do
85-
# Extract deployment ID (everything before first ":tenant:")
8693
id="${key%%:tenant:*}"
8794
if [[ -n "$id" && "$id" != "$key" ]]; then
8895
DEPLOYMENT_IDS+=("$id")
@@ -108,15 +115,15 @@ log "Found ${#DEPLOYMENT_IDS[@]} deployment(s): ${DEPLOYMENT_IDS[*]}"
108115
log ""
109116
log "Checking shared control plane keys..."
110117

111-
# Check outpostrc
112118
INSTALLATION_ID=$(rcli HGET outpostrc installation 2>/dev/null || echo "")
113119
if [[ -n "$INSTALLATION_ID" ]]; then
114120
log " outpostrc -> installation = $INSTALLATION_ID"
115121
else
116122
log " outpostrc -> (not found, each deployment will generate its own on startup)"
117123
fi
118124

119-
# Find all outpost:migration:* keys (excluding run history)
125+
# Collect migration keys and their hash data
126+
declare -A MIGRATION_DATA
120127
MIGRATION_KEYS=()
121128
cursor=0
122129
while true; do
@@ -126,6 +133,7 @@ while true; do
126133

127134
for key in $keys; do
128135
MIGRATION_KEYS+=("$key")
136+
MIGRATION_DATA["$key"]=$(rcli HGETALL "$key" 2>/dev/null | tr '\n' ' ')
129137
done
130138

131139
if [[ "$cursor" == "0" ]]; then
@@ -139,96 +147,76 @@ for key in "${MIGRATION_KEYS[@]}"; do
139147
log " $key -> $status"
140148
done
141149

142-
# Check lock key
143150
LOCK_EXISTS=$(rcli EXISTS ".outpost:migration:lock" 2>/dev/null || echo "0")
144151
if [[ "$LOCK_EXISTS" == "1" ]]; then
145152
warn " .outpost:migration:lock exists! A migration may be running."
146153
fi
147154

148-
# --- Step 3: Migrate for each deployment ---
155+
# --- Step 3: Build pipeline commands ---
149156
log ""
150-
log "=== Starting migration ==="
157+
log "=== Building migration commands ==="
151158

152-
for DEPLOY_ID in "${DEPLOYMENT_IDS[@]}"; do
153-
log ""
154-
log "--- Deployment: $DEPLOY_ID ---"
159+
PIPE_FILE=$(mktemp)
160+
CMD_COUNT=0
155161

156-
# 3a. Installation ID
162+
for DEPLOY_ID in "${DEPLOYMENT_IDS[@]}"; do
163+
# Installation ID (SET is idempotent)
157164
if [[ -n "$INSTALLATION_ID" ]]; then
158165
target_key="${DEPLOY_ID}:outpost:installation_id"
159-
existing=$(rcli GET "$target_key" 2>/dev/null || echo "")
160-
if [[ -n "$existing" ]]; then
161-
log " $target_key already exists ($existing), skipping"
162-
else
163-
dry " SET $target_key $INSTALLATION_ID"
164-
if ! $DRY_RUN; then
165-
rcli SET "$target_key" "$INSTALLATION_ID" > /dev/null
166-
fi
167-
fi
166+
echo "SET ${target_key} ${INSTALLATION_ID}" >> "$PIPE_FILE"
167+
((CMD_COUNT++))
168168
fi
169169

170-
# 3b. Migration status keys
170+
# Migration status keys (HSET is idempotent)
171171
for old_key in "${MIGRATION_KEYS[@]}"; do
172-
# Derive the new key: outpost:migration:X -> {id}:outpost:migration:X
173172
new_key="${DEPLOY_ID}:${old_key}"
174-
175-
existing=$(rcli EXISTS "$new_key" 2>/dev/null || echo "0")
176-
if [[ "$existing" == "1" ]]; then
177-
log " $new_key already exists, skipping"
178-
continue
179-
fi
180-
181-
# Copy the hash field by field
182-
dry " COPY $old_key -> $new_key"
183-
if ! $DRY_RUN; then
184-
# Read all field-value pairs and write them to the new key
185-
hset_args=()
186-
while IFS= read -r field && IFS= read -r value; do
187-
hset_args+=("$field" "$value")
188-
done < <(rcli HGETALL "$old_key" 2>/dev/null)
189-
190-
if [[ ${#hset_args[@]} -gt 0 ]]; then
191-
rcli HSET "$new_key" "${hset_args[@]}" > /dev/null
192-
else
193-
warn " $old_key has no fields, skipping"
194-
fi
195-
196-
# Verify the copy
197-
if [[ $(rcli EXISTS "$new_key" 2>/dev/null) != "1" ]]; then
198-
warn " FAILED to copy $old_key -> $new_key"
199-
fi
173+
hash_data="${MIGRATION_DATA[$old_key]}"
174+
if [[ -n "$hash_data" ]]; then
175+
echo "HSET ${new_key} ${hash_data}" >> "$PIPE_FILE"
176+
((CMD_COUNT++))
200177
fi
201178
done
202179
done
203180

204-
# --- Step 4: Cleanup (optional) ---
205-
if $CLEANUP && ! $DRY_RUN; then
206-
log ""
207-
log "=== Cleaning up old shared keys ==="
208-
181+
# Cleanup commands
182+
if $CLEANUP; then
209183
if [[ -n "$INSTALLATION_ID" ]]; then
210-
log " DEL outpostrc"
211-
rcli DEL outpostrc > /dev/null
184+
echo "DEL outpostrc" >> "$PIPE_FILE"
185+
((CMD_COUNT++))
212186
fi
213-
214187
for key in "${MIGRATION_KEYS[@]}"; do
215-
log " DEL $key"
216-
rcli DEL "$key" > /dev/null
188+
echo "DEL ${key}" >> "$PIPE_FILE"
189+
((CMD_COUNT++))
217190
done
218-
219191
if [[ "$LOCK_EXISTS" == "1" ]]; then
220-
log " DEL .outpost:migration:lock"
221-
rcli DEL ".outpost:migration:lock" > /dev/null
192+
echo "DEL .outpost:migration:lock" >> "$PIPE_FILE"
193+
((CMD_COUNT++))
222194
fi
223-
elif $CLEANUP && $DRY_RUN; then
224-
log ""
225-
log "=== Cleanup (dry run) ==="
226-
log " Would delete: outpostrc, .outpost:migration:lock, and ${#MIGRATION_KEYS[@]} migration key(s)"
227195
fi
228196

229-
log ""
197+
log " ${CMD_COUNT} commands for ${#DEPLOYMENT_IDS[@]} deployments"
198+
199+
# --- Step 4: Execute or dry-run ---
230200
if $DRY_RUN; then
201+
log ""
202+
log "=== Dry run — commands that would be sent ==="
203+
cat "$PIPE_FILE"
204+
log ""
231205
log "Dry run complete. Re-run with --apply to execute."
232206
else
207+
log ""
208+
log "=== Executing pipeline ==="
209+
rcli_pipe < "$PIPE_FILE"
210+
log ""
211+
212+
# Verify
213+
migrated=$(rcli --scan --pattern "*:outpost:installation_id" | wc -l | tr -d ' ')
214+
shared_remaining=$(rcli KEYS "outpostrc" 2>/dev/null | wc -l | tr -d ' ')
215+
log "Deployments with installation_id: ${migrated}"
216+
if $CLEANUP; then
217+
log "Shared keys remaining: ${shared_remaining} (should be 0)"
218+
fi
233219
log "Migration complete."
234220
fi
221+
222+
rm -f "$PIPE_FILE"

0 commit comments

Comments
 (0)