폴더가 바뀌었다는 사실과 다른 화면에 그 상태가 전달됐다는 사실은 다르다. 나는 이 경계를 실험하려고 최신 상태만 대표하는 작은 메모리 모듈을 만들었다. 파일을 감시하거나 서버에 보내지는 않는다. 전달할 변화가 남아 있는지, 어떤 버전을 확인했다고 선언했는지 계산하는 독립 합성 예제다.

초기 상태와 새 변화를 구별한다

SnapshotRelay를 만들 때 첫 토큰은 비교 기준이다. 생성만으로 전달 대기가 생기지 않는다. observe에 다른 합성 토큰이 들어오면 로컬 revision이 증가하고 같은 토큰이면 변하지 않는다. 시작 화면도 반드시 전송해야 하는 서비스라면 그 정책을 별도로 정해야 한다. 이 예제의 초기 기준을 운영 프로그램의 최초 동기화 성공으로 바꾸지 않는다.

여러 변화가 시도 예약 전에 들어오면 가장 최근 revision과 토큰을 예약한다. 중간 사건을 모두 보관하는 큐가 아니다. 화면에 보여 줄 현재 파일 개수처럼 덮어써도 되는 상태를 위한 선택이다. 주문 변경이나 입금 사건을 하나씩 남겨야 하는 경우에는 기존 SQLite 업무 사건 모듈처럼 다른 구조가 필요하다.

시도 예약은 전송도 성공도 아니다

begin은 호스트가 전달한 단조 증가 정수 시각을 검사한다. 남은 변화가 있고 진행 중인 예약과 미확정 결과가 없으며 대기 간격이 지났을 때 Ticket을 만든다. 이 순간 네트워크 동작은 0이다. 표에 찍힌 시도 횟수도 외부 요청 수가 아니라 로컬 예약 수다. 원래 업무를 다시 시작하는 기능은 없다.

Ticket은 ordinal, revision, token을 가진 읽기 전용 객체다. 같은 인스턴스가 만든 정확한 객체만 finish와 resolve에 쓸 수 있다. 모양이 같은 사본이나 다른 인스턴스의 표는 거절한다. 이는 작은 단일 호출자 실험의 범위이며 서명 검증이나 여러 프로세스의 인증 계약을 대신하지 않는다.

모르는 결과를 실패라고 바꿔 쓰지 않는다

finish의 confirmed는 호스트가 확인을 선언했다는 뜻이다. not_applied는 외부에 적용되지 않았다는 근거가 있는 선언이며 재시도 간격 뒤 같은 대기를 다시 예약할 수 있다. unknown은 받았는지 모르는 경우다. 시간이 오래 흘러도 새 예약을 막고 resolve의 명시적 확인을 기다린다. 단순한 응답 시간 초과를 not_applied로 넘기면 이 경계는 깨진다.

resolve도 서버를 읽지 않는다. 호스트가 따로 조회한 결과를 confirmed 또는 not_applied로 전달하는 자리다. remote_receipt_authenticated와 business_completion_proven은 계속 False다. 아래 구현이 확인하는 것은 로컬 상태 전이의 일관성이며 실제 전달이나 고객 업무 완료가 아니다.

코드와 구현 살펴보기

독립 합성 전체 구현

"""In-memory teaching model for replaceable snapshots. No transport or scheduler."""
from dataclasses import dataclass
import re


@dataclass(frozen=True)
class Ticket:
    ordinal: int
    revision: int
    token: str


class SnapshotRelay:
    def __init__(self, initial_token, *, minimum_gap=2, retry_delay=3):
        self._validate_token(initial_token)
        for value in (minimum_gap, retry_delay):
            if type(value) is not int or value < 1:
                raise ValueError('INVALID_DELAY')
        self._token = initial_token
        self._latest = self._acked = self._serial = 0
        self._active = self._unknown = None
        self._clock = -1
        self._ready_at = 0
        self._gap, self._retry = minimum_gap, retry_delay
        self._last_outcome = 'not_attempted'

    @staticmethod
    def _validate_token(token):
        if type(token) is not str or not re.fullmatch(r'[A-Za-z0-9_-]{1,64}', token):
            raise ValueError('INVALID_SYNTHETIC_TOKEN')

    def _tick(self, now):
        if type(now) is not int or now < 0 or now < self._clock:
            raise ValueError('INVALID_OR_REVERSED_CLOCK')
        self._clock = now

    def observe(self, token):
        """Supply an alias, not a filesystem path or a content-authenticated digest."""
        self._validate_token(token)
        if token == self._token:
            return False
        self._token = token
        self._latest += 1
        return True

    def begin(self, now):
        """Reserve a local attempt; returning a ticket performs no transmission."""
        self._tick(now)
        if (self._latest == self._acked or self._active is not None
                or self._unknown is not None or now < self._ready_at):
            return None
        self._serial += 1
        self._active = Ticket(self._serial, self._latest, self._token)
        return self._active

    def finish(self, ticket, outcome, now):
        """Outcome is a caller declaration, never a verified remote receipt."""
        if ticket is not self._active or ticket is None:
            raise ValueError('NOT_THE_ACTIVE_TICKET')
        if type(outcome) is not str or outcome not in {'confirmed', 'not_applied', 'unknown'}:
            raise ValueError('INVALID_OUTCOME')
        self._tick(now)
        self._active = None
        self._last_outcome = outcome
        if outcome == 'confirmed':
            self._acked = ticket.revision
            self._ready_at = now + self._gap
        elif outcome == 'not_applied':
            self._ready_at = now + self._retry
        else:
            self._unknown = ticket

    def resolve(self, ticket, observation, now):
        """Host explicitly supplies reconciled outcome; this method reads no server."""
        if ticket is not self._unknown or ticket is None:
            raise ValueError('NOT_THE_UNKNOWN_TICKET')
        if type(observation) is not str or observation not in {'confirmed', 'not_applied'}:
            raise ValueError('INVALID_RECONCILIATION')
        self._tick(now)
        self._unknown = None
        self._last_outcome = 'reconciled_' + observation
        if observation == 'confirmed':
            self._acked = ticket.revision
            self._ready_at = now + self._gap
        else:
            self._ready_at = now + self._retry

    def inspect(self):
        return {'latest_revision': self._latest, 'declared_acked_revision': self._acked,
                'pending': self._latest != self._acked,
                'in_flight': self._active is not None,
                'needs_reconciliation': self._unknown is not None,
                'ready_at': self._ready_at, 'last_outcome': self._last_outcome,
                'attempt_reservations': self._serial,
                'network_operations': 0, 'durable_state_verified': False,
                'remote_receipt_authenticated': False, 'business_completion_proven': False}

이전 버전 확인이 새 변화를 지우지 않게 한다

revision 1의 시도를 예약한 뒤 observe가 revision 2를 만들 수 있다. 이때 이전 Ticket의 confirmed는 확인한 번호를 1까지만 올린다. 최신 번호 2와 다르므로 pending은 남는다. 미확정 응답을 나중에 확인하는 resolve에도 같은 규칙을 적용한다. 답이 도착한 시점의 최신 상태 전체를 무조건 확인한 것으로 처리하지 않는다.

한 번에 예약 하나만 허용해도 비동기 전달 사이의 관측 변화는 따로 생각해야 한다. 이 코드에는 스레드 잠금이나 원격 서버의 순서 검사 기능이 없다. 여러 작업자가 보내거나 오래된 요청이 나중에 도착할 수 있다면 수신 측에도 버전 경계와 덮어쓰기 정책이 필요하다. 로컬 번호만으로 모든 전달 순서가 보장되지는 않는다.

다섯 합성 기록을 같은 기준으로 읽는다

초기 기준 유지, 조용한 폴더에서 재시도 예약, 이전 확인 뒤 새 변화 보존, 미확정 결과 대기, 명시적 확인 뒤 대기 해제의 기록을 생성했다. 각 기록은 이번 Python 코드의 실제 로컬 결과다. 실제 고객 폴더나 네트워크 장애 기록이 아니다. 실패를 더 크게 보이게 하거나 운영 성공 횟수로 옮기지 않는다.

20개 시험은 동일 관측, 변화 합치기, 실패 뒤 대기, 간격, 오래된 확인, 미확정 차단, 외부 Ticket과 잘못된 시각을 확인한다. ZIP을 푼 뒤에도 같은 시험을 실행한다. 아래 선택기는 저장된 다섯 기록을 보여 주며 실제 스크립트는 DOM 대역에서 초기 표시와 선택을 대조한다. 렌더링 브라우저 검수와 외부 전송 시험은 별도다.

합성 로컬 기록의 읽기 뷰어

운영에 연결할 때 남는 책임

다운로드에는 전체 구현, 시험, 기록 생성기와 실행 결과, README, 재사용 허가가 들어 있다. 표준 라이브러리로 실행하지만 파일 감시기·전송기·예약 작업은 들어 있지 않다. 한 호출자가 순서대로 사용해야 하며 영속 저장, 재시작 복구, 재시도 총량, 취소, 권한과 실제 결과 조회는 호스트가 맡아야 한다. 정수 간격도 설명용 값이다.

나는 이 상태 계산기를 원래 관제탑에 적용했다고 쓰지 않는다. 원래 프로그램과 자동화는 그대로 보존했다. 기존 업무 사건 모듈은 별도 책임을 비교할 자료로 연결하며 자동 결합은 없다. 무엇을 남겨야 하는지에 따라 최신 상태 한 벌과 사건 하나하나를 구분하는 것이 이 공유 예제의 출발점이다.

코드·시험·실행 기록 ZIP 받기
Python 표준 라이브러리 · 합성 자료 · 수정과 재사용 가능