@yolo-croket-dev/core 1.10.4 → 1.10.5
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
|
@@ -148,10 +148,39 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
148
148
|
}
|
|
149
149
|
return obj;
|
|
150
150
|
}
|
|
151
|
-
async requeueMessageOnShutdown(channel, message) {
|
|
151
|
+
async requeueMessageOnShutdown(channel, message, traceId) {
|
|
152
152
|
msaLogger.debug('RPCInterceptor isApplicationExit called', { traceId: 'IS_APPLICATION_EXIT' });
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
const failures = [];
|
|
154
|
+
try {
|
|
155
|
+
await channel.cancel(message.fields.consumerTag);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
failures.push({
|
|
159
|
+
operation: 'cancel',
|
|
160
|
+
diagnostic: (0, msa_logger_1.normalizeDiagnosticError)(err),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
channel.nack(message, false, true);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
// 닫힌 채널의 미확인 메시지는 브로커가 재전달하므로 기존 Pod에서 오류 응답을 보내지 않는다.
|
|
168
|
+
failures.push({
|
|
169
|
+
operation: 'nack',
|
|
170
|
+
diagnostic: (0, msa_logger_1.normalizeDiagnosticError)(err),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (failures.length) {
|
|
174
|
+
msaLogger.warn('RPCInterceptor 종료 중 메시지 재전달 처리 실패', {
|
|
175
|
+
traceId,
|
|
176
|
+
diagnosticInfo: failures[0].diagnostic.diagnosticInfo,
|
|
177
|
+
}, {
|
|
178
|
+
failures: failures.map(({ operation, diagnostic }) => ({
|
|
179
|
+
operation,
|
|
180
|
+
...diagnostic.details,
|
|
181
|
+
})),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
155
184
|
// 기존 서버에서 응답을 완료하면 재전달된 요청의 실제 응답을 클라이언트가 기다릴 수 없다.
|
|
156
185
|
return new rxjs_1.Observable();
|
|
157
186
|
}
|
|
@@ -182,7 +211,7 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
182
211
|
let abortController = new AbortController();
|
|
183
212
|
/** consume 메시지 전부 cancel */
|
|
184
213
|
if (this.gracefulShutdownService.isApplicationExit) {
|
|
185
|
-
return this.requeueMessageOnShutdown(channel, message);
|
|
214
|
+
return this.requeueMessageOnShutdown(channel, message, execInterceptor.traceId);
|
|
186
215
|
}
|
|
187
216
|
if (!staticRetryCount && reDeliveryCount > 0) {
|
|
188
217
|
msaLogger.debug('RETRY_COUNT_ZERO_OR_NO_RETRY 발생 전, redelivery 발생', { traceId: ((_b = content.data.param) === null || _b === void 0 ? void 0 : _b.traceId) || 'RETRY_COUNT_ZERO_OR_NO_RETRY' }, {
|
|
@@ -195,7 +224,7 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
195
224
|
});
|
|
196
225
|
const isRedeliveryMessage = await this.isRedeliveryMessage(message.properties.replyTo, execInterceptor.traceId);
|
|
197
226
|
if (this.gracefulShutdownService.isApplicationExit) {
|
|
198
|
-
return this.requeueMessageOnShutdown(channel, message);
|
|
227
|
+
return this.requeueMessageOnShutdown(channel, message, execInterceptor.traceId);
|
|
199
228
|
}
|
|
200
229
|
if (!isRedeliveryMessage || reDeliveryCount > 5) {
|
|
201
230
|
msaLogger.debug('isRedeliveryMessage RETRY_COUNT_ZERO_OR_NO_RETRY false', { traceId: ((_c = content.data.param) === null || _c === void 0 ? void 0 : _c.traceId) || 'RETRY_COUNT_ZERO_OR_NO_RETRY' });
|
|
@@ -368,7 +397,7 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
368
397
|
}
|
|
369
398
|
// 재전달 여부를 비동기로 확인하는 동안 종료가 시작될 수 있으므로 실행 직전에 다시 확인한다.
|
|
370
399
|
if (this.gracefulShutdownService.isApplicationExit) {
|
|
371
|
-
return this.requeueMessageOnShutdown(channel, message);
|
|
400
|
+
return this.requeueMessageOnShutdown(channel, message, execInterceptor.traceId);
|
|
372
401
|
}
|
|
373
402
|
this.gracefulShutdownService.pushActiveInterceptors(execInterceptor);
|
|
374
403
|
// eslint-disable-next-line consistent-return
|
package/package.json
CHANGED
|
@@ -2,6 +2,9 @@ const assert = require('node:assert/strict');
|
|
|
2
2
|
const test = require('node:test');
|
|
3
3
|
const { createRequire } = require('node:module');
|
|
4
4
|
const { RmqContext, ServerRMQ } = require('@nestjs/microservices');
|
|
5
|
+
const { RpcProxy } = require('@nestjs/microservices/context/rpc-proxy');
|
|
6
|
+
const { RpcExceptionsHandler } = require('@nestjs/microservices/exceptions/rpc-exceptions-handler');
|
|
7
|
+
const { Logger } = require('@nestjs/common');
|
|
5
8
|
const { callModuleDestroyHook } = require('@nestjs/core/hooks/on-module-destroy.hook');
|
|
6
9
|
const { InstanceWrapper } = require('@nestjs/core/injector/instance-wrapper');
|
|
7
10
|
const { Subject } = require('rxjs');
|
|
@@ -49,6 +52,60 @@ test('종료 중 받은 미실행 RPC는 즉시 재전달하고 기존 요청의
|
|
|
49
52
|
assert.equal(shutdown.getActiveInterceptorsCount(), 0);
|
|
50
53
|
});
|
|
51
54
|
|
|
55
|
+
for (const scenario of [
|
|
56
|
+
{ name: '취소 실패', cancelFails: true, nackFails: false, duringRedelivery: false },
|
|
57
|
+
{ name: '반환 실패', cancelFails: false, nackFails: true, duringRedelivery: false },
|
|
58
|
+
{ name: '취소·반환 실패', cancelFails: true, nackFails: true, duringRedelivery: false },
|
|
59
|
+
{ name: '재전달 확인 중 종료와 취소 실패', cancelFails: true, nackFails: false, duringRedelivery: true },
|
|
60
|
+
]) {
|
|
61
|
+
test(`${scenario.name} 시에도 기존 Pod는 RPC 오류나 완료 응답을 보내지 않는다`, async t => {
|
|
62
|
+
t.mock.method(MsaLogger.prototype, 'debug', () => {});
|
|
63
|
+
t.mock.method(Logger.prototype, 'error', () => {});
|
|
64
|
+
const warning = t.mock.method(MsaLogger.prototype, 'warn', () => {});
|
|
65
|
+
const shutdown = new GracefulShutdownService();
|
|
66
|
+
shutdown.isApplicationExit = !scenario.duringRedelivery;
|
|
67
|
+
const calls = [];
|
|
68
|
+
const channel = {
|
|
69
|
+
cancel: async () => {
|
|
70
|
+
calls.push('cancel');
|
|
71
|
+
if (scenario.cancelFails) throw Error('channel closed during cancel');
|
|
72
|
+
},
|
|
73
|
+
nack: (_message, multiple, requeue) => {
|
|
74
|
+
calls.push(['nack', multiple, requeue]);
|
|
75
|
+
if (scenario.nackFails) throw Error('channel closed during nack');
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const { message, context } = createRequest(channel, scenario.duringRedelivery ? { 'x-delivery-count': 1 } : {});
|
|
79
|
+
const interceptor = new RPCInterceptor({ get: () => undefined }, shutdown);
|
|
80
|
+
if (scenario.duringRedelivery) {
|
|
81
|
+
const server = new GracefulServerRMQ({ queue: 'TEST', noAck: false });
|
|
82
|
+
shutdown.registerRmqServer(server);
|
|
83
|
+
t.mock.method(interceptor, 'isRedeliveryMessage', async () => {
|
|
84
|
+
await shutdown.onModuleDestroy();
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const next = { handle: t.mock.fn(() => { throw Error('실행하면 안 되는 신규 요청'); }) };
|
|
89
|
+
const handler = new RpcProxy().create(() => interceptor.intercept(context, next), new RpcExceptionsHandler());
|
|
90
|
+
const response = await handler(...context.getArgs());
|
|
91
|
+
const replies = [];
|
|
92
|
+
const subscription = new ServerRMQ({ queue: 'TEST', noAck: false }).send(response, value => replies.push(value));
|
|
93
|
+
t.after(() => subscription.unsubscribe());
|
|
94
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
95
|
+
assert.deepEqual(replies, []);
|
|
96
|
+
assert.deepEqual(calls, ['cancel', ['nack', false, true]]);
|
|
97
|
+
assert.equal(next.handle.mock.callCount(), 0);
|
|
98
|
+
assert.equal(shutdown.getActiveInterceptorsCount(), 0);
|
|
99
|
+
assert.equal(warning.mock.callCount(), 1);
|
|
100
|
+
const [logMessage, logInfo, logDetails] = warning.mock.calls[0].arguments;
|
|
101
|
+
assert.match(logMessage, /종료/);
|
|
102
|
+
assert.equal(logInfo.traceId, 'shutdown-test');
|
|
103
|
+
assert.deepEqual(logDetails.failures.map(item => item.operation),
|
|
104
|
+
[scenario.cancelFails ? 'cancel' : null, scenario.nackFails ? 'nack' : null].filter(Boolean));
|
|
105
|
+
assert.equal(message.properties.replyTo, 'reply-queue');
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
52
109
|
test('종료 전에 실행한 RPC는 계속 처리하고 종료 이후 도착한 메시지만 반환한다', async t => {
|
|
53
110
|
t.mock.method(MsaLogger.prototype, 'debug', () => {});
|
|
54
111
|
const shutdown = new GracefulShutdownService();
|