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.
- package/bin/mega-ws-hub.js +2 -2
- package/package.json +32 -8
- package/sample/crud/.env +1 -1
- package/sample/crud/.env.example +1 -1
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
- package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
- package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
- package/sample/crud/apps/main/models/log-partition-model.js +105 -0
- package/sample/crud/apps/main/models/note-model.js +79 -0
- package/sample/crud/apps/main/models/user-level-model.js +24 -0
- package/sample/crud/apps/main/models/user-model.js +146 -0
- package/sample/crud/apps/main/models/user-type-model.js +21 -0
- package/sample/crud/apps/main/models/wallet-model.js +24 -0
- package/sample/crud/apps/main/routes/users.js +55 -10
- package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
- package/sample/crud/apps/main/services/auth-service.js +39 -24
- package/sample/crud/apps/main/services/log-partition-service.js +101 -0
- package/sample/crud/apps/main/services/note-service.js +6 -6
- package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
- package/sample/crud/apps/main/services/user-service.js +62 -21
- package/sample/crud/apps/main/views/auth/login.ejs +6 -6
- package/sample/crud/apps/main/views/auth/register.ejs +46 -5
- package/sample/crud/apps/main/views/users/edit.ejs +42 -5
- package/sample/crud/apps/main/views/users/list.ejs +6 -2
- package/sample/crud/apps/main/views/users/new.ejs +56 -4
- package/sample/crud/docs/log_partition_design.mm.md +23 -0
- package/sample/crud/mega.config.js +3 -2
- package/sample/crud/package.json +1 -1
- package/sample/crud/scripts/start-ws-hub.sh +2 -2
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +30 -0
- package/src/adapters/maria-adapter.js +26 -3
- package/src/adapters/mega-db-adapter.js +7 -1
- package/src/adapters/mongo-adapter.js +19 -1
- package/src/adapters/postgres-adapter.js +25 -2
- package/src/adapters/sqlite-adapter.js +20 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +137 -33
- package/src/cli/generators/index.js +82 -2
- package/src/cli/index.js +353 -100
- package/src/core/ajv-mapper.js +27 -2
- package/src/core/boot.js +464 -245
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/ctx-builder.js +6 -2
- package/src/core/envelope.js +112 -12
- package/src/core/hub-link.js +65 -4
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +6 -2
- package/src/core/mega-app.js +201 -463
- package/src/core/mega-cluster.js +4 -1
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +129 -0
- package/src/core/router.js +70 -65
- package/src/core/security.js +67 -9
- package/src/core/workers-manager.js +12 -1
- package/src/core/ws-cluster.js +10 -3
- package/src/core/ws-message.js +48 -4
- package/src/core/ws-presence.js +624 -0
- package/src/core/ws-roster.js +4 -1
- package/src/core/ws-upgrade.js +118 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-health.js +25 -4
- package/src/lib/mega-job-queue.js +98 -21
- package/src/lib/mega-job.js +29 -0
- package/src/lib/mega-metrics.js +3 -12
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +114 -39
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +5 -1
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +51 -8
- package/src/models/crud-sql-builder.js +133 -0
- package/src/models/mega-model.js +82 -2
- package/src/models/model-crud.js +483 -0
- package/src/models/mongo-crud.js +285 -0
- package/templates/model/code-mongo.tpl +35 -0
- package/templates/model/code.tpl +15 -1
- package/templates/model/test-mongo.tpl +38 -0
- package/templates/model/test.tpl +4 -0
- package/types/adapters/adapter-manager.d.ts +95 -0
- package/types/adapters/adapter-options.d.ts +91 -0
- package/types/adapters/file-adapter.d.ts +94 -0
- package/types/adapters/file-session-adapter.d.ts +101 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +115 -0
- package/types/adapters/mega-adapter.d.ts +215 -0
- package/types/adapters/mega-bus-adapter.d.ts +45 -0
- package/types/adapters/mega-cache-adapter.d.ts +47 -0
- package/types/adapters/mega-db-adapter.d.ts +47 -0
- package/types/adapters/mega-lock-adapter.d.ts +62 -0
- package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
- package/types/adapters/mega-session-adapter.d.ts +32 -0
- package/types/adapters/mongo-adapter.d.ts +139 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +139 -0
- package/types/adapters/redis-adapter.d.ts +70 -0
- package/types/adapters/redis-session-adapter.d.ts +82 -0
- package/types/adapters/redlock-adapter.d.ts +149 -0
- package/types/adapters/registry.d.ts +46 -0
- package/types/adapters/sqlite-adapter.d.ts +106 -0
- package/types/auth/index.d.ts +24 -0
- package/types/cli/commands/console-cmd.d.ts +37 -0
- package/types/cli/commands/new.d.ts +16 -0
- package/types/cli/commands/routes.d.ts +36 -0
- package/types/cli/commands/scaffold.d.ts +78 -0
- package/types/cli/commands/test-cmd.d.ts +14 -0
- package/types/cli/generators/index.d.ts +112 -0
- package/types/cli/index.d.ts +249 -0
- package/types/cli/template-engine.d.ts +40 -0
- package/types/core/ajv-mapper.d.ts +27 -0
- package/types/core/boot.d.ts +233 -0
- package/types/core/cluster-metrics.d.ts +52 -0
- package/types/core/config-loader.d.ts +13 -0
- package/types/core/config-validator.d.ts +30 -0
- package/types/core/ctx-builder.d.ts +80 -0
- package/types/core/envelope.d.ts +79 -0
- package/types/core/error-mapper.d.ts +17 -0
- package/types/core/formbody.d.ts +41 -0
- package/types/core/hub-link.d.ts +264 -0
- package/types/core/i18n.d.ts +178 -0
- package/types/core/index.d.ts +28 -0
- package/types/core/mega-app.d.ts +529 -0
- package/types/core/mega-cluster.d.ts +104 -0
- package/types/core/mega-server.d.ts +91 -0
- package/types/core/mega-service.d.ts +31 -0
- package/types/core/migration/dialect-registry.d.ts +22 -0
- package/types/core/migration/dialects/maria.d.ts +99 -0
- package/types/core/migration/dialects/mongo.d.ts +89 -0
- package/types/core/migration/dialects/postgres.d.ts +117 -0
- package/types/core/migration/dialects/sqlite.d.ts +111 -0
- package/types/core/migration/differ.d.ts +47 -0
- package/types/core/migration/generate.d.ts +56 -0
- package/types/core/migration/journal.d.ts +52 -0
- package/types/core/migration/model-scan.d.ts +19 -0
- package/types/core/migration/mongo-migration-db.d.ts +7 -0
- package/types/core/migration/schema-builder.d.ts +197 -0
- package/types/core/migration/schema-validator.d.ts +20 -0
- package/types/core/migration-lock.d.ts +33 -0
- package/types/core/migration-runner.d.ts +101 -0
- package/types/core/multipart.d.ts +86 -0
- package/types/core/openapi.d.ts +62 -0
- package/types/core/pipeline.d.ts +92 -0
- package/types/core/router.d.ts +159 -0
- package/types/core/routes-loader.d.ts +21 -0
- package/types/core/scope-registry.d.ts +14 -0
- package/types/core/security.d.ts +77 -0
- package/types/core/services-loader.d.ts +27 -0
- package/types/core/session-cleanup-schedule.d.ts +19 -0
- package/types/core/session-store.d.ts +18 -0
- package/types/core/session.d.ts +77 -0
- package/types/core/static-assets.d.ts +73 -0
- package/types/core/template.d.ts +106 -0
- package/types/core/workers-manager.d.ts +79 -0
- package/types/core/ws-cluster.d.ts +208 -0
- package/types/core/ws-compression.d.ts +112 -0
- package/types/core/ws-controller.d.ts +65 -0
- package/types/core/ws-message.d.ts +106 -0
- package/types/core/ws-presence.d.ts +273 -0
- package/types/core/ws-roster.d.ts +96 -0
- package/types/core/ws-upgrade.d.ts +231 -0
- package/types/errors/config-error.d.ts +10 -0
- package/types/errors/http-errors.d.ts +120 -0
- package/types/errors/index.d.ts +3 -0
- package/types/errors/mega-error.d.ts +32 -0
- package/types/index.d.ts +39 -0
- package/types/lib/asp/config.d.ts +49 -0
- package/types/lib/asp/crypto.d.ts +43 -0
- package/types/lib/asp/errors.d.ts +30 -0
- package/types/lib/asp/nonce-cache.d.ts +52 -0
- package/types/lib/asp/plugin.d.ts +30 -0
- package/types/lib/asp/ws-terminator.d.ts +45 -0
- package/types/lib/env-mapper.d.ts +14 -0
- package/types/lib/hub-protocol.d.ts +106 -0
- package/types/lib/index.d.ts +22 -0
- package/types/lib/logger/telegram-core.d.ts +104 -0
- package/types/lib/logger/telegram-transport.d.ts +45 -0
- package/types/lib/mega-brute-force.d.ts +66 -0
- package/types/lib/mega-circuit-breaker.d.ts +241 -0
- package/types/lib/mega-cron.d.ts +66 -0
- package/types/lib/mega-hash.d.ts +32 -0
- package/types/lib/mega-health.d.ts +41 -0
- package/types/lib/mega-job-queue.d.ts +176 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +138 -0
- package/types/lib/mega-logger.d.ts +45 -0
- package/types/lib/mega-metrics.d.ts +285 -0
- package/types/lib/mega-plugin.d.ts +245 -0
- package/types/lib/mega-retry.d.ts +85 -0
- package/types/lib/mega-schedule.d.ts +260 -0
- package/types/lib/mega-shutdown.d.ts +135 -0
- package/types/lib/mega-tracing.d.ts +224 -0
- package/types/lib/mega-worker.d.ts +127 -0
- package/types/lib/otel-resource.d.ts +16 -0
- package/types/lib/worker-runner/process-entry.d.ts +1 -0
- package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
- package/types/lib/worker-runner/thread-entry.d.ts +1 -0
- package/types/lib/ws-hub.d.ts +234 -0
- package/types/models/crud-sql-builder.d.ts +48 -0
- package/types/models/index.d.ts +1 -0
- package/types/models/mega-model.d.ts +138 -0
- package/types/models/model-crud.d.ts +82 -0
- package/types/models/mongo-crud.d.ts +59 -0
- package/types/test/index.d.ts +84 -0
- package/.env +0 -127
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
- package/sample/crud/apps/main/models/note.js +0 -71
- package/sample/crud/apps/main/models/user.js +0 -86
- package/sample/crud/package-lock.json +0 -5665
- package/sample/crud/yarn.lock +0 -2142
- package/sample/simple/package-lock.json +0 -1851
package/src/lib/mega-plugin.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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, {
|
|
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
|
/**
|
package/src/lib/mega-schedule.js
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
* 먼저 잡은 1대만 실행하고, 못 잡은 나머지는 **조용히 건너뛴다**(skip). 락은 `ttl`(ms) 뒤 자동 만료돼
|
|
16
16
|
* 다음 주기엔 다시 경쟁한다.
|
|
17
17
|
*
|
|
18
|
-
* 구현: `lock.
|
|
19
|
-
* 즉시 skip 한다(retry 안 함 — retry+throw 는 "중복방지"가 아니라 "대기"라서 의미가 다름).
|
|
20
|
-
* `
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
/**
|
package/src/lib/mega-shutdown.js
CHANGED
|
@@ -3,33 +3,60 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* MegaShutdown — graceful shutdown 시퀀스 + 사용자 cleanup hook 묶음.
|
|
5
5
|
*
|
|
6
|
-
* 시퀀스 (docs/10 §3):
|
|
7
|
-
* Running →
|
|
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 캐치 +
|
|
16
|
-
*
|
|
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('
|
|
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
|
-
/**
|
|
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
|
-
|
|
32
|
-
|
|
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 등록.
|
|
51
|
-
*
|
|
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
|
-
|
|
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 진행 중에는
|
|
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
|
-
|
|
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
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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,
|
package/src/lib/mega-tracing.js
CHANGED
|
@@ -21,14 +21,23 @@
|
|
|
21
21
|
* `enterWith` 방식이 공유 동기 실행을 오염시켜 sibling 을 잘못 묶던 문제(스모크로 확인)를 구조적으로
|
|
22
22
|
* 제거한다. 이 seam 은 ADR-077 의 `_instrument` 에 추가된 **단 한 줄(run 위임)** 이다(ADR-114, 오너 결정).
|
|
23
23
|
*
|
|
24
|
-
* #
|
|
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 보강 — 사용자 승인)
|
|
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 {
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
*
|
|
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
|
package/src/lib/mega-worker.js
CHANGED
|
@@ -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 (
|
|
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
|
+
}
|