프로젝트를 나눴어도 실행 중 표시를 전역 변수 하나에 보관하면 서로 기다리거나 서로의 상태를 지울 수 있다. 북극성의 프로젝트별 실행 차선에서 핵심 규칙을 꺼내 독립 Python 예제로 만들었다. 외부 작업을 실행하지 않고, 누가 실행 자리를 차지했고 언제 그 자리를 비울 수 있는지만 보여 준다.
프로젝트마다 하나의 자리를 둔다
claim은 프로젝트와 실행 이름을 받아 Lease를 만든다. 같은 관리자의 같은 프로젝트가 사용 중이면 두 번째 요청은 PROJECT_BUSY로 거절한다. 다른 프로젝트에는 독립된 자리가 있으므로 동시에 소유할 수 있다. 먼저 온 요청을 자동 대기열에 넣거나 나중에 재실행하는 기능은 만들지 않았다.
이름이 같은 실행을 다시 보냈다고 기존 소유권을 돌려주지도 않는다. 재접속과 새 실행의 구분은 별도의 실행 원장이 맡아야 한다. 여기서 편의상 재사용을 허용하면 요청자가 이름만 맞춰 실행을 이어받는 모양이 된다. 이름은 화면에서 알아보기 위한 표식이고 실제 해제에는 별도 소유 증표를 사용한다.
오래된 실행의 finally를 먼저 의심했다
이전 실행이 끝난 뒤 같은 프로젝트에 새 실행이 들어갈 수 있다. 그때 이전 실행의 늦은 정리 함수가 프로젝트 이름만 보고 상태를 지우면 새 실행이 없는 것처럼 보인다. 그래서 토큰의 객체 동일성과 실행 이름을 현재 보관 중인 Lease와 함께 비교한다. 이름을 재사용해도 새 토큰은 이전 토큰과 다르다.
토큰은 view에 노출하지 않고 Lease의 표현에서도 숨겼다. 그러나 같은 Python 프로세스의 메모리를 읽을 수 있는 코드를 막는 보안 장치는 아니다. 이 예제의 소유권은 실수로 섞인 호출을 구별하는 규칙이다. 서로 신뢰하지 않는 사용자를 격리하려면 실제 인증과 프로세스 경계가 추가로 필요하다.
중지 요청을 실행 종료로 바꾸지 않는다
request_stop은 stop_requested 상태만 남기고 자리를 계속 점유한다. 이 함수에는 실제 프로세스나 브라우저를 종료할 방법이 없다. 중지 버튼을 누른 직후 다음 실행을 들이면 이전 요청이 외부에서 끝나지 않은 채 두 작업이 겹칠 수 있다. 실제 취소 전달과 종료 확인은 연결 계층의 역할로 남겼다.
종료 직전에 중지를 요청했지만 이미 작업이 성공했을 수도 있다. release가 succeeded와 stop_requested를 함께 남길 수 있도록 한 이유다. 최종 결과를 호출자가 선언한다는 한계도 영수증에 드러냈다. work_executed_here는 항상 거짓이다. 이 영수증은 메타데이터 정리의 기록이며 실제 업무 성공을 인증하지 않는다.
코드와 구현 살펴보기
독립 합성 전체 구현
"""One-process project lanes with ownership and explicit stop-request state."""
from dataclasses import dataclass, field
from threading import RLock
import re
class LaneError(ValueError):pass
def _label(value):
if not isinstance(value,str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,79}',value):raise LaneError('INVALID_LABEL')
@dataclass(frozen=True,eq=False)
class Lease:
project:str
run:str
_token:object=field(repr=False)
class ProjectLanes:
def __init__(self):
self._lock=RLock()
self._active={}
def claim(self,project,run):
_label(project);_label(run)
with self._lock:
if project in self._active:raise LaneError('PROJECT_BUSY')
lease=Lease(project,run,object())
self._active[project]={'lease':lease,'stop_requested':False}
return lease
def _owned(self,lease):
if type(lease) is not Lease:raise LaneError('INVALID_LEASE')
item=self._active.get(lease.project)
if not item or item['lease']._token is not lease._token or item['lease'].run!=lease.run:raise LaneError('LEASE_NOT_CURRENT')
return item
def request_stop(self,lease):
with self._lock:
item=self._owned(lease)
item['stop_requested']=True
return self.view(lease.project)
def release(self,lease,outcome):
if outcome not in ['succeeded','failed','cancelled','needs_review']:raise LaneError('INVALID_OUTCOME')
with self._lock:
item=self._owned(lease)
receipt={'project':lease.project,'run':lease.run,'outcome':outcome,'stop_requested':item['stop_requested'],'work_executed_here':False}
del self._active[lease.project]
return receipt
def view(self,project):
_label(project)
with self._lock:
item=self._active.get(project)
return {'project':project,'busy':bool(item),**({'run':item['lease'].run,'state':'stop_requested' if item['stop_requested'] else 'running'} if item else {})}
동시에 들어오는 요청은 짧은 임계 구역에서 나눈다
한 관리자 인스턴스의 RLock이 claim·중지 표시·해제·조회에 사용하는 메타데이터를 보호한다. 잠금 안에서 네트워크 요청이나 긴 업무를 실행하지 않는다. 여덟 스레드를 같은 시점에 출발시켜 같은 프로젝트를 요구했을 때 정확히 하나만 Lease를 얻는 시험을 넣었다. 이는 이 프로세스의 상태 갱신에 대한 확인이다.
전체21개 시험은 두 프로젝트의 독립 점유와 다른 프로젝트 중지의 무영향, 옛 소유자의 해제·중지 거절, 실행 이름 재사용, 다른 관리자에서 온 증표와 잘못 만든 토큰을 다룬다. 잘못된 결과를 넣어도 자리가 유지되는지, 조회 사본을 고쳐도 내부 상태가 보존되는지도 확인했다. 외부 서비스의 동시 안전성을 시험한 것은 아니다.
다섯 상황은 실제 로컬 기록으로 남겼다
두 프로젝트 동시 점유, 이미 사용 중인 프로젝트, 중지만 요청한 상태, 명시한 최종 결과, 옛 실행의 늦은 해제를 예제로 실행했다. 아래 선택기는 Python으로 만든 다섯 합성 기록만 읽는다. 방문자 파일과 로그인 세션을 읽거나 실행을 시작하는 화면이 아니다. 기록에서 다른 프로젝트가 그대로 유지되는지 직접 비교할 수 있다.
공유 ZIP은 새 폴더에 추출한 뒤 같은21개 시험을 실행하고 원본 파일 해시와 대조한다. 두 언어 선택기의 초기 표시와 각 상황 선택은 DOM 대역에서 자료와 맞춘다. 저장된 기록 표시와 실제 코드 실행을 구분하며 이 확인을 브라우저 실화면 검수라고 부르지 않는다. 원래 북극성 앱의 시험을 이번 콘텐츠 작업에서 다시 실행한 것도 아니다.
복구와 실행 원장은 별도로 연결한다
전체 구현·시험·예제·기록 생성기와 README를 표준 라이브러리만 쓰는 패키지로 공유한다. 수정과 재사용이 가능하다. 북극성 원본의 프로젝트별 소유 증표를 참고해 새로 쓴 예제이며 중지 상태와 결과 영수증은 설명을 위해 추가했다. 원래 실행기를 복제한 제품으로 소개하지 않는다.
메모리 기반이므로 프로세스가 꺼지면 점유 상태도 사라진다. 여러 프로세스의 잠금, 영속 원장, 예약, 자동 복구는 없다. needs_review로 자리를 비웠어도 불확실한 외부 행동을 다시 해도 된다는 뜻은 아니다. 실제 업무에서는 종료를 확인한 근거와 미확정 실행을 별도로 남겨야 한다. 나는 실행 자리가 비었다는 사실과 업무를 안전하게 다시 할 수 있다는 판단을 마지막까지 분리한다.