두 작업자가 동시에 같은 키에 쓰기를 시도했습니다. 둘 다 '성공'했지만, 하나는 사라졌습니다.
Two workers wrote the same key at the same moment. Both writes "succeeded." One is gone.
핵심 요약
멀티 에이전트 시스템에서 발생하는 동시성 쓰기 오류와 이를 해결하기 위한 에포크 기반 펜싱 기법을 공유합니다.
- 동시성 쓰기 오류 — 공유 상태를 수정하는 여러 에이전트가 충돌하여 데이터가 유실됨
- 좀비 라이터 문제 — 중단된 작업이 뒤늦게 재개되어 이전 상태를 덮어쓰는 현상
- 에포크 기반 펜싱 — 소유권 에포크를 도입하여 좀비 쓰기를 원천 차단함
- TLA+ 검증 — 모델 체킹을 통해 동시성 제어 로직의 무결성을 보장함
병렬 워커를 돌리는 오케스트레이터나, 상태(메모리 저장소, 결정 문서, 계획 파일 등)를 공유하는 장기 실행 에이전트를 굴리다 보면 멀티 에이전트 환경에서 꼭 마주치는 실패 유형 두 가지가 있다. 겉보기엔 똑같다. 실행은 깔끔하게 끝나는데, 나중에 시스템을 보면 업데이트가 아예 안 된 것처럼 굴거든. 다들 일단 모델 탓부터 하는데, 둘 다 모델 문제는 아니다.
실패 1: 동시성 업데이트 유실(Concurrent lost update). 플래너가 워커 6개를 뿌리고, 각자 결과를 공유 키에 쓴다. 두 놈이 동시에 끝난다. 둘 다 쓰기 성공이 뜬다. 근데 나중에 보면 하나가 증발해 있다. 전형적인 '마지막 쓰기가 승리(last-write-wins)'하는 상황인데, 에이전트가 끼면 일반 서비스보다 더 골치 아파진다. 아무도 문서를 의심하고 다시 읽지 않거든. 다음 프롬프트는 그냥 살아남은 데이터만 물려받아서 아주 자연스럽게 추론을 이어가고, 세 단계쯤 지나서야 "어, 에이전트가 X를 까먹었네?" 하는 식으로 문제가 터진다.
실패 2: 좀비 라이터(Zombie writer). 장기 실행 에이전트가 쓰기 권한을 쥔 채로 작업 도중에 멈춰버린다. 복구 프로세스가 (정상적으로) 권한을 회수해서 나머지 애들이 안 막히게 한다. 근데 한 시간 뒤에 멈췄던 놈이 깨어나서 쓰기를 완료해 버린다. 여기서 함정이 터진다. 그사이에 아무도 그 아티팩트를 안 건드렸으면 버전 번호가 그대로거든. 버전 체크는 다 통과한다. 결국 시스템이 이미 한참 지나온 상태 위에 낡은 커밋이 덮어씌워지는 거다.
그래서 내가 쓰기 경로에 필요하다고 판단해서 결국 직접 만든 건 이거다:
- 같은 키에 동시에 쓰려는 놈들은 딱 하나만 승리하게 만든다. 패자는 그냥 조용히 씹히는 게 아니라, 타입이 지정된 재시도 가능한 충돌(최신 상태 읽기, 재계산, 다시 커밋)을 뱉게 한다.
- 회수된 라이터는 펜싱(fencing) 처리한다. 권한을 회수할 때마다 소유권 에포크(epoch)를 올리고, 권한을 얻을 때마다 그 에포크를 기록한다. 커밋할 때 버전이랑 에포크를 원자적으로 체크한다. 이러면 버전 번호가 안 바뀌었어도 좀비 쓰기는 바로 거부된다.
이 보장들은 TLA+로 모델 체크를 마쳤다. CI에서 체커가 돌아가고, 각 스펙에는 일부러 가드를 삭제하는 뮤턴트가 포함되어 있어서 체커가 빨간불을 띄우는지 확인한다. 가드를 지웠는데도 모델이 안 터지면, 그건 사실상 의미 없는 가드라는 뜻이니까.
범위는 딱 여기까지다. 괜히 더 기대하지 마라: 코디네이터 하나, 호스트 하나, 그리고 그걸 거쳐 가는 라이터들만 대상이다. 호스트 간 통신은 안 만들었다. 진짜 필요한 놈이 나타나면 그때 고려해 볼 생각이다.
이건 프로세스 간에 공유되는 일반 파일 위에서 돌아가고, LangGraph, CrewAI, AutoGen, OpenAI Agents SDK용 어댑터도 있다. 레포에 업데이트 유실 상황을 재현하는 결정론적이고 키가 필요 없는 테스트 코드도 넣어놨다.
다들 실무에서 공유 에이전트 상태에 동시 쓰기 할 때 어떻게 처리하고 있냐? 그냥 재시도하면서 기도 메타 돌리는 중? 아니면 아예 아키텍처를 단일 라이터로 짰냐? 아니면 아직 안 당해본 거냐? 혹시 내가 만든 걸로도 못 잡는 실패 유형 겪어본 사람 있으면 공유 좀 해줘라. 궁금하다.


