hikoutei 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/application/sync/outbound/SheetsEffectDispatcher.d.ts +64 -0
  2. package/dist/application/sync/outbound/SheetsEffectDispatcher.d.ts.map +1 -0
  3. package/dist/application/sync/outbound/SheetsEffectDispatcher.js +440 -0
  4. package/dist/application/sync/outbound/SheetsEffectDispatcher.js.map +1 -0
  5. package/dist/application/sync/service/SyncServiceBootstrap.d.ts +3 -4
  6. package/dist/application/sync/service/SyncServiceBootstrap.d.ts.map +1 -1
  7. package/dist/application/sync/service/SyncServiceBootstrap.js +7 -4
  8. package/dist/application/sync/service/SyncServiceBootstrap.js.map +1 -1
  9. package/dist/application/sync/telemetry/syncTiming.d.ts +20 -33
  10. package/dist/application/sync/telemetry/syncTiming.d.ts.map +1 -1
  11. package/dist/application/sync/telemetry/syncTiming.js +14 -12
  12. package/dist/application/sync/telemetry/syncTiming.js.map +1 -1
  13. package/dist/infrastructure/storage/errors.d.ts +10 -5
  14. package/dist/infrastructure/storage/errors.d.ts.map +1 -1
  15. package/dist/infrastructure/storage/errors.js +10 -7
  16. package/dist/infrastructure/storage/errors.js.map +1 -1
  17. package/dist/infrastructure/storage/index.d.ts +6 -4
  18. package/dist/infrastructure/storage/index.d.ts.map +1 -1
  19. package/dist/infrastructure/storage/index.js +3 -2
  20. package/dist/infrastructure/storage/index.js.map +1 -1
  21. package/dist/infrastructure/storage/sqlite/schema.d.ts +12 -11
  22. package/dist/infrastructure/storage/sqlite/schema.d.ts.map +1 -1
  23. package/dist/infrastructure/storage/sqlite/schema.js +17 -109
  24. package/dist/infrastructure/storage/sqlite/schema.js.map +1 -1
  25. package/dist/infrastructure/storage/state/canonical/canonicalCommit.d.ts +2 -2
  26. package/dist/infrastructure/storage/state/canonical/canonicalCommit.d.ts.map +1 -1
  27. package/dist/infrastructure/storage/state/canonical/canonicalCommit.js +2 -2
  28. package/dist/infrastructure/storage/state/canonical/canonicalCommit.js.map +1 -1
  29. package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.d.ts +1 -2
  30. package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.d.ts.map +1 -1
  31. package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.js +1 -2
  32. package/dist/infrastructure/storage/state/mapped/mappedPersistenceContext.js.map +1 -1
  33. package/dist/infrastructure/storage/state/observation/observationCanonical.d.ts +1 -1
  34. package/dist/infrastructure/storage/state/observation/observationCanonical.d.ts.map +1 -1
  35. package/dist/infrastructure/storage/state/observation/observationQuarantine.d.ts +1 -1
  36. package/dist/infrastructure/storage/state/observation/observationQuarantine.d.ts.map +1 -1
  37. package/dist/infrastructure/storage/state/observation/observationQuarantine.js +1 -1
  38. package/dist/infrastructure/storage/state/observation/observationQuarantine.js.map +1 -1
  39. package/dist/infrastructure/storage/state/observation/observationTypes.d.ts +1 -1
  40. package/dist/infrastructure/storage/state/observation/observationTypes.d.ts.map +1 -1
  41. package/dist/infrastructure/storage/state/observation/observationValidation.d.ts +1 -1
  42. package/dist/infrastructure/storage/state/observation/observationValidation.d.ts.map +1 -1
  43. package/dist/infrastructure/storage/state/observation/observationWriter.d.ts +1 -1
  44. package/dist/infrastructure/storage/state/observation/observationWriter.d.ts.map +1 -1
  45. package/dist/infrastructure/storage/state/observation/observationWriter.js +1 -2
  46. package/dist/infrastructure/storage/state/observation/observationWriter.js.map +1 -1
  47. package/dist/infrastructure/storage/state/resolution/resolutionWriter.d.ts +1 -2
  48. package/dist/infrastructure/storage/state/resolution/resolutionWriter.d.ts.map +1 -1
  49. package/dist/infrastructure/storage/state/resolution/resolutionWriter.js +1 -2
  50. package/dist/infrastructure/storage/state/resolution/resolutionWriter.js.map +1 -1
  51. package/dist/infrastructure/storage/state/resolution/resolutionWriterContracts.d.ts +1 -1
  52. package/dist/infrastructure/storage/state/resolution/resolutionWriterContracts.d.ts.map +1 -1
  53. package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.d.ts +1 -1
  54. package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.d.ts.map +1 -1
  55. package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.js +1 -1
  56. package/dist/infrastructure/storage/state/resolution/resolutionWriterHelpers.js.map +1 -1
  57. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.d.ts +1 -1
  58. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.d.ts.map +1 -1
  59. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.js +2 -2
  60. package/dist/infrastructure/storage/state/resolution/resolutionWriterSql.js.map +1 -1
  61. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.d.ts +1 -1
  62. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.d.ts.map +1 -1
  63. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.js +1 -1
  64. package/dist/infrastructure/storage/sync/shared/spreadsheetAuthority.js.map +1 -1
  65. package/dist/infrastructure/storage/sync/shared/syncRegistry.d.ts +1 -1
  66. package/dist/infrastructure/storage/sync/shared/syncRegistry.js +1 -1
  67. package/package.json +13 -7
  68. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.d.ts +0 -66
  69. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.d.ts.map +0 -1
  70. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.js +0 -123
  71. package/dist/application/sync/outbound/effects/AdaptiveEffectBatchController.js.map +0 -1
  72. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.d.ts +0 -111
  73. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.d.ts.map +0 -1
  74. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.js +0 -369
  75. package/dist/application/sync/outbound/effects/SyncEffectSupervisor.js.map +0 -1
  76. package/dist/application/sync/outbound/effects/SyncEffectWorker.d.ts +0 -127
  77. package/dist/application/sync/outbound/effects/SyncEffectWorker.d.ts.map +0 -1
  78. package/dist/application/sync/outbound/effects/SyncEffectWorker.js +0 -552
  79. package/dist/application/sync/outbound/effects/SyncEffectWorker.js.map +0 -1
  80. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.d.ts +0 -85
  81. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.d.ts.map +0 -1
  82. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.js +0 -74
  83. package/dist/application/sync/outbound/effects/SyncEffectWorkerConstants.js.map +0 -1
  84. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.d.ts +0 -31
  85. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.d.ts.map +0 -1
  86. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.js +0 -222
  87. package/dist/application/sync/outbound/effects/SyncEffectWorkerDispatch.js.map +0 -1
  88. package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.d.ts +0 -14
  89. package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.d.ts.map +0 -1
  90. package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.js +0 -25
  91. package/dist/application/sync/outbound/effects/SyncEffectWorkerHelpers.js.map +0 -1
  92. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.d.ts +0 -61
  93. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.d.ts.map +0 -1
  94. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.js +0 -296
  95. package/dist/application/sync/outbound/effects/SyncEffectWorkerRouting.js.map +0 -1
  96. package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.d.ts +0 -17
  97. package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.d.ts.map +0 -1
  98. package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.js +0 -80
  99. package/dist/application/sync/outbound/effects/SyncEffectWorkerTiming.js.map +0 -1
  100. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.d.ts +0 -13
  101. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.d.ts.map +0 -1
  102. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.js +0 -248
  103. package/dist/application/sync/outbound/effects/SyncEffectWorkerTransitions.js.map +0 -1
  104. package/dist/infrastructure/storage/sync/outbound/effectOutbox.d.ts +0 -141
  105. package/dist/infrastructure/storage/sync/outbound/effectOutbox.d.ts.map +0 -1
  106. package/dist/infrastructure/storage/sync/outbound/effectOutbox.js +0 -318
  107. package/dist/infrastructure/storage/sync/outbound/effectOutbox.js.map +0 -1
  108. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.d.ts +0 -143
  109. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.d.ts.map +0 -1
  110. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.js +0 -16
  111. package/dist/infrastructure/storage/sync/outbound/effectOutboxContracts.js.map +0 -1
  112. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.d.ts +0 -33
  113. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.d.ts.map +0 -1
  114. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.js +0 -259
  115. package/dist/infrastructure/storage/sync/outbound/effectOutboxSql.js.map +0 -1
  116. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.d.ts +0 -27
  117. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.d.ts.map +0 -1
  118. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.js +0 -316
  119. package/dist/infrastructure/storage/sync/outbound/effectOutboxSupport.js.map +0 -1
  120. package/dist/infrastructure/storage/sync/shared/writerLease.d.ts +0 -76
  121. package/dist/infrastructure/storage/sync/shared/writerLease.d.ts.map +0 -1
  122. package/dist/infrastructure/storage/sync/shared/writerLease.js +0 -189
  123. package/dist/infrastructure/storage/sync/shared/writerLease.js.map +0 -1
  124. package/docs/advanced-sheets-gateway-concurrency-problem.md +0 -434
  125. package/docs/architecture.md +0 -224
  126. package/docs/ci.md +0 -293
  127. package/docs/code-guidelines.md +0 -248
  128. package/docs/development.md +0 -74
  129. package/docs/gateway-removal-inventory.md +0 -147
  130. package/docs/git-workflow.md +0 -224
  131. package/docs/google-sheets-sync-scaling-strategy.md +0 -459
  132. package/docs/mikro-orm-adapter-spike.md +0 -89
  133. package/docs/quick-start.md +0 -137
  134. package/docs/sql-layer-plan.md +0 -59
  135. package/docs/sync-bulk-write-benchmark.md +0 -2117
  136. package/docs/sync-observability.md +0 -100
  137. package/docs/task-queue-write-model.md +0 -640
  138. package/docs/typed-sheets-mvp-scope-2026-06-29.md +0 -490
  139. package/docs/typed-sheets-plan.md +0 -417
  140. package/docs/write-and-synchronization-flow.md +0 -179
@@ -1,434 +0,0 @@
1
- # Hikoutei Advanced Sheets Gateway 동시성/정합성 문제 해결 요청서
2
-
3
- > 이 문서만 읽고도 현재 문제의 원인을 분석하고, 락 제거 여부와 대체 정합성 모델을 설계할 수 있도록 작성한 독립 문서다.
4
-
5
- ## 1. 해결해야 할 문제
6
-
7
- Hikoutei는 SQLite를 애플리케이션의 authority로 사용하고, Google Sheets를 비동기 projection 및 human input surface로 사용한다. SQLite의 flush는 entity table, canonical sync state, durable Sheet effect outbox를 하나의 SQLite transaction으로 커밋한다. Google Sheets 원격 반영은 flush 이후 worker가 비동기로 수행한다.
8
-
9
- 현재 Apps Script Gateway write path에 Advanced Sheets `Sheets.Spreadsheets.batchUpdate`를 도입했다. 그러나 Locust 부하에서 Gateway latency와 오류가 급증한다.
10
-
11
- 핵심 질문은 다음이다.
12
-
13
- > `batchUpdate`를 사용해도 Apps Script 전역 Script Lock과 원격 read/validate/write 구조 때문에 병목이 남는다. 전역 락을 제거하고 SQLite authority, effect receipt, CAS/fencing, durable outbox를 이용한 다른 정합성 모델로 바꾸는 것이 안전한가? 안전하다면 정확한 설계와 단계별 변경안을 제시하라.
14
-
15
- 단순히 락을 제거하는 패치를 제안하지 말고, 동시 append, response loss, retry, stale CAS, duplicate identity, multi-worker fencing을 모두 고려해야 한다.
16
-
17
- ---
18
-
19
- ## 2. 반드시 보존해야 하는 불변조건
20
-
21
- 1. **SQLite authority**
22
- - 애플리케이션은 정상 entity data를 Sheets에서 읽지 않는다.
23
- - Sheets는 비동기 projection이며 User_Input을 제외하면 사람이 직접 수정하는 source of truth가 아니다.
24
-
25
- 2. **Durable outbox**
26
- - 효과는 flush 시 SQLite outbox에 저장된다.
27
- - 메모리에 효과를 30초 이상 모아두지 않는다.
28
- - process crash 이후에도 pending/retry/recovery가 가능해야 한다.
29
-
30
- 3. **Effect idempotency**
31
- - effect ID와 payload hash가 receipt에 기록된다.
32
- - 같은 effect ID를 같은 payload로 재시도하면 `already_applied`/`applied`로 안전하게 회복해야 한다.
33
- - 같은 effect ID를 다른 payload로 재사용하면 fail-closed 해야 한다.
34
- - effect ID를 확인했다고 해서 row postcondition이 검증된 것으로 간주하면 안 된다.
35
-
36
- 4. **CAS와 fencing**
37
- - guarded update/delete는 expected visible revision/hash와 target evidence를 사용한다.
38
- - 오래된 worker나 lease를 잃은 worker가 원격 row를 덮어쓰면 안 된다.
39
- - SQLite writer lease와 effect lease의 fencing semantics를 보존해야 한다.
40
-
41
- 5. **Duplicate identity fail-closed**
42
- - business identity, `Conflict_ID`, physical anchor가 중복되면 자동 삭제하지 않는다.
43
- - 중복 row는 진단/quarantine 대상으로 남겨야 한다.
44
-
45
- 6. **Public boundary 유지**
46
- - `src/index.ts`와 public EntityManager API를 변경하지 않는다.
47
- - Apps Script `Code.gs` dispatcher/business logic은 현재 단계에서 변경하지 않는다.
48
- - SQLite authority, outbox, receipt, recovery semantics를 약화하지 않는다.
49
-
50
- ---
51
-
52
- ## 3. 현재 시스템 구조
53
-
54
- ### 3.1 Node/Application side
55
-
56
- - Entity flush가 SQLite entity table과 canonical state를 저장한다.
57
- - 같은 transaction에서 Sheet effect outbox를 만든다.
58
- - `SyncEffectWorker`가 SQLite에서 effect를 claim하고 route별로 전송한다.
59
- - worker는 adaptive batch controller를 사용한다.
60
- - coalescing window: 기본 500ms
61
- - 내부 effect batch 범위: 5~20
62
- - initial batch: 10
63
- - gateway의 공식 제한이 아니라 내부 방어 상한이다.
64
- - effect lease 기본값은 120초, writer lease 기본값은 180초다.
65
- - worker와 polling supervisor가 별도 경로로 Gateway에 요청한다.
66
- - load harness의 `POST /__test/user-input`은 테스트용으로 `User_Input` row를 직접 수정하는 별도 control operation을 보낸다. 이것은 production effect가 아니다.
67
-
68
- ### 3.2 Apps Script Gateway
69
-
70
- `apps-script/gateway/Code.gs`는 다음만 수행한다.
71
-
72
- 1. signed POST 검증
73
- 2. HMAC/body hash/time/sheet allowlist 검증
74
- 3. `applyOperations`의 serialized function source를 실행
75
- 4. 응답 JSON envelope 생성
76
- 5. 마지막에 `SpreadsheetApp.flush()` 호출
77
-
78
- Data-plane의 `Code.gs` 자체는 일반 effect마다 Script Lock을 잡지 않는다. 다만 eval로 실행되는 operation source들이 `LockService.getScriptLock()`을 사용한다.
79
-
80
- ### 3.3 Operation source별 락
81
-
82
- #### `batchAppendOperation.ts`
83
-
84
- `LockService.getScriptLock()`을 잡은 뒤 다음 전체를 수행한다.
85
-
86
- - target sheet와 header 확인
87
- - receipt sheet 확인/생성
88
- - 모든 receipt 읽기
89
- - effect ID/payload hash 확인
90
- - target sheet의 identity 중복 확인
91
- - `lastRow` 계산
92
- - Advanced Sheets `batchUpdate` 실행
93
- - row reservation
94
- - row cell write
95
- - developer metadata anchor 생성
96
- - receipt row 삽입/기록
97
- - receipt/visible evidence 검증
98
- - lock release
99
-
100
- 즉, lock critical section 안에 원격 read, 전체 receipt scan, identity scan, write가 모두 들어 있다.
101
-
102
- #### `effectOperationScript.ts`
103
-
104
- 일반 update/delete/effect materialization도 `LockService.getScriptLock()`을 약 20초 timeout으로 잡고, effect별 precondition/read/write/receipt 처리를 수행한다.
105
-
106
- #### `observationOperation.ts`
107
-
108
- anchor assignment가 포함된 full observation은 Script Lock을 사용한다. 이 경로는 다음을 할 수 있다.
109
-
110
- - registered range read
111
- - Developer Metadata anchor 검색
112
- - 누락 anchor 생성
113
- - values/formulas/display values/merged range 읽기
114
- - snapshot hash 생성
115
- - duplicate anchor 진단
116
-
117
- 반면 `READ_SNAPSHOT_OPERATION_SOURCE`는 anchor mutation이 없는 read-only snapshot path로 만들기 위해 lock prologue/epilogue를 제거한 별도 source다.
118
-
119
- ---
120
-
121
- ## 4. 왜 `batchUpdate`만으로 문제가 해결되지 않는가
122
-
123
- Advanced Sheets `batchUpdate`는 **하나의 API 요청 안의 request 배열을 원자적으로 처리**할 수 있다. 그러나 다음까지 보장하지 않는다.
124
-
125
- - 서로 다른 두 HTTP 요청 사이의 serializable isolation
126
- - read 후 write 사이의 compare-and-set
127
- - effect ID/receipt의 unique constraint
128
- - developer metadata anchor의 create-if-absent
129
- - `lastRow` 계산의 동시성 안전성
130
- - User_Input row에 대한 conditional update
131
-
132
- 예를 들어 두 요청 A/B가 동시에 다음을 수행하면 된다.
133
-
134
- ```text
135
- A: receipt에 e1 없음
136
- B: receipt에 e1 없음
137
- A: identity u1 없음
138
- B: identity u1 없음
139
- A: lastRow = 100
140
- B: lastRow = 100
141
- A: row 101에 u1 append
142
- B: row 101 또는 shifted row에 u1 append
143
- ```
144
-
145
- 각 요청의 `batchUpdate`는 내부적으로 원자적일 수 있어도, A와 B 사이의 unique identity 보장은 없다. 락을 완전히 제거하면 다음 문제가 가능하다.
146
-
147
- - 같은 effect의 duplicate row
148
- - 같은 business identity의 duplicate row
149
- - 같은 anchor의 duplicate metadata
150
- - receipt와 data row의 서로 다른 상태
151
- - stale `User_Input` candidate overwrite
152
- - response loss 이후 재시도 시 duplicate materialization
153
-
154
- 따라서 “`batchUpdate`이므로 전역 락을 그냥 삭제해도 된다”는 가정은 성립하지 않는다.
155
-
156
- ---
157
-
158
- ## 5. 관측된 실제 증상
159
-
160
- ### 5.1 이전 누적 상태 run
161
-
162
- 이전 run은 stale SQLite/Sheet state와 duplicate/failure effect가 섞여 있었다.
163
-
164
- - 2,648 requests
165
- - HTTP failure 12.61%
166
- - Gateway p50 4.34초
167
- - Gateway p95 34.25초
168
- - 최대 60.01초
169
-
170
- 이 run은 신규 구현의 공정한 성능 비교로 사용하면 안 된다.
171
-
172
- ### 5.2 Fresh run
173
-
174
- - 날짜: 2026-08-03
175
- - branch: `perf/adaptive-sync-performance`
176
- - fresh SQLite 및 fresh `System_State`, `User_Input`, `Sync_Conflicts` tabs
177
- - Locust: 10 users, spawn rate 2/s, 60초
178
- - command:
179
-
180
- ```sh
181
- locust -f .local/locustfile.py \
182
- --host http://127.0.0.1:8787 \
183
- --headless \
184
- --users 10 \
185
- --spawn-rate 2 \
186
- --run-time 60s \
187
- --csv .local/locust-20260803-231300-clean2 \
188
- --html .local/locust-20260803-231300-clean2.html \
189
- --only-summary
190
- ```
191
-
192
- Locust 결과:
193
-
194
- | Endpoint | Requests | Failures | p50 | p95 | Max |
195
- | --- | ---: | ---: | ---: | ---: | ---: |
196
- | `GET /users/:id` | 134 | 0 | 2ms | 4ms | 6ms |
197
- | `POST /users` | 81 | 0 | 5ms | 6ms | 19ms |
198
- | `PATCH /users/:id` | 76 | 0 | 5ms | 8ms | 22ms |
199
- | `POST /__test/user-input` | 38 | 8 | 2.2s | 31.0s | 31.9s |
200
- | **Total** | **339** | **8 (2.36%)** | **4ms** | **2.3s** | **31.9s** |
201
-
202
- 서버-side Gateway snapshot은 workload 종료 후 background worker/polling을 포함해 다음과 같았다.
203
-
204
- - Gateway requests: 100
205
- - Gateway failures: 34
206
- - Gateway p50: 3.71초
207
- - Gateway p95: 33.47초
208
- - Gateway max: 60.00초
209
- - outbox: `pending 306`, `processing 8`, `applied 2`, `superseded 2`
210
-
211
- 주요 오류:
212
-
213
- - `Code.gs response was not valid JSON`
214
- - `Code.gs operation request timed out`
215
- - 이전 run에는 `Could not acquire the sync observation gateway lock`도 관측됨
216
-
217
- Fresh server startup 중에도 Gateway가 간헐적으로 HTTP 404/non-JSON을 반환했다. idle 상태에서 단일 signed no-op probe를 순차 실행하면 성공하기도 했으므로, 단순한 함수 문법 오류보다 원격 실행 queue/lock/transport/deployment 상태를 분리해 조사해야 한다.
218
-
219
- ### 5.3 해석
220
-
221
- 이 run은 다음을 보여준다.
222
-
223
- - local SQLite/entity flush 경로는 낮은 latency로 동작한다.
224
- - entity create/read/update HTTP API는 실패하지 않았다.
225
- - Gateway boundary는 10-user 정도의 mixed workload에서도 tail latency와 non-JSON/timeout을 보였다.
226
- - `User_Input` test control path가 stage-1 bulk append benchmark와 섞여 결과를 오염시켰다.
227
- - outbox가 workload보다 빠르게 drain되지 않았으므로 성공적인 성능 benchmark가 아니다.
228
-
229
- 따라서 현재 결과만으로 `batchUpdate` 자체의 throughput이나 correctness를 판정할 수 없다. 하지만 Gateway의 concurrency/transport 병목은 실제로 존재한다.
230
-
231
- ---
232
-
233
- ## 6. 해결책을 설계할 때 반드시 분리할 문제
234
-
235
- ### A. Transport/deployment 문제
236
-
237
- 다음은 Script Lock 제거와 별개일 수 있다.
238
-
239
- - HTTP 404
240
- - redirect 뒤 non-JSON response
241
- - Apps Script execution timeout
242
- - deployed Code.gs 버전/manifest/Advanced Sheets service 불일치
243
- - Gateway quota 또는 transient execution failure
244
-
245
- 먼저 no-op signed operation, provisioning operation, single append operation을 각각 독립적으로 측정해야 한다.
246
-
247
- ### B. Script Lock 경합
248
-
249
- 다음은 현재 전역 Script Lock으로 직접 악화될 수 있다.
250
-
251
- - effect operation이 receipt/identity 전체 scan 중인 동안 observation이 대기
252
- - full metadata observation이 anchor/hash를 계산하는 동안 append가 대기
253
- - 20초 `tryLock` timeout과 60초 HTTP timeout이 겹침
254
- - polling, outbound worker, test control operation이 동일 spreadsheet/script에 동시에 접근
255
-
256
- ### C. 락 제거 시 생기는 정합성 문제
257
-
258
- 락을 삭제하면 해결되는 latency와 새로 생기는 correctness failure를 구분해야 한다.
259
-
260
- - lock-free append에서 duplicate identity를 어떻게 막을 것인가?
261
- - receipt의 create-if-absent를 어떻게 보장할 것인가?
262
- - `lastRow`/row reservation race를 어떻게 처리할 것인가?
263
- - stale CAS를 어떻게 원격에서 거절할 것인가?
264
- - response loss 후 같은 effect의 duplicate row를 어떻게 방지할 것인가?
265
- - 여러 Node process가 동시에 같은 spreadsheet를 dispatch할 때 어떻게 fencing할 것인가?
266
-
267
- ---
268
-
269
- ## 7. 우선 검토할 대체 설계
270
-
271
- 아래 설계를 비교하고, 더 나은 설계가 있다면 이유를 제시하라.
272
-
273
- ### 설계안 1: SQLite durable single remote writer
274
-
275
- - spreadsheet/route별 remote dispatch ownership을 SQLite writer lease로 결정한다.
276
- - lease holder만 해당 spreadsheet의 mutation Gateway call을 보낸다.
277
- - 효과를 메모리에 오래 모으지 않고, SQLite outbox에서 bounded batch만 읽어 보낸다.
278
- - process가 죽어도 outbox가 남으므로 재시작 후 recovery한다.
279
- - 같은 process의 worker/polling/test control이 Gateway로 직접 병렬 mutation을 보내지 않게 한다.
280
- - remote lock을 제거하거나 최소화하고, serialization을 SQLite authority 쪽으로 옮긴다.
281
-
282
- 검토할 것:
283
-
284
- - polling read는 mutation writer와 병렬화해도 되는가?
285
- - full observation의 anchor mutation은 어느 lane에 넣어야 하는가?
286
- - 여러 Node process의 writer lease fencing이 충분한가?
287
- - lease 만료 중 remote call이 끝나는 경우 response-loss/postcondition 처리는 어떻게 하는가?
288
-
289
- ### 설계안 2: operation class별 락
290
-
291
- - 순수 values/read snapshot: lock-free
292
- - append-only `System_State`/`Sync_Conflicts`: 별도 mutation lane 또는 짧은 route별 serialization
293
- - CAS update/delete/User_Input: 당분간 serialization 유지
294
- - full metadata/anchor assignment: 낮은 빈도의 별도 safety lane
295
-
296
- Apps Script `LockService`에는 일반적인 arbitrary route key lock이 없으므로, “route별 lock”을 제안할 경우 실제 구현 방식(SQLite lease, PropertiesService, sheet lease row 등)의 원자성과 crash recovery를 설명해야 한다.
297
-
298
- ### 설계안 3: append-only command/effect ledger
299
-
300
- Sheet에 직접 unique row를 materialize하기보다 effect command/receipt를 append-only로 기록하고, 단일 materializer가 projection row를 만든다.
301
-
302
- 장점:
303
-
304
- - append-only write가 row update보다 단순하다.
305
- - replay와 audit가 쉽다.
306
-
307
- 단점:
308
-
309
- - ledger 자체의 duplicate effect ID 방지 문제가 남는다.
310
- - materializer가 결국 단일 writer가 되어야 한다.
311
- - 기존 Sheet 사용자-facing layout과의 migration 비용이 크다.
312
-
313
- ### 설계안 4: optimistic concurrency/version column
314
-
315
- 각 row에 revision/version을 두고 expected version을 같이 전송한다.
316
-
317
- 단, Google Sheets Advanced API의 `batchUpdate`가 서버-side conditional compare-and-set을 제공하는지 확인해야 한다. 단순히 version을 읽은 뒤 update하는 것은 CAS가 아니다. 실제로 조건부 update를 보장하지 못한다면 User_Input guarded path의 정합성 대체안으로 인정하지 않는다.
318
-
319
- ---
320
-
321
- ## 8. 권장 단계
322
-
323
- 1. **Gateway 단독 안정성 확인**
324
- - signed no-op 10회
325
- - provisioning/read 10회
326
- - 단일 append 1회
327
- - status, redirect, body classification, execution duration 기록
328
-
329
- 2. **단일 effect correctness 확인**
330
- - `System_State` append
331
- - receipt 확인
332
- - 같은 effect replay
333
- - payload hash mismatch
334
- - response loss simulation
335
- - duplicate identity simulation
336
-
337
- 3. **stage-1 benchmark 분리**
338
- - Locust에서 `User_Input` control traffic 제외
339
- - `System_State`와 `Sync_Conflicts` outbound path만 측정
340
- - batch 1/5/10/20 sweep
341
- - workload window와 background drain window를 분리
342
-
343
- 4. **concurrency correctness 확인**
344
- - 같은 effect ID 동시 2회
345
- - 서로 다른 effect의 동시 append
346
- - 같은 business identity 동시 append
347
- - full observation과 append 동시 실행
348
- - worker lease expiry 중 원격 response 도착
349
-
350
- 5. **User_Input 별도 검증**
351
- - guarded update/delete의 stale candidate 거부
352
- - remote candidate 재관찰/quarantine
353
- - CAS path를 lock-free로 바꿀 수 있는지 별도 판단
354
-
355
- 6. **최종 Locust**
356
- - fresh SQLite/tabs
357
- - 한 writer/한 Gateway deployment
358
- - Gateway p50/p95/max
359
- - non-JSON/timeout 비율
360
- - lock wait
361
- - pending/processing/applied/failed/superseded/blocked_candidate
362
- - outbox drain rate와 effect creation rate 비교
363
-
364
- ---
365
-
366
- ## 9. 금지해야 할 단순 해결책
367
-
368
- 다음 제안은 충분한 정합성 증명 없이는 채택하지 않는다.
369
-
370
- - `LockService`를 모두 삭제하고 retry만 추가
371
- - `batchUpdate`이므로 cross-request CAS도 된다고 가정
372
- - duplicate identity를 자동 삭제
373
- - receipt에 effect ID만 있으면 성공으로 판정
374
- - timeout을 늘려 lock contention을 숨김
375
- - pending effect를 메모리에 모아 Gateway latency를 숨김
376
- - User_Input 실패를 전체 benchmark에서 제외하고 성공으로 보고
377
- - 404/non-JSON을 단순 retry로만 처리
378
- - public API나 `Code.gs` dispatcher를 변경해 내부 병목을 숨김
379
-
380
- ---
381
-
382
- ## 10. 답변을 작성할 GPT에게 요구하는 결과
383
-
384
- 다음 형식으로 답하라.
385
-
386
- 1. **Root cause 분리**
387
- - Script Lock contention
388
- - Apps Script/HTTP transport/deployment instability
389
- - read/validate/write race
390
- - worker scheduling contention
391
- 을 각각 어느 증거가 지지하는지 설명하라.
392
-
393
- 2. **추천 consistency model**
394
- - 어떤 경로에서 lock을 제거하는가?
395
- - 무엇이 SQLite에서 serialize되는가?
396
- - 어떤 경로는 왜 lock/CAS가 여전히 필요한가?
397
- - response loss와 duplicate identity를 어떻게 처리하는가?
398
-
399
- 3. **상태 전이와 fencing**
400
- - outbox status
401
- - writer lease
402
- - effect lease
403
- - remote receipt
404
- - postcondition
405
- 사이의 정확한 전이를 제시하라.
406
-
407
- 4. **구체적인 코드 변경 위치**
408
- - 변경할 파일
409
- - 변경하지 않을 파일
410
- - operation source의 critical section
411
- - worker dispatch/scheduler
412
- - 필요한 schema/telemetry
413
- 를 명시하라.
414
-
415
- 5. **실패 주입 테스트**
416
- - timeout
417
- - HTTP 404/non-JSON
418
- - response loss
419
- - lock contention
420
- - concurrent append
421
- - stale CAS
422
- - duplicate identity
423
- 를 재현하고 기대 결과를 정의하라.
424
-
425
- 6. **성능 측정 계획**
426
- - stage-1과 stage-2를 분리하라.
427
- - setup/no-setup/steady-state/drain을 분리하라.
428
- - 성공률뿐 아니라 outbox convergence와 correctness를 포함하라.
429
-
430
- 7. **채택 기준과 rollback 기준**
431
- - 어떤 수치와 invariant를 만족해야 lock-free 또는 reduced-lock 설계를 채택하는가?
432
- - 어떤 오류가 발생하면 기존 안전 경로로 rollback하는가?
433
-
434
- 핵심은 **락을 무조건 제거하는 것**이 아니라, Apps Script의 긴 전역 critical section을 줄이면서도 SQLite authority, durable outbox, effect receipt, CAS/fencing, duplicate fail-closed를 유지하는 것이다.
@@ -1,224 +0,0 @@
1
- # Hikoutei Architecture
2
-
3
- Hikoutei owns a local SQLite entity store and exposes Google Sheets as an
4
- asynchronous human-facing projection. SQLite is the authority; Sheets is an
5
- internal service-side projection and human input surface.
6
-
7
- > The root package API is ORM-only. Sheet synchronization is implemented inside
8
- > `src`, but its bootstrap, worker, and storage contracts are not root
9
- > exports.
10
-
11
- ## System shape
12
-
13
- ```text
14
- Application code
15
- └─ hikoutei root API
16
- └─ EntityManager
17
- └─ SQLite entity tables
18
- └─ [internal sync service in the same process]
19
- ├─ canonical sync state
20
- ├─ durable Sheet effect outbox
21
- ├─ outbound effect worker
22
- ├─ User_Input polling
23
- └─ sync provider (googleSheetsApi)
24
- └─ Google Sheets projections
25
- ```
26
-
27
- The internal service reuses the same MikroORM SQLite adapter and transaction
28
- boundary as the entity manager. A future deployment can extract the worker
29
- process without changing the root entity lifecycle contract.
30
-
31
- ## Source boundaries
32
-
33
- ```text
34
- src/domain/ pure normalization/evaluation/conflict rules
35
- src/application/orm/ public ORM facade and mapped flush planning
36
- src/application/sync/ internal sync engine and service bootstrap
37
- src/adapter/persistence/ SQLite/MikroORM implementation
38
- src/adapter/sheets/ Google Sheets API provider (sync provider contracts + implementation)
39
- src/infrastructure/storage/ canonical, observation, resolution, outbox state
40
- src/api/ root-facing entity and EntityManager facade
41
- src/index.ts root public barrel only
42
- ```
43
-
44
- `src` does not mean public. The only application-facing package entrypoint is
45
- `src/index.ts`; package subpaths for providers, sync operations, polling,
46
- and sync state are not part of the contract.
47
-
48
- ## Root public API
49
-
50
- Applications define scalar entities and open a local SQLite runtime:
51
-
52
- ```ts
53
- import { createTypedSheets, defineTypedSheetsEntity } from "hikoutei";
54
-
55
- const User = defineTypedSheetsEntity({
56
- name: "User",
57
- tableName: "users",
58
- properties: {
59
- id: { type: "string", primary: true },
60
- name: { type: "string" },
61
- },
62
- });
63
-
64
- const hikoutei = await createTypedSheets({
65
- dbName: "./hikoutei.sqlite",
66
- entities: [User],
67
- });
68
-
69
- const em = hikoutei.em.fork();
70
- const user = em.create(User, { id: "u1", name: "Ada" });
71
- em.persist(user);
72
- await em.flush();
73
- ```
74
-
75
- The public surface contains entity definition, runtime creation, and the
76
- request-local `EntityManager` lifecycle: `fork()`, `create()`, `find()`,
77
- `findOne()`, `persist()`, `remove()`, `flush()`, and `transactional()`.
78
- MikroORM, raw SQL, provider clients, Sheet routes, provisioning, polling, and
79
- outbox controls are internal.
80
-
81
- ## SQLite authority
82
-
83
- Business entity tables are the authoritative application data. Normal reads
84
- always come from SQLite and never from a Sheet. The public local runtime opens
85
- entity tables only and does not contact Google Sheets or create sync tables.
86
-
87
- When the internal sync service is active, its mapped flush coordinator extends
88
- the same SQLite transaction with:
89
-
90
- ```text
91
- entity mutation
92
- canonical sync state
93
- projection registry/state
94
- Sheet effect outbox
95
- ```
96
-
97
- The service-side configuration supplies the required `System_State`,
98
- `User_Input`, and `Sync_Conflicts` routes, spreadsheet identity, and user-owned
99
- fields. Those values are not entity metadata or public ORM options. Every
100
- internal sync runtime fails closed if any of the three physical routes or its
101
- fixed headers are missing or drifted.
102
-
103
- ## Google Sheets projection
104
-
105
- The internal service provisions and validates registered projection tabs before
106
- starting delivery. It owns the sync provider (service-account Google Sheets API
107
- by default), effect worker, response-loss recovery, reconciliation, and
108
- User_Input polling.
109
-
110
- The application does not call Sheet operations. A successful public `flush()`
111
- means that the local SQLite transaction committed. It does not mean that a
112
- remote Sheet write has completed.
113
-
114
- ### Full direct provider (service account, preferred)
115
-
116
- The preferred production path uses the internal `googleSheetsApi` service
117
- option: ONE Google Sheets API provider under
118
- `src/adapter/sheets/providers/google-sheets-api/` implements every provider
119
- capability the runtime needs — provisioning, outbound effects (fast append,
120
- guarded update/delete, receipts, response-loss recovery), values-only table
121
- reads, row-anchor assignment, and full metadata snapshots. It authenticates
122
- with Application Default Credentials (`GOOGLE_APPLICATION_CREDENTIALS` service
123
- account shared on the spreadsheet), disables SDK auto-retry (retries are the
124
- durable worker's job), enforces request-start intervals of 1,100 ms per
125
- request class (reads and writes separately), and batches every applicable
126
- target mutation plus its receipt writes into one atomic
127
- `spreadsheets.batchUpdate`. The provider paces and reports every transport
128
- call individually; a serialized batch over ~2 MB is trimmed to an
129
- order-preserving prefix with `hasMore`. Transport failures are classified by
130
- the shared `classifyTransportOutcome` boundary: proven pre-mutation 4xx
131
- rejections are explicit remote failures, while timeouts, network errors,
132
- 408/429/5xx, and malformed 2xx replies stay delivery-uncertain and are
133
- recovered through the shared postcondition probe path. Telemetry carries only
134
- operation names, counts, durations, and stable codes — never credentials,
135
- spreadsheet IDs, URLs, or payloads.
136
-
137
- Provisioning creates missing tabs and initializes header rows in one atomic
138
- batch and fails closed on any header drift; snapshot wire shapes (merged,
139
- error, formula, literal/blank cells plus stableHash evidence) are stable
140
- across providers, so reconciliation evidence is provider-independent. No Apps
141
- Script deployment is required.
142
-
143
- ### Removed: Apps Script gateway
144
-
145
- The signed Apps Script gateway and the `appsScript`/`googleApiWorker` options
146
- were removed in this cleanup. The service-account `googleSheetsApi` provider
147
- above is the only sync path; existing deployments should switch to it. The
148
- provider reads and writes the same tabs, receipt sheet, and anchor metadata,
149
- so existing spreadsheets keep working without re-provisioning. Sheet
150
- consistency does not rely on cross-request Sheet transactions; it comes from
151
- the hidden effect-receipt tab, effect-id/payload-hash dedupe, the SQLite
152
- durable outbox, fencing, and postcondition recovery.
153
-
154
- `System_State` is materialized from canonical SQLite state. `User_Input` is
155
- observed by the internal polling loop, evaluated with ownership and field-level
156
- compare-and-set rules, and then committed back to SQLite. `Sync_Conflicts` is a
157
- system-owned audit projection of field-level conflict evidence and resolution
158
- outcomes; it is created even when empty and resolved rows remain visible for
159
- audit. A detected conflict is resolved with the fenced `acknowledge_system`
160
- policy: revision, candidate-hash, and epoch CAS clear the active candidate and
161
- queue the canonical User_Input rewrite. A newer remote edit that fails that CAS
162
- starts a new conflict instead of being overwritten. Stale, conflicting, and
163
- malformed input is therefore never silently written over the entity table.
164
-
165
- ## Transaction and lifecycle boundary
166
-
167
- ```text
168
- em.persist(entity) / em.remove(entity)
169
- │
170
- ▼
171
- SQLite transaction (internal sync service mode)
172
- ├─ entity table
173
- ├─ canonical state and conflict evidence
174
- ├─ automatic system-wins resolution receipt/state
175
- └─ durable Sheet effect outbox
176
- │
177
- ▼
178
- internal outbound effect supervisor
179
- ├─ claim leases and send bounded signed operation batches
180
- ├─ persist delivery uncertainty and due postcondition probes in SQLite
181
- ├─ fence remote routes with the current spreadsheet authority epoch/token
182
- └─ reconcile remote drift
183
- │
184
- ▼
185
- sync provider (googleSheetsApi) ──▶ Google Sheets
186
-
187
- internal User_Input polling
188
- ├─ adaptive values-only preflight over registered projections
189
- ├─ escalate changed/ambiguous/schema tables to full metadata
190
- ├─ periodic safety full scan for formula/merged/error fidelity
191
- ├─ evaluate ownership/revisions/CAS
192
- ├─ persist conflict evidence and automatic system-wins resolution in SQLite
193
- └─ persist accepted observations and entity mutations in SQLite
194
- ```
195
-
196
- Inbound polling defaults to an adaptive values-only preflight that reads the
197
- cheap value surface and only escalates a table to the metadata-preserving
198
- snapshot read when it changed, is ambiguous (unknown or duplicate business key,
199
- a missing expected entity, or a type-invalid cell), or when the periodic safety
200
- full scan falls due. The safety scan (default one minute, configurable) keeps
201
- formula, merged, and error cell fidelity because the values-only read drops that
202
- metadata; the scan's overdue lag is recorded for diagnostics. Outbound effects
203
- are split into bounded sub-batches per physical route so one provider call returns
204
- a complete result set, and a pass that only requeued uncertain work backs off with
205
- a bounded delay instead of retrying a struggling remote in a tight loop.
206
-
207
- The service bootstrap starts provisioning, outbound delivery, and inbound polling
208
- as one internal runtime. Outbound dispatch keeps the SQLite outbox as its only
209
- durable buffer, coalesces only a short in-process burst, and adapts each route's
210
- provider batch between five and twenty effects from latency and response-loss
211
- signals. The effect lease is longer than the configured provider timeout so a
212
- slow but valid request is recovered only after its remote result is checked.
213
- Ambiguous delivery is stored as `delivery_uncertain` with `uncertain_since`,
214
- `next_probe_at`, and `dispatch_id`; only a due probe may return it to processing.
215
- A terminal structured remote failure does not enter the ambiguous probe path.
216
- Shutdown stops polling first, waits for remote calls, stops the outbound
217
- supervisor, and only then closes SQLite.
218
-
219
- ## Design limits
220
-
221
- Hikoutei targets one local SQLite writer process and low-traffic MVP or internal
222
- workflows. It is not a distributed transaction coordinator, a general-purpose
223
- database, or a Google Sheets API replacement. Live Google integration remains
224
- opt-in; normal tests use fake providers and SQLite fixtures.