=== ToDo (Low) ==== Admin - Export / Import ==== #cmpebhmf4hubz1f ===== Property * Creator: [User:Aha00a] * dateCreated: [2026-05-20]T17:09:52Z ===== Activity * [User:Aha00a] [2026-05-20]T17:09:52Z * Card Add - ["#cmpebhmf4hubz1f" #Admin - Export / Import] === ToDo ==== 삭제 권한 도입 ==== #cmpebn29sgylr3q ===== Description 이미 된 것으로 보인다. 카드를 만든 2026-05-20 다음 주에 Delete(32) action 과 WikiPermission.isDeletable 이 들어갔고(커밋 51deff34, f659ef31), 지금 삭제하는 네 경로가 전부 그것으로 막힌다 — 페이지 삭제, 마지막 revision 삭제, 첨부 삭제, API v1 삭제. 자세한 건 [AccessControl] 의 쓰기 권한 적용 지점. 그래서 이 카드가 그 일이면 Done 이고, 더 하려던 것(예: 휴지통, 복구)이 있으면 그것을 카드 제목으로 다시 적는 편이 낫다. 어느 쪽인지는 만든 사람만 안다 — 옮기지 않고 둔다. --[2026-09-25] ===== Property * Creator: [User:Aha00a] * dateCreated: [2026-05-20]T17:14:06Z ===== Activity * [User:Aha00a] [2026-05-20]T17:14:06Z * Card Add - ["#cmpebn29sgylr3q" #삭제 권한 도입] === Done
원래... AhaMark를 rendering한 결과물을 손수 caching하려 했는데, 뭐 그럴 필요 있을까 하는 생각이 문득 들어 끄적여본다.
CloudFront 뒤에 숨어서 옵션 잘 적용하고, Cache Invalidation만 제대로 해주면 되는 문제라는 생각이 드네..
아직 안 했다. 페이지 렌더 결과에 Cache-Control도 ETag도 없고, 메모리 캐시는 domain·permission·siteAdmin·links 뿐이다.
다만 "옵션만 잘 주면 되는 문제"라는 위 판단은 지금 기준으로 틀리다. 그 사이 읽기 권한이 Permission 행 기준으로 사용자마다 갈리게 됐다(AccessControl). 앞단 CDN 이 응답을 공유 캐시에 담으면 비공개 페이지가 다른 사람에게 그대로 나간다. 하려면 익명이 읽을 수 있는 페이지로 한정하거나 사용자별로 vary 해야 하고, 후자는 캐시 적중률을 거의 없앤다. 즉 캐시 무효화보다 무엇을 캐시해도 되는지가 먼저다. --2026-08-31
Bot을 만들어볼까 싶다. 문법 변화나 기타 노가다로 고쳐야 하는 것들을 자동으로 수정하고, minor edit으로 남긴다. 그 외 rename같은 action에는 rename하기 전의 문서로 연결된 문서들이 있는데 이를 고쳐주는 등의 자동화.
문법을 바꾸었을 경우 예전 페이지들에 대한 migration이 고민됐었는데, 나무위키의 경우 쿨하게 history는 건드리지 않는다. 나도 이쪽으로 마음이 살짝 기울었다.
아직도 결정을 못하겠다. 문법이 변화되었는데, history의 내용을 건드리지 않는다면 세월이 흐른 뒤 새 엔진에서 history를 통해 그 페이지를 본다면? 개판일텐데?
그럼 두가지 해결책이 있는데
진짜 쓸데없는거로 고민하는 거 같긴 한데.. 뭐 이런게 내 취미라고 해두자. --2020-01-08
위에서 봇이 할 일로 꼽은 것 중 rename 은 봇 없이 해결됐다. rename 이 원래 자리에 #!redirect 페이지를 남기므로(Wiki.rename), 옛 이름으로 걸린 링크는 고치지 않아도 끊기지 않는다. rename 이 무엇을 바꾸는지는 AccessControl 에 있다.
봇이 쓸 기반도 이미 있다 — API v1(/api/v1/page/*name), API Key, 그리고 봇 편집을 구분해 기록하는 viaApi 다(Api). 문서 sync 가 그 경로로 돌고 있다.
그래서 남은 건 처음의 그 결정 하나다 — 문법이 바뀌었을 때 history 까지 마이그레이션할 것인가, 렌더러를 버전관리해 그 시점 문법으로 해석할 것인가. 아직 정하지 않았고, 정하기 전에는 봇을 만들 이유가 없다. --2026-08-31
2026-09-13 에 정해졌다 — history 까지 마이그레이션한다. [a b] 를 페이지 이름 하나로 읽게 바꾸면서, 옛 뜻으로 쓴 링크를 모든 사이트의 모든 리비전에서 DB 를 직접 고쳐 [a|b] 로 바꿨다. 소유자의 지시였고, 까닭은 위 2020-01-08 의 걱정 그대로다 — 옛 리비전도 오늘의 파서로 읽히니, API 로 고치면 새 리비전 하나만 맞고 그 앞의 리비전은 전부 틀리게 보인다. 렌더러 버전관리는 택하지 않았다. 무엇을 어떻게 바꿨는지는 InterpreterWiki 의 Link 절과 저장소의 scripts/README.md. 봇은 여전히 없다 — 이번 일은 한 번 도는 스크립트였다. --2026-09-14
현재 InterpreterMap에 너무 많은 기능이 들어있다.
이를 분리할까 하고 고민해본다.
또 다양한 옵션을 지원하기 위해서 Interpreter의 Argument로 JSON을 받아들일까 싶기도..
일반 맵
일반 테이블
#!Table tsv
Name Address
some addr
#!Map tsv
Name Address
some addr
새로운 InterpreterFetcher? InterpreterDataSource
#!DataSource { id: "dataSource1", "url": "https://spreadsheet" option: }
Column1 Column2
Data1 Data2
#!Table { "DataSource": "dataSource1" }
#!Map { "DataSource": "dataSource1" }
#!Map { "DataSource": "dataSource1", "option": { "hideMap": false, "hideTable": false, showList: true } }
인자의 경우 default를 false로 고정하는게 나을지, show/hide중 하나만 쓰는게 나을지 모르겠다.
JSON을 직접 쓰게 되면 "key": "value"와 같이 따옴표가 귀찮다..세 가지 책임 중 데이터를 GoogleSpreadsheet 에서 읽어오는 부분은 이미 분리됐다. InterpreterMap 에는 spreadsheet 참조가 남아 있지 않고, 그 일은 WikiSpreadsheet.syncGoogleSpreadsheet 가 한다. 남은 것은 보관과 표시 둘이다.
Interpreter 인자로 JSON 을 받는 안은 아무것도 하지 않았다. 위에서 스스로 짚은 따옴표 문제도 그대로다. --2026-08-31
https://mottie.github.io/tablesorter/docs/example-widget-filter.html --2019-12-27
Map 에는 적용돼 있다. Wiki/map.scala.html 이 위 링크의 그 filter widget 을 widgets: ["filter"] 로 켜고, Tag · Category · Score 에 필터를 건다.
표에는 없다. InterpreterTable 이 모든 #!Table 에 tablesorter 클래스를 붙이므로 정렬은 전부 되지만, Wiki/view.scala.html 이 옵션 없이 .tablesorter() 로 초기화해 filter widget 이 꺼진 채로 남는다. widget 스크립트는 _baseSkeleton.scala.html 에서 이미 전역으로 불러오고 있으니, 표에 켜는 일 자체는 작다. --2026-08-31
표에도 켰다. Wiki/view.scala.html 의 초기화에 widgets: ["filter"] 를 줬다 — 예상대로 그 한 줄이다. Map 표는 제 초기화가 열마다 다른 필터를 거니 .not('[id^="mapTable"]') 로 뺐다. 무엇을 어떻게 하는지는 InterpreterTable 정렬과 필터. --2026-09-15
위키엔진 비교표
https://en.wikipedia.org/wiki/Comparison_of_wiki_software
항목들은 다음과 같다.
이정도가 기본 기능이라는 거겠지.. --2019-12-27
https://www.wikimatrix.org/ 발견. 여기 기준에 좀 맞춰볼까.. --2020-11-26
취소선이 없는 항목 중 WYSIWYG editing 은 여전히 없다 — 편집기는 CodeMirror 기반 소스 편집기이고 미리보기가 따로 있다(Dev Editor). --2026-08-31
설계안을 정리했다(2026-09-16). Dev Editor WYSIWYG 절에 — 핵심 어려움은 «편집한 리치 문서 → AhaMark» 무손실 왕복(블록·매크로·커스텀 링크 보존)이다. 권장은 하이브리드(프로즈만 리치, 블록/매크로는 아톰 노드로 기존 표·간트 편집기 재사용), 단계별. 저비용 대안은 소스 편집기 위 서식 툴바. 착수 전 소유자가 정할 열린 결정 넷(접근·범위·에디터 라이브러리·정본)을 남겼다. 크고 데이터 손상 위험이 있어 blind 구현은 안 하고 결정 대기.
서식 툴바(접근 3의 첫걸음)를 구현·배포했다(2026-09-16). 소유자가 «서식 툴바» 를 택했다. 소스 편집기 위 버튼 — 굵게·기울임·밑줄·취소선·인라인 코드·제목·불릿·링크 — 이 선택을 감싸거나 줄 앞에 붙인다. 텍스트 연산은 순수 함수 AhaWiki.Format(단위테스트 ahawiki.format.test.mjs), CodeMirror replaceRange 로 적용해 undo 보존. Chrome 실편집기에서 8개 버튼 삽입 결과 확인(저장 아님, reCAPTCHA 무관). Scala 259·node 206 통과. 양쪽 배포, 태그 v20260916T112205-aha00a. 자세한 건 Dev Editor 서식 툴바. 진짜 WYSIWYG(접근 2)은 여전히 열린 결정.
슬라이드로 보여준다.
구현했다(2026-09-16, v1). 소유자와 토의해 «접근 B» 로 지었다 — InterpreterPaper 가 이미 슬라이드 모델의 대부분(‹----› 분할·#!var·조각별 렌더·쪽번호)을 하므로, 그 분할·렌더·인쇄 코어를 PaperSlideCore 로 추출해 Paper·Slide 가 공유하고(중복 제거), #!Slide 는 «발표 정체성»만 얹는다 — 화면은 16:9 덱을 한 장씩(키보드·클릭·전체화면, #/n 딥링크), 인쇄는 한 장에 한 슬라이드씩 정적 유인물(InterpreterPaper 의 @media print 와 같은 성격). 이유: ① 발표 자료도 인쇄하면 결국 Paper 유인물이다(현장에서 PDF 유인물로만 발표하기도 함) → 인쇄 경로를 새로 만들 이유가 없다. ② Word(Paper)/PPT(Slide) 의 가장 깊은 차이인 자유 배치(캔버스)는 AhaWiki 에 애초에 없다(전부 흐름 마크업) → 코어 공유가 성립한다. 기각: 순수 최대공유(새 디렉티브 없이 Paper 에 «발표» 버튼만 — 발표 정체성이 약해 «전체화면 Word» 느낌), 완전 분리(인쇄 경로가 Paper 와 갈라져 handout 논리에 어긋남). 화면은 순수 CSS(_slide.less, 컨테이너 쿼리 cqw 로 글자 크기 자동)+작은 JS(AhaWiki.Slide.js, 순수 인덱스 함수는 ahawiki.slide.test.mjs), 외부 라이브러리 없음. SlideUnit 포함 Scala 260·node 211 통과, 정적 하네스로 넘김·경계·유인물 fallback 을 눈으로 확인. 문서 InterpreterSlide. 전환·fragment·슬라이드별 배경·발표자 노트는 fast-follow 로 남겼다.
PageCursorHub 의 발행을 인스턴스 사이로 넓힌다(예: Redis pub/sub). 운영은 인스턴스 둘이 요청을 나눠 받는데, 커서·page.updated 는 저장한 요청과 같은 인스턴스에 붙은 사람에게만 간다 — 무엇이 빠지는지는 Dev WebSocket. 권한 캐시의 같은 문제는 만료 시간으로 묶었지만, 이벤트는 기다려서 오는 것이 아니라 설계를 정해야 한다. --2026-09-10page.updated 만 넓히고(커서는 고빈도라 인스턴스 안에 둔다), 브로커는 이미 공유 중인 Redis 의 pub/sub 를 권장(대안은 동일호스트 loopback HTTP relay), 전달은 at-most-once 로 충분. 착수 전 소유자가 정할 열린 결정 네 가지(범위·브로커·Redis 공유 확인·새 의존성 허용)를 남겼다. 결정 대기 중이라 구현은 안 했다.page.updated 만 구현했다(2026-09-15). 배포 전. 소유자가 «Redis pub/sub·page.updated만» 으로 정했다. CrossInstanceBus(신규) 가 저장 시 로컬 broadcast 후 캐시 Redis 채널 ahawiki:<db>:page.updated 로 PUBLISH 하고, 각 인스턴스가 SUBSCRIBE 해 남의 메시지만(자기 origin 은 건너뜀) 로컬로 재broadcast 한다. saveSenderId 를 메시지에 실어 상대 인스턴스도 저장자 탭을 제외한다. play-redis 는 pub/sub 를 안 열어 줘서 Jedis 를 따로 썼고, Redis 는 두 인스턴스가 이미 공유(로컬 redis, db0). 실패 시 로그만 남기고 로컬 동작 유지. 테스트 모드에선 끔. 단위테스트 CrossInstanceBusUnit(전송 형식), Scala 258 통과. 배포는 canary(10001) 로 부팅·저장·publish 확인 → 양쪽 올리고 두 인스턴스 간 왕복 확인 순서. 자세한 건 Dev WebSocket 의 그 절.PUBSUB NUMSUB=2), 채널에 넣은 page.updated 하나가 10001·10000 양쪽 소켓에 도달, 전송 형식 단위테스트, Scala 258 통과. 태그 v20260915T234918-aha00a. 남은 것 하나 — API·Spreadsheet·Diary 저장 경로는 예나 지금이나 page.updated 를 아예 발행하지 않는다(웹 저장 Wiki.save 만 발행). 그쪽도 알림이 필요하면 별도 작업.ApiV1.savePage)·Diary(Diary.write)·Spreadsheet(WikiSpreadsheet.syncGoogleSpreadsheet) 도 발행한다. 문서 sync 가 API 로 도는 지금은 API 저장이 가장 흔한 저장인데, 그것만 아무에게도 안 알리고 있었다. 호출부가 넷이 되므로 payload 조립을 CrossInstanceBus 안으로 옮겨 한 곳에서 만든다. rename 은 일부러 뺐다 — 옛 이름에 남는 건 #!redirect 리비전 1 이라 「새로고침할까요」 가 리다이렉트로 떨어진다. 단위테스트 CrossInstanceBusUnit 에 payload 필드·타입 추가, Scala 261·node 223 통과. 자세한 건 Dev WebSocket 발행하는 경로.20260925-143710. 배포 뒤 API 저장(문서 sync)이 그 페이지를 열어 둔 브라우저에 새로고침 토스트를 띄우는 것을 실제로 확인했다.rebaseKanbanColumns. 지금 동작은 InterpreterKanban 규칙. --2026-09-15#! 줄이 있는 페이지의 보드 저장이, 원격 업데이트를 받기 전까지 그 줄 수만큼 위에 들어간다. 저장 경로도 편집 화면처럼 디렉티브 줄 수를 보정하거나, 화면이 원문 줄 번호를 내려 주게 한다. --2026-09-12 고쳤다: 둘 다 아니고, 페이지를 연 뒤 첫 저장이 먼저 원문을 읽어 블록의 자리를 원문에서 잡는다. 409 가 쓰는 길과 같다. --2026-09-15[[[ 와 {{변수}} 보호가 깨지므로, 구간 안을 백틱 처리에서 빼는 쪽으로 고친다. --2026-09-12ExtractConvertInjectBackQuote 만 구간을 알게 했다 — extract 가 텍스트를 왼쪽부터 한 번 훑어, 리터럴 구간(#!Text·#!Vim·#!WikiSyntaxPreview, 그리고 Text 로 읽히는 이름 없는 [[[...]]])만 통째로 비켜 두고 그 안 백틱을 안 건드린다. 나머지는 종전대로 백틱을 먼저 뽑는다. 백틱 span 은 건너뛰므로 백틱 안의 [[[ 는 여전히 구간을 안 연다. 구간 추출·단계 순서는 그대로고, Vim 구간 본문에 자리표시자가 안 들어가니 캐시 미스도 사라진다.#!Quote·#!Table 같은 위키형 구간은 본문을 다시 위키로 렌더해 백틱이 어차피 코드가 되는데, 바깥에서 뽑던 걸 안쪽에서 뽑게 되며 코드만 있는 줄의 <p> 감쌈이 사라져 인용문 두 줄이 한 줄로 붙었다(표 칸은 td p 여백이 0 이라 그대로였다). 그래서 위키형은 손대지 않고 리터럴 구간으로만 좁혔다. 단위테스트 BackQuoteBlockUnit(리터럴은 원문 유지, 위키형은 종전대로), Scala 257·node 200 통과. 배포 순서: canary(한 인스턴스) → 백틱 든 전 페이지 전·후 렌더 비교 → 이상 없으면 나머지 인스턴스.#!Kanban · #!Gantt · #!Mermaid · #!Math · #!Html. Kanban 이 실제로 글자를 잃고 있었다: 보드는 그려진 본문을 태그를 떼고 읽으므로 백틱이 사라진 채로 다음 저장에 문서를 다시 썼다. 카드 글은 보드가 AhaMark 로 다시 그리니, 백틱은 거기서 코드가 되는 게 맞다. #!Markdown 은 그대로 뒀다(txtmark 이 같은 결과를 만든다), #!Graph · #!Map 도 본문이 데이터라 손대지 않았다. 단위테스트 BackQuoteBlockUnit 에 다섯 구간과 Kanban 왕복을 추가, Scala 261 통과. ahawiki.net 사본 113쪽에는 이 다섯 구간에 백틱이 든 곳이 없다 — 다른 사이트는 canary 렌더 비교로 확인할 것.20260925-143710, 태그 v20260925T144110-aha00a. canary(10001 만 새 코드)로 ahawiki.net 사본 113쪽을 전·후 렌더 비교했다 — 107쪽 동일, 6쪽은 옛 코드에서도 매번 다른 페이지(Kanban·Graph·Map·Include·Uptime 이 든 것들), 달라진 쪽 0. 그 뒤 나머지 인스턴스를 올리고 세 URL 을 밖에서 확인했다.v20260915T193157-aha00a.[[WeekdayName(2026-13-45)]] — 모양만 날짜면 통과시키고 실제 파싱에서 예외가 났다. 이제 Argument Error 상자다. MacroMonthName 이 9-12 에 같은 이유로 고쳐졌는데 이쪽은 남아 있었다.#!Map 블록 — 빈 블록은 첫 줄을 읽다가, 장소 0개인 블록은 «주소 0개» 를 IN () 로 물어서 깨졌다. 이제 열 이름이 없으면 오류 상자를 보여 준다. 그리고 진짜 원인인 빈 IN () 은 쿼리 함수 셋 안에서 막았다(GeocodeCache.select, CalculatedLink.selectDstMaxMinCountWhereSrcIsDatePage, Page.selectLastRevision(Seq)) — 나머지 다섯 곳은 이미 막고 있었다. 규칙은 Dev Database 빈 목록을 IN 에 넣지 말 것, 검사는 EmptyInClauseSpec.POST /w/FrontPage 에 아무 필드나 던지는 봇 때문에 bindFromRequest().get 이 None.get 으로 죽었다 — 7주에 727건, 그중 726건이 그 한 경로다. 지금은 400 이다. .get 을 쓰던 일곱 엔드포인트(save·delete·deleteLastRevision·rename·preview·Diary·Spreadsheet)가 모두 같은 FormResults.withForm 을 쓴다. 검사는 WikiSaveReCaptchaNotConfiguredSpec 의 두 항목.Home.random 이 빈 목록에서 고르려다 bound must be positive 를 냈다. 읽을 수 있는 페이지가 하나도 없는 방문자(전부 비공개인 사이트)가 Random 을 누르면 매일 한두 번씩 나고 있었다. 이제 FrontPage 로 보낸다.20260925-210723, 태그 v20260925T211116-aha00a. canary 로 옛 인스턴스와 대조했다 — 폼 아닌 POST 가 500 대 400, Random 은 양쪽 303, preview 는 UUID 말고 같은 HTML(옛 인스턴스끼리도 그 UUID 만 다르다). 그 뒤 나머지를 올리고 밖에서 세 URL 확인.conf/logback.xml 에서 인스턴스별 파일로 나눠 날마다 말고 30일·1GB 로 잡았다(2026-09-26). 배포함 — 릴리스 20260926-015144, 태그 v20260926T015509-aha00a. 재시작 직후 application-10001.log·application-10000.log 가 생겨 쓰이는 것을 봤다. 옛 application.log 는 손으로 지운다(Dev Logs).PageMeta.upsert 의 외래키 경합이 아직 난다(마지막 9-22). 계산 중에 그 리비전이 사라지면 나는 것이고 한 시간 안에 저절로 낫지만, ERROR 로 남아 진짜 오류를 가린다(Dev Logs 액터 쪽 경합).PageMetaSpec 이 늦게 도착한 쓰기를 재현하고, 고치기 전 코드로는 운영과 같은 외래키 예외로 실패한다. 배포했다(2026-10-03, 태그 v20261003T053233-aha00a) — 대조에서 바뀐 페이지 0.viaApi 가 이미 답한다. 아닌 경우에 쓰는 --major 가 생겼다(전에는 반대로 --minor 를 줘야 했다). 플래그는 저장소의 scripts/README.md.cookie has invalid signature 가 하루 1,400건씩 났다. 서명 키를 그때 바꿨는지 확인이 필요하다 — 맞다면 그 자체는 정상이고, 기록만 남기면 된다.SingleConnectionRenderSpec 이 풀 1개로 페이지를 그려 지킨다.conf/base.conf). 그 시험으로 실제 방문자 요청 하나가 500 을 받았다.v20261003T061551-aha00a). 같은 30개 동시 요청이 500 9건·37초에서 전부 정상·3.1초가 됐다. 대조에서 바뀐 페이지 0. 교착 재시도(Dev Page)도 같은 배포다.Connection is not available 이 보이면 한다. 읽는 쪽(미리보기, 검색, pagePreview·statistics·change)은 같은 날 마저 했고 배포했다(태그 v20261003T065503-aha00a) — 스펙이 못 돌리는 셋을 포함해, 두 인스턴스의 응답이 같은 것을 운영 DB 에 대고 확인했다. --2026-10-03type ALL 이었다. 고쳤다(2026-10-04), 배포 전 — 기준 seq 를 먼저 읽고 값으로 지운다(ExpiredRows). 지우는 행은 그대로라는 것을 ExpiredRowsSpec 이 옛 코드와 새 코드 양쪽에서 확인했다. 배포했다(2026-10-04 08:03, 태그 v20261004T080337-aha00a). 정리 작업 한 번(세 테이블)이 전날 135번 중간값 4,484ms·최대 17초에서 174·213ms 가 됐고, 08:00~08:33 의 slow log 에 이 문장은 없다. 첫 실행이 264행을 지워 가장 오래된 행이 정확히 한 달 전이 됐다. --2026-10-04socketTimeout 10분을 줬다(Dev Database). 배포했다(2026-10-04 08:37, 태그 v20261004T083747-aha00a) — 새 설정으로 두 대가 DB 에 붙고, canary 대조는 228경로 모두 같았다. DB 가 멈춘 상황에서 정말 10분 안에 끊는지는 다음에 그런 일이 나야 본다. --2026-10-04[[Image]] 처럼 괄호 없이 쓰면 매크로에 인자로 null 이 가서, 인자를 바로 읽는 다섯(Image · Attachment · Copyable · Embed · Kbd)이 NullPointerException 을 냈고 문서 전체가 500 이었다. [[Image()]] 는 멀쩡했다. 운영에서 매크로 42개 · 인터프리터 19개를 /api/renderAhaMark 로 하나씩 그려 보다 찾았다. 고친 자리는 매크로 하나하나가 아니라 인자를 넘기는 곳(ExtractConvertInjectMacro) 하나 — 괄호 없음은 빈 인자와 같게 읽는다. 단위테스트 WikiMacrosUnit 이 다섯의 «괄호 없음 == 빈 괄호» 를 확인하고, 고치기 전 코드로 돌리면 운영과 같은 NPE 로 실패한다. Scala 261 통과. 문서는 Macro 괄호를 빼고 써도 됩니다.[[Image]] · [[Kbd]] 가 옛 인스턴스에서 500, 새 인스턴스에서 200 인 것을 대조했다.IOException: Cannot run program "sh": error=0, Failed to exec spawn helper 였다. 인스턴스는 9-20 19:06 부터 돌고 있었는데 /usr/lib/jvm/java-17-openjdk-amd64 는 9-23 06:29 에 패키지로 갈렸고, 돌고 있는 프로세스의 /proc/PID/exe 는 (deleted) 였다. 그래서 JVM 이 자식 프로세스를 띄우지 못했다 — vi 를 못 부르니 새 캐시 항목이 안 만들어진다. 캐시 디렉터리 권한도 디스크도 멀쩡했다(.txt·.sh 는 쓰였고 .html 만 없었다). 고치는 방법은 재시작 이고, 9-25 배포가 그 재시작이었다 — 배포 뒤 새 Vim 블록이 강조까지 정상으로 나온다. 패키지가 JDK 를 갈면 그 뒤로 재시작 전까지 프로세스 실행이 전부 막힌다는 뜻이므로, 그 사실은 Dev Deploying 에도 적었다.#!Vim 블록을 그리면 500 이다 — /api/renderAhaMark 로 확인했다. 이미 캐시에 있는 블록(예: Dev Deploying 의 배포 명령)은 200 이고, 같은 새 블록을 네 번 보내도 매번 500 이라 캐시 항목이 아예 안 만들어진다. 즉 지금 어느 문서든 Vim 블록을 새로 넣으면 그 문서가 안 열린다. /hc 는 OK 라 디스크는 5% 넘게 남았다. 남은 후보는 cache/Vim 에 쓸 수 없음(권한·작업 디렉터리·릴리스의 cache 링크)이나 sh 부재인데, 어느 쪽인지는 서버 로그와 그 디렉터리를 봐야 안다.InterpreterVim 이 캐시 항목을 만들다 실패하면 강조 없이 본문을 보여 주고 페이지는 산다. 전에는 vi 실행 실패(RuntimeException)만 잡아서 파일 쓰기의 IOException 이 그대로 올라갔다. 로그에 Vim highlighting unavailable 과 실제 예외를 남기므로, 배포하면 원인이 로그에 이름을 대고 나온다. 단위테스트는 쓸 수 없는 캐시 디렉터리를 넘겨 확인한다(InterpreterVimUnit), Scala 261 통과. 자세한 건 InterpreterVim 강조를 못 하면 본문만 보여준다.20260926-025601, 태그 v20260926T025916-aha00a. canary 대조에서 달라진 쪽은 S3 서명 URL 의 시각뿐이었다(옛 인스턴스끼리도 1.5초 차이로 다르고, 서명을 가리면 옛·새가 같다). Interpreters.render 와 ExtractConvertInjectMacro.render 가 예외를 잡아 그 자리에 오류 상자를 놓고 로그에 예외 전체를 남긴다. 단위테스트는 일부러 던지는 인터프리터·매크로로 확인(InterpreterBlockUnit·WikiMacrosUnit). 문서는 Interpreter 그리다 실패하면, Macro.20260926-195055, 태그 v20260926T200032-aha00a. 옛 리비전이면 본문 위에 안내 상자를 두고, 읽기 전용으로 그리고, 소켓을 열지 않는다. 검사는 WikiOldRevisionViewSpec. 자세한 건 Dev Page 옛 리비전 화면./public/ 파일은 max-age=3600 이고 주소에 버전이 없어서, 배포 전 한 시간 안에 온 브라우저는 새 HTML 을 옛 CSS·JS 로 그린다. 주소에 내용 해시나 릴리스를 붙일지 정해야 한다. --2026-09-26Using 의 리플렉션 close())을 고쳐 다시 올렸다 — Dev Resources «운영에서는 jar 안에서 읽는다». 대조에서 217곳이 바뀌었고 전부 CSS·JS 주소의 ?v= 줄뿐이다. 태그 v20261001T051852-aha00a. 템플릿이 자기 CSS·JS 를 전부 PublicAsset.url 로 불러 주소 뒤에 ?v=<내용 해시> 가 붙는다. 플러그인·빌드 변경 없음. 검사는 PublicAssetVersionSpec(렌더된 페이지의 해시 == 그 주소에서 받는 내용의 해시)과 test/templates.public-assets.test.mjs(맨 주소·없는 파일 이름). 둘 다 고치기 전 템플릿으로는 실패한다. 고르지 않은 것과 남은 틈은 Dev Deploying CSS·JS 주소에는 내용 해시가 붙는다.?v=<새 해시> 가 옛 인스턴스로 가면, 옛 파일이 새 주소로 최대 한 시간 캐시된다. /public/ 을 Assets 를 감싸는 컨트롤러로 받아 v 가 자기 해시와 다를 때 no-cache 를 붙이면 막힌다 — 그 코드가 옛 인스턴스에 있어야 하므로 들어간 다음 배포부터 듣는다(Dev Deploying 남은 틈). --2026-09-27PublicAssets 로 그렇게 했다(2026-10-02). PublicAssetCacheSpec 은 옛 라우트(Assets 직접)로는 no-cache 검사에서 실패한다. 배포했다(2026-10-02, 태그 v20261002T054014-aha00a) — canary 에서 맞는 v 는 max-age=3600, 틀린 v 는 no-cache 를 확인했다. 다음 배포부터 듣는다.deploy.sh 는 성공을 보고했다. /hc 는 페이지를 그리지 않고, 외부 확인은 프록시를 거쳐 옛 대가 답한다. --2026-10-02renders 3 verify URL(s).scripts/browser-sweep.mjs 로 ahawiki.net 전체와 whohow.net 전체(2026-10-03), aha00a.com 전체 3,823페이지(2026-10-03~04)를 헤드리스 Chrome 으로 열었다. 스크립트 예외와 console.error 는 한 건도 없었다. 서버는 aha00a.com 스윕 동안 받은 요청 33,384개(웹소켓 제외)에 모두 5초 안에 답했고 5xx 는 없었다 — «로드 이벤트 없음»·«응답 없음» 으로 나온 페이지는 Chrome 넷을 한꺼번에 돌린 탓이었고, 하나씩 다시 열어도 남는 것은 광고 iframe 이 로드 이벤트를 잡고 있는 경우였다. 엔진 쪽 문제는 고쳐서 배포했다 — 사이트 목록의 /favicon.ico, aha00a.com PageMap 의 스크립트 오류(줄을 넘는 링크와 이스케이프 안 된 이름), #! 없는 블록에서 생기던 링크, 그래프·검색 스크립트가 인코딩하지 않던 페이지명(둘 다 Dev Page, 태그 v20261004T002023-aha00a). 이미 저장된 링크는 그 페이지가 다시 계산될 때 바뀐다 — 저장할 때, 열람 열 번에 한 번꼴, 또는 관리 화면의 Calculate. 배포 뒤 다시 열어 보니 2017-03-07 의 ??? 는 200 이고, Robocopy 는 이미 다시 계산돼 깨끗했으며, DataUri 는 아직 옛 링크로 400 이었다. 배포하고 열일곱 시간 뒤에도 블록에만 있던 링크를 34페이지가 149개 쥐고 있어서(블록에서 나온 링크 가운데 본문 다른 곳에는 없는 것만 센 수다 — 위의 248개와 단위가 다르다), 2026-10-04 저녁 관리 화면의 Calculate 로 한 페이지씩 다시 계산했다(그사이 하나는 열람으로 저절로 바뀌어 33페이지). 이제 그런 링크는 40페이지 모두 0개다. 블록 밖 본문의 링크가 함께 지워지지 않았는지도 페이지마다 본문에서 뽑은 것과 저장된 것을 맞춰 봤다 — 어긋난 것은 자기 자신을 가리키는 링크와 백틱 안의 글자뿐이고, 둘 다 원래 링크가 아니다. 남은 것:
_imgRetry=… 를 붙인다(adjacentPagesD3Canvas.scala.html). S3 서명 URL 에 쿼리를 더하면 서명이 맞지 않아 403 이므로, S3 이미지의 재시도는 언제나 실패한다 — 서명 URL 은 그대로 200, 붙이면 403 인 것을 확인했다. 첫 로드가 실패할 때만 일어나고, 그런 실패는 대개 이미지가 정말 없는 경우라 영향은 작다. 고친다면 서명 URL(X-Amz-Signature)에는 붙이지 않는다. --2026-10-03/api/links 로 다 훑었다), aha00a.com 은 전체 스윕에서 S3 썸네일이 실패한 페이지가 하나도 없었다(이 버그는 첫 로드가 실패해야 일어난다). 배포했다(2026-10-04 08:03, 태그 v20261004T080337-aha00a). /api/links 응답의 썸네일 둘을 서명 모양 주소와 보통 주소로 바꾸고 각각의 첫 요청을 실패시켜 CDP 로 봤다 — 옛 코드는 서명 주소에도 &_imgRetry= 를 붙였고, 새 코드는 서명 주소를 그대로 다시 받고 보통 주소에만 붙인다.proxy_pass http://upstream_…/; 처럼 끝에 / 가 붙으면 nginx 는 %22·%5B·%3C 를 "·[·< 로 되돌려 넘기고, Play 는 그런 경로를 읽지 못해 400 이다("Cannot parse path from URI"). 페이지명에 그런 글자가 든 링크 — 예: Tip "T.I." Harris — 는 누르면 400 이었다. --2026-10-04proxy_pass 19줄에서 끝 / 를 뺐다(2026-10-04). 따옴표 이름은 이제 없는 페이지 화면(404), /api/links/%3CMIME-type%3E 는 200 이고, 페이지·웹소켓·정적 파일은 그대로다. 어디를 어떻게 바꿨는지와 되돌릴 백업은 운영 저장소(AhariseDevOps) ahawiki/README.md '앞단 nginx'.// 를 합쳐 주지 않는다 — //public/less.css 가 바꾸기 전에는 200, 지금은 404 다. 위키가 그런 주소를 만들던 곳이 하나 있었다: 인접 페이지 그래프의 썸네일 주소(/api/links)가 / 로 시작하는 대표 이미지를 https://<host>//… 로 만들었다. 같은 일을 제대로 하던 미리보기(/api/pagePreview)의 함수가 따로 있었으니 두 벌이던 것을 Api.pageImageUrl 하나로 합쳤다. 검사는 PageImageUrlSpec — 옛 코드로는 //public/…·///cdn… 이 나와 실패한다. 지금 세 사이트 그래프(aha00a.com 3,823 · ahawiki.net 114 · whohow.net 26개, 썸네일 339개)에 그런 주소는 하나도 없어서 깨진 것은 없었다 — 일주일치 nginx 로그의 // 요청도 스캐너와, 10-03 에 고친 사이트 목록 아이콘(//public/favicon.png)뿐이었다. 배포했다(같은 태그). 배포 전후 세 사이트의 /api/links·/api/pagePreview 답이 두 인스턴스에서 같았다. nginx 를 바꾼 뒤 ahawiki.net 114·whohow.net 26페이지를 헤드리스 Chrome 으로 다시 열어 모두 깨끗했다. --2026-10-04schema:)는 내용을 다 그리면서 상태는 404 다(Wiki.renderNotFoundPage). 리다이렉트 Movie → schema:Movie 도 404 로 끝난다. 브라우저에는 그대로 보이니 깨진 것은 아니지만 검색 엔진에는 없는 페이지다 — 2021 년부터 그랬다. 소유자가 고민 중(2026-10-04). --2026-10-04schema:Movie 404, Windows7 리다이렉트)과 소녀시대의 http 플래시뿐이다.upload.wikimedia.org 를 쓰는 281페이지, 주소 291개 중 13개가 죽어 있었다). 원본 직접 주소는 파일 이름이 바뀌면 따라가지 않아서, 같은 그림인데 404 였던 것이 5개 — Commons/위키백과 API 로 새 이름을 확인해 새 주소로 바꿨다(부산광역시·CAP Theorem·J·Haskell·C, 사소한 편집). 부산광역시의 것은 2023 년에 바뀌기 전 상징이라는 이름이 붙었다 — 같은 그림이지만 지금 상징으로 바꿀지는 소유자 판단. whohow.net 의 이재명 사진도 같은 경우라 고쳤다 — 그 사이트 26페이지가 이제 다 깨끗하다..png 인데 .PNG 로 불렀다), WebSequenceDiagramsDotCom(임베드 스크립트를 https 로 — http 스크립트는 https 페이지에서 막혀 다이어그램 셋이 그려지지 않았다), Prisma·Normalize.css(GitHub camo 주소가 403 이라 camo 가 감싸던 원래 주소로), TracTicketSystem(htdocs: 경로를 Trac 공식 사이트의 같은 그림으로), Burnout(시사저널이 사진을 cdn.sisajournal.com 으로 옮겼다 — 기사가 같은 파일을 그 주소로 쓴다).w.namu.la 도메인이 없어졌다), 자신(페이스북 CDN 서명 만료), 티구시포(imweb 403), Waveform(bbc.co.uk 서버가 503, 인증서도 안 맞는다), GoogleChartTools(Google Image Charts API 가 닫혔다). 막힘: ProjectEulerProblem15(projecteuler.net 이 403 — 봇 차단), Hadouken(트위터 영상이 다른 사이트에서 부르면 403, Referer 없이는 200 이고 <video> 에는 referrerpolicy 가 없다). 그 밖: 소녀시대(없어진 플래시 임베드, http), Windows7(없는 MicrosoftWindows7 로 가는 리다이렉트), PlantUML(예시 다이어그램이 지금 표준 라이브러리에 없는 awslib/Compute/EC2ContainerRegistry.puml 을 include 해서 오류 그림이 나온다).build.sbt 에서 지웠다. 그중 parboiled·jackson-module-scala 는 Play 쪽이 더 새 버전으로 끌어와 원래 그 버전이 실리고 있었고, 지금도 그대로 실린다. 지운 것 하나가 S3 밑의 HTTP 클라이언트 버전을 올려 주고 있어서, 그 버전은 그대로 두었다(Dev Attachment S3 클라이언트). 고쳤다(2026-10-04), 배포 전. 스펙은 S3 를 부르지 않으므로 canary 에서 첨부 업로드·썸네일·Admin 의 S3 Browser 를 눌러 본 뒤 닫는다. --2026-10-04v20261004T231606-aha00a). ahawiki.net 228경로는 두 대가 모두 같았다. S3 는 확인한 길이 처음 계획과 다르다 — 읽기는 루프백으로 그 대를 골라 볼 수 있었고, 쓰기는 고를 수 없어서 두 대를 다 올린 뒤 봤다(방법은 Dev Attachment S3 클라이언트). 읽기: aha00a.com 의 첨부 목록이 두 대에서 같았다(목록 조회 성공, 서명 URL 하나, S3 의 답은 둘 다 NoSuchKey 라 서명은 맞다). 쓰기: 같은 페이지에 8×8 PNG 를 올려 서명 URL 로 열리는 것을 보고 지웠고(10-05 03:33, 각각 200), 목록에서도 S3 에서도 사라졌다. 기동 뒤 두 대의 로그에 ERROR·WARN·클래스 누락은 없었다.신과함께-죄와 벌 의 첨부 하나는 DB 에는 있는데 S3 에는 객체가 없다(서명 URL 에 NoSuchKey). 첨부 목록은 그것을 integrityStatus OK 로 보여 준다 — 서명 URL 을 만들 수 있으면 OK 이고, 객체가 있는지는 보지 않는다. --2026-10-05MacroAttachmentList 는 05-05 에 지웠다) 독자에게는 보이지 않는다. 깨진 항목이 보이는 곳은 그 페이지 편집 화면의 첨부 칸뿐이다.AccessLog 는 한 달, nginx 로그는 2주만 남는다. 페이지 삭제 길은 아니다: 그 길은 지운 객체의 행을 markDeleted 로 함께 표시하는데 이 행은 Uploaded 그대로다. 행을 남기고 객체만 지우는 길은 관리 화면의 S3 Browser 삭제(ApiAdminS3.adminDeleteS3Objects, 05-05 에 생겼다)다.DB_ONLY 로 나오는 것을 보고, S3 Browser 삭제로 그 키를 지웠다 — S3 는 없는 키도 지웠다고 답하므로 행이 Deleted 로 표시됐고(markedAttachmentRows 1), 그 페이지의 첨부 목록은 비었다. 텔레그램 알림은 가지 않는 길이다.Attachment 행은 그대로 둔다 — 위와 같은 행이 다시 생길 수 있다. 지운 키의 행을 markDeleted 로 함께 표시하면 된다. --2026-10-05S3Logic.deleteObjects 가 실패한 키만이 아니라 지운 키도 돌려준다(S3LogicSpec). 이 길이 실제 S3 로 도는지는 운영에서 본다.v20261005T044925-aha00a). 실제 객체 하나(시험으로 올린 클립보드 그림)를 S3 Browser 로 지우니 markedAttachmentRows 1 이었고, 그 행은 Deleted, 목록에 S3_ONLY 로 남지도 않았다. 위 seq 1 처럼 객체가 없는 키도 같은 결과였다.integrityStatus 가 객체가 있는지 보지 않는다. 같은 응답이 이미 그 페이지의 S3 목록을 읽고 있으니, 거기 없는 행을 따로 표시하면 S3 요청을 더하지 않고 알 수 있다. --2026-10-05DB_ONLY 를 쓴다 — 편집 화면이 이미 «S3 확인 필요» 로 빨갛게 보여 주고 삭제 버튼도 남겨 두는 값이고, 뜻(DB 에만 있다)도 그대로 맞는다. 판정 규칙은 AttachmentLogic.integrityStatus 와 그 스펙. 위 36행에 같은 규칙을 적용해 보면 seq 1 하나만 DB_ONLY 다. 배포 뒤 seq 1 이 실제로 DB_ONLY 로 나오는지 보고, 그다음 그 행을 정리한다.OK, 새 대에서는 DB_ONLY 였다. 새로 올린 첨부 둘은 OK 였다.etag(따옴표 없이 들어가야 한다), 첨부 목록과 지우기, 첨부가 있는 페이지 지우기(시험용 페이지로), 썸네일(인접 페이지 그래프·미리보기), favicon 올리기·지우기, 관리 화면 S3 Browser(목록·지우기·다운로드 URL). canary 대조에서는 서명 URL 이 든 페이지가 모두 «바뀜» 으로 나온다 — 쿼리 순서 때문이고, 버킷 이름에 점이 있으면 호스트도 바뀐다(Dev Attachment presigned URL). 그 밖의 차이가 나오면 그것이 문제다. --2026-10-04v20261005T044925-aha00a, 위 첨부 고침과 함께). ahawiki.net 228경로는 두 대가 모두 같았다 — 이 사이트에는 첨부가 없어 서명 URL 차이도 나오지 않았다. 기동 뒤 두 대의 로그에 ERROR·클래스 누락은 없었다. 실제 S3 로 확인한 것:NoSuchKey(서명이 틀리면 403).신과함께-죄와 벌): 파일 첨부와 클립보드 그림 각각 200, 서명 URL 로 열렸다. 행의 etag 는 따옴표 없이 S3 의 ETag 와 같았고, S3 객체의 Content-Type 은 image/png 였다.deleteObject)와 S3 Browser 의 일괄 삭제(deleteObjects) 모두 200, 둘 다 행이 Deleted 가 됐다./api/pagePreview), 인접 페이지 그래프 썸네일이 모두 열렸다(aha00a.com HJH1 의 1080px 그림). S3 Browser 목록은 aha00a.com 객체 6개를 잘림 없이 돌려줬다.head-object 404).AttachmentLogic.deletePageAttachments)는 SDK 2.x 로 실제 S3 에서 아직 안 해 봤다 — 시험용 페이지를 만들고 지워야 해서 남겼다. 쓰는 S3 호출(목록, S3Logic.deleteObject)은 위에서 각각 확인했다. --2026-10-05OK 둘과 S3_ONLY 하나. API v1 로 페이지를 지우니 200, 페이지는 404, 접두사 아래 객체는 셋 다 사라졌고(행 없는 것은 S3 목록으로 찾아 지운 것이다) 두 행은 Deleted 가 됐다. 다른 페이지의 첨부 객체 수는 그대로였고 로그에 오류는 없었다.위키 JVM 둘이 hatchery 에서 메모리를 가장 많이 쓰고, 상자 swap 의 대부분이 위키 몫이다 — 운영 저장소의 server/README.md. 2026-10-07 06:05 에 잰 것(각 인스턴스, 2일 가동): RSS 약 700MB 에 swap 430~520MB, 힙은 314MB 커밋, Metaspace 140MB, glibc malloc 아레나 12개가 381MB(그중 324MB 가 swap). 시작 옵션은 -Xms128m -Xmx512m -XX:+ExitOnOutOfMemoryError 뿐이고 GC 는 G1 이다.
v20261007T070128-aha00a, 아래 렌더 CPU 고침과 함께). ahawiki.net 228경로는 두 대가 모두 같았고, 가장 큰 사이트에서 무작위로 고른 40쪽도 같았다. canary 와 옛 대를 루프백으로 같은 시각에 대어 보니 /search?q=a 가 14.6MB·2.39초 → 0.57MB·0.17초(92건, 100건 넘침 안내가 나온다), q=의 가 4.7MB·0.46초 → 0.47MB·0.13초. blame 과 페이지 보기는 바이트가 같았다./w/ 200 응답 시간(루프백 제외, 표본 약 230건씩)은 00:00~07:0x → 07:0x~14:26 에 p50 97·99ms → 62·59ms, p90 555·706ms → 163·199ms — 시간대가 달라 대략으로만 읽는다.ahawiki/README.md «MALLOC_ARENA_MAX=2». --2026-10-10MALLOC_ARENA_MAX=2 는 아레나 수를 묶었지만 malloc 이 쥔 양은 줄이지 못했고, 힙 커밋은 오히려 늘었다. 인스턴스당 약 1.1GB 그대로.-XX:TrimNativeHeapInterval 과 jcmd <pid> System.trim_native_heap 이 있다. 한 대에서 먼저 시험해 볼지 소유자 판단. 무엇이 malloc 하는지는 -XX:NativeMemoryTracking=summary 로 재시작해야 보인다. --2026-10-10VM.native_memory 는 «not enabled»), JVM 자신이 잡은 메모리(컴파일러·GC·스레드·심볼 등, DirectByteBuffer 는 «Other»)만 센다. zlib 처럼 JDK 의 네이티브 라이브러리가 직접 부른 malloc 은 세지 않는다. 또 지금 잡혀 있는 양만 세므로, trim 이 돌려준 «free 된 채 malloc 이 쥔» 빈자리는 NMT 에 나타나지 않는다 — 시점을 두고 summary.diff 로 어느 분류가 크게 오르내리는지 보는 데까지다. glibc 쪽 사정(아레나별 빈자리)을 보여 주는 System.native_heap_info 는 이 빌드(17.0.20.1)의 jcmd 에 없다. --2026-10-11jcmd <pid> System.trim_native_heap 을 한 번 했다(2026-10-10 18:21, 재시작 없음 · 10000 은 비교용으로 그대로). RSS+swap 1,059 → 718MB, 그 덩어리가 약 320 → 55MB 로 줄었다 — free 된 채 malloc 이 쥐고 있던 메모리였다. 걸린 시간 약 1초. 영역별 분해·재는 법·숫자의 정본은 운영 저장소 ahawiki/README.md «AhaWiki 의 swap — 무엇이 쥐고 있나». --2026-10-10-XX:TrimNativeHeapInterval 을 운영 유닛 드롭인으로 넣는다(재시작이 들어 소유자 판단). --2026-10-10JAVA_OPTS=-XX:TrimNativeHeapInterval=60000, 효력은 다음 재시작(배포)부터이고 지금 도는 두 대는 그대로다. 60초를 고른 이유·확인한 것·되돌리기는 운영 저장소 ahawiki/README.md «JAVA_OPTS — 유닛 드롭인»(그때 이름은 «TrimNativeHeapInterval»).10000 의 java 명령줄에 플래그가 있는지 보고(10001 은 아래 «평소 한 대» 뒤로는 배포 동안만 뜬다), 며칠 뒤 그 덩어리가 trim 직후 수준(55~74MB)에 머무는지와 표본기의 fault 수가 늘지 않았는지를 같은 방법으로 잰다. --2026-10-10TrimNativeHeapInterval = 60000 {command line} 을 확인했다. 며칠 뒤 재는 일은 남았다.10001 약 190MB/일, 10000 약 235MB/일. 재부팅 뒤 18.5시간(주기 trim 60초)의 비주 아레나 힙은 68·55MB 로 trim 직후 수준이고, major fault 는 시간당 약 250 이라 60초를 유지한다. 숫자는 운영 저장소 ahawiki/README.md «1회 trim» 의 «1일 뒤 재측정». --2026-10-11MALLOC_ARENA_MAX=2 — 아레나 381MB 가 힙 다음으로 크다. Tikize 유닛들이 같은 설정을 이미 쓴다(운영 저장소 server/README.md «glibc 셋»). 유닛 파일(ahawiki@.service)은 운영 쪽 변경이고 재시작이 들어 소유자 판단. --2026-10-07ahawiki/README.md. --2026-10-07-XX:G1PeriodicGCInterval 이나 힙 512MB 규모에 맞는 -XX:+UseSerialGC 는 실측 비교 뒤에 정한다. --2026-10-07G1PeriodicGCInterval 은 manageable 이라 재시작 없이도 켤 수 있다.jstat). 힙을 줄이는 일은 동시 표시 주기의 Remark(와 Full GC)에서만 일어나는데, 그 주기가 3.6일에 5번(약 17시간에 한 번)뿐이다. 그때도 MaxHeapFreeRatio 기본값 70 이 빈 칸을 70% 까지 허용해 줄일 것이 없다. 10000 은 커밋 430MB 중 eden 이 254MB, young GC 는 약 5분에 한 번 13ms. 그러니 G1PeriodicGCInterval 은 주기를 더 자주 돌릴 뿐 혼자서는 줄이지 못한다. 줄이는 손잡이는 MinHeapFreeRatio·MaxHeapFreeRatio 이고 둘 다 manageable 이다. --2026-10-10jcmd <pid> VM.set_flag 로 MinHeapFreeRatio 10, MaxHeapFreeRatio 30, 주기를 바로 일으키려고 G1PeriodicGCInterval 60000 — 주기가 돈 뒤 21:02:27 에 0 으로 되돌렸다. 30초 안에 힙 커밋 396 → 143MB, RSS+swap 786 → 535MB. 뒤따른 mixed GC 로 old 사용 92 → 64MB. Full GC 는 없었다. --2026-10-10jcmd 로 바꾼 값은 재시작(다음 배포)에서 사라진다.jstat 값은 누계라 2026-10-10 21:05 기준값을 뺀다 — 10001 YGC 1102 · YGCT 13.165s · CGC 10 · CGCT 1.986s, 10000 YGC 889 · YGCT 11.632s · CGC 10 · CGCT 0.053s. Remark 가 계속 길면 그 대가를 같이 본다. 괜찮으면 운영 유닛 드롭인의 JAVA_OPTS 에 공백으로 이어 배포 재시작에 얹는다. 다음 배포의 재시작이 이 시험을 지우므로, 비교는 그 배포 전에 한다. --2026-10-10jstat 누계도 함께 사라져 그 구간의 young GC·동시 표시 멈춤 횟수와 시간은 잴 수 없다. GC 로그는 켜져 있지 않다. --2026-10-11JAVA_OPTS 에 -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=30 을 넣을지 소유자 판단. 권고는 넣는 것, 그리고 같이 -Xlog:gc 를 파일로 켜서 다음 비교가 jstat 누계에 기대지 않게 하는 것. 평소 한 대가 된 뒤에는 AHAWIKI_RESTART_ONLY=1 로 적용한다 — 대기 인스턴스가 받는 동안이라 거의 끊기지 않지만, 첫 인계에서 주 인스턴스가 내려가는 7초에 503 한 건이 샜다(위 «인계 중 … 503»). --2026-10-11-Xlog:gc 와 함께 — 수집마다 한 줄, 로그 디렉터리에 포트마다 하나(10MB × 5). 같은 옵션으로 java -version 을 서비스 계정으로 먼저 띄워 봤고, AHAWIKI_RESTART_ONLY=1 인계로 10000 에 들어갔다(05:16:14 기동, 명령줄에 넷 다 있다). 경위·되돌리기는 운영 저장소 ahawiki/README.md «JAVA_OPTS — 유닛 드롭인». --2026-10-1210000 의 young GC 간격·멈춤, 동시 표시 주기의 Remark 멈춤, 힙 커밋을 본다 — 한 대가 요청을 다 받게 된 뒤 GC 가 얼마나 잦아졌는지, Remark 가 길어지는지. 힙 커밋은 재부팅 뒤 기본값(40/70)으로 282·289MB 였다. --2026-10-12deploy.sh 가 대기 인스턴스를 먼저 띄우고, 프록시를 reload 해 실패 기억을 지운 뒤 10000 을 재시작하고 대기 인스턴스를 내린다. canary(AHAWIKI_CANARY)와 빌드 없는 인계(AHAWIKI_RESTART_ONLY)를 더했다. 서버에 닿지 않게 가짜 ssh·curl 로 경우마다 돌려 보낸 명령의 순서를 봤다 — 정상, 대기 인스턴스가 안 뜸·안 그림, 10000 이 새 릴리스에서만 안 그림·늘 안 그림, 빌드 없는 인계에서 10000 이 안 그림, 대기 인스턴스만 서비스 중인 상태, canary, reload 실패. 그러다 10000 이 실패하면 «대기 인스턴스가 대신 받는다» 고 알리고 끝내던 첫 모양이 틀렸음을 찾았다 — 프록시는 10000 이 무엇이든 답하면 그쪽으로 돌아간다. --2026-10-1110001 을 backup 으로 두고, 10001 을 disable 한다. 10001 의 힙 비율 시험을 비교한 뒤(위 «하루 이상 지난 뒤»), 소유자 확인을 받아서. 할 일과 명령은 운영 저장소 ahawiki/README.md. --2026-10-1110001 을 backup 으로, nginx -t 통과·reload). 10001 의 disable 과 첫 인계는 예약 작업의 권한 검사에서 거부돼 하지 않았다 — 10001 은 아직 enabled·active 로 backup 자리에 떠 있다. 백업 파일과 남은 명령은 운영 저장소 ahawiki/README.md. --2026-10-1110001 disable 과 첫 인계(AHAWIKI_RESTART_ONLY=1)를 했다(소유자 «진행해»). 이제 10000 만 enabled·active 다. --2026-10-12deploy.sh 의 첫 실제 인계(배포든 AHAWIKI_RESTART_ONLY=1 이든)를 본다 — 프록시를 거친 요청이 그동안 끊기지 않는지, 끝나고 10001 이 내려가 있는지. --2026-10-11AHAWIKI_RESTART_ONLY=1. 순서대로 됐다: 10001 기동 → 10000 재시작 → 10001 내림. 외부 확인 URL 셋 다 200, 끝나고 10001 은 inactive 다. --2026-10-1210000 이 내려가던 7초(04:39:55~04:40:02) 사이에 들어온 GET 하나가 503 을 받았다(같은 몇 분 동안 요청 ~940건 중). 내려가는 서버가 503 으로 답했고, 프록시는 기본 설정상 503 을 backup 으로 넘기지 않는다. --2026-10-12ahawiki/README.md «평소 한 대» ⚠), 또는 deploy.sh 가 10000 을 멈추기 전에 프록시에서 먼저 빼는 것. 한 건이라 급하지 않다. --2026-10-12app/ 에서 확인) 다시 보내는 것은 내려가는 인스턴스의 503 뿐이고, POST 는 프록시 기본대로 다시 보내지 않는다. 바로 뒤의 인계(05:15:44~05:16:36)에서 1초마다 프록시로 부른 /hc 72건이 모두 200 이었다. 무엇을 어디에 바꿨는지는 운영 저장소 ahawiki/README.md «앞단 nginx». --2026-10-1210001 이 tarpit 으로 붙잡고 있던 중(05:16:14 부터, Dev BotDetection)에 내려갔고, Play 의 종료는 진행 중 요청을 5초 기다린 뒤 503 으로 끊는다. backup 으로 간 요청을 프록시가 주 인스턴스로 되돌리지는 않는 것으로 보인다. 정상 요청은 1초 안에 끝나니 지금 걸리는 것은 차단된 봇뿐이다. 막으려면 deploy.sh 가 대기 인스턴스를 내리기 전에 그 연결이 빠지기를 기다리면 된다(tarpit 이 최대 55초라 배포가 그만큼 길어진다). 막을지 소유자 판단 — 권고는 두는 것. --2026-10-12docs/ahawiki.net/ 114쪽을 H2 에 넣고 /w/… 로 다섯 번씩 그리며 JFR 로 표본을 떴다(스펙 하나를 임시로 써서 돌렸고 커밋하지 않았다). 고치기 전 한 쪽에 17.33ms, 고친 뒤 9.80ms. 고친 뒤 114쪽의 HTML 은 고치기 전과 글자 하나 다르지 않았다(푸터의 Uptime 만 가리고 비교). 무엇이 컸고 무엇을 바꿨는지(괄호는 고치기 전 렌더 시간 중 몫): --2026-10-07ExtractConvertInject.inject 가 키마다 String.replace 로 페이지 전체를 복사했다(11%). 키 자리를 먼저 다 찾고 한 번에 짓는다 — 같은 결과인 이유는 replaceKeys 의 주석과 ExtractConvertInjectSpec.AhaMarkLink 이 국가 코드 집합(약 250개)을 새로 만들었다(6%) — 동반 객체로. 날짜 모양 검사의 String.matches 가 링크마다 패턴을 다시 컴파일했다 — 컴파일된 Regex.matches 로.DefaultPageLogic 이 클래스패스(jar)를 뒤졌다(4%) — 답을 기억한다.[ 도 :// 도 없는 줄에서도 링크 정규식을 돌렸다 — 건너뛴다. inlineToHtmlString 의 다섯 정규식과 백틱 정규식 둘을 호출마다 컴파일했다 — 한 번만.PartialEdit.revision 이 리비전 번호 하나를 얻으려고 본문 전체와 User 조인을 읽었다(3%) — 번호만 읽는다(Page.selectLastRevisionNumber). 같은 요청 안에서 저장 뒤 다시 그리는 길이 있으면 낡을 수 있어서 컨텍스트에 기억시키지는 않았다. 블록이 없는 페이지는 아예 묻지 않는다.Wiki.view 가 최신 리비전을 두 번 읽었다(Page.select(name, 0) 이 다시 읽는다) — 한 번.ApplicationConf 가 설정값을 읽을 때마다 스레드 스택 트레이스를 뜬다(fqn). 고치지 않기로 했다 — 위 측정에서 렌더 시간 중 0.0% 였다. 그 측정은 필터를 끈 채라 FilterAccessLog 의 한 번은 빠졌지만, 요청마다 한 번이면 렌더 한 번에 비해 작다. 메서드 이름이 곧 설정 키라는 2015년부터의 설계를 바꿀 값이 없다. --2026-10-07TraitInterpreter.toText(jsoup, 고친 뒤 11%) — 페이지 설명이 PageMeta 에 없을 때 PageLogic.extractDescription 이 본문을 다시 그려 글자만 뽑는다. 측정용 H2 에는 설명이 없어 매번 돌았고, 운영에서는 설명이 있는 페이지가 많아 덜하다. 운영에서 얼마나 도는지 보고 정한다. --2026-10-07Wiki.scala 의 보기, 2026-10-12 코드로 확인). ① 최신 리비전이 아직 계산되지 않았을 때 — 편집 직후 잠깐이다. ② 최신 리비전으로 계산됐는데 설명이 없을 때 — 뽑을 문장이 없는 페이지(표·매크로만 있는 페이지 등)다. ② 는 다시 뽑아도 같은 코드가 같은 글에서 또 «없음» 을 내니 순전히 헛일이고, 그 페이지를 볼 때마다 돈다. 막는 법은 작다: 페이지 목록의 그 행이 지금 보는 리비전이면 그 설명(없음 포함)을 그대로 쓴다. 얼마나 되는지는 관리 화면의 PageMeta 목록(/api/Admin/Site/:seq/PageMetaList)에서 설명이 빈 행을 사이트마다 세면 안다 — 공개 API 로는 셀 수 없고 관리자 세션이 든다. --2026-10-12Calculate)을 보낸다 — 렌더 두 번과 유사도 셀프 조인이고, 봇의 조회도 센다. 줄일지는 재계산이 무엇을 늦게 하게 되는지 보고 정한다. --2026-10-07PageLogic.ViewRecalculationInterval 의 주석. --2026-10-12Calculate 줄): 보기에서 온 계산 16번, 합 8.7초, 하나에 최대 3.8초. 그중 셋은 30분 안에 같은 페이지를 다시 계산한 것이었다. 같은 시간에 위키 동기화의 저장 넷이 10.6초를 더 썼다 — 저장은 이번 변경과 상관없이 계산한다. --2026-10-12a a1 a2 b c c1 c2
* a * a1 * b * c * c1 * c2
InterpreterWiki 의 regexTabList, regexList 다음에 시도). 탭만 대상이고 공백 들여쓰기는 예전처럼 문단으로 둔다. blast radius 점검 결과 현재 맨앞 탭 프로즈 줄이 있는 페이지는 hands-off 3곳뿐(wiki.aharise.com 2, sst Gantt 1) — 그 3곳의 렌더만 바뀐다(내용은 안 건드림, 타인 위키). 단위테스트 TabListUnit, Scala 259 통과. 배포는 canary → 그 3곳만 바뀌는지 렌더 비교 → 양쪽.<li> 불릿), 표본 104곳 전부 SAME(회귀 없음). aharise 주주명부 페이지에서 <p> …</p> → <ul><li>…</li></ul> 확인. 양쪽 올림, 태그 v20260916T102816-aha00a. 문서는 InterpreterWiki 탭 들여쓰기. hands-off 3곳 렌더가 바뀐 건 예상대로이고 내용은 안 건드림.Similar pages by cosine similarity. Words after page name are term frequency.