mega-framework 0.1.6 → 0.1.7

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 (233) hide show
  1. package/bin/mega-ws-hub.js +2 -2
  2. package/package.json +32 -8
  3. package/sample/crud/.env +1 -1
  4. package/sample/crud/.env.example +1 -1
  5. package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
  6. package/sample/crud/.mega/journal/snapshot.json +261 -0
  7. package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
  8. package/sample/crud/apps/main/controllers/web-controller.js +7 -5
  9. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
  10. package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
  11. package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
  12. package/sample/crud/apps/main/models/log-partition-model.js +105 -0
  13. package/sample/crud/apps/main/models/note-model.js +79 -0
  14. package/sample/crud/apps/main/models/user-level-model.js +24 -0
  15. package/sample/crud/apps/main/models/user-model.js +146 -0
  16. package/sample/crud/apps/main/models/user-type-model.js +21 -0
  17. package/sample/crud/apps/main/models/wallet-model.js +24 -0
  18. package/sample/crud/apps/main/routes/users.js +55 -10
  19. package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
  20. package/sample/crud/apps/main/services/auth-service.js +39 -24
  21. package/sample/crud/apps/main/services/log-partition-service.js +101 -0
  22. package/sample/crud/apps/main/services/note-service.js +6 -6
  23. package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
  24. package/sample/crud/apps/main/services/user-service.js +62 -21
  25. package/sample/crud/apps/main/views/auth/login.ejs +6 -6
  26. package/sample/crud/apps/main/views/auth/register.ejs +46 -5
  27. package/sample/crud/apps/main/views/users/edit.ejs +42 -5
  28. package/sample/crud/apps/main/views/users/list.ejs +6 -2
  29. package/sample/crud/apps/main/views/users/new.ejs +56 -4
  30. package/sample/crud/docs/log_partition_design.mm.md +23 -0
  31. package/sample/crud/mega.config.js +3 -2
  32. package/sample/crud/package.json +1 -1
  33. package/sample/crud/scripts/start-ws-hub.sh +2 -2
  34. package/sample/simple/package.json +2 -2
  35. package/src/adapters/adapter-manager.js +2 -1
  36. package/src/adapters/adapter-options.js +30 -0
  37. package/src/adapters/maria-adapter.js +26 -3
  38. package/src/adapters/mega-db-adapter.js +7 -1
  39. package/src/adapters/mongo-adapter.js +19 -1
  40. package/src/adapters/postgres-adapter.js +25 -2
  41. package/src/adapters/sqlite-adapter.js +20 -1
  42. package/src/cli/commands/new.js +13 -3
  43. package/src/cli/commands/scaffold.js +137 -33
  44. package/src/cli/generators/index.js +82 -2
  45. package/src/cli/index.js +353 -100
  46. package/src/core/ajv-mapper.js +27 -2
  47. package/src/core/boot.js +464 -245
  48. package/src/core/cluster-metrics.js +13 -4
  49. package/src/core/ctx-builder.js +6 -2
  50. package/src/core/envelope.js +112 -12
  51. package/src/core/hub-link.js +65 -4
  52. package/src/core/i18n.js +11 -1
  53. package/src/core/index.js +6 -2
  54. package/src/core/mega-app.js +201 -463
  55. package/src/core/mega-cluster.js +4 -1
  56. package/src/core/mega-server.js +40 -9
  57. package/src/core/migration/dialect-registry.js +107 -0
  58. package/src/core/migration/dialects/README.md +62 -0
  59. package/src/core/migration/dialects/maria.js +496 -0
  60. package/src/core/migration/dialects/mongo.js +824 -0
  61. package/src/core/migration/dialects/postgres.js +563 -0
  62. package/src/core/migration/dialects/sqlite.js +476 -0
  63. package/src/core/migration/differ.js +456 -0
  64. package/src/core/migration/generate.js +508 -0
  65. package/src/core/migration/journal.js +167 -0
  66. package/src/core/migration/model-scan.js +84 -0
  67. package/src/core/migration/mongo-migration-db.js +97 -0
  68. package/src/core/migration/schema-builder.js +400 -0
  69. package/src/core/migration/schema-validator.js +315 -0
  70. package/src/core/migration-lock.js +205 -0
  71. package/src/core/migration-runner.js +166 -38
  72. package/src/core/multipart.js +28 -5
  73. package/src/core/pipeline.js +129 -0
  74. package/src/core/router.js +70 -65
  75. package/src/core/security.js +67 -9
  76. package/src/core/workers-manager.js +12 -1
  77. package/src/core/ws-cluster.js +10 -3
  78. package/src/core/ws-message.js +48 -4
  79. package/src/core/ws-presence.js +624 -0
  80. package/src/core/ws-roster.js +4 -1
  81. package/src/core/ws-upgrade.js +118 -12
  82. package/src/index.js +1 -1
  83. package/src/lib/hub-protocol.js +29 -0
  84. package/src/lib/mega-health.js +25 -4
  85. package/src/lib/mega-job-queue.js +98 -21
  86. package/src/lib/mega-job.js +29 -0
  87. package/src/lib/mega-metrics.js +3 -12
  88. package/src/lib/mega-plugin.js +34 -3
  89. package/src/lib/mega-schedule.js +40 -22
  90. package/src/lib/mega-shutdown.js +114 -39
  91. package/src/lib/mega-tracing.js +66 -19
  92. package/src/lib/mega-worker.js +5 -1
  93. package/src/lib/otel-resource.js +36 -0
  94. package/src/{cli → lib}/ws-hub.js +51 -8
  95. package/src/models/crud-sql-builder.js +133 -0
  96. package/src/models/mega-model.js +82 -2
  97. package/src/models/model-crud.js +483 -0
  98. package/src/models/mongo-crud.js +285 -0
  99. package/templates/model/code-mongo.tpl +35 -0
  100. package/templates/model/code.tpl +15 -1
  101. package/templates/model/test-mongo.tpl +38 -0
  102. package/templates/model/test.tpl +4 -0
  103. package/types/adapters/adapter-manager.d.ts +95 -0
  104. package/types/adapters/adapter-options.d.ts +91 -0
  105. package/types/adapters/file-adapter.d.ts +94 -0
  106. package/types/adapters/file-session-adapter.d.ts +101 -0
  107. package/types/adapters/index.d.ts +20 -0
  108. package/types/adapters/maria-adapter.d.ts +115 -0
  109. package/types/adapters/mega-adapter.d.ts +215 -0
  110. package/types/adapters/mega-bus-adapter.d.ts +45 -0
  111. package/types/adapters/mega-cache-adapter.d.ts +47 -0
  112. package/types/adapters/mega-db-adapter.d.ts +47 -0
  113. package/types/adapters/mega-lock-adapter.d.ts +62 -0
  114. package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
  115. package/types/adapters/mega-session-adapter.d.ts +32 -0
  116. package/types/adapters/mongo-adapter.d.ts +139 -0
  117. package/types/adapters/nats-adapter.d.ts +108 -0
  118. package/types/adapters/postgres-adapter.d.ts +139 -0
  119. package/types/adapters/redis-adapter.d.ts +70 -0
  120. package/types/adapters/redis-session-adapter.d.ts +82 -0
  121. package/types/adapters/redlock-adapter.d.ts +149 -0
  122. package/types/adapters/registry.d.ts +46 -0
  123. package/types/adapters/sqlite-adapter.d.ts +106 -0
  124. package/types/auth/index.d.ts +24 -0
  125. package/types/cli/commands/console-cmd.d.ts +37 -0
  126. package/types/cli/commands/new.d.ts +16 -0
  127. package/types/cli/commands/routes.d.ts +36 -0
  128. package/types/cli/commands/scaffold.d.ts +78 -0
  129. package/types/cli/commands/test-cmd.d.ts +14 -0
  130. package/types/cli/generators/index.d.ts +112 -0
  131. package/types/cli/index.d.ts +249 -0
  132. package/types/cli/template-engine.d.ts +40 -0
  133. package/types/core/ajv-mapper.d.ts +27 -0
  134. package/types/core/boot.d.ts +233 -0
  135. package/types/core/cluster-metrics.d.ts +52 -0
  136. package/types/core/config-loader.d.ts +13 -0
  137. package/types/core/config-validator.d.ts +30 -0
  138. package/types/core/ctx-builder.d.ts +80 -0
  139. package/types/core/envelope.d.ts +79 -0
  140. package/types/core/error-mapper.d.ts +17 -0
  141. package/types/core/formbody.d.ts +41 -0
  142. package/types/core/hub-link.d.ts +264 -0
  143. package/types/core/i18n.d.ts +178 -0
  144. package/types/core/index.d.ts +28 -0
  145. package/types/core/mega-app.d.ts +529 -0
  146. package/types/core/mega-cluster.d.ts +104 -0
  147. package/types/core/mega-server.d.ts +91 -0
  148. package/types/core/mega-service.d.ts +31 -0
  149. package/types/core/migration/dialect-registry.d.ts +22 -0
  150. package/types/core/migration/dialects/maria.d.ts +99 -0
  151. package/types/core/migration/dialects/mongo.d.ts +89 -0
  152. package/types/core/migration/dialects/postgres.d.ts +117 -0
  153. package/types/core/migration/dialects/sqlite.d.ts +111 -0
  154. package/types/core/migration/differ.d.ts +47 -0
  155. package/types/core/migration/generate.d.ts +56 -0
  156. package/types/core/migration/journal.d.ts +52 -0
  157. package/types/core/migration/model-scan.d.ts +19 -0
  158. package/types/core/migration/mongo-migration-db.d.ts +7 -0
  159. package/types/core/migration/schema-builder.d.ts +197 -0
  160. package/types/core/migration/schema-validator.d.ts +20 -0
  161. package/types/core/migration-lock.d.ts +33 -0
  162. package/types/core/migration-runner.d.ts +101 -0
  163. package/types/core/multipart.d.ts +86 -0
  164. package/types/core/openapi.d.ts +62 -0
  165. package/types/core/pipeline.d.ts +92 -0
  166. package/types/core/router.d.ts +159 -0
  167. package/types/core/routes-loader.d.ts +21 -0
  168. package/types/core/scope-registry.d.ts +14 -0
  169. package/types/core/security.d.ts +77 -0
  170. package/types/core/services-loader.d.ts +27 -0
  171. package/types/core/session-cleanup-schedule.d.ts +19 -0
  172. package/types/core/session-store.d.ts +18 -0
  173. package/types/core/session.d.ts +77 -0
  174. package/types/core/static-assets.d.ts +73 -0
  175. package/types/core/template.d.ts +106 -0
  176. package/types/core/workers-manager.d.ts +79 -0
  177. package/types/core/ws-cluster.d.ts +208 -0
  178. package/types/core/ws-compression.d.ts +112 -0
  179. package/types/core/ws-controller.d.ts +65 -0
  180. package/types/core/ws-message.d.ts +106 -0
  181. package/types/core/ws-presence.d.ts +273 -0
  182. package/types/core/ws-roster.d.ts +96 -0
  183. package/types/core/ws-upgrade.d.ts +231 -0
  184. package/types/errors/config-error.d.ts +10 -0
  185. package/types/errors/http-errors.d.ts +120 -0
  186. package/types/errors/index.d.ts +3 -0
  187. package/types/errors/mega-error.d.ts +32 -0
  188. package/types/index.d.ts +39 -0
  189. package/types/lib/asp/config.d.ts +49 -0
  190. package/types/lib/asp/crypto.d.ts +43 -0
  191. package/types/lib/asp/errors.d.ts +30 -0
  192. package/types/lib/asp/nonce-cache.d.ts +52 -0
  193. package/types/lib/asp/plugin.d.ts +30 -0
  194. package/types/lib/asp/ws-terminator.d.ts +45 -0
  195. package/types/lib/env-mapper.d.ts +14 -0
  196. package/types/lib/hub-protocol.d.ts +106 -0
  197. package/types/lib/index.d.ts +22 -0
  198. package/types/lib/logger/telegram-core.d.ts +104 -0
  199. package/types/lib/logger/telegram-transport.d.ts +45 -0
  200. package/types/lib/mega-brute-force.d.ts +66 -0
  201. package/types/lib/mega-circuit-breaker.d.ts +241 -0
  202. package/types/lib/mega-cron.d.ts +66 -0
  203. package/types/lib/mega-hash.d.ts +32 -0
  204. package/types/lib/mega-health.d.ts +41 -0
  205. package/types/lib/mega-job-queue.d.ts +176 -0
  206. package/types/lib/mega-job-worker.d.ts +130 -0
  207. package/types/lib/mega-job.d.ts +138 -0
  208. package/types/lib/mega-logger.d.ts +45 -0
  209. package/types/lib/mega-metrics.d.ts +285 -0
  210. package/types/lib/mega-plugin.d.ts +245 -0
  211. package/types/lib/mega-retry.d.ts +85 -0
  212. package/types/lib/mega-schedule.d.ts +260 -0
  213. package/types/lib/mega-shutdown.d.ts +135 -0
  214. package/types/lib/mega-tracing.d.ts +224 -0
  215. package/types/lib/mega-worker.d.ts +127 -0
  216. package/types/lib/otel-resource.d.ts +16 -0
  217. package/types/lib/worker-runner/process-entry.d.ts +1 -0
  218. package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
  219. package/types/lib/worker-runner/thread-entry.d.ts +1 -0
  220. package/types/lib/ws-hub.d.ts +234 -0
  221. package/types/models/crud-sql-builder.d.ts +48 -0
  222. package/types/models/index.d.ts +1 -0
  223. package/types/models/mega-model.d.ts +138 -0
  224. package/types/models/model-crud.d.ts +82 -0
  225. package/types/models/mongo-crud.d.ts +59 -0
  226. package/types/test/index.d.ts +84 -0
  227. package/.env +0 -127
  228. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
  229. package/sample/crud/apps/main/models/note.js +0 -71
  230. package/sample/crud/apps/main/models/user.js +0 -86
  231. package/sample/crud/package-lock.json +0 -5665
  232. package/sample/crud/yarn.lock +0 -2142
  233. package/sample/simple/package-lock.json +0 -1851
@@ -53,11 +53,23 @@ const LIFECYCLE_EVENTS = /** @type {const} */ (['beforeBoot', 'afterBoot', 'befo
53
53
  */
54
54
 
55
55
  /**
56
+ * 플러그인 scaffold generator manifest(03-api-spec §11) — `mega g <name>` 이 소비한다(ADR-187/199).
57
+ * `dir` 는 프로젝트 루트 상대 출력 기준, `files[].path`/`files[].template` 의 `{{token}}` 은
58
+ * cli/generators 의 `SCAFFOLD_TOKENS` 계약(Name/name/camelName/snake/app)으로 치환된다.
56
59
  * @typedef {Object} MegaScaffoldDef
57
60
  * @property {string} dir
58
61
  * @property {Array<{ path: string, template: string }>} files
62
+ * @property {string} [description] - `mega help` 가 병기하는 한 줄 설명(선택).
59
63
  */
60
64
 
65
+ /** 빌트인 generator 13종 이름 — 플러그인 scaffold 가 점유 금지(빌트인이 우선이라 silent shadow 가 됨).
66
+ * cli/generators 의 `GENERATOR_KINDS` 와 동일해야 한다(레이어 역전 회피를 위해 미러 — 동기화는
67
+ * mega-plugin 단위 테스트가 GENERATOR_KINDS 와 집합 비교로 강제한다). */
68
+ export const RESERVED_GENERATOR_NAMES = new Set([
69
+ 'app', 'controller', 'channel', 'service', 'model', 'middleware', 'route',
70
+ 'schedule', 'job', 'worker', 'locale', 'adapter', 'migration',
71
+ ])
72
+
61
73
  /**
62
74
  * @typedef {Object} LoadedPluginMeta
63
75
  * @property {string} name
@@ -230,7 +242,8 @@ export class MegaPluginHost {
230
242
  }
231
243
 
232
244
  /**
233
- * 스캐폴드 generator 등록. 같은 이름 재등록은 fail-fast.
245
+ * 스캐폴드 generator manifest 등록(ADR-199). 같은 이름 재등록·빌트인 13종 점유·파일 항목 모양 위반은
246
+ * **install 시점 fail-fast** — 첫 `mega g` 사용 때까지 잘못된 manifest 가 잠복하지 않게 한다.
234
247
  * @param {string} name
235
248
  * @param {MegaScaffoldDef} def
236
249
  * @returns {void}
@@ -239,15 +252,33 @@ export class MegaPluginHost {
239
252
  if (typeof name !== 'string' || name.length === 0) {
240
253
  throw new TypeError('mega.scaffold.register: name must be a non-empty string.')
241
254
  }
242
- if (!def || typeof def.dir !== 'string' || !Array.isArray(def.files)) {
255
+ if (RESERVED_GENERATOR_NAMES.has(name)) {
256
+ // 빌트인 kind 는 `mega g` 디스패치에서 항상 우선이라, 등록을 허용하면 도달 불가(silent shadow)다.
257
+ throw new MegaConfigError('plugin.builtin_generator_conflict', `Plugin cannot register builtin generator "${name}" (reserved).`, {
258
+ details: { name },
259
+ })
260
+ }
261
+ if (!def || typeof def.dir !== 'string' || def.dir.length === 0 || !Array.isArray(def.files)) {
243
262
  throw new TypeError(`mega.scaffold.register('${name}'): def must be { dir: string, files: array }.`)
244
263
  }
264
+ for (const f of def.files) {
265
+ if (!f || typeof f.path !== 'string' || f.path.length === 0 || typeof f.template !== 'string') {
266
+ throw new TypeError(`mega.scaffold.register('${name}'): files entries must be { path: string, template: string }. Got ${JSON.stringify(f)}.`)
267
+ }
268
+ }
269
+ if (def.description !== undefined && typeof def.description !== 'string') {
270
+ throw new TypeError(`mega.scaffold.register('${name}'): description must be a string when given.`)
271
+ }
245
272
  if (this.#generators.has(name)) {
246
273
  throw new MegaConfigError('plugin.duplicate_generator', `Scaffold generator '${name}' is already registered.`, {
247
274
  details: { name },
248
275
  })
249
276
  }
250
- this.#generators.set(name, { dir: def.dir, files: [...def.files] })
277
+ this.#generators.set(name, {
278
+ dir: def.dir,
279
+ files: def.files.map((f) => ({ path: f.path, template: f.template })),
280
+ ...(typeof def.description === 'string' ? { description: def.description } : {}),
281
+ })
251
282
  }
252
283
 
253
284
  /**
@@ -15,9 +15,10 @@
15
15
  * 먼저 잡은 1대만 실행하고, 못 잡은 나머지는 **조용히 건너뛴다**(skip). 락은 `ttl`(ms) 뒤 자동 만료돼
16
16
  * 다음 주기엔 다시 경쟁한다.
17
17
  *
18
- * 구현: `lock.acquire(key, { ttl, retryCount: 0 })` — **단 한 번** 시도하고 실패하면(이미 누가 잡음)
19
- * 즉시 skip 한다(retry 안 함 — retry+throw 는 "중복방지"가 아니라 "대기"라서 의미가 다름). 성공하면
20
- * `run(ctx)` 실행 **반드시 release**(finally).
18
+ * 구현: `lock.withLock(key, { ttl, retryCount: 0 }, run)` — **단 한 번** 시도하고 실패하면(이미 누가
19
+ * 잡음) 즉시 skip 한다(retry 안 함 — retry+throw 는 "중복방지"가 아니라 "대기"라서 의미가 다름).
20
+ * 성공하면 임계구역 동안 락이 **자동 연장**되고(redlock `using`), 종료 자동 release 된다 —
21
+ * run 이 ttl 을 넘겨도 락이 만료돼 다른 인스턴스가 끼어드는 중복 실행이 없다.
21
22
  *
22
23
  * # ⚠️ acquire 실패 = skip 의 한계 (ADR-118 기록)
23
24
  * `acquire` 는 (a) 경합(다른 인스턴스가 보유)과 (b) 락 backend 장애(Redis 다운)를 **둘 다** throw 로
@@ -46,8 +47,10 @@ import { MegaCron } from './mega-cron.js'
46
47
  * @typedef {Object} MegaScheduleLock
47
48
  * @property {string} lock - lock 어댑터 별명(`ctx.lock(alias)` 로 해석). 03-api-spec 의 옛 `cache` 필드를
48
49
  * 대체한다(lock 이 독립 도메인이 된 뒤 정합 — ADR-118).
49
- * @property {number} ttl - 락 보유 시간(**밀리초**, 양의 정수). **작업 예상 소요보다 넉넉히** 잡아야
50
- * 한다 작업이 ttl 을 넘기면 락이 자동 만료돼 다른 인스턴스가 중복 실행할 있다(자동 연장 미사용).
50
+ * @property {number} ttl - 락 보유 시간(**밀리초**, 양의 정수). 임계구역 동안 **자동 연장**되므로
51
+ * (redlock `using`) 작업이 ttl 을 넘겨도 중복 실행되지 않는다 ttl "연장 1회분 윈도우"다.
52
+ * ⚠️ redlock 은 `ttl ≥ automaticExtensionThreshold(기본 500ms) + 100ms` 를 요구한다(기본 설정 기준
53
+ * **600ms 이상**) — 미달이면 fire 시 skip(error 동봉)으로 표면화된다.
51
54
  * @property {string} [key] - 락 자원 키. 미지정 시 `mega:schedule:<클래스명>`.
52
55
  */
53
56
 
@@ -463,28 +466,43 @@ export class MegaScheduler extends EventEmitter {
463
466
  }
464
467
  const key = lock.key ?? `mega:schedule:${name}`
465
468
 
466
- let held
469
+ // withLock(= redlock `using`, ADR-113) — 단 한 번 시도(retryCount: 0) + **자동 연장** + 자동 release.
470
+ // run 이 ttl 을 넘겨도 redlock 이 임계구역 동안 락을 연장해 다른 인스턴스의 중복 실행을 막는다
471
+ // (기존 acquire/release 는 "ttl 을 넉넉히" 라는 운영자 추측에 의존 — ADR-118 재평가 조건 충족으로 전환).
472
+ // 경합/backend 장애는 routine 진입 **전에** throw 되므로 acquired 플래그로 skip(미획득)과
473
+ // release 실패(획득 후)를 구분한다. run(ctx) 계약은 무변경 — 연장 실패(signal.aborted)는
474
+ // 스케줄러가 관찰해 fail(lock-extend) 로 표면화한다.
475
+ let acquired = false
476
+ /** @type {MegaScheduleFireResult|undefined} */
477
+ let outcome
467
478
  try {
468
- // 시도(retryCount: 0). 실패=이미 누가 보유(또는 backend 장애) skip.
469
- held = await adapter.acquire(key, { ttl: lock.ttl, retryCount: 0 })
479
+ await adapter.withLock(key, { ttl: lock.ttl, retryCount: 0 }, async (/** @type {any} */ signal) => {
480
+ acquired = true
481
+ outcome = await this.#runBody(name, instance, ctx, key)
482
+ // 자동 연장 실패 = 임계구역 도중 배타성 상실(드묾 — lock backend 장애). run 결과와 별개로 표면화.
483
+ if (signal?.aborted) {
484
+ const cause = signal.error instanceof Error ? signal.error : undefined
485
+ const error = new Error(
486
+ `lock auto-extension failed for '${key}' — exclusivity may have been lost during run`,
487
+ cause ? { cause } : undefined,
488
+ )
489
+ this.emit('fail', { name, key, error, phase: 'lock-extend' })
490
+ }
491
+ })
470
492
  } catch (e) {
471
493
  const error = e instanceof Error ? e : new Error(String(e))
472
- // 에러를 실어 관측 가능하게 — 인프라 장애도 여기로 오므로 소비자가 구분/경보(상단 docstring).
473
- this.emit('skip', { name, key, reason: 'lock-not-acquired', error })
474
- return { name, ran: false, skipped: true, ok: false, error }
475
- }
476
-
477
- try {
478
- return await this.#runBody(name, instance, ctx, key)
479
- } finally {
480
- try {
481
- await adapter.release(held)
482
- } catch (e) {
483
- // 락 해제 실패는 비치명적(ttl 로 자동 만료) — 다음 주기 재경쟁. 묵히지 않고 fail 로 표면화.
484
- const error = e instanceof Error ? e : new Error(String(e))
485
- this.emit('fail', { name, key, error, phase: 'release' })
494
+ if (!acquired) {
495
+ // 획득 실패 = 경합(다른 인스턴스 보유) 또는 backend 장애 구분 불가(상단 docstring).
496
+ // 에러를 실어 관측 가능하게 소비자가 로그/경보로 인프라 장애를 알아챈다.
497
+ this.emit('skip', { name, key, reason: 'lock-not-acquired', error })
498
+ return { name, ran: false, skipped: true, ok: false, error }
486
499
  }
500
+ // routine 진입 후 throw = release 경로 실패(#runBody 는 throw 안 함) — 비치명적(ttl 만료로
501
+ // 자동 해제, 다음 주기 재경쟁). run 결과는 보존해 반환한다.
502
+ this.emit('fail', { name, key, error, phase: 'release' })
503
+ return outcome ?? { name, ran: true, skipped: false, ok: false, error }
487
504
  }
505
+ return /** @type {MegaScheduleFireResult} */ (outcome)
488
506
  }
489
507
 
490
508
  /**
@@ -3,33 +3,60 @@
3
3
  /**
4
4
  * MegaShutdown — graceful shutdown 시퀀스 + 사용자 cleanup hook 묶음.
5
5
  *
6
- * 시퀀스 (docs/10 §3):
7
- * Running → DrainingReady (health 'ready' = 503)
8
- * → ClosingHttp (Fastify close, 진행중 요청 grace)
9
- * → ClosingWs (placeholder)
10
- * → ClosingJobs / ClosingScheduler (placeholder)
11
- * → DisconnectingAdapters (placeholder)
12
- * → FlushingLogs (placeholder)
13
- * → Exited (process.exit(0))
6
+ * 시퀀스 (docs/10 §3) — 명시 stage 로 코드화({@link SHUTDOWN_STAGES}):
7
+ * Running → [server jobs app → workers → adapters → telemetry → logs] → Exited (process.exit)
14
8
  *
15
- * SIGTERM/SIGINT 캐치 + 사용자 hook 실행 (LIFO) + grace period + hardKill.
16
- * 후속 Phase 에서 단계 hook 자동 추가 (Fastify·어댑터·로거 등).
9
+ * SIGTERM/SIGINT 캐치 + stage 순서 실행(stage 안은 등록 역순 LIFO) + hook 별 grace + hardKill 데드라인.
10
+ * readiness 503 전환(DrainingReady)은 isShuttingDown() MegaHealth.checkAll 읽어 즉시 반영된다.
17
11
  *
18
12
  * API 는 docs/10·07 시퀀스에 맞춰 `MegaShutdown` 객체로 통일 (ADR — M-4).
19
13
  *
20
14
  * 사용 예:
21
- * MegaShutdown.register('http', async () => server.close())
22
- * MegaShutdown.register('db', async () => pool.end())
15
+ * MegaShutdown.register('my-queue', async () => consumer.stop()) // 기본 stage 'app'
16
+ * MegaShutdown.register('db', async () => pool.end(), { stage: 'adapters' }) // 명시 stage
23
17
  * MegaShutdown.setupSignals({ gracePeriodMs: 30_000, hardKillMs: 60_000 })
24
18
  * await MegaShutdown.now() // 수동 트리거
25
19
  */
26
20
 
27
- /** @type {Array<{ name: string, fn: () => Promise<void> | void }>} */
21
+ /**
22
+ * 종료 stage 정본 순서 (docs/10 §3 의 단계를 코드로 명시) — `register(name, fn, { stage })` 가 이 중
23
+ * 하나를 지정하고, `now()` 는 이 배열 순서대로 stage 를 실행한다(stage 안에서는 등록 역순 LIFO).
24
+ *
25
+ * server — HTTP/WS 수용 종료(서버 close, 진행 중 요청 drain). ClosingHttp/ClosingWs.
26
+ * jobs — 잡 컨슈머·스케줄러 정지(새 잡 수신 중단, 진행 중 잡 완료). ClosingJobs/ClosingScheduler.
27
+ * app — 앱 레벨 정리(플러그인 beforeShutdown·세션 store·WS cluster/roster 등 어댑터를 **쓰는** 정리).
28
+ * `stage` 미지정 기본값 — 사용자 cleanup 은 어댑터가 살아 있는 이 단계에서 돈다.
29
+ * workers — CPU 워커 풀·embedded wsHub 등 백그라운드 실행기 정지.
30
+ * adapters — 공유 어댑터 disconnect(DisconnectingAdapters, stage 안 LIFO = connect 역순).
31
+ * telemetry — 메트릭·트레이싱 SDK flush/shutdown. 어댑터 **뒤** — disconnect 까지의 span/메트릭을 내보낸다.
32
+ * logs — 로거 flush(FlushingLogs). 항상 마지막 — 종료 시퀀스 자체의 로그가 유실되지 않게.
33
+ */
34
+ export const SHUTDOWN_STAGES = Object.freeze(['server', 'jobs', 'app', 'workers', 'adapters', 'telemetry', 'logs'])
35
+
36
+ /** `register` 가 stage 미지정일 때 들어가는 기본 stage — 사용자 cleanup 의 표준 위치(어댑터 종료 전). */
37
+ const DEFAULT_STAGE = 'app'
38
+
39
+ /** @type {Array<{ name: string, fn: () => Promise<void> | void, stage: string }>} */
28
40
  const handlers = []
29
41
  let signalsRegistered = false
42
+ /** @type {Array<{ sig: string, fn: () => void }>} setupSignals 가 등록한 시그널 리스너(_reset 해제용). */
43
+ let signalHandlers = []
30
44
  let isShuttingDownFlag = false
31
- let gracePeriodMs = 30_000
32
- let hardKillMs = 60_000
45
+
46
+ /** hook 1개당 grace 기본값(ms) — 30초. `setupSignals({ gracePeriodMs })` 로 조정. */
47
+ export const DEFAULT_GRACE_PERIOD_MS = 30_000
48
+
49
+ /**
50
+ * graceful shutdown 전체 상한 기본값(ms) — 60초. 초과 시 hardKill(exit 1). 워커 프로세스의 종료 예산
51
+ * 상한이므로, 클러스터 마스터의 grace(`MegaCluster.gracePeriodMs`)는 **이 값 + 마진 이상**이어야
52
+ * 워커가 drain 을 끝내기 전에 마스터가 SIGKILL 하는 예산 역전이 없다(CLI 가 이 상수로 위계화).
53
+ */
54
+ export const DEFAULT_HARD_KILL_MS = 60_000
55
+
56
+ let gracePeriodMs = DEFAULT_GRACE_PERIOD_MS
57
+ let hardKillMs = DEFAULT_HARD_KILL_MS
58
+ /** hook 루프가 hardKill 직전에 끝나도록 남기는 여유(ms) — 루프가 hardKill 타이머를 clear 하고 정상 exit 한다. */
59
+ const HARD_KILL_MARGIN_MS = 100
33
60
  let exitedHandlers = new Set()
34
61
  /** 전역 에러 핸들러 등록 여부 + 참조(reset 시 removeListener 용). @type {boolean} */
35
62
  let globalErrorsRegistered = false
@@ -47,23 +74,30 @@ let uncaughtExceptionHandler = null
47
74
  let shutdownLogger = null
48
75
 
49
76
  /**
50
- * cleanup hook 등록. 순서는 등록 역순(LIFO)으로 실행.
51
- * @param {string} name - 식별자 (로그용)
77
+ * cleanup hook 등록. 실행 순서 = {@link SHUTDOWN_STAGES} 순서 → 같은 stage 안에서는 등록 역순(LIFO).
78
+ * `stage` 미지정이면 `'app'`(어댑터 종료 전, 사용자 cleanup 표준 위치) — 기존 2-인자 호출과 호환.
79
+ * @param {string} name - 식별자 (로그·unregister 용)
52
80
  * @param {() => Promise<void> | void} fn
81
+ * @param {{ stage?: string }} [opts] - 종료 stage({@link SHUTDOWN_STAGES} 중 하나).
53
82
  */
54
- function register(name, fn) {
83
+ function register(name, fn, opts = {}) {
55
84
  if (typeof name !== 'string' || name.length === 0) {
56
85
  throw new Error('MegaShutdown.register: name is required (string)')
57
86
  }
58
87
  if (typeof fn !== 'function') {
59
88
  throw new Error('MegaShutdown.register: fn must be a function')
60
89
  }
61
- handlers.push({ name, fn })
90
+ const stage = opts.stage ?? DEFAULT_STAGE
91
+ // stage 오타가 조용히 엉뚱한 순서로 실행되지 않게 등록 시점 fail-fast.
92
+ if (!SHUTDOWN_STAGES.includes(stage)) {
93
+ throw new Error(`MegaShutdown.register: unknown stage '${stage}' (valid: ${SHUTDOWN_STAGES.join(', ')})`)
94
+ }
95
+ handlers.push({ name, fn, stage })
62
96
  }
63
97
 
64
98
  /**
65
99
  * 등록된 cleanup hook 제거 (같은 name 전부). 런타임 중 동적으로 붙였다 떼는 hook(예: hub link)
66
- * 의 누수를 막는다(L1). shutdown 진행 중에는 인덱스 추적(exitedHandlers)과 충돌하므로 무시한다.
100
+ * 의 누수를 막는다(L1). shutdown 진행 중에는 실행 추적(exitedHandlers)과 충돌하므로 무시한다.
67
101
  * @param {string} name - register 시 쓴 식별자.
68
102
  * @returns {number} 제거된 hook 수.
69
103
  */
@@ -99,9 +133,13 @@ function setupSignals(opts = {}) {
99
133
  if (typeof opts.hardKillMs === 'number') hardKillMs = opts.hardKillMs
100
134
  const signals = opts.signals ?? ['SIGTERM', 'SIGINT']
101
135
  for (const sig of signals) {
102
- process.on(sig, () => {
136
+ // 리스너 참조를 보관해 _reset 이 떼어낼 수 있게 한다 — reset 후 재-setup 시 리스너가 중복돼
137
+ // 시그널 1회에 now() 가 2회 불리면 두 번째가 M-3(즉시 force exit 1)로 오인된다.
138
+ const fn = () => {
103
139
  void now({ signal: sig, exitCode: 0 })
104
- })
140
+ }
141
+ signalHandlers.push({ sig, fn })
142
+ process.on(sig, fn)
105
143
  }
106
144
  // 전역 에러 핸들러도 함께 등록(opt-out: globalErrorHandlers === false). REPL(console-cmd) 등은 끌 수 있다.
107
145
  if (opts.globalErrorHandlers !== false) setupGlobalErrorHandlers()
@@ -118,12 +156,23 @@ function setupSignals(opts = {}) {
118
156
  function setupGlobalErrorHandlers({ exitCode = 1 } = {}) {
119
157
  if (globalErrorsRegistered) return
120
158
  globalErrorsRegistered = true
159
+ // shutdown 진행 중의 에러는 now() 를 재호출하지 않는다 — teardown 구간(소켓·어댑터 정리)은 floating
160
+ // rejection 이 가장 나기 쉬운데, 여기서 now() 를 다시 부르면 "두 번째 시그널"(M-3)로 오인돼 남은
161
+ // hook(어댑터 disconnect·로그 flush)을 건너뛰고 즉시 exit(1) 된다. fatal 기록만 하고 정리를 계속한다.
121
162
  unhandledRejectionHandler = (reason) => {
122
163
  const err = reason instanceof Error ? reason : new Error(`unhandledRejection: ${String(reason)}`)
164
+ if (isShuttingDownFlag) {
165
+ logShutdown('fatal', 'unhandledRejection during shutdown (continuing cleanup)', { err })
166
+ return
167
+ }
123
168
  logShutdown('fatal', 'unhandledRejection — initiating graceful shutdown', { err })
124
169
  void now({ signal: 'unhandledRejection', exitCode })
125
170
  }
126
171
  uncaughtExceptionHandler = (err, origin) => {
172
+ if (isShuttingDownFlag) {
173
+ logShutdown('fatal', 'uncaughtException during shutdown (continuing cleanup)', { err, origin })
174
+ return
175
+ }
127
176
  logShutdown('fatal', 'uncaughtException — initiating graceful shutdown', { err, origin })
128
177
  void now({ signal: 'uncaughtException', exitCode })
129
178
  }
@@ -173,30 +222,49 @@ async function now({ signal, exitCode = 0 } = {}) {
173
222
  logShutdown('info', 'shutdown starting', { signal: signal ?? 'manual', handlers: handlers.length, graceMs: gracePeriodMs })
174
223
 
175
224
  // hardKill 보호 — hardKillMs 초과 시 강제 종료
225
+ const hardKillAt = Date.now() + hardKillMs
176
226
  const hardKillTimer = setTimeout(() => {
177
227
  logShutdown('error', 'grace period exceeded — force exit(1)', { hardKillMs })
178
228
  process.exit(1)
179
229
  }, hardKillMs)
180
230
  hardKillTimer.unref()
181
231
 
182
- // LIFO 순서로 hook 실행 (last registered runs first)
183
- for (let i = handlers.length - 1; i >= 0; i--) {
184
- const { name, fn } = handlers[i]
185
- if (exitedHandlers.has(i)) continue
186
- try {
187
- logShutdown('info', `running hook '${name}'`, { hook: name })
188
- await Promise.race([
189
- Promise.resolve(fn()),
190
- new Promise((_, reject) =>
191
- setTimeout(() => reject(new Error('handler timeout')), gracePeriodMs).unref(),
192
- ),
193
- ])
194
- exitedHandlers.add(i)
195
- logShutdown('info', `hook '${name}' done`, { hook: name })
196
- } catch (err) {
197
- // silent 금지. 핸들러 실패 시 warn 로그 + 다음 hook 진행.
198
- logShutdown('warn', `hook '${name}' failed (continuing)`, { hook: name, err: /** @type {any} */ (err)?.message ?? err })
232
+ // stage 순서(SHUTDOWN_STAGES)대로 실행, 같은 stage 안에서는 등록 역순(LIFO).
233
+ // 실패 격리는 2단 hook 실패는 warn 같은 stage 의 다음 hook 으로, stage 의 실패 수는
234
+ // stage done 로그에 집계된다(한 stage 가 망가져도 다음 stage 는 항상 진행).
235
+ for (const stage of SHUTDOWN_STAGES) {
236
+ /** @type {Array<{ name: string, fn: () => Promise<void> | void, stage: string }>} */
237
+ const staged = []
238
+ for (let i = handlers.length - 1; i >= 0; i--) {
239
+ if (handlers[i].stage === stage && !exitedHandlers.has(handlers[i])) staged.push(handlers[i])
199
240
  }
241
+ if (staged.length === 0) continue
242
+ const stageStartedAt = Date.now()
243
+ logShutdown('info', `stage '${stage}' starting`, { stage, hooks: staged.length })
244
+ let failed = 0
245
+ for (const entry of staged) {
246
+ const { name, fn } = entry
247
+ // hook 별 대기 예산 — gracePeriodMs 를 기본으로 하되 hardKill 데드라인을 넘기지 않게 남은
248
+ // 시간으로 캡한다(여유 HARD_KILL_MARGIN_MS). 앞 hook/stage 들이 grace 를 소진해도 뒤 hook 이
249
+ // "시작조차 못 한 채" hardKill 로 잘리지 않고, 루프가 데드라인 전에 끝나 정상 exit 한다.
250
+ const budgetMs = Math.max(0, Math.min(gracePeriodMs, hardKillAt - HARD_KILL_MARGIN_MS - Date.now()))
251
+ try {
252
+ logShutdown('info', `running hook '${name}'`, { hook: name, stage })
253
+ await Promise.race([
254
+ Promise.resolve(fn()),
255
+ new Promise((_, reject) =>
256
+ setTimeout(() => reject(new Error('handler timeout')), budgetMs).unref(),
257
+ ),
258
+ ])
259
+ exitedHandlers.add(entry)
260
+ logShutdown('info', `hook '${name}' done`, { hook: name, stage })
261
+ } catch (err) {
262
+ // silent 금지. 핸들러 실패 시 warn 로그 + 다음 hook 진행.
263
+ failed++
264
+ logShutdown('warn', `hook '${name}' failed (continuing)`, { hook: name, stage, err: /** @type {any} */ (err)?.message ?? err })
265
+ }
266
+ }
267
+ logShutdown('info', `stage '${stage}' done`, { stage, tookMs: Date.now() - stageStartedAt, failed })
200
268
  }
201
269
 
202
270
  clearTimeout(hardKillTimer)
@@ -227,6 +295,12 @@ function registeredCount() {
227
295
  */
228
296
  function _reset() {
229
297
  handlers.length = 0
298
+ // 시그널 리스너도 떼어낸다 — 남겨두면 reset 후 재-setup 시 리스너가 누적돼 시그널 1회에
299
+ // now() 가 2회 불리고, 두 번째가 M-3(즉시 force exit 1)로 처리된다.
300
+ for (const { sig, fn } of signalHandlers) {
301
+ process.removeListener(/** @type {any} */ (sig), fn)
302
+ }
303
+ signalHandlers = []
230
304
  signalsRegistered = false
231
305
  isShuttingDownFlag = false
232
306
  gracePeriodMs = 30_000
@@ -246,6 +320,7 @@ function _reset() {
246
320
  * docs/10·07 시퀀스가 `MegaShutdown.now()` 형태이므로 객체로 통일 (M-4).
247
321
  */
248
322
  export const MegaShutdown = {
323
+ STAGES: SHUTDOWN_STAGES,
249
324
  register,
250
325
  unregister,
251
326
  isShuttingDown,
@@ -21,14 +21,23 @@
21
21
  * `enterWith` 방식이 공유 동기 실행을 오염시켜 sibling 을 잘못 묶던 문제(스모크로 확인)를 구조적으로
22
22
  * 제거한다. 이 seam 은 ADR-077 의 `_instrument` 에 추가된 **단 한 줄(run 위임)** 이다(ADR-114, 오너 결정).
23
23
  *
24
- * # 신규 의존성 (ADR-114 + ADR-126 보강)
24
+ * # W3C trace context 전파 (ADR-196 F5 audit O-1)
25
+ * 서비스 경계를 넘는 분산 추적: inbound 는 {@link extractRemoteContext} 가 `traceparent`/`tracestate`
26
+ * 헤더를 부모 컨텍스트로 복원해 HTTP 루트 span 이 게이트웨이/업스트림 trace 에 이어지고, outbound 는
27
+ * {@link propagationHeaders}(= `ctx.tracer.propagationHeaders()`)가 현재 활성 span 을 헤더로 직렬화해
28
+ * 하류 HTTP 호출(fetch 등)에 싣는다. 포맷·파싱은 검증된 `W3CTraceContextPropagator`(@opentelemetry/core)
29
+ * 에 위임한다(P1 — 직접 파싱 금지). 무효 헤더는 무시(새 루트 — fail-safe), 옵트인 OFF 면 0 비용.
30
+ *
31
+ * # 신규 의존성 (ADR-114 + ADR-126 보강 + ADR-196)
25
32
  * `@opentelemetry/api`, `@opentelemetry/sdk-trace-base`, `@opentelemetry/resources`,
26
33
  * `@opentelemetry/semantic-conventions`, `@opentelemetry/exporter-trace-otlp-http`(OTLP exporter),
27
- * `@opentelemetry/exporter-zipkin`(Zipkin exporter, ADR-126 보강 — 사용자 승인). audit 신규 0건.
34
+ * `@opentelemetry/exporter-zipkin`(Zipkin exporter, ADR-126 보강 — 사용자 승인),
35
+ * `@opentelemetry/core`(W3C propagator, ADR-196 — 사용자 승인. 기존 2.x 라인 transitive 의 직접 승격).
28
36
  *
29
37
  * @module lib/mega-tracing
30
38
  */
31
- import { trace, ROOT_CONTEXT, SpanKind, SpanStatusCode } from '@opentelemetry/api'
39
+ import { trace, ROOT_CONTEXT, SpanKind, SpanStatusCode, isSpanContextValid, defaultTextMapGetter, defaultTextMapSetter } from '@opentelemetry/api'
40
+ import { W3CTraceContextPropagator } from '@opentelemetry/core'
32
41
  import {
33
42
  BasicTracerProvider,
34
43
  SimpleSpanProcessor,
@@ -40,14 +49,8 @@ import {
40
49
  TraceIdRatioBasedSampler,
41
50
  ParentBasedSampler,
42
51
  } from '@opentelemetry/sdk-trace-base'
43
- import { resourceFromAttributes } from '@opentelemetry/resources'
44
- import {
45
- ATTR_SERVICE_NAME,
46
- ATTR_SERVICE_VERSION,
47
- ATTR_DB_SYSTEM_NAME,
48
- ATTR_DB_QUERY_TEXT,
49
- ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
50
- } from '@opentelemetry/semantic-conventions'
52
+ import { ATTR_DB_SYSTEM_NAME, ATTR_DB_QUERY_TEXT } from '@opentelemetry/semantic-conventions'
53
+ import { buildOtelResource } from './otel-resource.js'
51
54
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
52
55
  import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'
53
56
  import { AsyncLocalStorage } from 'node:async_hooks'
@@ -80,6 +83,48 @@ function getActiveContext() {
80
83
  return activeContextStore.getStore() ?? ROOT_CONTEXT
81
84
  }
82
85
 
86
+ /** W3C trace context propagator(무상태) — traceparent/tracestate 파싱·직렬화 정본 구현(ADR-196). */
87
+ const w3cPropagator = new W3CTraceContextPropagator()
88
+
89
+ /**
90
+ * inbound 헤더의 `traceparent`(/`tracestate`)를 부모 OTel Context 로 복원한다(ADR-196, W3C trace context).
91
+ *
92
+ * 게이트웨이/업스트림이 보낸 trace 에 HTTP 루트 span 을 잇는 입구다 — {@link enterHttpSpan} 의 `headers`
93
+ * 옵션이 이 함수를 거친다. 헤더가 없거나 형식이 무효면 `undefined`(호출부가 새 루트로 시작 — fail-safe,
94
+ * 잘못된 외부 입력이 추적을 깨지 않게). 옵트인 OFF 면 `undefined`(0 비용).
95
+ *
96
+ * 비율 샘플링(`traceidratio`)은 `ParentBasedSampler` 라 복원된 원격 부모의 sampled flag 를 따른다 —
97
+ * 업스트림이 샘플한 trace 는 여기서도 기록되고, 버린 trace 는 여기서도 버려져 trace 가 반토막 나지 않는다.
98
+ *
99
+ * @param {Record<string, string | string[] | undefined>} headers - `req.headers`(대소문자 무관 키).
100
+ * @returns {import('@opentelemetry/api').Context | undefined} 유효한 원격 부모 컨텍스트, 없으면 undefined.
101
+ */
102
+ export function extractRemoteContext(headers) {
103
+ if (state === null) return undefined
104
+ if (!headers || typeof headers !== 'object') return undefined
105
+ const ctx = w3cPropagator.extract(ROOT_CONTEXT, headers, defaultTextMapGetter)
106
+ const sc = trace.getSpanContext(ctx)
107
+ if (!sc || !isSpanContextValid(sc)) return undefined // 무효 traceparent — 새 루트로(fail-safe).
108
+ return ctx
109
+ }
110
+
111
+ /**
112
+ * 현재 활성 span 을 outbound 헤더(`traceparent`/`tracestate`)로 직렬화해 carrier 에 채운다(ADR-196).
113
+ *
114
+ * 하류 HTTP 호출에 trace 를 잇는 출구다 — `fetch(url, { headers: ctx.tracer.propagationHeaders() })`.
115
+ * 활성 span 이 없거나 옵트인 OFF 면 carrier 를 그대로 반환(주입 없음 — 헤더 오염 없음).
116
+ *
117
+ * @param {Record<string, string>} [carrier={}] - 채울 헤더 객체(기존 키 보존, traceparent/tracestate 만 추가).
118
+ * @returns {Record<string, string>} carrier(체이닝용 동일 객체).
119
+ * @example
120
+ * const res = await fetch(downstreamUrl, { headers: MegaTracing.propagationHeaders({ accept: 'application/json' }) })
121
+ */
122
+ export function propagationHeaders(carrier = {}) {
123
+ if (state === null) return carrier
124
+ w3cPropagator.inject(getActiveContext(), carrier, defaultTextMapSetter)
125
+ return carrier
126
+ }
127
+
83
128
  /**
84
129
  * 비활성(옵트인 OFF) 시 사용자 코드에 넘길 **no-op span** — 메서드는 다 있지만 아무것도 기록 안 함.
85
130
  * `ctx.tracer.span(name, (span) => span.setAttribute(...))` 가 OFF 에서도 깨지지 않게 한다.
@@ -169,12 +214,8 @@ export function init(opts = /** @type {any} */ ({})) {
169
214
  const { exporter, processorKind } = buildExporter(opts)
170
215
  const SpanProcessor = processorKind === 'batch' ? BatchSpanProcessor : SimpleSpanProcessor
171
216
 
172
- const resource = resourceFromAttributes({
173
- [ATTR_SERVICE_NAME]: serviceName,
174
- ...(typeof opts.version === 'string' ? { [ATTR_SERVICE_VERSION]: opts.version } : {}),
175
- ...(typeof opts.environment === 'string' ? { [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: opts.environment } : {}),
176
- ...(opts.attributes && typeof opts.attributes === 'object' ? opts.attributes : {}),
177
- })
217
+ // resource 조립은 메트릭과 공유하는 단일 출처(otel-resource.js, ADR-196) — 두 SDK 간 드리프트 방지.
218
+ const resource = buildOtelResource({ serviceName, version: opts.version, environment: opts.environment, attributes: opts.attributes })
178
219
 
179
220
  const provider = new BasicTracerProvider({
180
221
  resource,
@@ -411,13 +452,17 @@ export function enterSpan(name, opts = {}) {
411
452
  * 기록(`setError`), `onResponse` 에서 상태코드와 함께 닫는다(`finish`). HTTP 시맨틱(5xx=ERROR)을 한곳에
412
453
  * 모아 배선 코드(mega-app)가 OTel enum 을 안 만지게 한다. 옵트인 OFF 면 no-op 핸들.
413
454
  *
414
- * @param {{ method: string, route: string, path: string, host?: string, app: string }} info
455
+ * `headers` 주면 inbound `traceparent` 부모로 복원해({@link extractRemoteContext}, ADR-196) 루트
456
+ * span 이 업스트림 trace 에 이어진다 — 무효/부재면 종전대로 새 루트.
457
+ *
458
+ * @param {{ method: string, route: string, path: string, host?: string, app: string, headers?: Record<string, string | string[] | undefined> }} info
415
459
  * @returns {{ span: import('@opentelemetry/api').Span, setError: (err: unknown) => void, finish: (statusCode: number) => void }}
416
460
  */
417
- export function enterHttpSpan({ method, route, path, host, app }) {
461
+ export function enterHttpSpan({ method, route, path, host, app, headers }) {
418
462
  if (state === null) return /** @type {any} */ (NOOP_HTTP_HANDLE)
419
463
  const handle = enterSpan(`http.${method} ${route}`, {
420
464
  kind: SpanKind.SERVER,
465
+ parent: extractRemoteContext(headers ?? {}),
421
466
  attributes: {
422
467
  'http.request.method': method,
423
468
  'http.route': route,
@@ -476,6 +521,8 @@ export function logMixin() {
476
521
  */
477
522
  export const tracer = Object.freeze({
478
523
  span,
524
+ /** outbound 헤더에 traceparent/tracestate 주입(ADR-196) — `fetch(url, { headers: ctx.tracer.propagationHeaders() })`. */
525
+ propagationHeaders,
479
526
  /** @returns {import('@opentelemetry/api').Span | undefined} 현재 활성 span(없으면 undefined — 03-api-spec §10). */
480
527
  activeSpan() {
481
528
  if (state === null) return undefined
@@ -479,7 +479,11 @@ export class MegaWorker extends EventEmitter {
479
479
 
480
480
  /** 비정상 exit 처리(정상 종료 code 0 는 stop 경로에서만 기대). @param {WorkerHandle} handle @param {number|null} code */
481
481
  #onExit(handle, code) {
482
- if (this.#stopping || handle.down) return // 종료 중 terminate 로 인한 exit 은 정상.
482
+ if (handle.down) return // terminate 로 인한 exit 은 정상(이중 발화 가드).
483
+ // stop() 진행 중 crash 도 #onWorkerDown 으로 보낸다 — in-flight task 를 reject(→ _notify)해야
484
+ // stop() 의 완료 대기(allSettled)가 풀린다. 예전엔 #stopping 이면 통째로 무시해 그 task 가 영영
485
+ // settle 되지 않아 stop() 이 hang 했다(상위 MegaShutdown 타임아웃으로만 회수). respawn/drain 은
486
+ // #scheduleRespawn/#drainIfDead 의 자체 #stopping 가드가 막으므로 종료 중 부작용이 없다.
483
487
  this.#onWorkerDown(handle, new Error(`worker exited unexpectedly (code ${code})`))
484
488
  }
485
489
 
@@ -0,0 +1,36 @@
1
+ // @ts-check
2
+ /**
3
+ * OTel Resource 공유 빌더 — 트레이싱·메트릭 SDK 의 단일 출처 (ADR-196, F5 audit O-SDK 단일화).
4
+ *
5
+ * `MegaTracing.init`/`MegaMetrics.init` 이 각자 동일한 resource 조립 블록을 들고 있어 service.name 류
6
+ * 시맨틱 키 매핑이 두 곳에서 드리프트할 수 있었다. 본 모듈이 그 조립을 한 곳으로 모은다 — 두 SDK 가
7
+ * 같은 입력(`serviceName`/`version`/`environment`/`attributes`)에 항상 같은 resource 속성을 낸다.
8
+ * (전면 NodeSDK 채택은 자동 instrumentation 의존이 끌려와 zero-dep 방침과 충돌 — 자체 일원화로 결정.)
9
+ *
10
+ * @module lib/otel-resource
11
+ */
12
+ import { resourceFromAttributes } from '@opentelemetry/resources'
13
+ import {
14
+ ATTR_SERVICE_NAME,
15
+ ATTR_SERVICE_VERSION,
16
+ ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
17
+ } from '@opentelemetry/semantic-conventions'
18
+
19
+ /**
20
+ * 공통 입력 → OTel Resource. 시크릿은 attributes 에 싣지 말 것(exporter 로 평문 전송됨).
21
+ *
22
+ * @param {object} opts
23
+ * @param {string} opts.serviceName - **필수**(호출부가 선검증). `service.name` 속성.
24
+ * @param {string} [opts.version] - `service.version`.
25
+ * @param {string} [opts.environment] - `deployment.environment.name`.
26
+ * @param {Record<string, any>} [opts.attributes] - 추가 resource 속성(머지).
27
+ * @returns {import('@opentelemetry/resources').Resource}
28
+ */
29
+ export function buildOtelResource({ serviceName, version, environment, attributes }) {
30
+ return resourceFromAttributes({
31
+ [ATTR_SERVICE_NAME]: serviceName,
32
+ ...(typeof version === 'string' ? { [ATTR_SERVICE_VERSION]: version } : {}),
33
+ ...(typeof environment === 'string' ? { [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: environment } : {}),
34
+ ...(attributes && typeof attributes === 'object' ? attributes : {}),
35
+ })
36
+ }