동일한 커밋이 Webhook 재시도, 수동 재실행, 예약 작업으로 동시에 트리거되면 몇 초 사이에 두 개의 Xcode 빌드가 같은 작업 공간에 진입할 수 있습니다. 즉시 실패하지 않더라도 DerivedData를 동시에 다시 쓰거나 아카이브 디렉터리를 덮어써서 간헐적이고 재현하기 어려운 오류를 남기는 경우가 더 흔합니다. Cloud Mac을 장기간 가동한다면 단일 명령 실패보다 이러한 중복 트리거를 사전에 제어하는 것이 더 중요합니다.
실제로 상호 배제가 필요한 리소스부터 식별하기
처음부터 노드 전체에 전역 잠금을 걸어서는 안 됩니다. 잠금 범위는 저장소 이름이 아니라 공유 쓰기 대상에 따라 결정해야 합니다. 두 작업이 다음 리소스 중 하나라도 공유한다면 동일한 잠금 키를 사용해야 합니다.
- 동일한 작업 공간의 생성 파일 또는 종속성 디렉터리
- 동일한 DerivedData 경로
- 동일한 아카이브, 내보내기 또는 업로드 디렉터리
- 직렬로만 접근할 수 있는 시뮬레이터 디바이스 세트
- 동일한 최종 배포 대상
반대로 테스트 샤드마다 독립된 checkout, DerivedData, 시뮬레이터 디바이스 세트가 있다면 서로 다른 잠금 키를 사용해 병렬로 실행할 수 있습니다. 잠금 키는 client-release-archive처럼 “저장소, Scheme, 작업”을 조합해 만드는 것이 좋습니다. 브랜치 이름을 직접 포함하면 두 브랜치가 여전히 동일한 아카이브 대상을 두고 경합할 수 있습니다.
| 시나리오 | 권장 잠금 단위 | 병렬 실행 허용 여부 |
|---|---|---|
| DerivedData를 공유하는 아카이브 | 저장소 + Scheme + archive | 아니요 |
| 독립된 디바이스 세트를 사용하는 테스트 샤드 | 저장소 + shard 번호 | 예 |
| 같은 이름의 배포 산출물 업로드 | 애플리케이션 + 배포 채널 | 아니요 |
| 읽기 전용 정적 검사 | 일반적으로 잠금 불필요 | 예 |
잠금이 보호하는 대상은 CPU가 아니라 공유 부작용입니다. 동시성을 높이려고 필요한 잠금을 우회하면 대기 시간이 정리 시간으로 바뀔 뿐인 경우가 많습니다.
mkdir로 원자적 경합 지점 만들기
macOS에 기본 제공되는 mkdir는 단순하고 안정적인 경합 지점으로 사용할 수 있습니다. 여러 프로세스가 동일한 경로를 동시에 생성하려고 해도 하나만 성공합니다. “파일이 존재하는지 먼저 확인한 다음 PID를 기록”하는 방식과 달리 확인과 쓰기 사이에 경쟁 조건이 발생하지 않습니다.
다음 스크립트는 잠금에 사용할 무작위 소유자 토큰을 생성하고 15초마다 하트비트를 갱신합니다. 종료할 때는 토큰이 여전히 일치하는 프로세스만 잠금을 삭제할 수 있으므로 이전 작업이 새 작업의 잠금을 잘못 삭제하는 상황을 방지합니다.
#!/bin/bash
set -euo pipefail
LOCK_ROOT="${HOME}/Library/Caches/ci-locks"
LOCK_KEY="client-release-archive"
LOCK_DIR="${LOCK_ROOT}/${LOCK_KEY}.lock"
TOKEN="$(uuidgen)"
mkdir -p "$LOCK_ROOT"
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
echo "build lock is already held"
test -f "$LOCK_DIR/owner" && cat "$LOCK_DIR/owner"
exit 75
fi
printf 'token=%s
pid=%s
started=%s
' \
"$TOKEN" "$$" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" > "$LOCK_DIR/owner"
touch "$LOCK_DIR/heartbeat"
heartbeat() {
while true; do
touch "$LOCK_DIR/heartbeat"
sleep 15
done
}
heartbeat &
HEARTBEAT_PID=$!
cleanup() {
kill "$HEARTBEAT_PID" 2>/dev/null || true
if grep -Fq "token=${TOKEN}" "$LOCK_DIR/owner" 2>/dev/null; then
rm -rf "$LOCK_DIR"
fi
}
trap cleanup EXIT
trap 'exit 130' INT TERM HUP
xcodebuild \
-workspace Client.xcworkspace \
-scheme Client \
-configuration Release \
-derivedDataPath "$PWD/.ci/DerivedData" \
archive
종료 코드 75를 사용하면 “현재 작업을 실행할 수 없음”을 명확히 나타낼 수 있습니다. 큐 계층에서 지연 후 재시도할 수 있지만 같은 초에 무한히 반복해서는 안 됩니다. 모든 경로는 따옴표로 감싸야 하며, 잠금 루트 디렉터리는 현재 실행 계정이 쓸 수 있고 저장소 정리 명령으로 삭제되지 않는 위치에 두어야 합니다.
만료 여부를 PID만으로 판단하지 않기
PID 파일은 진단에는 유용하지만 그 자체만으로 소유권을 증명하기에는 적합하지 않습니다. macOS는 PID를 재사용할 수 있으며, 노드가 재부팅된 뒤에는 이전 파일에 기록된 숫자가 전혀 관련 없는 새 프로세스를 가리킬 수도 있습니다. 따라서 정리 프로그램은 최소한 다음 세 가지를 함께 확인해야 합니다.
- 하트비트 수정 시각이 파이프라인의 최대 실행 시간을 초과했는지
- 작업 기록이 큐에서 이미 종료 또는 시간 초과로 표시되었는지
- 보호 대상 디렉터리에 관련 빌드 프로세스가 여전히 쓰고 있는지
하트비트가 오래되었다는 사실은 “조사가 필요하다”는 의미일 뿐 “잠금을 빼앗아도 된다”는 뜻은 아닙니다. 디스크 병목으로 빌드가 멈춘 경우에도 프로세스가 파일을 계속 점유하고 있을 수 있습니다. 이때 두 번째 빌드를 강제로 시작하면 손상 범위가 더 커질 수 있습니다. 잠금을 기다리는 작업이 이전 상태를 직접 삭제하도록 하기보다 별도의 회수 작업에서 정리하는 편이 더 안전합니다.
잠금에 관측 가능한 필드 추가하기
owner 파일에는 커밋 해시, 작업 번호, Scheme, 작업 디렉터리를 추가로 기록할 수 있지만 토큰, 서명 자료 또는 기타 민감한 값은 기록하지 않아야 합니다. 실패 로그에는 최소한 잠금 키, 시작 시각, 작업 번호를 출력해야 합니다. 그래야 엔지니어가 정상적인 대기인지 이전 작업의 비정상 종료인지 판단할 수 있습니다.
대기, 시간 초과, 취소를 구분하기
상호 배제를 적용한다고 해서 나중에 들어온 모든 작업이 실패해야 하는 것은 아닙니다. 실제 큐에서는 일반적으로 다음 세 가지 정책을 사용합니다.
- 배포 아카이브: 먼저 들어온 작업을 유지하고 이후 작업은 대기
- 브랜치 검증: 동일 브랜치의 이전 작업을 취소하고 최신 커밋을 유지
- 예약 빌드: 같은 키의 작업이 실행 중이면 이번 실행을 건너뜀
어떤 정책을 사용하더라도 대기 시간에는 상한을 설정해야 합니다. 대기 시간 초과는 대기 중인 작업만 종료해야 하며 잠금 소유자의 잠금을 삭제해서는 안 됩니다. 잠금 소유자를 취소할 때는 먼저 정상 종료 신호를 보내 trap이 정리를 완료하도록 해야 합니다. 프로세스가 종료되지 않을 때만 수동 또는 별도의 회수 절차로 넘어가야 합니다.
또한 잠금 안에서 관련 없는 작업을 실행하지 않아야 합니다. 코드 다운로드, 읽기 전용 검증, 매개변수 준비는 잠금을 획득하기 전에 수행하고 실제로 공유 디렉터리를 수정하는 단계에서 잠금을 획득할 수 있습니다. 이렇게 하면 임계 구역이 짧아지고 큐 혼잡도 줄어듭니다.
운영 적용 전 장애 훈련
먼저 배포와 무관한 작업에서 스크립트를 연속으로 두 번 트리거하여 하나만 xcodebuild에 진입하는지 확인합니다. 그런 다음 정상 완료, 명령 실패, 종료 신호 수신을 각각 시뮬레이션하고 잠금 디렉터리가 삭제되는지 점검합니다. 마지막으로 오래된 잠금을 수동으로 남겨 두고 대기 작업이 점유 상태만 보고할 뿐 임의로 정리하지 않는지 확인합니다.
검증할 때는 다음 체크리스트를 사용할 수 있습니다.
- 잠금 키가 공유 리소스에 정확히 대응하는가
- 한 번의 원자적 작업으로 잠금을 획득하는가
- 잠금 해제 전에 무작위 소유자 토큰을 확인하는가
EXIT,INT,TERM,HUP에 모두 정리 경로가 있는가- 하트비트를 진단 목적으로만 사용하고 단독으로 잠금 탈취를 트리거하지 않는가
- 로그로 점유 작업을 식별할 수 있으면서 민감한 내용은 포함하지 않는가
- 대기 시간 초과가 실행 중인 빌드에 영향을 주지 않는가
- 독립된 작업마다 서로 다른 작업 공간과 출력 디렉터리가 있는가
이러한 검사를 완료하면 중복 트리거는 빌드 상태를 무작위로 손상시키는 숨은 경쟁 조건이 아니라 원인을 파악할 수 있는 대기 이벤트가 됩니다. 이후 동시성을 높여야 한다면 상호 배제 보호를 바로 제거하지 말고 먼저 공유 디렉터리와 배포 대상을 분리한 다음 잠금 키를 더 세분화해야 합니다.
자주 묻는 질문
빌드 잠금에 PID 파일만 사용하면 왜 위험한가요?
PID는 다른 프로세스에 재사용될 수 있고 파일은 비정상 종료 뒤 남을 수 있습니다. 원자 디렉터리 생성으로 소유자를 정하고 무작위 토큰으로 해제 권한을 확인해야 합니다.
모든 Xcode 빌드가 하나의 전역 잠금을 공유해야 하나요?
아닙니다. 작업 공간, DerivedData, 아카이브 경로 또는 배포 대상을 공유하는 작업만 같은 잠금 키를 써야 하며 완전히 격리된 테스트는 병렬 실행할 수 있습니다.
하트비트가 오래되면 즉시 잠금 디렉터리를 삭제해도 되나요?
안 됩니다. 파이프라인 제한 시간을 넘겼는지와 보호 경로에 쓰는 관련 프로세스가 없는지를 먼저 확인한 뒤 별도의 복구 단계에서 삭제해야 합니다.
빌드, 개발 및 실험을 위한 전용 물리 Mac 노드를 선택하세요
두 가지 Mac mini M4 구성, 네 가지 대여 기간, 주문 가능한 다섯 개 노드를 비교한 뒤 실제 워크플로에 맞는 구성을 선택하세요.