[PonsWarp 개념 교실 17] 파티션 배리어 — PARTITION_ACK와 연속 프론티어
쉽게 말하면, **파티션 배리어는 "파일을 일정 구간으로 나눠 한 구간이 수신·저장될 때까지 다음 구간으로 넘어가지 않는 체크포인트"**다.

송신자가 너무 빨라 수신측 버퍼가 무너지는 문제를 막고, 재정렬 버퍼의 연속 위치를 기준으로 정확히 다음 구간을 시작한다.
이게 왜 필요한가
대용량 전송에서 송신자는 파일을 읽고 암호화하는 속도보다 빠르게 보낼 수 있다. DataChannel이 신뢰성을 제공해도 수신측 디스크 쓰기 큐는 별도 병목이다.
partition 1 전송 ──→ [수신·복호화·정렬·쓰기]
↓ 완료 확인
partition 2 전송 ──→ ...
구간 사이에 멈춤 지점을 두면 송신자는 수신측이 실제로 처리한 위치를 알 수 있다. 이 멈춤 지점이 **barrier(배리어)**다.
PARTITION 마커와 ACK
송신자는 구간 끝에 control 메시지 PARTITION을 보낸다. 수신자는 자신의 재정렬 버퍼가 해당 offset까지 연속적으로 도달했을 때 PARTITION_ACK를 반환한다.
json{"type":"PARTITION","offset":524288,"runId":7} {"type":"PARTITION_ACK","offset":524288,"runId":7}
여기서 중요한 말은 연속적으로다. offset 524288 이후 조각이 도착했어도 중간에 구멍이 있으면 ACK를 보내면 안 된다. 이 규칙이 out-of-order false ACK를 막는다.
코드의 대기 루프
typescript// PonsWarp/src/services/swarmManager.ts private async sendPartitionMarkerAndWait(offset, runId) { const waiter = { runId, peers: new Set(activePeerIds) }; this.partitionAckWaiters.set(offset, waiter); peer.send(JSON.stringify({ type: 'PARTITION', offset, runId })); while (this.isTransferring) { if (waiter.peers.size === 0) return; await this.waitForSendWindowSignal(PARTITION_ACK_POLL_INTERVAL_MS); } }
여러 receiver가 있으면 모든 peer가 ACK할 때까지 기다린다. 120초를 넘기면 타임아웃으로 실패한다.
그런데 항상 기다리지는 않는다
현재 코드에는 중요한 예외가 있다. host/unknown/relay 경로에서는 중간 ACK 배리어를 건너뛸 수 있다.
typescriptif (shouldSkipPartitionAckBarrier(candidatePathKind)) { peer.send(JSON.stringify({ type: 'PARTITION', offset, runId })); return; }
주석의 판단은 명확하다. 신뢰성은 SCTP가 이미 담당하고, Wi-Fi bulk에서 애플리케이션 ACK를 기다리면 속도만 떨어진다. 즉 배리어는 항상 켜는 안전 장치가 아니라 경로별 정책이다.
PonsWarp 적용 지점
| 항목 | 값 | 증거 |
|---|---|---|
| 전송 시작 | partition marker와 ACK waiter 초기화 | swarmManager.ts:2874-2883 |
| 배리어 목적 | 구간 전송 후 receiver ACK 대기 | swarmManager.ts:2942-2945 |
| marker 전송 | PARTITION(offset, runId) | swarmManager.ts:4001-4009 |
| peer 대기 | 모든 active peer ACK 필요 | swarmManager.ts:4001-4043 |
| 타임아웃 | 120초 | swarmManager.ts:4046-4049 |
| skip 정책 | host/unknown/relay 조건에 따라 생략 | swarmManager.ts:3974-3993 |
| false ACK 방지 | contiguous reordering frontier 기준 | swarmManager.ts:2885-2887 |
배리어가 없을 때 생기는 문제
DataChannel의 순서 보장이 파일의 저장 완료를 의미하지는 않는다. 수신자는 패킷을 받는 순서와 디스크에 연속으로 기록할 수 있는 위치를 따로 관리한다. 앞쪽 조각 하나가 빠지면 뒤의 조각을 모두 받아도 다음 파티션으로 안전하게 넘어갈 수 없다. PARTITION_ACK는 “받았다”가 아니라 이 파티션의 저장 경계가 연속적으로 닫혔다는 신호여야 한다.
textpartition 0 [0 -------- 8MiB] ── 저장 완료 ── PARTITION_ACK(0) partition 1 [8MiB ---- 16MiB] ── 대기 partition 2 [16MiB --- 24MiB] ── 먼저 도착해도 재정렬 버퍼 │ contiguous frontier = 8MiB
송신자는 ACK를 받은 뒤에만 다음 배리어를 해제한다. 이때 ACK payload에는 적어도 runId, partition 번호, frontier가 있어야 한다. 다른 전송의 늦은 ACK가 현재 run을 깨우면 재개 위치와 암호화 nonce가 함께 틀어질 수 있기 때문이다.
그냥 순서대로 전송하면 안 되나?
순차 전송은 이해하기 쉽지만, 한 파티션의 디스크 flush가 늦어지면 전체 회선이 빈다. 무제한 병렬 전송은 반대로 재정렬 버퍼와 writer 큐를 키운다. 배리어는 파티션 내부 처리는 병렬화하되, 파티션 경계를 넘는 속도만 제한한다. 즉 파일 전체를 잠그는 것이 아니라 저장 완료가 의미 있는 단위마다 안전한 프론티어를 만든다.
120초 timeout도 성공 판정이 아니다. 이는 ACK가 오지 않는 상태를 발견하는 상한이다. timeout 뒤에는 재전송·재협상·resume 중 어떤 경로로 갈지 명시해야 하며, 단순히 다음 파티션으로 진행하면 누락을 숨긴다.
| 상태 | 송신자 | 수신자 |
|---|---|---|
OPEN | 파티션 패킷 전송 | 조각 수신·재정렬 |
WAIT_ACK | 다음 경계에서 대기 | flush와 contiguous 계산 |
ACKED | 다음 파티션 시작 | frontier 갱신 |
TIMEOUT | 재시도 또는 resume | 현재 run 정리 |
재구축할 때의 체크리스트
- ACK를 패킷 수신과 저장 완료로 구분한다.
runId를 검증해 오래된 ACK를 폐기한다.- 파티션 번호만 믿지 말고 global frontier와 파일 경계를 함께 검사한다.
- timeout은 “성공”이 아니라 복구가 필요한 상태다.
- 취소·재연결 시 기존 waiters를 정리한다.
이 구조의 핵심은 배리어가 속도를 낮추는 장치가 아니라 다음 위치를 믿어도 된다는 증거를 만드는 장치라는 점이다.
한 문장 정리
PARTITION_ACK 배리어는 일정 구간의 연속 offset이 수신·저장될 때까지 다음 데이터를 멈추는 체크포인트지만, SCTP가 충분한 경로에서는 ACK 대기를 생략해 안전성과 속도를 경로별로 조절한다.
관련 개념: [개념 9 — 재정렬 버퍼] · [개념 15 — prepare-ahead 크레딧] · [개념 16 — in-flight 윈도우]
다음 글: [개념 18 — AIMD: 혼잡 제어의 고전]
배리어가 없을 때 생기는 문제
송신자가 partition 경계를 무시하고 계속 보내면 수신측에는 두 종류의 완료가 섞인다. 네트워크에 도착한 것과 writer에 연속적으로 저장된 것은 다르다. 뒤쪽 조각이 먼저 도착해도 중간 구멍이 있으면 파일을 안전하게 다음 상태로 넘길 수 없다.
text도착: 0 1 2 4 5 저장: 0 1 2 ← frontier = 3
이 상태에서 5까지 받았다고 ACK하면 sender는 6부터 보낼 수 있지만, 파일에는 3이 비어 있다. PonsWarp는 ACK의 의미를 “도착했다”가 아니라 “연속적으로 처리됐다”로 정의한다.
runId가 필요한 이유
재연결이나 새 전송이 시작되면 이전 partition ACK가 늦게 도착할 수 있다. runId를 marker와 ACK에 같이 넣어 오래된 실행의 신호가 현재 실행을 깨우지 못하게 한다.
json{"type":"PARTITION_ACK","offset":524288,"runId":7}
이 작은 필드가 없으면 이전 transfer의 ACK가 새 transfer의 waiter를 잘못 해제할 수 있다.
120초 timeout의 의미
ACK를 무한히 기다리면 sender가 영원히 멈춘다. 120초가 지나면 timeout error로 전환한다. timeout은 네트워크가 느리다는 뜻이 아니라, 현재 실행이 더 이상 진행되지 않는다는 경계다.
또한 host/unknown/relay에서는 shouldSkipPartitionAckBarrier가 중간 ACK 대기를 생략할 수 있다. 이 선택은 모든 경로에 같은 안전장치를 강제하지 않고, SCTP 신뢰성이 충분한 경로에서 애플리케이션 대기 비용을 줄이는 방식이다.
다시 만든다면
partition marker에는 offset뿐 아니라 다음 정보를 함께 남기겠다.
- 누적 바이트
- 마지막 contiguous offset
- runId
- 수신 writer persisted bytes
- ACK 생성 시각
그러면 배리어가 지연된 이유가 네트워크인지, 복호화인지, 디스크 flush인지 구분된다.