← 문제로

49. 완료 카운트다운(Decrement) 종료 판정과 컨텍스트 재사용 경합 (C#)

난이도 최상
내 리뷰 · C#
해설 · C#

해설 — 완료 카운트다운(Decrement) 종료 판정과 컨텍스트 재사용 경합 (C#)

난이도: 최상

요약

완료 판정을 Interlocked.Decrement(ref ctx.Pending) == 0으로만 하고(A), 중복 응답을 걸러내지 않는다. 신뢰성 UDP 재전송으로 이미 처리된 샤드의 응답이 한 번 더 도착하면 Pending이 추가로 감소해, (1) 이미 0인 카운터가 음수가 되며 0 전이를 다시 만들어 개더가 두 번 완성되거나, (2) 마지막 정상 응답보다 먼저 중복이 도착해 카운터가 조기에 0이 되어 아직 응답 안 온 슬롯을 비운 채 완성한다. 더 치명적으로, 0을 만든 콜백이 Return(ctx)로 컨텍스트를 풀에 반납(B)한 뒤 다른 개더가 같은 객체를 Rent 하면, 뒤늦게 도착한 옛 개더의 중복 응답이 재사용된 컨텍스트의 Pending/Results를 오염시킨다(ABA형 use-after-return).

문제점

  • 분류태그: fetch-and-decrement 종료 판정 오용 / 중복 완료 미방어(dedup 부재) / 객체 풀 use-after-return(ABA)
  • 증상: 검색 결과가 다른 요청 결과와 섞임, 같은 개더 이중 완성(콜백 2회), 간헐적 결과 누락.
  • 재현조건: 샤드 응답 중복 도착(재전송) + 개더 완료 후 컨텍스트 풀 재사용이 겹칠 때.
  • 근본원인: ① 응답 처리가 멱등하지 않다 — 슬롯이 이미 채워졌는지 확인 없이 무조건 Decrement 한다. ② 컨텍스트 수명이 "마지막 감소"에 묶여 있는데, 그 이후에도 도착 가능한 늦은/중복 응답이 참조를 붙든다(유예 기간 부재).

수정안

슬롯 단위로 "처음 채워질 때만 감소"하도록 멱등화하고, 완성/반납은 한 번만 일어나도록 게이트를 둔다. 재사용은 세대(generation) 토큰으로 늦은 응답을 무시한다.

public sealed class GatherContext
{
    public int Generation;                 // Begin 마다 증가
    public int Pending;
    public int[] SlotFilled;               // 0/1, 슬롯별 1회 감소 보장
    public int Completed;                  // 0/1, 완성 1회 게이트
    public string?[] Results;
    public Action<string?[]>? OnComplete;
}

public void OnShardResponse(GatherContext ctx, int gen, int idx, string? value)
{
    if (Volatile.Read(ref ctx.Generation) != gen) return;      // 재사용된 옛 응답 무시
    if (Interlocked.Exchange(ref ctx.SlotFilled[idx], 1) == 1) return; // 중복 슬롯 무시

    ctx.Results[idx] = value;
    if (Interlocked.Decrement(ref ctx.Pending) != 0) return;

    if (Interlocked.Exchange(ref ctx.Completed, 1) == 1) return; // 이중 완성 방지
    ctx.OnComplete?.Invoke(ctx.Results);
    Return(ctx);                                                 // Begin 에서 Generation++ 후 재사용
}

핵심 3가지: (1) 슬롯별 1회 감소(멱등), (2) 완성 1회 게이트, (3) 세대 토큰으로 재사용 후 늦은 응답 차단.

더 나은 설계

  • 컨텍스트를 풀로 재사용하는 대신 참조 카운팅/유예(quiescent) 회수로 in-flight 응답이 모두 끝난 뒤 회수한다. 트레이드오프: 회수 지연·복잡도.
  • 애초에 응답 경로를 **응답 dedup(요청 correlation id + 슬롯 seen 집합)**으로 멱등하게 만들면 카운트다운이 견고해진다.
  • 완성 판정을 "감소로 0"이 아니라 "채워진 슬롯 수 == N"처럼 상태로부터 파생하면 중복 감소에 영향받지 않는다.

면접 포인트

  • if(--n==0) 완료 패턴이 왜 "정확히 한 번" 도착을 전제로만 안전한지, at-least-once 환경에서 무엇을 추가해야 하는지.
  • 객체 풀 재사용에서 발생하는 ABA/use-after-return과 세대 토큰(generation/epoch)의 역할.