Repository navigation
fix(api): refuse a build under another owner's interrupted run id - #1027
Merged
Merged
Conversation
check_existing_run_access decided by the manifest and the job registry. A run a restart interrupted has neither, so its id looked free although the event store records who submitted it. Read that record, by the same ownership rule check_active_run_access uses. Refs #1025 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
yeongseon
approved these changes
Oct 5, 2026
yeongseon
left a comment
Collaborator
There was a problem hiding this comment.
diff 를 읽었고 CLEAN 이라 승인합니다. #1022 리뷰에서 짚은 경로를 그대로 닫습니다 — 매니페스트 → 레지스트리 → 제출 기록 순으로 같은 소유 규칙을 쓰고, 제출자 본인의 재제출은 통과시키고, 기록이 없는 id 는 새 빌드로 둡니다. 수정 전 가드에서 새 테스트가 실패하는 것도 확인돼 있습니다.
Refs 로 둔 판단도 맞습니다: 비동기 POST /builds 는 #1008 이 이 함수를 연결해야 닫힙니다. 이 PR 이 먼저 들어가면 #1008 은 _guards.py 의 docstring 에서 충돌할 수 있는데, 그쪽 브랜치는 제가 main 을 합쳐 맞추겠습니다.
로컬에서 테스트를 돌리지는 않았습니다.
Eomdahyeon
added a commit
that referenced
this pull request
Oct 5, 2026
Closes #1025 #1027 이 `check_existing_run_access` 가 제출 기록을 보게 했고, #1008 이 비동기 `POST /builds` 를 그 가드로 연결했다. 이슈가 적은 경로(다른 사용자가 중단된 run 의 id 로 **비동기** 제출)는 그 둘이 합쳐져야 닫히므로, #1027 은 `Refs` 로 두고 이 테스트를 #1008 뒤로 미뤘었다. 코드 변경은 없다. ## 테스트 (`tests/unit/test_interrupted_run_status.py`) - 다른 사용자가 중단된 run id 로 `POST /builds` → 403 `forbidden: not run owner`. 레지스트리에 작업이 생기지 않고, 그 사용자는 여전히 그 run 의 이벤트를 404 로 받으며, 원래 제출자가 읽는 이벤트에 두 번째 `run_submitted` 가 없고 제출 기록의 소유자가 그대로다. - 제출자 본인은 같은 id 로 다시 제출할 수 있다 (202) — 중단된 run 의 메시지가 안내하는 동작. ## 검증 - 가드의 제출 기록 확인을 임시로 꺼 두고 돌리면 새 테스트가 실패한다: `FAILED …::test_another_user_cannot_submit_under_the_interrupted_run_id` (동기 경로 테스트와 함께 `2 failed, 9 passed`). 되돌린 뒤 통과. - `pytest tests/unit/test_interrupted_run_status.py tests/unit/test_json_array_reader.py` 를 5회 → 5회 모두 `28 passed`. (이 fixture 가 한 번 메모리 측정 테스트를 건드린 적이 있어 함께 돌렸다.) - 처음 쓴 단언 하나는 버렸다: 제출 전후의 이벤트 목록 전체를 비교했더니 3회 모두 실패했다. 테스트의 "첫 프로세스" 워커가 gate 에 닿기 전까지 자기 이벤트를 계속 쓰기 때문이다 — 실제 재시작에서는 죽었을 스레드다. 그래서 `run_submitted` 개수와 소유자만 본다. - `ruff check tests`, `ruff format` 통과. 전체 스위트는 CI 에 맡긴다. CHANGELOG 의 #1025 항목에서 "비동기 경로는 #1008 뒤에" 라는 문장을 지금 사실로 고쳤다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 5, 2026
yeongseon
pushed a commit
that referenced
this pull request
Oct 5, 2026
Refs #1042 — 1부(재사용 거절). `retry_of` 는 이슈의 Notes 에 적은 제안을 확인받은 뒤 따로 올린다. ## 왜 kpubdata#812 §3: **한 `run_id` 는 한 번의 시도다.** 종료 이벤트가 붙으면 바뀌지 않고, 재시도는 새 `run_id` 를 받는다. 그 기록이 직접 짚었듯 제가 넣은 #1027 과 #1037 이 반대로 동작한다 — 재시작으로 중단된 run 의 제출자가 같은 id 로 다시 빌드할 수 있었고, 그러면 두 번째 시도의 이벤트가 첫 시도의 `run_failed` 뒤에 붙어 서로의 종료가 상대의 상태로 읽힌다. ## 변경 - `routes/_guards.py` `check_existing_run_access`: 매니페스트도 레지스트리 항목도 없는데 제출 기록이 있는 id 는 **제출자 본인에게도** 거절한다. 다른 사용자는 전처럼 403. - `POST /builds` → **409** `{"error": "run_id already ended; submit the retry under a new run_id", "run_id": …}`. 완료된 run 의 id 가 이미 받는 상태 코드다. - `POST /build` → **400**, 같은 본문. - 중단된 run 의 메시지: "…; submit it again **under a new run_id**". - 레지스트리가 아직 들고 있는 작업(대기·실행 중, 매니페스트 없이 실패한 것)은 그대로다 — 그 id 를 대면 그 작업을 돌려준다(ADR 0008). - 계약 1.83.0 (additive): 두 라우트의 설명에 이 경우를 적었다. 이름 붙은 예시는 더하지 않았다 — Studio 의 drift 검사에 새 항목이 필요 없다. ## `POST /build` 가 409 가 아니라 400 인 이유 처음에는 두 라우트 모두 409 로 했다. `check_contract_compat.py` 가 막았다: ``` error: the change breaks existing clients, which needs a MAJOR version raise … - POST /build 409: $ref #/components/schemas/BuildSuccessResponse -> None ``` 그 라우트의 409 는 "빌드는 됐지만 테이블이 커밋되지 않음"(#788)의 **빌드 응답**으로 선언돼 있어서, 거기에 오류 본문을 섞으면 409 를 빌드 응답으로 파싱하는 클라이언트가 깨진다. 그래서 그 라우트에서는 이미 `Error` 본문으로 선언된 400 을 쓴다. 상태 코드가 두 라우트에서 다른 것은 깔끔하지 않다 — 다른 선택(예: `POST /build` 의 409 를 major 로 바꾸기)이 맞다고 보시면 말씀해 달라. ## 테스트 (`tests/unit/test_interrupted_run_status.py`) **예전 동작을 고정하던 두 테스트를 바꿨다** — `test_the_submitter_can_submit_the_interrupted_run_again`(202 기대)와 `test_the_submitter_may_use_the_interrupted_run_id_again`(가드 통과 기대). 결정이 뒤집은 바로 그 동작이다. - 제출자가 같은 id 로 `POST /builds` → 409, 작업이 생기지 않고, 그 run 은 여전히 `failed` / `credentials_required` 로 읽히고, `run_submitted` 이벤트는 1개다. - `POST /build` → 400, 같은 본문. - **새 id 로는 202** — 재시도 자체는 된다. - 중단 메시지가 "under a new run_id" 로 끝난다. - 다른 사용자의 403, 제출 기록이 없는 id 의 통과는 그대로다(기존 테스트). ## 검증 - 가드만 고치고 테스트를 그대로 두면 위 두 테스트가 실패했다(`2 failed, 9 passed`) — 바뀐 것이 그 동작임을 보여 준다. - 관련 5개 파일 → `659 passed`. 새 테스트 파일을 메모리 측정 테스트와 3회 → 3회 모두 `30 passed`. - `check_contract_compat.py --base origin/main` → `contract compatible with origin/main: 1.82.0 -> 1.83.0`. - Studio `main` 의 `contractDrift.test.ts` 를 이 브랜치의 계약(1.83.0)으로 → `Tests 577 passed (577)`. Studio 쪽 선행 변경은 필요 없다. - `ruff`, `mypy src` 통과. 전체 스위트는 CI 에 맡긴다. ## 동작 변경 — 알아 둘 것 - 중단된 run 을 예전 id 로 다시 제출하던 클라이언트는 이제 409/400 을 받는다. Studio 는 재시도에 같은 `run_id` 를 보내지 않는 것으로 보인다(`features/` 에서 그런 경로를 찾지 못했다 — 전수 확인은 아니다). - 제출 기록은 있는데 종료 이벤트가 아직 없는 id(프로세스가 죽은 직후, 재기동 때 중단으로 표시되기 전)도 같은 답을 받는다. 재기동하면 곧 중단으로 표시되므로 따로 가르지 않았다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #1025
문제
#1022 리뷰에서 짚은 경로다.
check_existing_run_access는 매니페스트와 작업 레지스트리만 본다. 재시작으로 중단된 run 은 둘 다 없어서 그run_id가 "새 run" 으로 통과하는데, 이벤트 저장소에는 제출자가 남아 있고(#996) 그 사람은 여전히 그 run 이 왜 끝났는지 읽을 수 있다. 다른 사용자가 그 id 로 빌드하면 자기 작업이 도는 동안 첫 제출자의 이벤트를 읽고, 다시 중단되면 자기 실패를 첫 제출자에게 남긴다.변경
routes/_guards.py:check_existing_run_access가 매니페스트 → 레지스트리 다음에 제출 기록을 본다.check_active_run_access와 같은 소유 규칙(ownership_allows)이다.forbidden: not run owner(이 함수의 기존 거절과 같은 본문)[Unreleased]Fixed. 계약은 바꾸지 않았다(403 은 이미 선언된 응답이고 본문도 같다).#1008 과의 관계
지금
main에서 이 함수를 부르는 것은 동기POST /build뿐이다(routes/core.py). 이슈가 적은 비동기POST /builds경로는 #1008 이 같은 함수를 연결해야 닫힌다. 그래서Closes가 아니라Refs로 두었다 — #1008 머지 뒤POST /builds로 같은 시나리오를 고정하는 테스트를 더하고 이슈를 닫겠다. #1008 은routes/builds.py와 이 파일의 주석 두 곳을 바꾸고 이 PR 은 함수 본문을 바꾸므로, 어느 쪽이 먼저 들어가도 충돌은 작을 것으로 본다(직접 합쳐 보지는 않았다).테스트
tests/unit/test_interrupted_run_status.py에 3개:POST /build→ 403, 제출 기록의 소유자는 그대로검증
FAILED …::test_another_user_cannot_build_under_the_interrupted_run_id,1 failed, 8 passed.pytest tests/unit/test_interrupted_run_status.py tests/unit/test_json_array_reader.py tests/unit/test_service.py tests/unit/test_service_jobs.py→235 passed.ruff check src tests,ruff format --check src tests,mypy src통과.🤖 Generated with Claude Code