설날에 머지된 첫 오픈소스 PR: OpenTelemetry .NET LogRecordSharedPool Thread-Safety 버그 수정기
첫 오픈소스 PR이 머지됐다.
대상은 open-telemetry/opentelemetry-dotnet, 이슈는 LogRecordSharedPool의 thread-safety 문제였다.
PR은 2026-02-17, 설날 당일에 main 브랜치로 들어갔다.
TL;DR
- 이슈 #6233:
LogRecordSharedPool.Rent()가 동시성 상황에서 동일한LogRecord를 여러 스레드에 반환해, 로그 데이터가 섞이거나 중복 전송되는 버그 - PR #6833:
TryRentCoreRare실패 시 루프를 계속 돌던 로직을 break로 바꾸고, 새LogRecord를 생성하도록 수정 - 타임라인: 이슈 오픈(2025-04-07) → PR 연결(2026-01-17) → 리뷰 반영(2026-01-30 전후) → 머지(2026-02-17)
1. 이슈: 로그가 섞여서 두 번 나간다
이슈 #6233의 내용은 이렇다.
The Rent() method of LogRecordSharedPool is not thread safe and this causes the same LogRecord to be given out multiple times and then the data from one is overwritten or combined into a single log which then gets submitted twice.
멀티스레드 환경에서 Rent()를 동시에 호출하면 같은 LogRecord 인스턴스를 두 스레드가 받아간다.
두 스레드가 각자의 데이터를 같은 객체에 쓰면, attribute가 뒤섞인 로그 하나가 두 번 전송된다.
이슈 작성자가 남긴 힌트가 있었다.
By forcing the use of the LogRecordThreadStaticPool this never happens.
ThreadStaticPool은 스레드마다 별도 인스턴스를 쓰니 당연히 재현이 안 된다.
이 힌트 하나로 원인을 공유 풀의 동시 접근으로 좁힐 수 있었다.
왜 이 문제가 발생하는가?
LogRecordSharedPool은 LogRecord 객체를 재사용하는 풀이다.
Rent()로 꺼내서 쓰고, 다 쓰면 Return()으로 돌려준다.
문제는 Rent() 내부의 TryRentCoreRare가 실패했을 때의 처리였다.
race condition으로 TryRentCoreRare가 실패하면, 기존 코드는 continue로 루프를 이어가며 새 인덱스를 시도했다.
이 과정에서 이미 다른 스레드가 가져간 슬롯을 다시 반환하는 경우가 생겼다.
sequenceDiagram
participant A as Thread A
participant B as Thread B
participant P as LogRecordSharedPool
A->>P: Rent()
B->>P: Rent()
Note over P: TryRentCoreRare 실패 후 continue
P-->>A: LogRecord #1
P-->>B: LogRecord #1 (동일 객체)
A->>A: attribute 쓰기
B->>B: attribute 쓰기 (덮어씀)
Note over A,B: 섞인 로그 두 번 전송2. 수정 방향
수정은 단순했다.
TryRentCoreRare가 실패하면 루프를 continue로 이어가지 말고, break로 끊고 새 LogRecord 인스턴스를 생성한다.
이렇게 하면 race condition이 발생해도 중복 반환 대신 새 객체를 만들어 돌려준다.
PR #6833의 Changes 요약:
- Race Condition 수정:
TryRentCoreRare실패 시continue→break로 변경, 새LogRecord생성 - 계산 최적화:
slotIndex계산을 추출해 중복 modulo 연산 제거 - 회귀 테스트 추가:
LogRecordSharedPoolTests.cs에RentShouldNeverReturnSameInstanceConcurrently추가
테스트에서 failure detail 수집은 ConcurrentQueue로 처리했다.
버그 자체가 동시성 문제인데, 테스트 코드가 thread-safe하지 않으면 테스트 결과를 신뢰할 수 없다.
// 멀티스레드 환경에서 List는 안전하지 않다
var failures = new List<string>();
// ConcurrentQueue로 race condition 없이 수집
var failures = new ConcurrentQueue<string>();3. 오픈소스 PR 흐름에서 예상 못 한 것들
Stale 봇
중간에 봇이 PR을 stale로 표시했다. 수백 개의 PR이 열려 있는 레포에서 이런 자동화는 당연한 거지만, 처음 보면 당황스럽다. "활동 없으면 닫는다"는 문구를 보고 바로 코멘트를 달아서 살렸다.
CHANGELOG 요구
리뷰어 martincostello가 남긴 피드백 중 하나가 이거였다.
Appropriate CHANGELOG.md files updated for non-trivial changes
코드는 고쳤는데 CHANGELOG가 없으면 불완전하다는 관점이었다. "이 버전에서 뭐가 바뀌었는지" 사용자 입장에서 추적할 수 있어야 한다는 거다.
이 피드백이 "오픈소스는 제품이다"를 가장 직접적으로 느끼게 한 지점이었다.
63개 CI 체크
승인이 났다고 바로 머지되는 게 아니었다. merge queue에 들어가서 CI 체크 63개를 통과해야 최종 머지가 이루어진다.
graph LR
A["이슈 #6233"] --> B["PR #6833"]
B --> C["리뷰 (martincostello)"]
C --> D["CHANGELOG 업데이트"]
D --> E["승인"]
E --> F["Merge Queue"]
F --> G["CI 63개 통과"]
G --> H["main 머지"]4. 설날에 머지됐다
PR #6833은 2026-02-17, rajkumar-rangaraj에 의해 main으로 머지됐다.
커밋은 84d0d3e, merge queue를 통해 들어갔다.
2026년 설날이 2/17이고 공식 연휴가 2/16~2/18이다. "설날에 첫 오픈소스 PR이 머지됐다"는 말이 날짜까지 맞아떨어졌다.
5. 얻은 것과 한계
얻은 것
코드 변경 자체보다 이슈 → PR → 리뷰 → CHANGELOG → merge queue → 머지 흐름을 직접 밟아본 게 컸다.
| 시스템 | 역할 |
|---|---|
| Stale 봇 | 비활성 PR 정리, 메인테이너 부담 감소 |
| CHANGELOG 요구 | 사용자 관점의 변경 이력 보장 |
| CI 체크 63개 | 다양한 환경/버전에서 안정성 검증 |
| Merge Queue | 동시 머지로 인한 충돌 방지 |
한계
첫 PR이라 조건이 좋았다.
good first issue 라벨이 붙어 있었고, 수정 범위가 명확했고, 리뷰어도 친절했다.
더 복잡한 이슈, 더 많은 이해관계자가 얽힌 PR에서는 다른 어려움이 있을 것이다.
다음
"한 번 해봤다"로 끝내고 싶지 않다. 다음에는 이슈 분석부터 테스트 / 문서 / CHANGELOG까지 한 번에 챙기는 방식으로 기여를 이어가는 게 목표다.
참고 자료
Loading comments...