@yolo-croket-dev/core 1.10.3 → 1.10.4
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/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
2
25
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
27
|
};
|
|
5
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
29
|
exports.GracefulServerRMQ = void 0;
|
|
7
30
|
const microservices_1 = require("@nestjs/microservices");
|
|
31
|
+
const msa_logger_1 = __importStar(require("@yolo-croket-dev/msa-logger"));
|
|
8
32
|
const os_1 = __importDefault(require("os"));
|
|
9
33
|
const node_util_1 = require("node:util");
|
|
10
34
|
const rmq_diagnostics_function_1 = require("../../functions/rmq-diagnostics.function");
|
|
11
35
|
const sleep = (0, node_util_1.promisify)(setTimeout);
|
|
36
|
+
const msaLogger = new msa_logger_1.default(msa_logger_1.ServerTypeEnum.SERVICE);
|
|
12
37
|
const RMQ_DEFAULT_WAITING_ENDING_HANDLERS_TIMEOUT_MS = 5000;
|
|
13
38
|
const RMQ_DEFAULT_WAITING_ENDING_HANDLERS_INTERVAL_MS = 500;
|
|
14
39
|
const RMQ_UNKNOWN_CONNECTION_IDENTITY = 'unknown';
|
|
@@ -128,13 +153,25 @@ class GracefulServerRMQ extends microservices_1.ServerRMQ {
|
|
|
128
153
|
}
|
|
129
154
|
async cancelConsumer() {
|
|
130
155
|
const consumerTag = this.consumerTag;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
156
|
+
try {
|
|
157
|
+
if (this.channel) {
|
|
158
|
+
await this.channel.removeSetup(undefined, async (channel) => {
|
|
159
|
+
if (consumerTag)
|
|
160
|
+
await channel.cancel(consumerTag);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
// 취소 실패가 Nest 종료 훅을 중단하면 실행 중인 요청 완료 대기와 연결 정리가 생략된다.
|
|
166
|
+
const diagnostic = (0, msa_logger_1.normalizeDiagnosticError)(err);
|
|
167
|
+
msaLogger.warn('GracefulServerRMQ consumer 취소 실패: 기존 요청 완료 후 연결 정리', {
|
|
168
|
+
traceId: 'RMQ_SHUTDOWN_CANCEL_FAILED',
|
|
169
|
+
diagnosticInfo: diagnostic.diagnosticInfo,
|
|
170
|
+
}, diagnostic.details);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
this.consumerTag = null;
|
|
136
174
|
}
|
|
137
|
-
this.consumerTag = null;
|
|
138
175
|
}
|
|
139
176
|
async close() {
|
|
140
177
|
var _a;
|
package/package.json
CHANGED
|
@@ -78,17 +78,35 @@ test('primitive and getter failures retain the original rejection and emit diagn
|
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
test('consumer
|
|
81
|
+
test('consumer setup removal failure is logged and native close still disposes the observer', async t => {
|
|
82
82
|
const flag = process.env.RMQ_DIAGNOSTICS_ENABLED;
|
|
83
83
|
process.env.RMQ_DIAGNOSTICS_ENABLED = 'true';
|
|
84
84
|
t.after(() => { if (flag === undefined) delete process.env.RMQ_DIAGNOSTICS_ENABLED; else process.env.RMQ_DIAGNOSTICS_ENABLED = flag; });
|
|
85
85
|
const manager = new EventEmitter();
|
|
86
86
|
t.mock.method(ServerRMQ.prototype, 'createClient', () => manager);
|
|
87
|
-
t.mock.method(ServerRMQ.prototype, 'close', () => {});
|
|
87
|
+
const close = t.mock.method(ServerRMQ.prototype, 'close', () => {});
|
|
88
|
+
const warning = t.mock.method(MsaLogger.prototype, 'warn', () => {});
|
|
88
89
|
const server = new GracefulServerRMQ({ queue: 'USER_SERVICE' });
|
|
89
90
|
server.createClient();
|
|
90
91
|
server.channel = { removeSetup: async () => { throw Error('channel closed'); } };
|
|
91
|
-
await assert.
|
|
92
|
+
await assert.doesNotReject(server.close());
|
|
93
|
+
assert.equal(close.mock.callCount(), 1);
|
|
94
|
+
assert.equal(warning.mock.callCount(), 1);
|
|
95
|
+
assert.equal(manager.listenerCount('connect'), 0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('native server close failure retains its rejection after observer disposal', async t => {
|
|
99
|
+
const flag = process.env.RMQ_DIAGNOSTICS_ENABLED;
|
|
100
|
+
process.env.RMQ_DIAGNOSTICS_ENABLED = 'true';
|
|
101
|
+
t.after(() => { if (flag === undefined) delete process.env.RMQ_DIAGNOSTICS_ENABLED; else process.env.RMQ_DIAGNOSTICS_ENABLED = flag; });
|
|
102
|
+
const manager = new EventEmitter();
|
|
103
|
+
const failure = Error('native close failed');
|
|
104
|
+
t.mock.method(ServerRMQ.prototype, 'createClient', () => manager);
|
|
105
|
+
t.mock.method(ServerRMQ.prototype, 'close', async () => { throw failure; });
|
|
106
|
+
const server = new GracefulServerRMQ({ queue: 'USER_SERVICE' });
|
|
107
|
+
server.createClient();
|
|
108
|
+
server.channel = { removeSetup: async () => {} };
|
|
109
|
+
await assert.rejects(server.close(), err => err === failure);
|
|
92
110
|
assert.equal(manager.listenerCount('connect'), 0);
|
|
93
111
|
});
|
|
94
112
|
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
const assert = require('node:assert/strict');
|
|
2
2
|
const test = require('node:test');
|
|
3
|
+
const { createRequire } = require('node:module');
|
|
3
4
|
const { RmqContext, ServerRMQ } = require('@nestjs/microservices');
|
|
5
|
+
const { callModuleDestroyHook } = require('@nestjs/core/hooks/on-module-destroy.hook');
|
|
6
|
+
const { InstanceWrapper } = require('@nestjs/core/injector/instance-wrapper');
|
|
4
7
|
const { Subject } = require('rxjs');
|
|
5
|
-
const
|
|
8
|
+
const coreRequire = createRequire(require.resolve('../modules/graceful-shutdown/graceful-shutdown.rmq'));
|
|
9
|
+
const { MsaLogger } = coreRequire('@yolo-croket-dev/msa-logger');
|
|
6
10
|
const { RPCInterceptor } = require('../interceptors/rpc.interceptor');
|
|
7
11
|
const { GracefulShutdownService } = require('../modules/graceful-shutdown/graceful-shutdown.service');
|
|
12
|
+
const { GracefulShutdownModule } = require('../modules/graceful-shutdown/graceful-shutdown.module');
|
|
8
13
|
const { GracefulServerRMQ } = require('../modules/graceful-shutdown/graceful-shutdown.rmq');
|
|
9
14
|
|
|
10
15
|
function createRequest(channel, headers = {}) {
|
|
@@ -152,6 +157,60 @@ test('수신 중단과 서버 close가 겹쳐도 consumer는 한 번만 취소
|
|
|
152
157
|
assert.deepEqual(calls, [['cancel', 'consumer-1'], ['close']]);
|
|
153
158
|
});
|
|
154
159
|
|
|
160
|
+
for (const failurePoint of ['cancel', 'setup']) {
|
|
161
|
+
test(`${failurePoint} 실패 후에도 Nest 종료 훅은 기존 RPC 완료를 기다리고 연결을 정리한다`, async t => {
|
|
162
|
+
t.mock.method(MsaLogger.prototype, 'debug', () => {});
|
|
163
|
+
t.mock.method(MsaLogger.prototype, 'info', () => {});
|
|
164
|
+
const warning = t.mock.method(MsaLogger.prototype, 'warn', () => {});
|
|
165
|
+
const shutdown = new GracefulShutdownService();
|
|
166
|
+
const server = new GracefulServerRMQ({ queue: 'TEST', noAck: false });
|
|
167
|
+
const calls = [];
|
|
168
|
+
const failure = new Error('Channel ended during consumer cancellation');
|
|
169
|
+
server.consumerTag = 'consumer-1';
|
|
170
|
+
server.channel = {
|
|
171
|
+
removeSetup: async (_setup, teardown) => {
|
|
172
|
+
if (failurePoint === 'setup') throw failure;
|
|
173
|
+
await teardown({ cancel: async () => { calls.push('cancel'); throw failure; } });
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const active = { method: 'test.payment', traceId: 'shutdown-test', correlationId: 'active-request' };
|
|
177
|
+
shutdown.pushActiveInterceptors(active);
|
|
178
|
+
t.after(() => shutdown.popActiveInterceptors(active));
|
|
179
|
+
shutdown.registerRmqServer(server);
|
|
180
|
+
let enteredDrain;
|
|
181
|
+
const startedDrain = new Promise(resolve => { enteredDrain = resolve; });
|
|
182
|
+
const originalDrain = shutdown.waitForActiveInterceptors.bind(shutdown);
|
|
183
|
+
t.mock.method(shutdown, 'waitForActiveInterceptors', async () => {
|
|
184
|
+
calls.push('drain');
|
|
185
|
+
enteredDrain();
|
|
186
|
+
await originalDrain();
|
|
187
|
+
calls.push('finished');
|
|
188
|
+
});
|
|
189
|
+
t.mock.method(ServerRMQ.prototype, 'close', async () => { calls.push('close'); });
|
|
190
|
+
const moduleInstance = new GracefulShutdownModule(shutdown);
|
|
191
|
+
const moduleFixture = {
|
|
192
|
+
getNonAliasProviders: () => [
|
|
193
|
+
['module', new InstanceWrapper({ instance: moduleInstance, metatype: GracefulShutdownModule })],
|
|
194
|
+
['service', new InstanceWrapper({ instance: shutdown, metatype: GracefulShutdownService })],
|
|
195
|
+
],
|
|
196
|
+
controllers: new Map(), injectables: new Map(), middlewares: new Map(),
|
|
197
|
+
};
|
|
198
|
+
const destroying = callModuleDestroyHook(moduleFixture);
|
|
199
|
+
await Promise.race([startedDrain, destroying]);
|
|
200
|
+
assert.equal(shutdown.isApplicationExit, true);
|
|
201
|
+
assert.equal(shutdown.getActiveInterceptorsCount(), 1);
|
|
202
|
+
assert.equal(calls.includes('close'), false);
|
|
203
|
+
shutdown.popActiveInterceptors(active);
|
|
204
|
+
await destroying;
|
|
205
|
+
await server.close();
|
|
206
|
+
assert.deepEqual(calls, failurePoint === 'cancel'
|
|
207
|
+
? ['cancel', 'drain', 'finished', 'close'] : ['drain', 'finished', 'close']);
|
|
208
|
+
assert.equal(warning.mock.callCount(), 1);
|
|
209
|
+
assert.equal(server.consumerTag, null);
|
|
210
|
+
assert.equal(shutdown.getActiveInterceptorsCount(), 0);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
155
214
|
test('채널 설정 중 종료가 시작되면 새 consumer를 만들지 않는다', async () => {
|
|
156
215
|
let finishPrefetch;
|
|
157
216
|
const prefetch = new Promise(resolve => { finishPrefetch = resolve; });
|