Workspace IndexAlgorithms › Day 77

Streaming Semantics — Watermarks and Exactly-Once TODO

Algorithms · Day 77 / 100 · E. Data & Storage Engines (Day 69-81)

Concept

In stream processing, the time an event actually occurred (event time) differs from the time the system processes it (processing time), and network delays or retries can scramble the order further. A watermark is the system's estimate that 'essentially all events earlier than this timestamp have arrived by now' — it's the signal that decides when to close an event-time window and emit a result. Because a watermark is only an estimate, late data can still show up, which means you need a policy — an allowed lateness window — for updating results or routing late data down a separate path. This is where the tradeoff between completeness and latency appears: a conservative watermark is accurate but slow, an aggressive one is fast but lets more late data through. Exactly-once semantics keep operator state consistent via periodic checkpoints, and pair that with transactional commits or idempotent writes at the sink so results aren't duplicated after a restart.

Most cases of 'why are my aggregates slightly off' aren't bugs — they come from never having pinned down an event-time and watermark policy in the first place.

Code & Formula

# 스트리밍 처리 의미론 — 워터마크로 이벤트 시간 윈도우를 닫고, 늦은 데이터를 걸러낸다.
# event_time 이 뒤섞여 도착해도 워터마크(진행 추정치) 기준으로 윈도우 완결을 판단한다.

events = [
    # (event_time, payload) — 도착 순서는 뒤섞여 있다 (네트워크 지연 시뮬레이션)
    (1, "a"), (2, "b"), (5, "c"), (3, "d"), (9, "e"), (4, "late-for-w1"),
]

WINDOW = 5          # [0,5), [5,10) 처럼 크기 5 윈도우
ALLOWED_LATENESS = 1  # 워터마크를 지난 뒤에도 1만큼은 늦은 데이터로 받아준다

def window_of(t):
    start = (t // WINDOW) * WINDOW
    return (start, start + WINDOW)

state = {}          # window -> 누적 payload 리스트
closed = set()       # 이미 결과를 낸(닫힌) 윈도우
late_dropped = []
watermark = -1

for event_time, payload in events:
    watermark = max(watermark, event_time - 1)  # 단순화한 워터마크 추정: max(event_time) - 1
    w = window_of(event_time)
    if w in closed and watermark - ALLOWED_LATENESS >= w[1]:
        late_dropped.append((event_time, payload))
        continue
    state.setdefault(w, []).append(payload)
    # 워터마크가 윈도우 끝을 지나면 그 윈도우를 닫고 결과를 낸다(exactly-once: 한 번만 emit)
    if watermark >= w[1] and w not in closed:
        closed.add(w)

for w in sorted(state):
    status = "closed" if w in closed else "open"
    print(f"window {w} ({status}): {state[w]}")
print("late data (dropped after allowed lateness):", late_dropped)
print("final watermark:", watermark)

Exercise

Build an event stream that's deliberately out of order with some events significantly delayed, then vary the watermark delay and the allowed lateness, and record how the windowed aggregate results and the count of late events change.

Practical Connection

Chain event indexing has its own gap between block time and receipt time, and reorgs can even flip the past, so Verex's volume and position aggregation pipeline needs exactly this kind of watermark and correction-handling design.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

스트리밍 처리 의미론 TODO

Algorithms · Day 77 / 100 · E. 데이터·스토리지 엔진 (Day 69–81)

워터마크·정확히 한 번

개념

스트리밍 처리에서는 이벤트가 실제 발생한 시각(event time)과 시스템이 처리한 시각(processing time)이 다르고, 네트워크 지연이나 재시도 때문에 순서도 뒤섞인다. 워터마크는 '이 타임스탬프보다 이른 이벤트는 사실상 모두 도착했다'는 시스템의 추정치로, 이벤트 시간 윈도우를 언제 닫고 결과를 낼지 결정하는 신호다. 워터마크는 추정이므로 늦게 온 데이터(late data)가 있을 수 있고, 허용 지연 시간을 두어 결과를 갱신하거나 별도 경로로 빼내는 정책이 필요하다. 여기서 완결성과 지연 사이의 트레이드오프가 생긴다. 워터마크를 보수적으로 잡으면 정확하지만 느리고, 공격적으로 잡으면 빠르지만 늦은 데이터가 많아진다. exactly-once 의미론은 주기적 체크포인트로 연산자 상태를 일관되게 저장하고, 싱크에는 트랜잭션 커밋이나 멱등 쓰기를 결합해 재시작 후에도 결과가 중복되지 않게 만든다.

집계 값이 '왜 조금씩 틀리냐'는 문제의 대부분은 버그가 아니라 이벤트 시간과 워터마크 정책을 명시하지 않은 데서 나온다.

코드 · 수식

# 스트리밍 처리 의미론 — 워터마크로 이벤트 시간 윈도우를 닫고, 늦은 데이터를 걸러낸다.
# event_time 이 뒤섞여 도착해도 워터마크(진행 추정치) 기준으로 윈도우 완결을 판단한다.

events = [
    # (event_time, payload) — 도착 순서는 뒤섞여 있다 (네트워크 지연 시뮬레이션)
    (1, "a"), (2, "b"), (5, "c"), (3, "d"), (9, "e"), (4, "late-for-w1"),
]

WINDOW = 5          # [0,5), [5,10) 처럼 크기 5 윈도우
ALLOWED_LATENESS = 1  # 워터마크를 지난 뒤에도 1만큼은 늦은 데이터로 받아준다

def window_of(t):
    start = (t // WINDOW) * WINDOW
    return (start, start + WINDOW)

state = {}          # window -> 누적 payload 리스트
closed = set()       # 이미 결과를 낸(닫힌) 윈도우
late_dropped = []
watermark = -1

for event_time, payload in events:
    watermark = max(watermark, event_time - 1)  # 단순화한 워터마크 추정: max(event_time) - 1
    w = window_of(event_time)
    if w in closed and watermark - ALLOWED_LATENESS >= w[1]:
        late_dropped.append((event_time, payload))
        continue
    state.setdefault(w, []).append(payload)
    # 워터마크가 윈도우 끝을 지나면 그 윈도우를 닫고 결과를 낸다(exactly-once: 한 번만 emit)
    if watermark >= w[1] and w not in closed:
        closed.add(w)

for w in sorted(state):
    status = "closed" if w in closed else "open"
    print(f"window {w} ({status}): {state[w]}")
print("late data (dropped after allowed lateness):", late_dropped)
print("final watermark:", watermark)

연습

의도적으로 순서가 뒤섞이고 일부는 크게 지연되는 이벤트 스트림을 만들어, 워터마크 지연값과 허용 지연 시간을 바꿔가며 윈도우 집계 결과와 late data 건수가 어떻게 달라지는지 기록하라.

실무 · Verex 연결

체인 이벤트 인덱싱은 블록 타임과 수신 시각이 다르고 재조직(reorg)으로 과거가 뒤집히기까지 하므로, Verex의 거래량·포지션 집계 파이프라인에서 워터마크와 정정 처리 설계가 그대로 필요하다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 76. 컬럼 스토어와 벡터화 실행(OLAP)78. 벡터 DB와 ANN 인덱스(HNSW·IVF-PQ) →