mega-framework 0.1.5 → 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 +156 -8
- package/sample/crud/.env.example +153 -28
- 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 +63 -3
- package/sample/crud/package.json +3 -3
- 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 +478 -104
- package/src/core/ajv-mapper.js +27 -2
- package/src/core/boot.js +485 -237
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/config-validator.js +25 -0
- 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 +223 -481
- package/src/core/mega-cluster.js +54 -13
- 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/scope-registry.js +0 -1
- 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-logger.js +1 -1
- 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 +162 -49
- 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
|
|
@@ -37,25 +64,40 @@ let globalErrorsRegistered = false
|
|
|
37
64
|
let unhandledRejectionHandler = null
|
|
38
65
|
/** @type {((err: Error, origin?: string) => void) | null} */
|
|
39
66
|
let uncaughtExceptionHandler = null
|
|
67
|
+
/**
|
|
68
|
+
* 전역 에러 핸들러가 fatal 로그에 쓸 로거. boot 이 pino 공유 인스턴스(appLogger)를 주입한다(setLogger).
|
|
69
|
+
* 미설정(부팅 전 조기 크래시·로거 비활성 프로세스)이면 console.error 로 폴백한다. process 레벨 핸들러는
|
|
70
|
+
* bootApp 의 DI 그래프 밖이라 모듈 스코프 변수로 받아 둔다(전역 오염 회피, ADR-178).
|
|
71
|
+
* @typedef {{ info?: (...args: any[]) => void, warn?: (...args: any[]) => void, error?: (...args: any[]) => void, fatal?: (...args: any[]) => void }} ShutdownLogger
|
|
72
|
+
* @type {ShutdownLogger | null}
|
|
73
|
+
*/
|
|
74
|
+
let shutdownLogger = null
|
|
40
75
|
|
|
41
76
|
/**
|
|
42
|
-
* cleanup hook 등록.
|
|
43
|
-
*
|
|
77
|
+
* cleanup hook 등록. 실행 순서 = {@link SHUTDOWN_STAGES} 순서 → 같은 stage 안에서는 등록 역순(LIFO).
|
|
78
|
+
* `stage` 미지정이면 `'app'`(어댑터 종료 전, 사용자 cleanup 표준 위치) — 기존 2-인자 호출과 호환.
|
|
79
|
+
* @param {string} name - 식별자 (로그·unregister 용)
|
|
44
80
|
* @param {() => Promise<void> | void} fn
|
|
81
|
+
* @param {{ stage?: string }} [opts] - 종료 stage({@link SHUTDOWN_STAGES} 중 하나).
|
|
45
82
|
*/
|
|
46
|
-
function register(name, fn) {
|
|
83
|
+
function register(name, fn, opts = {}) {
|
|
47
84
|
if (typeof name !== 'string' || name.length === 0) {
|
|
48
85
|
throw new Error('MegaShutdown.register: name is required (string)')
|
|
49
86
|
}
|
|
50
87
|
if (typeof fn !== 'function') {
|
|
51
88
|
throw new Error('MegaShutdown.register: fn must be a function')
|
|
52
89
|
}
|
|
53
|
-
|
|
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 })
|
|
54
96
|
}
|
|
55
97
|
|
|
56
98
|
/**
|
|
57
99
|
* 등록된 cleanup hook 제거 (같은 name 전부). 런타임 중 동적으로 붙였다 떼는 hook(예: hub link)
|
|
58
|
-
* 의 누수를 막는다(L1). shutdown 진행 중에는
|
|
100
|
+
* 의 누수를 막는다(L1). shutdown 진행 중에는 실행 추적(exitedHandlers)과 충돌하므로 무시한다.
|
|
59
101
|
* @param {string} name - register 시 쓴 식별자.
|
|
60
102
|
* @returns {number} 제거된 hook 수.
|
|
61
103
|
*/
|
|
@@ -91,9 +133,13 @@ function setupSignals(opts = {}) {
|
|
|
91
133
|
if (typeof opts.hardKillMs === 'number') hardKillMs = opts.hardKillMs
|
|
92
134
|
const signals = opts.signals ?? ['SIGTERM', 'SIGINT']
|
|
93
135
|
for (const sig of signals) {
|
|
94
|
-
|
|
136
|
+
// 리스너 참조를 보관해 _reset 이 떼어낼 수 있게 한다 — reset 후 재-setup 시 리스너가 중복돼
|
|
137
|
+
// 시그널 1회에 now() 가 2회 불리면 두 번째가 M-3(즉시 force exit 1)로 오인된다.
|
|
138
|
+
const fn = () => {
|
|
95
139
|
void now({ signal: sig, exitCode: 0 })
|
|
96
|
-
}
|
|
140
|
+
}
|
|
141
|
+
signalHandlers.push({ sig, fn })
|
|
142
|
+
process.on(sig, fn)
|
|
97
143
|
}
|
|
98
144
|
// 전역 에러 핸들러도 함께 등록(opt-out: globalErrorHandlers === false). REPL(console-cmd) 등은 끌 수 있다.
|
|
99
145
|
if (opts.globalErrorHandlers !== false) setupGlobalErrorHandlers()
|
|
@@ -110,23 +156,52 @@ function setupSignals(opts = {}) {
|
|
|
110
156
|
function setupGlobalErrorHandlers({ exitCode = 1 } = {}) {
|
|
111
157
|
if (globalErrorsRegistered) return
|
|
112
158
|
globalErrorsRegistered = true
|
|
159
|
+
// shutdown 진행 중의 에러는 now() 를 재호출하지 않는다 — teardown 구간(소켓·어댑터 정리)은 floating
|
|
160
|
+
// rejection 이 가장 나기 쉬운데, 여기서 now() 를 다시 부르면 "두 번째 시그널"(M-3)로 오인돼 남은
|
|
161
|
+
// hook(어댑터 disconnect·로그 flush)을 건너뛰고 즉시 exit(1) 된다. fatal 기록만 하고 정리를 계속한다.
|
|
113
162
|
unhandledRejectionHandler = (reason) => {
|
|
114
|
-
const log = /** @type {any} */ (globalThis).logger
|
|
115
163
|
const err = reason instanceof Error ? reason : new Error(`unhandledRejection: ${String(reason)}`)
|
|
116
|
-
if (
|
|
117
|
-
|
|
164
|
+
if (isShuttingDownFlag) {
|
|
165
|
+
logShutdown('fatal', 'unhandledRejection during shutdown (continuing cleanup)', { err })
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
logShutdown('fatal', 'unhandledRejection — initiating graceful shutdown', { err })
|
|
118
169
|
void now({ signal: 'unhandledRejection', exitCode })
|
|
119
170
|
}
|
|
120
171
|
uncaughtExceptionHandler = (err, origin) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
172
|
+
if (isShuttingDownFlag) {
|
|
173
|
+
logShutdown('fatal', 'uncaughtException during shutdown (continuing cleanup)', { err, origin })
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
logShutdown('fatal', 'uncaughtException — initiating graceful shutdown', { err, origin })
|
|
124
177
|
void now({ signal: 'uncaughtException', exitCode })
|
|
125
178
|
}
|
|
126
179
|
process.on('unhandledRejection', unhandledRejectionHandler)
|
|
127
180
|
process.on('uncaughtException', uncaughtExceptionHandler)
|
|
128
181
|
}
|
|
129
182
|
|
|
183
|
+
/**
|
|
184
|
+
* 종료 시퀀스 로깅 — 주입된 로거(setLogger)가 있으면 그 레벨 메서드로, 없으면 console 폴백.
|
|
185
|
+
* process 레벨 종료 경로라 로거 미주입(부팅 전·로거 비활성 프로세스)일 수 있어 항상 폴백을 갖춘다.
|
|
186
|
+
* `fatal` 은 console 에 없으므로 폴백에선 `console.error` 로 매핑한다. fields 는 구조적 로그의 payload
|
|
187
|
+
* (pino `log.level(fields, msg)`)이자 console 폴백의 보조 인자.
|
|
188
|
+
* @param {'info'|'warn'|'error'|'fatal'} level - 로그 레벨.
|
|
189
|
+
* @param {string} msg - 메시지.
|
|
190
|
+
* @param {Record<string, any>} [fields] - 구조적 필드(선택).
|
|
191
|
+
* @returns {void}
|
|
192
|
+
*/
|
|
193
|
+
function logShutdown(level, msg, fields) {
|
|
194
|
+
const log = shutdownLogger
|
|
195
|
+
if (log && typeof (/** @type {any} */ (log)[level]) === 'function') {
|
|
196
|
+
if (fields) /** @type {any} */ (log)[level](fields, msg)
|
|
197
|
+
else /** @type {any} */ (log)[level](msg)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
const consoleFn = level === 'warn' ? console.warn : level === 'info' ? console.log : console.error
|
|
201
|
+
if (fields) consoleFn(`[mega-shutdown] ${msg}`, fields)
|
|
202
|
+
else consoleFn(`[mega-shutdown] ${msg}`)
|
|
203
|
+
}
|
|
204
|
+
|
|
130
205
|
/**
|
|
131
206
|
* 즉시 graceful shutdown 트리거.
|
|
132
207
|
*
|
|
@@ -138,46 +213,75 @@ function setupGlobalErrorHandlers({ exitCode = 1 } = {}) {
|
|
|
138
213
|
async function now({ signal, exitCode = 0 } = {}) {
|
|
139
214
|
// M-3 — 두 번째 shutdown 시그널: 이미 진행 중이면 grace 무시하고 즉시 force exit 1.
|
|
140
215
|
if (isShuttingDownFlag) {
|
|
141
|
-
|
|
216
|
+
logShutdown('error', 'second shutdown signal — force exit 1')
|
|
142
217
|
process.exit(1)
|
|
143
218
|
return // process.exit mock 시(테스트) 아래로 흐르지 않도록 가드
|
|
144
219
|
}
|
|
145
220
|
isShuttingDownFlag = true
|
|
146
221
|
|
|
147
|
-
|
|
222
|
+
logShutdown('info', 'shutdown starting', { signal: signal ?? 'manual', handlers: handlers.length, graceMs: gracePeriodMs })
|
|
148
223
|
|
|
149
224
|
// hardKill 보호 — hardKillMs 초과 시 강제 종료
|
|
225
|
+
const hardKillAt = Date.now() + hardKillMs
|
|
150
226
|
const hardKillTimer = setTimeout(() => {
|
|
151
|
-
|
|
227
|
+
logShutdown('error', 'grace period exceeded — force exit(1)', { hardKillMs })
|
|
152
228
|
process.exit(1)
|
|
153
229
|
}, hardKillMs)
|
|
154
230
|
hardKillTimer.unref()
|
|
155
231
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
new Promise((_, reject) =>
|
|
165
|
-
setTimeout(() => reject(new Error('handler timeout')), gracePeriodMs).unref(),
|
|
166
|
-
),
|
|
167
|
-
])
|
|
168
|
-
exitedHandlers.add(i)
|
|
169
|
-
console.log(`[mega-shutdown] '${name}' done`)
|
|
170
|
-
} catch (err) {
|
|
171
|
-
// silent 금지. 핸들러 실패 시 warn 로그 + 다음 hook 진행.
|
|
172
|
-
console.warn(`[mega-shutdown] '${name}' failed (continuing):`, 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])
|
|
173
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 })
|
|
174
268
|
}
|
|
175
269
|
|
|
176
270
|
clearTimeout(hardKillTimer)
|
|
177
|
-
|
|
271
|
+
logShutdown('info', `shutdown complete — exit(${exitCode})`, { exitCode })
|
|
178
272
|
process.exit(exitCode)
|
|
179
273
|
}
|
|
180
274
|
|
|
275
|
+
/**
|
|
276
|
+
* 전역 에러 핸들러(unhandledRejection/uncaughtException)가 fatal 로그에 쓸 로거를 주입한다(ADR-178).
|
|
277
|
+
* boot 이 pino 공유 인스턴스(appLogger)를 만들 때 호출한다. 미호출이면 핸들러는 console.error 로 폴백한다.
|
|
278
|
+
* @param {ShutdownLogger | null | undefined} logger - pino 호환 로거(info/warn/error/fatal 사용) 또는 null.
|
|
279
|
+
* @returns {void}
|
|
280
|
+
*/
|
|
281
|
+
function setLogger(logger) {
|
|
282
|
+
shutdownLogger = logger ?? null
|
|
283
|
+
}
|
|
284
|
+
|
|
181
285
|
/**
|
|
182
286
|
* 테스트용 — 등록된 hook 수 (디버그).
|
|
183
287
|
* @returns {number}
|
|
@@ -191,6 +295,12 @@ function registeredCount() {
|
|
|
191
295
|
*/
|
|
192
296
|
function _reset() {
|
|
193
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 = []
|
|
194
304
|
signalsRegistered = false
|
|
195
305
|
isShuttingDownFlag = false
|
|
196
306
|
gracePeriodMs = 30_000
|
|
@@ -202,6 +312,7 @@ function _reset() {
|
|
|
202
312
|
unhandledRejectionHandler = null
|
|
203
313
|
uncaughtExceptionHandler = null
|
|
204
314
|
globalErrorsRegistered = false
|
|
315
|
+
shutdownLogger = null
|
|
205
316
|
}
|
|
206
317
|
|
|
207
318
|
/**
|
|
@@ -209,11 +320,13 @@ function _reset() {
|
|
|
209
320
|
* docs/10·07 시퀀스가 `MegaShutdown.now()` 형태이므로 객체로 통일 (M-4).
|
|
210
321
|
*/
|
|
211
322
|
export const MegaShutdown = {
|
|
323
|
+
STAGES: SHUTDOWN_STAGES,
|
|
212
324
|
register,
|
|
213
325
|
unregister,
|
|
214
326
|
isShuttingDown,
|
|
215
327
|
setupSignals,
|
|
216
328
|
setupGlobalErrorHandlers,
|
|
329
|
+
setLogger,
|
|
217
330
|
now,
|
|
218
331
|
registeredCount,
|
|
219
332
|
_reset,
|
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
|
|