티켓이 지나는 상태
보드의 레인은 셋입니다. 대기, 진행중, 완료. 그 셋이 파일 셋과 정확히 짝지어져 있습니다. 레인은 파일명 접미사를 그대로 표시한 결과이고, 사람이 칸을 지정하지 않습니다.
카드를 끌어 다른 레인에 놓을 수는 있습니다. 다만 놓는다고 카드가 그 레인으로 옮겨지지는 않습니다. 드롭은 엔진 작업을 부르는 입구입니다. 그 작업이 파일 이름을 바꾸면 카드는 그때 따라 움직입니다.
- 대기 카드를 진행중 레인에 놓으면 지금 시작합니다. 쉬는 워커가 있으면 그 워커에게 이
티켓을 지목해 맡깁니다. 쉬는 워커가 없으면 도는 세션 하나를 끊고 시작할지 묻습니다. 끊을
세션은 엔진이 고르고, 확인 창에 그 세션이 뜹니다.
끊고 시작을 누르면 끊긴 티켓은 대기로 돌아가 다시 디스패치되고, 비워진 워커가 방금 놓은 티켓을 가져갑니다. 진행중 티켓이 없거나 전부 유효 우선순위 5라 끊을 세션이 없으면 시작하지 않고 그 이유를 알려 줍니다. - 진행중 카드를 대기 레인에 놓으면 티켓 상세의
할당 해제를 누른 것과 같습니다. 세션이 아직 살아 있으면강제 중단으로 끊을지 먼저 묻습니다. - 완료 카드는 끌리지 않습니다.
| 보드 레인 | 파일 | 뜻 |
|---|---|---|
| 대기 | <해시>.md | 열려 있고, 다음 tick의 후보입니다 |
| 진행중 | <해시>.wip.md | 누군가 잡았습니다 |
| 완료 | <해시>.done.md | 끝났습니다 |
상태는 이 셋이 전부입니다. 워커가 티켓을 가져가면 파일 이름에 .wip이 붙고 카드가 오른쪽 레인으로
넘어갑니다. 세션이 끝나면서 .done으로 바꾸면 카드가 한 칸 더 이동합니다. 앱은 그 파일을
읽어서 화면에 표시하고, 상태를 직접 만들지는 않습니다.
접미사 문자열 자체는 환경변수 TICKET_INPROGRESS와 TICKET_DONE으로 바꿉니다(기본값은
.wip과 .done입니다. 워커 환경변수 참고). 어떤 문자열을 쓰든 판정 방식은
같습니다. 그 파일 이름이 그 문자열로 끝나는지만 확인합니다.
다 못 끝내고 닫힌 완료
완료 배지는 두 종류입니다. 완료와 완료(이어짐). 파일은 둘 다 <해시>.done.md 하나로 같고,
구분되는 지점은 그 티켓 맨 위에 continued: 한 줄이 있는지 여부입니다. 상태가 넷으로
늘어나지는 않습니다.
완료(이어짐)은 남은 일을 다른 티켓에 넘기고 닫힌 완료입니다. 세션이 이 티켓을 다
끝내지는 못하겠다고 판단하면 그대로 종료하지 않습니다. 여기까지 진행한 내용을 기록하고, 남은
범위를 담은 새 티켓 한 장을 발행합니다. 그 해시를 자기 continued:에 적고 완료로 닫습니다.
그래서 이 배지가 붙은 티켓에는 체크되지 않은 채 남은 Done when 항목이 있습니다. 세션이
빠뜨린 항목이 아닙니다. 그 항목은 후속 티켓이 그대로 물려받습니다.
배지 자체가 그 후속 티켓으로 가는 링크입니다. 눌러서 이동하면 남은 범위가 그 티켓의
## Goal에 있습니다. continued:에 적힌 해시가 큐에 없으면 링크 없이 배지만 뜹니다.
이어받기는 세 장에서 멈춥니다. 이어받기 티켓은 자기가 몇 번째인지를 handoffs: 한 줄에
기록합니다. 원본 다음이 1, 그 다음이 2, 이렇게 하나씩 증가합니다. 네 번째가 되면 워커가 그
티켓을 가져가자마자 다시 반납하고 답변 대기로 잠급니다. 계속 넘기기만 하고 아무도 끝내지 못하는
티켓을 사람에게 한 번은 보여 주려는 장치입니다. 답을 쓰면 잠금이 풀리고, 같은 티켓이 이
상한에 다시 걸리지는 않습니다. 설정에서 오토파일럿을 켜 두었으면 이 상한이 없습니다.
배지가 뜨는 자리는 보드 표의 상태 칸과 티켓 상세의 제목 옆입니다. 상태 필터에는 이 구분이
없습니다. 완료를 고르면 둘 다 나옵니다.
사람이 완료 티켓에 한 줄 써서 여는 이어받기와는 다릅니다. 그쪽은 원본을
읽기만 하므로 원본 배지는 완료 그대로입니다.
레인에 없는 표시 넷
레인은 티켓이 지나가는 단계를 그립니다. 그런데 대기에 놓여 있는 티켓 중에는 순서가 와도
넘어가지 못하는 것이 있습니다. 단계가 하나 더 생긴 상황은 아닙니다. 그 단계 안에서 벌어지는
사정이라서 레인을 늘리지 않고 카드에 표시를 붙여 알려 줍니다.
deps 대기 - 주황색 deps 태그. 선행 티켓이 아직 안 끝났습니다. 카드에 미충족
해시가 주황색으로 뜨고 그 티켓은 대기 레인 제자리에 그대로 놓여 있습니다. 막혔다고 아래로
내리지 않습니다. 내려 버리면 위쪽 카드가 곧 디스패치된다는 잘못된 정보를 주기 때문입니다.
사람이 할 일은 없습니다. 선행이 완료되는 순간 아무도 손대지 않아도 태그가 사라지고
그 티켓이 후보가 됩니다.
답변 대기 - 배지와 경과일(답변 대기 · 3일). 이 배지가 붙어 있으면 사람이 쓸 답이
반드시 있습니다. 대개는 세션이 사람에게 질문하고 작업을 중단한 자리입니다. 세션이 안 물어도
엔진이 대신 잠그는 자리가 셋 있습니다. 셋 다 묻는 것은 같습니다. 남은 범위를 어떻게 할까요.
세션이 끝까지 정상으로 실행되고도 티켓을 닫지 않아 자동 회수가 세 번째로 일어났을 때, 위에서
말한 이어받기 상한에 걸렸을 때, 그리고 주도성이 1 - 2 - 3일 때 폴링이 상한까지 기다렸는데
조건이 오지 않았을 때입니다. 주도성 4와 5에서는 폴링 상한을 넘긴 티켓이 이 배지로 오지 않고
엔진이 묻지 않은 채 다시 디스패치합니다(아래 폴링 대기 참고). 나머지 경우는 다시 디스패치해도
같은 자리로 돌아오므로 범위를 자르든 쪼개든 사람이 판정합니다.
세션이 죽어서 멎은 티켓은 여기 안 옵니다. 세션이 끝을 보지 못하고 죽은 자리라면 사람이 답을
써도 달라지는 것이 없습니다. 그런 티켓은 아래 재시도 대기로 갑니다. 그래서 이 배지는 열어
보셔야 하는 신호입니다.
답변 대기인 티켓은 답이 파일로 생길 때까지 잠겨 있어서 아무 워커도 가져가지 않습니다. 같은
질문에 세션을 반복해서 소모하지 않으려고 걸어 둔 잠금입니다. 답을 쓰는 자리는 셋인데 폼은
하나입니다. 티켓 상세의 진행 기록 상자에서는 입력칸이 답변 모드로 뜹니다. 보드 카드에서도
다이얼로그로 열리고, 헤더 알림 종에는 답변 쓰기 링크가 붙습니다. 어디서 쓰든 앱이
kind: answer 파일 하나를 만듭니다. 그 파일이 생성되는 순간 잠금이 풀리고 배지가 대기로
돌아갑니다.
경과일이 배지에 같이 붙는 것은 방치를 눈에 보이게 하려는 것입니다. 자동 만료도 자동 취소도
없습니다. 오래된 답변 대기는 정체의 신호입니다. 그 판단은 사람이 합니다.
오토파일럿 실패 - 빨간 배지. 답변 대기의 한 종류입니다. 말풍선 대신 팔각형 경고 아이콘이
붙고, 배지와 보드 카드 테두리가 빨갛게 뜹니다. 설정에서 오토파일럿을
켜 두면 세션은 묻지 않기로 되어 있습니다. 그런데도 답 없이는 진행할 방법을 하나도 찾지 못해
결국 물었다는 표시가 이 배지입니다. 세션이 반복해서 끝을 못 내 엔진이 자동 회수 상한에서 잠근
티켓도 같은 배지를 답니다.
평범한 답변 대기는 세션이 고른 수준 안에서 물을 일이라고 보고 물은 자리입니다. 이 배지는 묻지
않겠다는 약속이 깨진 자리입니다. 그래서 질문 첫 줄에 세션이 이미 시도한 방법이 나열되어 있습니다.
그 목록을 먼저 읽고, 거기 없는 길이나 세션이 가질 수 없는 값(비밀번호, 인증 코드 같은 것)을 답으로
주세요. 목록에 있는 방법을 다시 권하면 같은 자리로 돌아옵니다.
답하는 폼은 평범한 답변 대기와 같습니다. 티켓 상세의 답변칸 머리에 빨간 경고 한 줄이 더
붙을 뿐입니다. 오토파일럿이 답 없이 진행할 방법을 찾지 못했습니다. 아래 질문에 답하면 다시 자율 진행합니다. 답을 쓰면 잠금이 풀리고 배지가 대기로 돌아가며 빨간 표시도 같이 사라집니다.
보드의 상태 필터에서는 답변 대기를 고르면 이 티켓도 함께 걸립니다.
빨간 표시는 그 티켓이 잠긴 시점에 정해집니다. 나중에 오토파일럿을 꺼도 이미 빨갛게 뜬 티켓은
답이 달릴 때까지 빨간 채로 남습니다. 반대로 오토파일럿이 켜져 있어도 사람이 강제 중단해서 생긴
답변 대기는 평범한 색으로 뜹니다. 중단은 사람이 내린 판단이지 오토파일럿의 실패가 아니기
때문입니다.
폴링 대기 - 배지와 남은 시간(폴링 대기 · 3시간 남음). 이 티켓은 큐 밖의 조건을
기다립니다. 빌드가 끝나기를, 다른 팀이 값을 넣어 주기를 기다리는 자리입니다.
세션은 그 조건을 판정하는 스크립트 하나를 티켓에 등록하고 작업을 중단합니다. 그 뒤로는 워커가
실행될 때마다 스크립트를 대신 돌려 보고, 조건이 왔다고 나오면 아무도 손대지 않아도 그
자리에서 배지가 사라지고 티켓이 후보가 됩니다. 기다리는 동안 워커 한 자리를 차지하지도 않습니다.
재시도 대기 - 배지와 남은 시간(재시도 대기 · 2시간 남음). 이 티켓을 맡은 세션이 잇달아
죽었습니다. 요청이 잘못됐거나, 세션이 아예 못 떴거나, 실행 상한을 넘겨 강제종료된 회차입니다.
어느 쪽도 사람이 답을 써서 달라지지 않으니 엔진은 묻지 않고 그 티켓을 잠시 후보에서 빼 둡니다. 그
티켓은 여전히 열린 파일이고 워커 한 자리를 차지하지도 않습니다. 그런 티켓이 몇 건인지는 헤더 알림
종이 되풀이해 죽어서 쉬는 티켓 <n>건으로 알려 줍니다. 회차를 세는 규칙은 frontmatter
필드의 attempts에 있습니다. 계정 한도에 걸려 끊긴 회차나 더 급한 티켓에
밀려 끊긴 회차는 그 수에 포함되지 않습니다.
쉬는 시간은 되풀이할수록 길어집니다. 첫 대기는 몇 분입니다. 같은 티켓이 또 죽으면 다음 대기가
앞선 두 대기를 더한 길이가 됩니다. 그래서 계속 죽는 티켓일수록 간격이 크게 벌어집니다. 대신
간격이 얼마나 벌어지든 그 티켓은 반드시 다시 후보가 됩니다. 쉬는 횟수에는 상한이 없습니다.
그래도 잃는 것은 없습니다. 사람이 안 건드려도 대기가 끝나는 순간 다시 후보가 됩니다. 지금까지 몇
번 쉬었고 다음이 언제인지는 티켓 상세의 재시도 대기 절에 누적 횟수와 다음 시각으로 적혀
있습니다.
대기가 끝나기를 안 기다려도 됩니다. 같은 절에 지금 다시 보내기 버튼이 있습니다. 누르면
쉬는 표식이 그 자리에서 사라지고 그 티켓이 다음 tick의 후보로 돌아갑니다. 누적 횟수도 같이
지워져서 다음에 또 죽으면 첫 대기부터 다시 셉니다. 이 절에는 답을 쓰는 칸이 없습니다. 물음이
없으니까요.
넷을 구분하는 물음은 하나입니다. 사람이 답을 쓰면 이 자리가 풀리나. 답이 예인 것은
답변 대기 하나뿐입니다. 나머지 셋이 기다리는 것은 사람이 아닙니다. deps 대기는 앞선 티켓이
끝나기를 기다리고 폴링 대기는 큐 밖의 조건을 기다립니다. 재시도 대기가 기다리는 것은
시간뿐입니다. 셋 다 기다리던 것이 오면 아무도 손대지 않아도 풀립니다. 그래서 이 배지를
보셨을 때 하실 일은 대개 없습니다.
대기 레인 하나에 deps 대기와 답변 대기와 폴링 대기와 재시도 대기가 나란히 놓입니다.
카드를 열지 않고 넷을 구분하는 기준이 배지에 적힌 이름입니다. 엔진도 같은 말을 써서,
workers/<워커>.sh list가 이 티켓을 출력하는 이름이 그대로 폴링 대기입니다.
무엇을 왜 기다리는지는 배지 아래 한 줄에 적혀 있습니다. 외부 빌드가 끝나기를 기다린다처럼
사람이 읽는 한 문장이고, 카드 폭을 넘기면 거기서 잘립니다. 티켓을 열면 폴링 대기 절의 사유
행에 같은 문장이 그대로 있습니다.
그 줄의 출처는 티켓 본문입니다. 맨 마지막 ## 결과 절의 첫 줄을 화면이 그대로 옮깁니다.
대기를 걸고 작업을 중단하는 세션이 그 자리에 한 문장으로 적는 것이 규약입니다. 여러 번 대기를 건
티켓이면 마지막 회차에 적은 문장이 지금의 사유입니다. 규약을 안 지킨 티켓에서는 이 줄이 아예
안 뜹니다. 배지만 붙은 카드가 되고 상세에도 사유 행이 없습니다.
조건 자체는 카드에 안 적습니다. 사유와 조건은 다릅니다. 사유는 세션이 쓴 한 문장이고, 조건은
스크립트가 판정합니다. 조건을 한 줄로 줄여 옮기면 스크립트와 그 줄 중 어느 쪽이 정본인지
혼동됩니다. 스크립트 본문은 티켓 상세의 폴링 대기 절에 통째로 뜹니다. 폴링 주기와
상한, 마지막으로 돌린 시각, 마지막 출력도 거기 같이 있습니다.
대신 폴링 대기에는 끝이 정해져 있습니다. 배지에 붙은 남은 시간이 그 상한까지 얼마나 남았는지를
알려 줍니다. 상한을 넘기면 배지가 상한 지남 표시로 바뀌고, 다음 tick에서 엔진이 폴링을
그만둡니다. 그다음은 설정 다이얼로그의 주도성 수준에 따라 갈립니다.
주도성 4와 5에서는 엔진이 사람에게 묻지 않고 그 티켓을 곧바로 디스패치 후보로 돌립니다. 조건이
아직 오지 않았어도 그렇습니다. 기다리던 조건은 사람이 답한다고 바뀌지 않고, 상한을 늘려도 조건이
안 오면 같은 자리로 돌아오기 때문입니다. 대신 티켓 본문 끝에 ## 폴링 만료 1 절이 붙습니다.
거기에 마지막 폴링 출력과 함께 다음 세션에게 주는 지시가 적힙니다. 같은 조건으로 다시 기다리지
말고 지금 상태에서 진행하라는 지시입니다. 이 절은 질문이 아니어서 답변 대기 배지가 붙지 않습니다.
주도성 1 - 2 - 3에서는 엔진이 그 티켓을 답변 대기로 옮깁니다. 무엇을 기다렸고 마지막 폴링에서
무엇을 봤는지가 질문에 인용으로 붙습니다. 고를 것은 셋입니다. 상한을 늘려 더 기다린다, 조건과
상관없이 지금 디스패치한다, 이 티켓을 닫는다. 직접 쓰는 칸은 여느 질문처럼 그 아래 있습니다.
상한을 넘긴 티켓을 묻게 할지 바로 다시 돌릴지 바꾸고 싶으면 설정의 주도성을 고치시면 됩니다.
엔진은 상한을 넘기는 그 tick에 설정된 수준을 읽습니다(요구사항 접수하기
§주도성 참고).
폴링 스크립트가 연속 세 번 오류로 끝났을 때는 재시도 대기로 갑니다. 문제가 생긴 대상은
스크립트이고 그것을 고치는 쪽은 다음 세션입니다.
상한까지 기다릴 이유가 없으면 그 전에 중단하시면 됩니다. 티켓 상세의 폴링 대기 절에
지금 디스패치와 상한 늘리기가 늘 떠 있습니다(화면 소개 참고). 어느 쪽을
눌러도 답변 파일은 안 생깁니다. 사람이 묻지 않은 질문이라 답도 없습니다.
deps 대기와 답변 대기와 재시도 대기는 보드 상태 필터의 선택지로도 있습니다. 필터
선택지는 일곱인데 레인은 셋이고, 그게 정상입니다. 필터는 엔진이 아는 상태를 고르는 자리이고,
레인은 흐름의 단계를 표시하는 자리이기 때문입니다. 폴링 대기만 그 일곱에 없습니다. 엔진은 이
티켓을 여전히 열린 티켓으로 세기 때문에 필터에서는 대기로 잡히고, 지금 무엇을 기다리는 중인지는
카드의 배지와 그 아래 한 줄이 알려 줍니다.
디스패치되지 않는 티켓
열려 있는데 아무 워커도 가져가지 않는 티켓의 사유는 여섯입니다. 위 넷, 그러니까 deps 대기와
답변 대기와 폴링 대기와 재시도 대기는 정상입니다. 밖의 조건이 풀리면 저절로
해소됩니다.
다섯째도 정상입니다. 우선순위 1은 진행중 티켓이 0건일 때만 후보입니다. 다른 티켓이 하나라도 돌고 있으면 순서가 와도 건너뛰고 그 세션이 끝나면 아무도 손대지 않아도 다시 후보가 됩니다. 배지도 태그도 안 붙습니다. 카드의 우선순위 미터가 한 칸만 차 있는 것이 유일한 표시입니다. 더 높은 값을 물려받은 1은 그 값으로 떠서 이 조건에 걸리지 않습니다(티켓 직접 발행하기 참고).
여섯째만 사고에 해당합니다. 열린 파일에 session_id가 적혀 있는 경우(배지 이름은
할당됨)인데, 이 조합을 엔진은 만들지 않습니다. 사람이 파일에 session_id를 손으로 적었거나, 그
값이 든 티켓을 통째로 베껴 새 티켓으로 만들었을 때 생깁니다. 그러면 이렇게 됩니다.
- 후보를 고르는 쪽은
session_id가 있으면 이미 할당됐다고 보고 영구히 건너뜁니다. - 죽은 세션을 회수하는 쪽은
.wip만 봅니다. 이 티켓은.wip이 아니니 회수 대상도 아닙니다.
아무도 가져가지 않고 회수도 되지 않은 채 아무 처리도 받지 못하고 방치됩니다. 배지 하나로는 그
화면을 열지 않는 한 영원히 모르기 때문에, 앱은 이것을 헤더 알림 종 안의 항목으로 올립니다.
아무도 가져가지 않는 티켓 <n>건.
- 프로젝트 화면 어디에 있든 보입니다. 보드에만 두면 워커 화면을 보고 있는 사람은 모릅니다.
- 필터로 끌 수 없고 상위 몇 건으로 자르지도 않습니다. 해당 티켓 전부를 나열하고 행마다
할당 해제버튼이 붙습니다. 그 버튼이 하는 일은session_id를 지우는 것 하나입니다. - 닫기도 읽음 표시도 없습니다. 보드가 5초마다 큐를 다시 읽으므로 0건이 되면 항목이 저절로 사라집니다. "해결됨"을 앱이 따로 기록하지는 않습니다. 기록하는 순간 파일의 사실과 어긋날 수 있는 두 번째 정보가 생기기 때문입니다.
이 티켓들은 칸반 어느 레인에도 없습니다. 그래서 보드의 티켓 N건 합계가 레인 합계보다 클 수
있고 그 자리에 (디스패치되지 않는 N건은 상단 알림)이 같이 적혀 있습니다.
session_id를 비롯해 디스패처와 엔진이 쓰는 키는 사람이 채우는 값이 아닙니다. 어떤 키가 있고
누가 언제 쓰는지는 frontmatter 필드에 표로 있습니다.
파일명이 곧 락인 이유
상태를 파일명에 둔 이유는 하나입니다. 이름을 바꾸는 동작 자체가 락이어서 워커 여럿이 같은 큐를 봐도 충돌이 없습니다.
잡기(claim)는 os.link로 <해시>.wip.md를 만들고 원본을 지우는 동작입니다. 목적지가 이미
있으면 커널이 EEXIST를 돌려줍니다. 이미 잡혔습니다. 판정("아직 안 잡혔나")과
획득("내가 잡는다")이 한 시스템콜 안에 있어서 그 사이에 다른 프로세스가 끼어들 틈이 없습니다.
동시에 같은 티켓을 본 워커 여섯 중 먼저 링크에 성공한 하나만 티켓을 가져가고 나머지는 다음
후보로 넘어갑니다. 새로 고안한 방식은 아닙니다. 메일 하나를 new/에서 cur/로 옮기는
rename을 락으로 쓰는 Maildir와 같습니다.
하드링크를 지원하지 않는 파일시스템(구글드라이브 같은 FUSE, SMB 마운트)에서는
O_CREAT|O_EXCL로 폴백합니다. 이것도 원자적 배타 생성이라 같은 성질을 유지합니다. 상태를
frontmatter로 옮기면 값을 읽고 고쳐 쓰는 사이가 프로세스 간에 원자적이지 않아, 파일명이
그냥 제공하던 락을 따로 구현해야 합니다.
다음은 워커입니다.