From 61752e6fb1827dc56d166baf4d5f058a98832b40 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sun, 4 Oct 2026 15:09:36 +0100 Subject: Say how pure-serve pushes a view's value to its subscribers, and what it must show (019) --- docs/design/019-pushing-views.md | 91 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 docs/design/019-pushing-views.md (limited to 'docs/design') diff --git a/docs/design/019-pushing-views.md b/docs/design/019-pushing-views.md new file mode 100644 index 0000000..b7e59aa --- /dev/null +++ b/docs/design/019-pushing-views.md @@ -0,0 +1,91 @@ +# 019 — 보기를 미는 길 (SSE) + +작성일: 2026-10-04. 상태: 설계와 착수 기준(구현 전). +[외부 리뷰 005](../outside-feedback/feedback-005.md)의 4장 (2) "실시간 클라이언트 푸시(SSE / WebSocket) 미지원"에서 시작한다. + +## 문제 + +- `pure-serve`는 요청 하나에 답 하나다. 보기가 바뀐 것을 알려면 클라이언트가 `GET /views/…`를 되풀이해야 한다(폴링). 채팅, 함께 + 쓰는 보드, 대시보드에서 클라이언트 수만큼, 원하는 신선도만큼 요청이 온다. 대부분은 바뀌지 않은 값을 다시 계산해 다시 보낸다. +- Vega의 웹 표면(`vega web`)은 이미 바뀔 때마다 이벤트 스트림으로 알리지만, 그것은 앱을 제 프로세스에서 돌리는 Vega의 것이다. + 노드(`pure-serve`)의 보기에는 없다. + +## 원칙 + +- **미는 것은 값이다, 사건이 아니다.** 구독자는 늘 가장 새 값으로 수렴한다. 저장 사이의 값들을 모두 받는다는 약속은 하지 않는다 + (그것은 출력과 링크의 일이다, 012). 그래서 느린 구독자 때문에 쌓이는 것이 없다. +- **한 번 계산해 여럿에게.** 같은 보기와 같은 인자를 구독한 연결들은 저장마다 한 번의 계산을 나눈다. 값이 그대로면 보내지 않는다. +- **구독자는 작다.** 요청을 읽는 연결(약 6 KB)이 아니라 소켓과 무엇을 구독했는지만 가진 기록이다. 그래야 많은 구독자를 품는다. +- **길은 보기의 길 그대로다.** 새 경로를 만들지 않는다: `GET /views/이름?인자`에 `Accept: text/event-stream`이면 구독이다 + (브라우저의 `EventSource`가 그렇게 보낸다). 인자, 표의 쪽(`from`·`count`), 앱의 접두(`/앱/`), 토큰(`read`)이 모두 같다. +- **소유자는 거의 아무것도 하지 않는다.** 저장 뒤에 스냅숏을 공개할 때(지금도 한다) 구독자가 있으면 연결 스레드를 깨울 뿐이고, + 깨우기는 스레드가 다음을 처리하기 전까지 한 번으로 모인다. 계산과 보내기는 연결 스레드가 한다. + +## 모양 + +```text +GET /views/total HTTP/1.1 +Accept: text/event-stream + +HTTP/1.1 200 OK +Content-Type: text/event-stream +Cache-Control: no-store + +id: 41 +data: 120 + +id: 57 +data: 135 +``` + +- 구독하면 곧 지금의 값이 한 번 온다. 그 뒤로는 공개된 스냅숏에서 그 보기의 값이 앞서 보낸 것과 다를 때마다 온다. `id`는 그 값을 + 계산한 스냅숏의 바뀜 번호다(늘기만 한다). 값의 바이트는 같은 보기의 `GET` 본문과 같다(수, text, 표의 JSON). 여러 줄의 text는 + 줄마다 `data:` 한 줄이다(받는 쪽이 줄바꿈으로 잇는다). +- 값이 없는 보기(인자를 받지 못한 보기, 결함난 보기)는 `event: none`과 그 까닭(`data: work` 같은)을 보낸다. +- 앱이 멈추거나 모듈이 바뀌면(017의 3단계) 그 앱의 스트림은 끝난다. `EventSource`는 다시 붙고, 새 모듈의 보기를 구독한다. +- 느린 구독자: 보내던 이벤트를 다 보내기 전에 새 값이 오면, 기다리던 이벤트를 새 값으로 바꾼다. 구독자 하나가 쥐는 것은 많아야 + 이벤트 둘(보내던 것의 나머지와 다음 것)이다. 보내던 것을 `--timeout-ms` 동안 하나도 못 보내면 끊는다. +- 구독자의 수는 `--subscribers N`(기본 65,536)이 묶는다. 다 차면 503 `busy`다. + +## 구현 + +- `pure-serve`의 연결 스레드는 머리를 읽고 길과 토큰을 확인한 뒤, 보기의 `GET`이 이벤트 스트림을 바라면 그 연결을 구독자로 바꾼다: + 답의 머리를 쓰고, 구독(앱, 모듈, 보기, 인자의 문자열)의 무리에 넣고, 연결 기록은 돌려준다(소켓은 닫지 않는다). +- 무리는 연결 스레드마다 있다. 무리는 마지막으로 계산한 스냅숏의 바뀜 번호와 그 값의 바이트를 가진다. +- 소유자는 스냅숏을 공개할 때(`publish`) 그 앱에 구독자가 있으면, 아직 깨우지 않은 연결 스레드를 깨운다(스레드마다 깃발 하나, + eventfd). 스레드는 깨면 깃발을 내리고 자기 무리마다 새 스냅숏을 한 번 계산해, 값이 다르면 이벤트를 한 번 만들어 무리의 구독자에게 + 보낸다. + +## 단계 + +1. 위의 전부. 한 앱의 호스트와 여러 앱의 호스트(`--apps`) 모두. +2. (나중) 측정이 요구하면: 하트비트(프록시의 유휴 끊김), 보기가 읽는 상태가 바뀌지 않은 스냅숏을 계산하지 않고 건너뛰기, 긴 보기를 + 연결 스레드 밖에서(119). + +## 착수 기준 + +- **바꿀 결정:** `pure-serve`가 보기의 변화를 SSE로 밀지. 아래를 채우지 못하면 클라이언트는 폴링한다(지금처럼). +- **최소 확인**(`tests/check.py`의 `server_push`): + - 구독하면 지금의 값이 먼저 온다. 쓰기를 50 ms마다 하면 바뀐 값이 하나도 빠짐없이 차례로 온다. 쓰기를 몰아서 하면 사이 값은 + 빠질 수 있어도 마지막 값은 반드시 오고, 먼저 온 값보다 오래된 값(`id`가 작은 것)은 오지 않는다. + - 보기의 값을 바꾸지 않는 쓰기에는 이벤트가 없다. + - 인자를 받는 보기, 표의 쪽, 여러 줄의 text, 계산하는 보기(018), 토큰(없으면 401, `read`면 된다), 여러 앱의 호스트의 `/앱/` 접두. + - SIGHUP으로 앱의 모듈을 바꾸면 그 앱의 스트림이 끝나고, 다시 붙으면 새 모듈의 값이 온다. + - 읽지 않는 구독자에게 값을 1만 번 바꿔도 호스트의 메모리가 그만큼 늘지 않는다. 그 구독자가 다시 읽으면 마지막 값이 온다. + - 구독자들이 끊으면 그 기록이 모두 돌아온다(호스트가 멈출 때 알리는 줄의 수가 0으로 돌아온다). + - Pure, aup, Vega의 모든 검사가 초록이다. +- **통과 조건**(RAM의 Pi 5): + - 신선도와 값: 구독자 1,000이 카운터 보기를 보는 동안 쓰기가 초당 100번 온다. 쓰기의 답을 받은 때부터 구독자가 그 값(이나 더 새 + 값)을 받기까지의 p99가 20 ms 이하다. 그동안 호스트의 CPU가, 같은 클라이언트 1,000이 100 ms마다 폴링할 때(신선도 100 ms)의 + 10분의 1 이하다. + - 크기: 구독자 1만에서 Pure의 메모리(호스트의 RSS 증가 / 구독자 수, 커널의 소켓 버퍼는 RSS에 들지 않는다)가 구독자마다 1 KiB 이하다. + - 퍼뜨리기: 구독자 1만이 한 보기를 볼 때 값 하나가 바뀌면 모두가 받기까지의 p99가 100 ms 이하다. + - 지금의 길: TPC-C와 은행 질의를 전과 후 번갈아 여섯 쌍 재서 중앙값이 3% 넘게 느려지지 않고, 은행 질의의 명령 수가 1% 넘게 + 늘지 않는다. + +## 하지 않는 것 + +- WebSocket. 보기를 미는 데는 한 방향이면 된다. 입력은 지금처럼 `POST`다(HTTP/1.1 keep-alive). 양방향 채널이 필요하다는 측정이 + 나오면 다시 본다. +- 모든 값의 이력. 구독자는 가장 새 값을 받는다. 빠짐없는 사건의 흐름은 출력(`send`)과 링크가 한다. +- 구독자마다 다른 계산. 무리(같은 보기와 인자)가 계산을 나눈다. -- cgit v1.2.3