본문으로 건너뛰기

6. 함정

「빌드 캐시」로 돌아가기


이 절이 이 문서의 본체입니다.

6.1 스윕이 캐시된 파일을 삭제합니다

StagingFiles.Sweep이 실행이 쓰지 않은 생성 파일을 삭제합니다. 이름이 바뀐 테이블의 낡은 파일을 제거하는 장치이고, 판정 근거는 「이 실행이 쓴 파일 집합」입니다.

타깃 하나를 건너뛰면 그 타깃의 파일 수천 개가 「이 실행이 쓰지 않은 것」이 됩니다. 커밋 직후 전부 삭제됩니다. 익스포터는 매니페스트를 근거로 같은 일을 합니다 (Manifest.PruneStaleFiles).

그래서 건너뛴 타깃의 출력 목록을 인장에서 읽어 「이 실행이 쓴 것」에 포함시켜야 합니다. 이것은 부수 작업이 아니라 캐시를 켜는 변경과 같은 커밋에 들어가야 하는 요구사항이고, 게이트가 필요합니다(§9).

그리고 반대쪽도 있습니다 — 건너뛴 실행도 스윕은 해야 합니다. 아무것도 하지 않고 종료하면 스윕에 도달하지 못하므로, 더 이상 생성되지 않는 파일이 남습니다. 그러면 「테이블을 지웠을 때 그 파일이 없어지는가」가 그날 다른 것이 바뀌었는지에 달리게 됩니다. 없어진 테이블의 이름을 가진 파일이 조용한 한 주를 넘겼다고 해서 무해해지지 않습니다.

그래서 인장은 스윕할 디렉터리도 기록합니다 — 타깃만이 자기 산출 위치를 알고, 건너뛴 실행은 타깃에 도달하지 않기 때문입니다. 허가 조건은 그대로입니다: 그 디렉터리 아래에 있고, 파일 스스로 Generated by Tabbit이라고 적고 있는 것. 「이 실행이 쓴 것」의 자리에는 인장의 출력 목록이 들어가고, 그 목록은 방금 전부 확인한 그 목록입니다.

6.2 히스토리 지문을 캐시 키로 재사용하면 안 됩니다

ModelFingerprint는 모델·테이블·행 3단의 내용 해시를 이미 만듭니다. 그것을 캐시 키로 쓰고 싶어지는데, 틀립니다.

그 지문이 판정하는 것은 「데이터가 같은가」이고 캐시가 판정해야 하는 것은 「산출이 같은가」입니다. 둘이 갈라지는 자리가 실제로 있습니다.

자리지문산출
enum 라벨의 코멘트없습니다 — hash.Add(label.Name).Add(value)가 전부입니다 (ModelFingerprint.cs:92)들어갑니다 — csharp-enum.sbn:22label.comment
enum 자체의 코멘트없습니다 — hash.Add(enumm.Name)뿐입니다들어갑니다 — 같은 템플릿의 enumm.comment

시트의 코멘트만 고치면 지문은 그대로이고 생성 코드는 달라집니다. 지문을 캐시 키로 쓰면 낡은 주석이 붙은 코드가 그대로 남습니다.

지문 쪽이 틀린 것이 아닙니다. 히스토리에서는 코멘트 수정이 데이터 변경이 아닌 것이 맞고, 그렇게 되어 있는 것이 의도입니다. 결론은 지문을 고치는 것이 아니라 캐시는 입력을 키로 삼는다는 것입니다. 입력이 같으면 산출이 같다는 것은 변환이 결정적이라는 것 하나만 요구하고, 무엇이 산출에 영향을 주는지 목록을 유지할 필요가 없습니다. 그 목록은 유지되지 않으며, 유지되지 않았을 때의 증상이 위의 표입니다.

6.3 검증은 외부 상태를 읽습니다

rules/runtime/의 규칙은 데이터베이스와 캐시를 읽습니다. 시트가 그대로여도 그쪽이 바뀌면 결과가 달라집니다. 그래서 인장은 「산출이 같다」는 보장할 수 있지만 「검증이 여전히 통과한다」는 보장할 수 없습니다.

정한 것은 3가지입니다.

  • --validate-only는 캐시를 사용하지 않습니다.
  • 변환에서 인장이 맞으면 검증도 함께 건너뛰고, 건너뛰었다고 보고합니다.
  • 매 실행 외부 검증이 필요한 곳은 --validate-only를 별도로 실행합니다.

6.4 출력이 손으로 변경된 것

인장의 출력 목록을 크기와 해시로 확인합니다. 하나라도 없거나 다르면 전체 실행입니다. recipe-all의 출력이 9,470개이므로 이 확인의 비용을 §8의 0단계에서 측정해야 하고, 비용이 문제가 되면 「크기만 확인」과 「전부 해시」를 갈라야 합니다.

6.5 구글 시트는 스코프를 추가해서 확인합니다

파일이 아니므로 크기와 수정 시각이 없습니다. 동의를 다시 받고 Drive의 메타데이터를 읽는 것이 답입니다 — 가져오기 전에 확인할 수 없는 것으로 두면 구글 시트를 쓰는 레시피는 무변경 재실행이라는 이 문서의 목적 자체를 얻지 못합니다.

무엇
추가하는 스코프drive.metadata.readonly
호출files.get(SheetsId, fields: "version,modifiedTime") — 문서당 1회
인장에 기록하는 것version

modifiedTime이 아니라 version을 비교합니다. Drive의 version은 서버에서 일어난 모든 변경에 증가하는 단조 증가 카운터입니다. 시각을 비교하는 것은 §3의 「크기와 수정 시각」이 가진 구멍을 네트워크 건너편에 다시 만드는 것이고, 카운터는 그 구멍이 없습니다. 사용자에게 보이지 않는 변경에도 증가하므로 필요 없는 전체 실행이 간혹 발생합니다 — 안전한 방향입니다. headRevisionId는 쓸 수 없습니다. 이진 내용을 가진 파일에만 있고 구글 시트 문서에는 없습니다.

동의는 계정 종류에 따라 다릅니다. 적어 두지 않으면 두 계정 중 하나에서 예상하지 못한 동작을 만나게 됩니다.

계정 종류무엇이 필요한가
서비스 계정아무것도. 키를 코드에서 스코프에 붙이므로(CreateScoped) 대화형 단계가 없습니다. 워크스페이스가 도메인 전체 위임의 스코프를 허용 목록으로 관리하면 관리자가 한 줄 추가합니다
개인 OAuth브라우저가 저절로 열리지 않습니다. FileDataStore에 캐시된 토큰은 스코프를 비교하지 않고 그대로 사용되므로, 시트 읽기는 되고 Drive 호출만 403으로 거부됩니다. 사람이 토큰을 지워야 다음 실행이 동의를 다시 받습니다 — 그러니 무엇을 해야 하는지 알려 주는 것은 도구의 몫입니다(§6.5.1)

스코프가 넓어지는 것을 함께 적습니다. drive.metadata.readonly는 그 계정이 접근할 수 있는 모든 파일의 메타데이터를 포함합니다. 내용은 포함하지 않고, drive.readonly보다 좁습니다. 서비스 계정에서는 공유된 문서로 한정되므로 이 점에서도 서비스 계정이 나은 선택입니다 (다중 사용자 운영).

조회가 안 되면 일단 가져오는 것을 감수합니다. 스코프가 거부되었거나, 관리자가 막았거나, 프로젝트에서 Drive API가 켜져 있지 않거나, 네트워크가 닿지 않는 경우입니다. 그때는 바뀐 것으로 보고 가져옵니다 — 조회 하나가 안 되는 것 때문에 변환을 멈추는 쪽이 더 나쁩니다. 캐시는 최적화이고, 최적화가 실패했을 때의 결과는 잘못된 산출이 아니라 느린 실행이어야 합니다. 그리고 왜 느려졌는지 보고합니다(§7) — 조용히 매번 느린 것은 원인을 찾을 수 없는 쪽입니다.

6.5.1 무엇을 해야 하는지 알려 주는 메시지

가져오는 것을 감수하는 것과 왜 감수하고 있는지 알 수 없는 것은 다릅니다. 후자는 매 실행 느려지는데 원인이 어디에도 적히지 않는 상태이고, 그러면 동의 한 번으로 끝날 일이 「원래 느린 도구」가 됩니다. 그래서 이 실패는 Debug가 아니라 Warning이고, 조치를 함께 적습니다.

원인마다 조치가 다르므로 응답의 사유를 보고 갈라야 합니다. 갈리지 않으면 후보를 함께 적습니다 — 후보 3개가 「읽지 못했습니다」보다 낫습니다.

개인 OAuth의 토큰이 옛 스코프인 경우. 가장 자주 나올 자리입니다.

[W] [Caching ] Could not read the version of document `1aB2...`: this token was issued
before Tabbit asked for `drive.metadata.readonly`, so Google refused the
metadata call (403).
[W] [Caching ] Delete `~/.credentials/sheets.googleapis.com-tabbit` and run again. The
next run opens a browser once to ask for the new consent.
[I] [Caching ] Importing the document meanwhile - and every run will, until it is granted.

마지막 줄이 이 절의 이유입니다. 한 번의 동의로 끝나는 일임을 그 자리에서 알 수 있어야 합니다. 그리고 지울 경로는 실제 경로를 적습니다 — 이 도구가 그 경로를 정하므로 (GoogleSheetsCredentials.Personal), 읽는 사람이 찾아야 할 이유가 없습니다.

서비스 계정이 스코프를 받지 못한 경우.

[W] [Caching ] Could not read the version of document `1aB2...`: the service account is
not allowed `drive.metadata.readonly` (403). If your workspace lists which
scopes a delegated account may use, an administrator adds it there.

자격 증명이 속한 프로젝트에서 Drive API가 켜져 있지 않은 경우.

[W] [Caching ] Could not read the version of document `1aB2...`: the Drive API is not
enabled for the project this credential belongs to (403). Enabling it once
is all this needs - Tabbit only reads the document's version number.

일시적인 실패는 조용히 넘어갑니다. 네트워크가 닿지 않거나 5xx가 온 경우입니다. 조치할 것이 없는 자리에 경고를 내면 조치할 것이 있는 위의 3가지가 묻힙니다.

어디에무엇
수준조치가 있으면 Warning, 일시적 실패는 Information
횟수실행당 1회. 문서 5개가 같은 이유로 실패한 것은 같은 조치 하나이므로, 문서 수를 세어 한 번에 보고합니다
조건캐시를 사용하는 실행에서만. --full은 조회하지 않으므로 할 말이 없습니다

6.5.2 그래도 두 시점이 남습니다

version으로 임포트 전 확인이 되므로 구글 시트도 무변경 재실행을 건너뜁니다. 그러나 인장 확인이 두 시점으로 갈리는 구조 자체는 남습니다.

시점무엇을 확인하는가건너뛰는 것
임포트 전파일 입력 · 문서의 version · 레시피 · 옵션 · 도구 버전전부
임포트 후가져온 원시 격자의 해시쿠킹 · 검증 · 산출

두 번째가 필요한 자리가 둘 있습니다. 하나는 위의 실패 경로입니다 — 조회가 안 되어 가져왔는데 격자가 지난번과 같은 경우, 그 뒤는 건너뛸 수 있습니다. 다른 하나는 version의 과잉 무효화입니다 — 권한만 바뀌어 카운터가 올라간 문서는 격자가 같습니다. 그리고 이 구조는 3단계 (§8)의 원시 격자 캐시가 붙는 자리와 같습니다.

6.6 나머지

함정무엇
동시 실행한 인장에 두 실행이 씁니다. 인장은 스테이징을 통해 원자적으로 씁니다 — 반쯤 기록된 인장을 읽는 일이 없어야 하고, 읽을 수 없는 인장은 전체 실행으로 처리합니다
인장이 커밋되는 것절대 경로와 기계별 상태를 담습니다. .gitignore에 넣습니다 — logs/가 이미 그렇게 되어 있습니다
「아무것도 안 함」이 「성공」으로 보이는 것§7
골든이 캐시 때문에 갱신되지 않는 것골든 재기록이 캐시를 읽으면 낡은 골든이 그대로 통과합니다. 재기록 절차는 --full을 씁니다(§9)