@yolo-croket-dev/core 1.7.25-slowquery.13 → 1.7.25-slowquery.2
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.
|
@@ -44,7 +44,6 @@ const rxjs_1 = require("rxjs");
|
|
|
44
44
|
const operators_1 = require("rxjs/operators");
|
|
45
45
|
const msa_logger_1 = __importStar(require("@yolo-croket-dev/msa-logger"));
|
|
46
46
|
const amqplib_1 = __importDefault(require("amqplib"));
|
|
47
|
-
const abortable_promise_1 = require("@xuchaoqian/abortable-promise");
|
|
48
47
|
// import Logger from '@yolo-croket-dev/logger';
|
|
49
48
|
const functions_1 = require("../functions");
|
|
50
49
|
const exceptions_1 = require("../exceptions");
|
|
@@ -180,16 +179,7 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
180
179
|
const arg = args[0];
|
|
181
180
|
const rmq = args[1];
|
|
182
181
|
// 메시지에서 AbortController 가져오기
|
|
183
|
-
|
|
184
|
-
const abortController = new AbortController();
|
|
185
|
-
const handler = context.getHandler();
|
|
186
|
-
console.log('args: ', args);
|
|
187
|
-
console.log('context: ', context);
|
|
188
|
-
const abortablePromise = new abortable_promise_1.AbortablePromise((...args) => handler(...args), abortController.signal);
|
|
189
|
-
console.log('abortablePromise: ', abortablePromise);
|
|
190
|
-
// (context as any).handler = abortablePromise;
|
|
191
|
-
console.log('handler: ', handler);
|
|
192
|
-
console.log('context: ', context);
|
|
182
|
+
const abortController = rmq.args[0].abortController;
|
|
193
183
|
// const messageId = (rmq as any).messageId;
|
|
194
184
|
// if (!abortController) {
|
|
195
185
|
// // AbortController가 없으면 기본 처리
|
|
@@ -360,11 +350,23 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
360
350
|
return next
|
|
361
351
|
.handle()
|
|
362
352
|
.pipe(
|
|
353
|
+
// AbortController 신호 감지
|
|
354
|
+
(0, operators_1.takeUntil)(new rxjs_1.Observable(subscriber => {
|
|
355
|
+
const abortHandler = () => {
|
|
356
|
+
console.log('RPC Interceptor abortHandler called');
|
|
357
|
+
subscriber.next();
|
|
358
|
+
subscriber.complete();
|
|
359
|
+
};
|
|
360
|
+
abortController.signal.addEventListener('abort', abortHandler, { once: true });
|
|
361
|
+
// 정리 함수
|
|
362
|
+
return () => {
|
|
363
|
+
abortController.signal.removeEventListener('abort', abortHandler);
|
|
364
|
+
};
|
|
365
|
+
})),
|
|
363
366
|
// timeout(1000 * 60 * 29), // 30분 타임아웃
|
|
364
|
-
(0, operators_1.timeout)(1000 *
|
|
367
|
+
(0, operators_1.timeout)(1000 * 30), // 30분 타임아웃
|
|
365
368
|
(0, operators_1.tap)({
|
|
366
369
|
next: async (val) => {
|
|
367
|
-
console.log('RPC Interceptor next called');
|
|
368
370
|
val.isError = false;
|
|
369
371
|
// const duration = Date.now() - now;
|
|
370
372
|
// logger.debug(`<= (success:${traceId}): ${val.message}`, {
|
|
@@ -392,6 +394,7 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
392
394
|
if (isTimeoutError) {
|
|
393
395
|
if (abortController) {
|
|
394
396
|
abortController.abort();
|
|
397
|
+
throw new Error('Message aborted due to timeout');
|
|
395
398
|
}
|
|
396
399
|
msaLogger.error('[타임 아웃] 메시지 재전송 처리', { traceId: execInterceptor.traceId }, {
|
|
397
400
|
error: {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
1
3
|
import { RmqOptions, ServerRMQ } from '@nestjs/microservices';
|
|
2
4
|
import { Channel } from 'amqp-connection-manager';
|
|
3
5
|
import { ConsumeMessage } from 'amqplib';
|
|
@@ -12,9 +14,14 @@ export declare class GracefulServerRMQ extends ServerRMQ {
|
|
|
12
14
|
protected consumerTag: string | null;
|
|
13
15
|
protected readonly waitingEndingHandlersTimeoutMs: number;
|
|
14
16
|
protected readonly waitingEndingHandlersIntervalMs: number;
|
|
17
|
+
protected messageAbortControllers: Map<string, AbortController>;
|
|
18
|
+
protected messageTimeouts: Map<string, NodeJS.Timeout>;
|
|
15
19
|
constructor(options: GracefulRmqOptions);
|
|
16
20
|
setupChannel(channel: Channel, callback: () => void): Promise<void>;
|
|
17
21
|
protected waitingHandlers(): Promise<void>;
|
|
18
22
|
handleMessage(message: ConsumeMessage, channel: Channel): Promise<void>;
|
|
23
|
+
private cleanupMessage;
|
|
24
|
+
abortMessage(messageId: string): void;
|
|
25
|
+
abortAllMessages(): void;
|
|
19
26
|
close(): Promise<void>;
|
|
20
27
|
}
|
|
@@ -6,6 +6,8 @@ const node_util_1 = require("node:util");
|
|
|
6
6
|
const sleep = (0, node_util_1.promisify)(setTimeout);
|
|
7
7
|
const RMQ_DEFAULT_WAITING_ENDING_HANDLERS_TIMEOUT_MS = 5000;
|
|
8
8
|
const RMQ_DEFAULT_WAITING_ENDING_HANDLERS_INTERVAL_MS = 500;
|
|
9
|
+
// const RMQ_DEFAULT_MESSAGE_TIMEOUT_MS = 1000 * 60 * 29; // 29분
|
|
10
|
+
const RMQ_DEFAULT_MESSAGE_TIMEOUT_MS = 1000 * 2; // 2분
|
|
9
11
|
class GracefulServerRMQ extends microservices_1.ServerRMQ {
|
|
10
12
|
constructor(options) {
|
|
11
13
|
super(options);
|
|
@@ -13,6 +15,10 @@ class GracefulServerRMQ extends microservices_1.ServerRMQ {
|
|
|
13
15
|
this.runningMessages = 0;
|
|
14
16
|
this.closing = false;
|
|
15
17
|
this.consumerTag = null;
|
|
18
|
+
// 메시지별 AbortController 관리
|
|
19
|
+
this.messageAbortControllers = new Map();
|
|
20
|
+
// eslint-disable-next-line no-undef
|
|
21
|
+
this.messageTimeouts = new Map();
|
|
16
22
|
this.waitingEndingHandlersTimeoutMs = this.getOptionsProp(this.options, 'waitingEndingHandlersTimeoutMs')
|
|
17
23
|
|| RMQ_DEFAULT_WAITING_ENDING_HANDLERS_TIMEOUT_MS;
|
|
18
24
|
this.waitingEndingHandlersIntervalMs = this.getOptionsProp(this.options, 'waitingEndingHandlersIntervalMs')
|
|
@@ -36,13 +42,91 @@ class GracefulServerRMQ extends microservices_1.ServerRMQ {
|
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
async handleMessage(message, channel) {
|
|
45
|
+
const messageId = message.properties.messageId || message.properties.correlationId || `msg_${Date.now()}_${Math.random()}`;
|
|
46
|
+
// AbortController 생성
|
|
47
|
+
const abortController = new AbortController();
|
|
48
|
+
this.messageAbortControllers.set(messageId, abortController);
|
|
49
|
+
// 타임아웃 설정
|
|
50
|
+
const timeoutId = setTimeout(() => {
|
|
51
|
+
console.log(`Message timeout: ${messageId}`);
|
|
52
|
+
abortController.abort();
|
|
53
|
+
this.cleanupMessage(messageId);
|
|
54
|
+
}, RMQ_DEFAULT_MESSAGE_TIMEOUT_MS);
|
|
55
|
+
this.messageTimeouts.set(messageId, timeoutId);
|
|
39
56
|
this.runningMessages += 1;
|
|
40
|
-
|
|
57
|
+
try {
|
|
58
|
+
// AbortController를 메시지에 추가
|
|
59
|
+
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
|
|
60
|
+
message.abortController = abortController;
|
|
61
|
+
// eslint-disable-next-line no-param-reassign
|
|
62
|
+
message.messageId = messageId;
|
|
63
|
+
// abort 상태 확인
|
|
64
|
+
if (abortController.signal.aborted) {
|
|
65
|
+
console.log(`Message already aborted: ${messageId}`);
|
|
66
|
+
this.cleanupMessage(messageId);
|
|
67
|
+
this.runningMessages -= 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Promise.race를 사용하여 abort와 handleMessage 경쟁
|
|
71
|
+
const handleMessagePromise = super.handleMessage(message, channel);
|
|
72
|
+
const abortPromise = new Promise((_, reject) => {
|
|
73
|
+
// 1초마다 abort 상태를 확인
|
|
74
|
+
const interval = setInterval(() => {
|
|
75
|
+
if (abortController.signal.aborted) {
|
|
76
|
+
reject(new Error(`Message aborted: ${messageId}`));
|
|
77
|
+
clearInterval(interval);
|
|
78
|
+
}
|
|
79
|
+
}, 1000);
|
|
80
|
+
});
|
|
81
|
+
// 둘 중 하나가 먼저 완료되면 그 결과를 반환
|
|
82
|
+
await Promise.race([
|
|
83
|
+
handleMessagePromise,
|
|
84
|
+
abortPromise,
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.message.includes('Message aborted')) {
|
|
89
|
+
console.log(`Message processing aborted: ${messageId}`);
|
|
90
|
+
// abort로 인한 에러는 정상적으로 처리된 것으로 간주
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
this.cleanupMessage(messageId);
|
|
41
98
|
this.runningMessages -= 1;
|
|
42
|
-
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
cleanupMessage(messageId) {
|
|
102
|
+
// 타임아웃 정리
|
|
103
|
+
const timeoutId = this.messageTimeouts.get(messageId);
|
|
104
|
+
if (timeoutId) {
|
|
105
|
+
clearTimeout(timeoutId);
|
|
106
|
+
this.messageTimeouts.delete(messageId);
|
|
107
|
+
}
|
|
108
|
+
// AbortController 정리
|
|
109
|
+
this.messageAbortControllers.delete(messageId);
|
|
110
|
+
}
|
|
111
|
+
// 특정 메시지 강제 중단
|
|
112
|
+
abortMessage(messageId) {
|
|
113
|
+
const abortController = this.messageAbortControllers.get(messageId);
|
|
114
|
+
if (abortController) {
|
|
115
|
+
abortController.abort();
|
|
116
|
+
this.cleanupMessage(messageId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 모든 실행 중인 메시지 중단
|
|
120
|
+
abortAllMessages() {
|
|
121
|
+
for (const [messageId, abortController,] of this.messageAbortControllers) {
|
|
122
|
+
abortController.abort();
|
|
123
|
+
this.cleanupMessage(messageId);
|
|
124
|
+
}
|
|
43
125
|
}
|
|
44
126
|
async close() {
|
|
45
127
|
this.closing = true;
|
|
128
|
+
// 모든 실행 중인 메시지 중단
|
|
129
|
+
this.abortAllMessages();
|
|
46
130
|
if (this.channel) {
|
|
47
131
|
await this.channel.removeSetup(undefined, (channel) => channel.cancel(this.consumerTag));
|
|
48
132
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolo-croket-dev/core",
|
|
3
|
-
"version": "1.7.25-slowquery.
|
|
3
|
+
"version": "1.7.25-slowquery.2",
|
|
4
4
|
"author": "Yolo Co., Ltd.",
|
|
5
5
|
"description": "Yolo commonization module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
"license": "UNLICENSED",
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@nestjs/common": "^8.2.3",
|
|
18
|
-
"@xuchaoqian/abortable-promise": "^1.11.2",
|
|
19
18
|
"@yolo-croket-dev/logger": "^1.0.15",
|
|
20
19
|
"@yolo-croket-dev/msa-logger": "^3.0.4",
|
|
21
20
|
"@yolo-croket-dev/redis": "^1.0.23",
|