티켓이 지나는 상태
보드의 레인은 셋입니다. 대기 · 진행중 · 완료. 그 셋이 파일 셋과 정확히 짝지어져 있습니다. 카드를 끌어 옮기는 손잡이가 없는 이유가 이것입니다. 레인은 사람이 정하는 칸이 아니라 파일명 접미사를 그린 것이니까요.
| 보드 레인 | 파일 | 뜻 |
|---|---|---|
| 대기 | <해시>.md | 열려 있고, 다음 tick의 후보입니다 |
| 진행중 | <해시>.wip.md | 누군가 잡았습니다 |
| 완료 | <해시>.done.md | 끝났습니다 |
상태는 이 셋이 전부입니다. 워커가 티켓을 집으면 파일 이름이 .wip을 얻고 카드가 오른쪽 레인으로
넘어갑니다. 세션이 끝나며 .done으로 바꾸면 한 칸 더 갑니다. 앱은 그 파일을 읽어 그릴 뿐,
상태를 만들지는 않습니다.
접미사 문자열 자체는 환경변수 TICKET_INPROGRESS·TICKET_DONE으로 바꿉니다(기본값
.wip·.done. 워커 환경변수 참고). 어떤 문자열을 쓰든 판정 방식은 같습니다.
그 파일 이름이 그 문자열로 끝나는지만 봅니다.
레인에 없는 표시 둘
레인은 티켓이 지나가는 단계를 그립니다. 그런데 대기에 앉아 있는 티켓 중에는 순서가 와도
넘어가지 못하는 것이 있습니다. 단계가 하나 더 있는 게 아니라 그 단계 안의 사정이라서, 레인을
넷으로 늘리는 대신 카드에 표시를 붙여 말합니다.
deps 대기 — 주황색 deps 태그. 선행 티켓이 아직 안 끝났습니다. 카드에 미충족
해시가 주황색으로 뜨고 그 티켓은 대기 레인 제자리에 그대로 앉아 있습니다. 막혔다고 아래로
밀지 않습니다. 밀어 버리면 위쪽 카드가 "곧 디스패치"라는 거짓말이 되기 때문입니다.
사람이 할 일은 없습니다. 선행이 완료되는 순간 아무도 손대지 않아도 태그가 사라지고
그 티켓이 후보가 됩니다.
답변 대기 — 배지와 경과일(답변 대기 · 3일). 세션이 사람에게 물어보고 손을 뗀
상태입니다. 그 티켓은 답이 파일로 생길 때까지 잠겨 있어서 아무 워커도 집어 가지 않습니다.
같은 질문에 세션을 반복해서 태우지 않으려고 걸어 둔 잠금입니다. 답을 쓰는 자리는 셋인데 폼은
한 벌입니다. 티켓 상세의 진행 기록 상자에서는 입력칸이 답변 모드로 섭니다. 보드 카드에서도
다이얼로그로 열리고, 헤더 알림 종에는 답변 쓰기 링크가 붙습니다. 어디서 쓰든 앱이
kind: answer 파일 하나를 만듭니다. 그 파일이 태어나는 순간 잠금이 풀리고 배지가 대기로
돌아갑니다.
경과일이 배지에 같이 붙는 것은 방치를 눈에 보이게 하려는 것입니다. 자동 만료도 자동 취소도
없습니다. 오래된 답변 대기는 정체의 신호입니다. 그 판단은 사람이 합니다.
둘 다 보드 상태 필터의 선택지로도 있습니다. 필터 선택지는 여섯인데 레인은 셋이고, 그게 정상입니다. 필터는 엔진이 아는 상태를 고르는 자리고, 레인은 흐름의 단계를 그리는 자리니까요.
디스패치되지 않는 티켓
열려 있는데 아무 워커도 집지 않는 티켓의 사유는 넷입니다. 위 둘, 그러니까 deps 대기와
답변 대기는 정상입니다. 밖의 조건이 풀리면 저절로 열립니다.
셋째도 정상입니다. 우선순위 1은 진행중 티켓이 0건일 때만 후보입니다. 다른 티켓이 하나라도 돌고 있으면 순서가 와도 건너뛰고 그 세션이 끝나면 아무도 손대지 않아도 다시 후보가 됩니다. 배지도 태그도 안 붙습니다. 카드의 우선순위 미터가 한 칸만 차 있는 것이 유일한 표시입니다. 더 높은 값을 물려받은 1은 그 값으로 떠서 이 게이트에 안 걸립니다(티켓 직접 발행하기 참고).
넷째만 사고입니다. 열린 파일에 session_id가 박혀 있는 경우(배지 이름은 할당됨)인데,
이 조합을 엔진은 만들지 않습니다. 사람이 파일에 session_id를 손으로 적었거나, 그 값이 든
티켓을 통째로 베껴 새 티켓으로 만들었을 때 생깁니다. 그러면 이렇게 됩니다.
- 후보를 고르는 쪽은
session_id가 있으면 이미 할당됐다고 보고 영구히 건너뜁니다. - 죽은 세션을 회수하는 쪽은
.wip만 봅니다. 이 티켓은.wip이 아니니 회수 대상도 아닙니다.
집히지도 회수되지도 않고 조용히 죽습니다. 배지 하나로는 그 화면을 열지 않는 한 영원히 모르기
때문에, 앱은 이것을 헤더 알림 종 안의 항목으로 올립니다. 아무도 집지 않는 티켓 <n>건.
- 프로젝트 화면 어디에 있든 보입니다. 보드에만 두면 워커 화면을 보고 있는 사람은 모릅니다.
- 필터로 끌 수 없고 상위 몇 건으로 자르지도 않습니다. 해당 티켓 전부를 나열하고 행마다
할당 해제버튼이 붙습니다. 그 버튼이 하는 일은session_id를 지우는 것 하나입니다. - 닫기도 읽음 표시도 없습니다. 보드가 5초마다 큐를 다시 읽으므로 0건이 되면 항목이 저절로 사라집니다. "해결됨"을 앱이 따로 기록하지는 않습니다. 기록하는 순간 파일의 사실과 갈릴 수 있는 두 번째 진실이 생기기 때문입니다.
이 티켓들은 칸반 어느 레인에도 없습니다. 그래서 보드의 티켓 N건 합계가 레인 합계보다 클 수
있고 그 자리에 (디스패치되지 않는 N건은 상단 알림)이 같이 적혀 있습니다.
session_id를 비롯해 디스패처와 엔진이 쓰는 키는 사람이 채우는 값이 아닙니다. 어떤 키가 있고
누가 언제 쓰는지는 frontmatter 필드에 표로 있습니다.
파일명이 곧 락인 이유
상태를 frontmatter가 아니라 파일명에 둔 이유는 하나입니다. 이름을 바꾸는 동작 자체가 락이라서 워커 여럿이 같은 큐를 봐도 충돌이 없습니다.
잡기(claim)는 os.link로 <해시>.wip.md를 만들고 원본을 지우는 동작입니다. 목적지가 이미
있으면 커널이 EEXIST를 돌려줍니다. 이미 잡혔습니다. 판정("아직 안 잡혔나")과
획득("내가 잡는다")이 한 시스템콜 안에 있어서 그 사이에 다른 프로세스가 끼어들 틈이 없습니다.
동시에 같은 티켓을 본 워커 여섯 중 먼저 링크에 성공한 하나만 이기고 나머지는 다음 후보로
넘어갑니다. 새로 발명한 계보는 아닙니다. 메일 하나를 new/에서 cur/로 옮기는 rename을 락으로
쓰는 Maildir와 같습니다.
하드링크를 지원하지 않는 파일시스템(구글드라이브 같은 FUSE·SMB 마운트)에서는
O_CREAT|O_EXCL로 폴백합니다. 이것도 원자적 배타 생성이라 같은 성질을 유지합니다. 상태를
frontmatter로 옮기면 값을 읽고 고쳐 쓰는 사이가 프로세스 간에 원자적이지 않아, 파일명이
공짜로 주던 락을 따로 사야 합니다.
다음은 워커입니다.