系譜と再現 — 二度動かしてバイトが同じか
한국어 원문으로 표시합니다.
목표
매일 떨어지는 주문 드롭을 집계하는 변환기 lineage.py 를 만든다. 산출물마다 어떤 입력(내용 해시)과 어떤 코드와 어떤 매개변수로 만들어졌는지를 매니페스트에 남기고, 같은 입력으로 두 번 돌려 바이트가 같은지를 증명하고, 칼럼 하나가 어느 입력 칼럼에서 왔는지를 실험으로 확인한다.
왜 중요한가
보고서 숫자가 회계와 어긋났을 때 답해야 하는 질문은 하나다 — 이 숫자는 어디서 왔나. 파일 이름과 수정 시각으로는 답할 수 없다. 이름은 같은데 상류가 덮어쓴 경우가 흔하고, 수정 시각은 복사만으로도 바뀐다. 근거가 되는 것은 내용 그 자체의 해시다. 그리고 "다시 돌려 보자" 가 통하려면 같은 입력이 같은 출력을 내야 한다. 그것은 저절로 되지 않는다. 산출물에 적어 넣은 현재 시각, 실행마다 새로 뽑은 식별자, 정렬하지 않은 파일 목록과 집합 순회, 부동소수점을 더하는 순서가 조용히 재현을 깨뜨린다. 이 실습은 그 넷을 하나씩 찾아 없앤다. 계보에는 굵기도 있다. 자료 집합 단위는 "이 표는 저 세 파일에서 나왔다" 까지이고, 칼럼 단위는 "이 칼럼은 저 입력 칼럼들에서 나왔다" 까지다. 상류가 칼럼 하나를 바꾼다고 알려 왔을 때 답할 수 있는 굵기는 뒤엣것이다. 그리고 칼럼 계보는 적어 두면 곧 낡으므로 실험으로 다시 잰다. 채점기는 여러분이 적어 낸 문구를 믿지 않는다. 임시 디렉터리에 채점기가 만든 드롭을 차려 놓고 여러분의 변환기를 실제로 실행해 집계 값과 매니페스트를 채점기가 직접 센 값과 대조하고, 두 번 돌려 바이트를 비교하고, 입력 칼럼을 하나씩 흔들어 여러분이 적어 낸 칼럼 계보와 맞는지 확인한다. 상호와 금액은 실행마다 바뀐다.
단계
- /root/lineage/gen_drops.py 를 만들어 실행해 /root/lineage/drops 아래 날짜별 드롭 세 개를 만드세요.
- /root/lineage/lineage.py 에
digest를 만들어 내용 해시와 크기와 레코드 수를 내게 하세요. run을 더해 집계 산출물 /root/lineage/out/shops.csv 를 내게 하세요.run --manifest로 매니페스트 /root/lineage/out/shops.manifest.json 을 같이 내게 하세요.- 두 번 돌려 바이트가 같아지게 고치고, 그 결과를 /root/lineage/repro.json 에 남기세요.
columns를 더해 칼럼 단위 계보를 내고 /root/lineage/columns.json 에 남기세요.verify를 더해 매니페스트대로 다시 만들어 대조하게 하세요.trace를 더하고 /root/lineage/lineage_report.md 를 쓰세요.
참고
- 실행 계약:
python3 /root/lineage/lineage.py <명령> .... 답은 JSON 한 덩어리로 표준출력에 냅니다. 성공하면 종료 코드 0, 파일이 없으면 3,verify·trace가 어긋나면 4, 사용법이 틀리면 2 입니다. - 드롭 파일 이름은
orders-<날짜>.csv이고 머리글은order_id,shop,qty,amount,dropped_at입니다.dropped_at은 집계가 쓰지 않는 칼럼입니다. - 집계 규칙(순서가 답을 바꿉니다): 파일을 이름순으로 읽고, 같은
order_id는 처음 나온 줄만 남기고(중복 제거를 먼저 합니다), 그다음qty가min_qty미만인 줄을 버리고,shop으로 묶습니다.min_qty기본값은 1 입니다. - 산출물 머리글은
shop,orders,qty,amount_cents이고shop오름차순으로 정렬합니다. 줄 끝은 LF 입니다.amount_cents는amount를 정수 센트로 바꿔 더한 값입니다 — 부동소수점을 안 쓰면 더하는 순서가 답을 못 바꿉니다. digest응답:{"path": 절대경로, "sha256": 문자열, "bytes": 정수, "rows": 정수}.rows는 머리글을 뺀 레코드 수입니다.run응답:{"out": 절대경로, "rows": 정수, "sha256": 문자열, "min_qty": 정수}이고 매니페스트를 냈으면manifest와run_id가 더해집니다.- 매니페스트 키:
tool·code_sha256·drops_dir·params·inputs·output·run_id·created_at.inputs는 이름순 목록이고 항목마다name·sha256·bytes·rows입니다.output은name·path·sha256·rows입니다.code_sha256은lineage.py자신의 내용 해시입니다. - 재현 비교에서 빠지는 칸은
created_at하나뿐입니다. 나머지는 두 번 돌려도 같아야 합니다. columns응답과columns.json: 출력 칼럼 이름을 키로, 그 칼럼이 기대는 입력 칼럼 이름의 정렬된 목록을 값으로 둡니다. 판정 기준은 실험입니다 — 그 입력 칼럼을 흔들었을 때 이 출력 칼럼의 값이 바뀌면 의존이 있는 것이고, 출력의 줄(열쇠) 자체가 달라지면 모든 출력 칼럼이 그 입력 칼럼에 기댑니다.verify응답:{"manifest": 절대경로, "ok": 참거짓, "input_missing": [이름], "input_changed": [이름], "code_changed": 참거짓, "output_changed": 참거짓}. 하나라도 어긋나면 종료 코드 4 입니다.trace응답:{"key": 상호, "orders": 정수, "qty": 정수, "amount_cents": 정수, "sources": [{"name": 이름, "sha256": 문자열, "rows": 정수}]}.sources는 그 상호의 줄을 실제로 보탠 파일만 이름순으로 담고,rows는 그 파일이 보탠 줄 수입니다. 없는 상호면 종료 코드 4 입니다.- 재현을 깨뜨리는 것: 산출물 안의 현재 시각, 난수로 뽑은 식별자,
os.listdir와 집합(set)의 순회 순서, 부동소수점을 더하는 순서. 파이썬은 실행마다 문자열 해시 씨앗이 달라서 집합 순회 순서가 실행 사이에 바뀝니다. - 공식 문서: hashlib · csv · json · os.listdir · PYTHONHASHSEED
- 흔한 실수: 파일 이름과 수정 시각으로 같음을 판정하기, 매니페스트를 나중에 붙이기, 기본값 매개변수를 안 적기, 정렬 없이
os.listdir쓰기. - 두 번 돌린 결과를 눈으로 비교하려면
cmp -s 파일A 파일B또는sha256sum 파일A 파일B를 쓰세요.
매일 떨어지는 드롭 만들기
/root/lineage/gen_drops.py 를 만들어 실행해 /root/lineage/drops 아래 날짜별 드롭 세 개를 만드세요. 머리글은 order_id,shop,qty,amount,dropped_at 이고, 수량이 0 인 줄과 같은 전표가 금액만 바뀐 채 다시 온 줄이 섞여 있어야 합니다.
현장에서는 파일이 먼저 있습니다. 여기서는 그 파일을 우리가 만듭니다. 상호는 넷 이상, 상호마다 받아들여질 줄이 셋 이상 있어야 하고, 전표 번호 하나는 서로 다른 두 파일에 나와야 합니다. dropped_at 은 집계가 쓰지 않는 칼럼이니 값이 여러 가지면 됩니다.
이름이 아니라 내용으로 같음을 판정하기
/root/lineage/lineage.py 에 digest <파일> 을 만들어 path·sha256·bytes·rows 를 JSON 으로 내게 하세요. rows 는 머리글을 뺀 레코드 수입니다.
파일을 바이너리로 조각내어 읽어 hashlib.sha256 에 넣습니다. 같은 내용을 다른 이름으로 복사하거나 touch 로 수정 시각만 바꿔 보고, 해시가 그대로인지 직접 확인하세요. 레코드 수는 줄 수가 아니라 csv 로 세어야 합니다.
집계 산출물 내기
run --drops <디렉터리> --out <파일> [--min-qty N] 을 더해 /root/lineage/out/shops.csv 를 내게 하세요. 파일을 이름순으로 읽고, 같은 전표는 처음 나온 줄만 남기고, 그다음 qty 가 min_qty 미만인 줄을 버리고, shop 으로 묶어 shop,orders,qty,amount_cents 를 상호 오름차순으로 씁니다.
중복 제거를 수량 필터보다 먼저 한다는 점이 답을 바꿉니다 — 처음 나온 줄이 수량 0 이면 그 전표는 통째로 빠집니다. 금액은 문자열을 정수 센트로 바꿔 더하세요. 파일 목록은 정렬해서 읽습니다. os.listdir 의 순서는 약속된 것이 아닙니다.
산출물과 매니페스트를 같이 내기
run --manifest <파일> 을 더해 /root/lineage/out/shops.manifest.json 을 같이 내게 하세요. tool·code_sha256·drops_dir·params·inputs·output·run_id·created_at 여덟 키를 담습니다.
입력은 이름순 목록으로 name·sha256·bytes·rows 를 담고, code_sha256 은 lineage.py 자신의 해시입니다. 매개변수는 기본값도 적으세요 — 기본값은 나중에 바뀝니다. 나중에 붙이면 반드시 빠지므로 산출물과 같은 함수 안에서 만드세요.
두 번 돌려 바이트가 같은가
같은 입력으로 두 번 돌렸을 때 산출물의 바이트가 같고 매니페스트가 created_at 을 뺀 모든 칸에서 같아지도록 고치세요. 그리고 두 번 돌린 결과를 /root/lineage/repro.json 에 run_a·run_b·identical·manifest_diff_keys 로 남기세요.
지금 판은 실행 식별자를 난수로 뽑고 있어서 매니페스트가 매번 다릅니다. 식별자는 내용에서 파생하세요 — 코드 해시와 매개변수와 출력 해시를 이어 붙여 해시하면 같은 실행은 같은 이름을 갖습니다. 파일 목록과 그룹 목록을 정렬했는지도 다시 보세요. 파이썬은 실행마다 문자열 해시 씨앗이 달라서 집합 순회 순서가 실행 사이에 바뀝니다.
칼럼 하나가 어디서 왔는지 실험으로 재기
columns 를 더해 출력 칼럼마다 기대는 입력 칼럼의 정렬된 목록을 내게 하고, 같은 내용을 /root/lineage/columns.json 에 남기세요.
적어 두는 것으로 끝내지 말고 실험으로 확인하세요 — 입력 칼럼 하나를 흔들어 다시 돌리고 어느 출력 칼럼이 바뀌는지 봅니다. 상호를 바꾸면 출력의 줄 자체가 달라지므로 모든 칼럼이 거기에 기댑니다. 전표 번호를 다른 줄과 같게 만들면 그 줄이 중복으로 빠지므로 건수와 수량과 금액이 함께 움직입니다. 집계가 아예 읽지 않는 칼럼은 어디에도 나오지 않아야 합니다.
매니페스트대로 다시 만들어 대조하기
verify <매니페스트> 를 더해 입력 해시와 코드 해시를 지금 값과 견주고, 매니페스트의 입력과 매개변수로 다시 만들어 출력 해시를 대조하게 하세요. 하나라도 어긋나면 종료 코드 4 입니다.
입력은 없어진 것과 내용이 바뀐 것을 갈라 보고하세요 — 조사하는 사람에게는 다른 이야기입니다. 매니페스트에 적힌 매개변수로 다시 돌려야 합니다. 지금 기본값으로 돌리면 그때와 다른 답이 나와도 원인을 못 짚습니다.
그 숫자가 어느 입력에서 왔는지 답하기
trace --manifest <파일> --key <상호> 를 더해 그 상호의 건수와 수량과 금액, 그리고 줄을 실제로 보탠 입력 파일을 이름과 내용 해시와 줄 수로 내게 하세요. 그리고 /root/lineage/lineage_report.md 에 ## 이 숫자는 어디서 왔나 ## 이름과 시각은 왜 근거가 못 되나 ## 두 번 돌려 같았는가 ## 칼럼 하나가 어디서 왔나 네 절로 쓰세요.
sources 에는 그 상호의 줄을 실제로 보탠 파일만 담습니다 — 읽기는 했지만 한 줄도 안 보탠 파일은 계보가 아닙니다. 해시는 매니페스트에 적힌 값을 그대로 쓰세요. 보고서에는 숫자를 적으세요, 조사하는 사람이 자기 파일에서 그 줄을 열어 볼 수 있어야 합니다.