@yolo-croket-dev/core 1.10.0-jun.2-beta.1 → 1.10.1
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 +16 -0
- package/filters/grpc-exception.filter.js +2 -2
- package/functions/amqp-call-manager.function.js +7 -2
- package/functions/amqp-handler.function.js +66 -11
- package/functions/grpc-param-metadata.function.d.ts +1 -1
- package/functions/index.d.ts +1 -0
- package/functions/index.js +1 -0
- package/functions/rmq-diagnostics.function.d.ts +89 -0
- package/functions/rmq-diagnostics.function.js +300 -0
- package/interceptors/logging-service.interceptor.js +51 -14
- package/interceptors/rpc.interceptor.js +29 -7
- package/interfaces/exception.js +3 -3
- package/interfaces/http.d.ts +3 -2
- package/interfaces/log-level.enum.js +1 -1
- package/interfaces/msa-exception.d.ts +11 -1
- package/interfaces/msa-exception.js +6 -1
- package/modules/graceful-shutdown/graceful-shutdown.module.js +2 -2
- package/modules/graceful-shutdown/graceful-shutdown.rmq.d.ts +12 -4
- package/modules/graceful-shutdown/graceful-shutdown.rmq.js +79 -12
- package/modules/graceful-shutdown/graceful-shutdown.service.js +2 -2
- package/modules/redis.module.js +2 -2
- package/package.json +2 -2
- package/pipes/rpc-validation.pipe.js +2 -2
- package/tests/amqp-handler-timeout.test.cjs +47 -0
- package/tests/graceful-shutdown-rmq.test.cjs +102 -0
- package/tests/rmq-consumer-diagnostics.test.cjs +150 -0
- package/tests/rmq-diagnostics.test.cjs +177 -0
|
@@ -39,11 +39,22 @@ const common_1 = require("@nestjs/common");
|
|
|
39
39
|
const msa_logger_1 = __importStar(require("@yolo-croket-dev/msa-logger"));
|
|
40
40
|
const operators_1 = require("rxjs/operators");
|
|
41
41
|
const dd_trace_1 = __importDefault(require("dd-trace"));
|
|
42
|
+
const rmq_diagnostics_function_1 = require("../functions/rmq-diagnostics.function");
|
|
42
43
|
const redact_auth_data_function_1 = require("../functions/redact-auth-data.function");
|
|
43
44
|
const grpc_param_metadata_function_1 = require("../functions/grpc-param-metadata.function");
|
|
44
45
|
const { SERVER_NAME } = process.env;
|
|
45
46
|
const msaLogger = new msa_logger_1.default(msa_logger_1.ServerTypeEnum.SERVICE);
|
|
46
47
|
const MAX_CUT_LENGTH = 500;
|
|
48
|
+
function readErrorProperty(error, key) {
|
|
49
|
+
if (error === null || error === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
try {
|
|
52
|
+
return Reflect.get(Object(error), key);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
47
58
|
dd_trace_1.default.init();
|
|
48
59
|
let LoggingServiceInterceptor = class LoggingServiceInterceptor {
|
|
49
60
|
keyCutter(data) {
|
|
@@ -108,6 +119,7 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
|
|
|
108
119
|
pattern = content.pattern;
|
|
109
120
|
payload = context.switchToHttp().getRequest().payload;
|
|
110
121
|
}
|
|
122
|
+
const connection = isGrpc ? undefined : (0, rmq_diagnostics_function_1.getRmqDiagnosticSnapshot)(rmqContext.getChannelRef());
|
|
111
123
|
const pastParam = JSON.parse(JSON.stringify(param));
|
|
112
124
|
const pastFrom = pastParam.from || '-';
|
|
113
125
|
const pastFroms = pastParam.froms || [pastFrom];
|
|
@@ -171,22 +183,47 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
|
|
|
171
183
|
loggingSpan.finish();
|
|
172
184
|
}
|
|
173
185
|
})).pipe((0, operators_1.catchError)((error) => {
|
|
186
|
+
const diagnostic = connection ? (0, msa_logger_1.normalizeDiagnosticError)(error, 'service_error') : undefined;
|
|
187
|
+
const frame = diagnostic === null || diagnostic === void 0 ? void 0 : diagnostic.details.errors[0];
|
|
188
|
+
const subMessageValue = readErrorProperty(error, 'subMessage');
|
|
189
|
+
let subMessage = typeof subMessageValue === 'string' ? subMessageValue : undefined;
|
|
190
|
+
if (subMessage && diagnostic)
|
|
191
|
+
subMessage = (0, msa_logger_1.sanitizeDiagnosticText)(subMessage);
|
|
192
|
+
const messageValue = readErrorProperty(error, 'message');
|
|
193
|
+
const errorMessage = (frame === null || frame === void 0 ? void 0 : frame.message) || (typeof messageValue === 'string' ? messageValue : undefined)
|
|
194
|
+
|| (typeof error === 'string' ? error : undefined);
|
|
195
|
+
const nameValue = readErrorProperty(error, 'name');
|
|
196
|
+
const errorName = (frame === null || frame === void 0 ? void 0 : frame.name) || (typeof nameValue === 'string' ? nameValue : 'UNKNOWN_ERROR');
|
|
197
|
+
const stackValue = readErrorProperty(error, 'stack');
|
|
198
|
+
const errorStack = (frame === null || frame === void 0 ? void 0 : frame.stack) || (typeof stackValue === 'string' ? stackValue : undefined);
|
|
199
|
+
const statusValue = readErrorProperty(error, 'status');
|
|
200
|
+
const status = typeof statusValue === 'number' ? statusValue : 500;
|
|
201
|
+
const errOption = readErrorProperty(error, 'errOption');
|
|
174
202
|
if (loggingSpan) {
|
|
175
203
|
loggingSpan.setTag('error', true);
|
|
176
|
-
loggingSpan.setTag('error.message',
|
|
177
|
-
loggingSpan.setTag('error.status',
|
|
178
|
-
loggingSpan.setTag('error.subMessage',
|
|
179
|
-
loggingSpan.setTag('error.stack',
|
|
180
|
-
loggingSpan.setTag('error.type',
|
|
204
|
+
loggingSpan.setTag('error.message', errorMessage);
|
|
205
|
+
loggingSpan.setTag('error.status', status);
|
|
206
|
+
loggingSpan.setTag('error.subMessage', subMessage);
|
|
207
|
+
loggingSpan.setTag('error.stack', errorStack);
|
|
208
|
+
loggingSpan.setTag('error.type', errorName);
|
|
181
209
|
}
|
|
182
210
|
msaLogger.printExceptionFliterService({
|
|
211
|
+
diagnosticInfo: diagnostic ? {
|
|
212
|
+
...diagnostic.diagnosticInfo,
|
|
213
|
+
...(0, rmq_diagnostics_function_1.getRmqDiagnosticSnapshot)(rmqContext.getChannelRef()),
|
|
214
|
+
event: 'handler_failed',
|
|
215
|
+
messagePattern: (0, msa_logger_1.sanitizeDiagnosticText)(pattern).slice(0, 240),
|
|
216
|
+
elapsedMs: Date.now() - now,
|
|
217
|
+
startGeneration: connection === null || connection === void 0 ? void 0 : connection.generation,
|
|
218
|
+
startConnectionState: connection === null || connection === void 0 ? void 0 : connection.connectionState,
|
|
219
|
+
} : undefined,
|
|
183
220
|
traceId,
|
|
184
221
|
errInfo: {
|
|
185
|
-
errType:
|
|
186
|
-
message:
|
|
187
|
-
status
|
|
188
|
-
stack:
|
|
189
|
-
errOption
|
|
222
|
+
errType: subMessage ? errorMessage || errorName : errorName,
|
|
223
|
+
message: subMessage || errorMessage,
|
|
224
|
+
status,
|
|
225
|
+
stack: errorStack,
|
|
226
|
+
errOption,
|
|
190
227
|
},
|
|
191
228
|
messageBrokerInfo: {
|
|
192
229
|
messagePatten: pattern,
|
|
@@ -196,12 +233,12 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
|
|
|
196
233
|
from: pastFrom,
|
|
197
234
|
froms: pastFroms,
|
|
198
235
|
},
|
|
199
|
-
});
|
|
236
|
+
}, diagnostic === null || diagnostic === void 0 ? void 0 : diagnostic.details);
|
|
200
237
|
const elapsed = Date.now() - now;
|
|
201
238
|
msaLogger.printOutService({
|
|
202
239
|
traceId,
|
|
203
240
|
responseInfo: {
|
|
204
|
-
status
|
|
241
|
+
status,
|
|
205
242
|
ms: elapsed,
|
|
206
243
|
},
|
|
207
244
|
messageBrokerInfo: {
|
|
@@ -220,7 +257,7 @@ let LoggingServiceInterceptor = class LoggingServiceInterceptor {
|
|
|
220
257
|
}));
|
|
221
258
|
}
|
|
222
259
|
};
|
|
223
|
-
LoggingServiceInterceptor =
|
|
260
|
+
exports.LoggingServiceInterceptor = LoggingServiceInterceptor;
|
|
261
|
+
exports.LoggingServiceInterceptor = LoggingServiceInterceptor = __decorate([
|
|
224
262
|
(0, common_1.Injectable)()
|
|
225
263
|
], LoggingServiceInterceptor);
|
|
226
|
-
exports.LoggingServiceInterceptor = LoggingServiceInterceptor;
|
|
@@ -48,6 +48,7 @@ const functions_1 = require("../functions");
|
|
|
48
48
|
const exceptions_1 = require("../exceptions");
|
|
49
49
|
const context_promise_patch_1 = require("../functions/abort/context-promise-patch");
|
|
50
50
|
const graceful_shutdown_service_1 = require("../modules/graceful-shutdown/graceful-shutdown.service");
|
|
51
|
+
const rmq_diagnostics_function_1 = require("../functions/rmq-diagnostics.function");
|
|
51
52
|
const msaLogger = new msa_logger_1.default(msa_logger_1.ServerTypeEnum.SERVICE);
|
|
52
53
|
// const MAX_CUT_LENGTH = 500;
|
|
53
54
|
const MAX_LENGTH = 300;
|
|
@@ -154,6 +155,7 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
154
155
|
const arg = args[0];
|
|
155
156
|
const rmq = args[1];
|
|
156
157
|
const channel = rmq.getChannelRef();
|
|
158
|
+
const startConnection = (0, rmq_diagnostics_function_1.getRmqDiagnosticSnapshot)(channel);
|
|
157
159
|
const message = rmq.getMessage();
|
|
158
160
|
const now = Date.now();
|
|
159
161
|
const content = JSON.parse(message.content.toString());
|
|
@@ -206,7 +208,7 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
206
208
|
}
|
|
207
209
|
const noAck = message.fields.routingKey.includes('ASYNC');
|
|
208
210
|
let localErrorProcessor = async (err) => {
|
|
209
|
-
var _a;
|
|
211
|
+
var _a, _b;
|
|
210
212
|
const errMsg = err.response ? err.response : err.message;
|
|
211
213
|
const status = err.status ? err.status : 500;
|
|
212
214
|
const errType = err.errType;
|
|
@@ -278,10 +280,28 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
278
280
|
}
|
|
279
281
|
}
|
|
280
282
|
const sendMessage = Buffer.from(JSON.stringify(abstractAmqpHandlerData));
|
|
281
|
-
|
|
283
|
+
const diagnostic = startConnection
|
|
284
|
+
? (0, msa_logger_1.normalizeDiagnosticError)(err, isTimeoutError ? 'response_timeout' : 'service_error') : undefined;
|
|
285
|
+
const diagnosticInfo = diagnostic ? {
|
|
286
|
+
...diagnostic.diagnosticInfo,
|
|
287
|
+
...(0, rmq_diagnostics_function_1.getRmqDiagnosticSnapshot)(channel),
|
|
288
|
+
event: 'handler_failed',
|
|
289
|
+
messagePattern: (0, msa_logger_1.sanitizeDiagnosticText)(String(method)).slice(0, 240),
|
|
290
|
+
elapsedMs: duration,
|
|
291
|
+
startGeneration: startConnection === null || startConnection === void 0 ? void 0 : startConnection.generation,
|
|
292
|
+
startConnectionState: startConnection === null || startConnection === void 0 ? void 0 : startConnection.connectionState,
|
|
293
|
+
} : undefined;
|
|
294
|
+
if (!isAck || diagnostic) {
|
|
282
295
|
const currentContext = functions_1.AbortContext.getCurrentContext();
|
|
283
|
-
|
|
284
|
-
|
|
296
|
+
msaLogger.warn(isAck ? 'RPCInterceptor 오류 처리' : '에러 응답 전송 처리', {
|
|
297
|
+
traceId: execInterceptor.traceId,
|
|
298
|
+
diagnosticInfo,
|
|
299
|
+
}, diagnostic ? {
|
|
300
|
+
error: diagnostic.details,
|
|
301
|
+
reDeliveryCount,
|
|
302
|
+
staticRetryCount,
|
|
303
|
+
currentContext: (_a = currentContext === null || currentContext === void 0 ? void 0 : currentContext.requestId) !== null && _a !== void 0 ? _a : null,
|
|
304
|
+
} : {
|
|
285
305
|
error: {
|
|
286
306
|
message: err.message,
|
|
287
307
|
stack: err.stack,
|
|
@@ -293,11 +313,13 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
293
313
|
messagePattern: method,
|
|
294
314
|
currentContext: currentContext && currentContext.requestId ? currentContext.requestId : null,
|
|
295
315
|
});
|
|
316
|
+
}
|
|
317
|
+
if (!isAck) {
|
|
296
318
|
channel.sendToQueue(message.properties.replyTo, sendMessage, { correlationId: message.properties.correlationId });
|
|
297
319
|
}
|
|
298
320
|
const messageContent = message.content.toString();
|
|
299
321
|
/** 파일 객체 처리 */
|
|
300
|
-
(0, functions_1.reWriteFileValue)((
|
|
322
|
+
(0, functions_1.reWriteFileValue)((_b = messageContent === null || messageContent === void 0 ? void 0 : messageContent.data) === null || _b === void 0 ? void 0 : _b.file);
|
|
301
323
|
if (!noAck && !isAck) {
|
|
302
324
|
channel.ack(message);
|
|
303
325
|
execInterceptor.isMessageAck = true;
|
|
@@ -370,9 +392,9 @@ let RPCInterceptor = class RPCInterceptor {
|
|
|
370
392
|
})));
|
|
371
393
|
}
|
|
372
394
|
};
|
|
373
|
-
RPCInterceptor =
|
|
395
|
+
exports.RPCInterceptor = RPCInterceptor;
|
|
396
|
+
exports.RPCInterceptor = RPCInterceptor = __decorate([
|
|
374
397
|
(0, common_1.Injectable)(),
|
|
375
398
|
__metadata("design:paramtypes", [core_1.Reflector,
|
|
376
399
|
graceful_shutdown_service_1.GracefulShutdownService])
|
|
377
400
|
], RPCInterceptor);
|
|
378
|
-
exports.RPCInterceptor = RPCInterceptor;
|
package/interfaces/exception.js
CHANGED
|
@@ -18,9 +18,10 @@ var ErrActionTypeEnum;
|
|
|
18
18
|
ErrActionTypeEnum["LEGACY_SHOW_TOAST"] = "LEGACY_SHOW_TOAST";
|
|
19
19
|
ErrActionTypeEnum["FORCE_UPDATE"] = "FORCE_UPDATE";
|
|
20
20
|
ErrActionTypeEnum["UPDATE_ACCESS_TOKEN"] = "UPDATE_ACCESS_TOKEN";
|
|
21
|
-
})(ErrActionTypeEnum
|
|
21
|
+
})(ErrActionTypeEnum || (exports.ErrActionTypeEnum = ErrActionTypeEnum = {}));
|
|
22
22
|
class ErrAction {
|
|
23
23
|
}
|
|
24
|
+
exports.ErrAction = ErrAction;
|
|
24
25
|
__decorate([
|
|
25
26
|
(0, swagger_1.ApiProperty)({
|
|
26
27
|
description: '액션 타입',
|
|
@@ -57,9 +58,9 @@ __decorate([
|
|
|
57
58
|
}),
|
|
58
59
|
__metadata("design:type", String)
|
|
59
60
|
], ErrAction.prototype, "webViewUrl", void 0);
|
|
60
|
-
exports.ErrAction = ErrAction;
|
|
61
61
|
class ErrInfo {
|
|
62
62
|
}
|
|
63
|
+
exports.ErrInfo = ErrInfo;
|
|
63
64
|
__decorate([
|
|
64
65
|
(0, swagger_1.ApiProperty)({
|
|
65
66
|
description: '에러 종류 식별자',
|
|
@@ -102,4 +103,3 @@ __decorate([
|
|
|
102
103
|
}),
|
|
103
104
|
__metadata("design:type", Number)
|
|
104
105
|
], ErrInfo.prototype, "retryAfterSeconds", void 0);
|
|
105
|
-
exports.ErrInfo = ErrInfo;
|
package/interfaces/http.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Method } from 'axios';
|
|
2
1
|
import { ErrInfo } from './exception';
|
|
2
|
+
type RequestLogMethod = 'GET' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'POST' | 'PUT' | 'PATCH' | 'PURGE' | 'LINK' | 'UNLINK';
|
|
3
3
|
export interface ExternalTokenValue {
|
|
4
4
|
provider: string;
|
|
5
5
|
client_id: string;
|
|
@@ -16,7 +16,7 @@ export interface DeviceRequest {
|
|
|
16
16
|
export interface RequestLog {
|
|
17
17
|
query?: unknown;
|
|
18
18
|
body?: unknown;
|
|
19
|
-
method:
|
|
19
|
+
method: RequestLogMethod | Lowercase<RequestLogMethod>;
|
|
20
20
|
endpoint: string;
|
|
21
21
|
}
|
|
22
22
|
/** 최신 에러 정보 */
|
|
@@ -98,3 +98,4 @@ export interface ExternalResponseNormalize<T> {
|
|
|
98
98
|
export interface ResponseAction {
|
|
99
99
|
result: boolean;
|
|
100
100
|
}
|
|
101
|
+
export {};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { HttpException } from '@nestjs/common';
|
|
2
2
|
import { LogLevelEnum } from './log-level.enum';
|
|
3
3
|
import type { ErrAction } from './exception';
|
|
4
|
+
export declare enum MsaExceptionMessageAckBehaviorEnum {
|
|
5
|
+
ACK = "ACK",
|
|
6
|
+
NACK = "NACK"
|
|
7
|
+
}
|
|
4
8
|
export interface MsaExceptionOption {
|
|
5
9
|
/**
|
|
6
10
|
* 로그 레벨
|
|
@@ -8,6 +12,12 @@ export interface MsaExceptionOption {
|
|
|
8
12
|
* - 기본값은 ERROR 입니다.
|
|
9
13
|
* */
|
|
10
14
|
logErrorLevel?: LogLevelEnum;
|
|
15
|
+
/**
|
|
16
|
+
* 메시지 consumer 실패 시 ACK/NACK override 옵션
|
|
17
|
+
* - 현재는 amqp-rabbit-mq subscribe consumer에서만 해석합니다.
|
|
18
|
+
* - option이 없으면 consumer handler의 기본 errorBehavior를 따릅니다.
|
|
19
|
+
*/
|
|
20
|
+
messageAckBehavior?: MsaExceptionMessageAckBehaviorEnum;
|
|
11
21
|
}
|
|
12
22
|
/**
|
|
13
23
|
* @author jun
|
|
@@ -27,7 +37,7 @@ export interface MsaException {
|
|
|
27
37
|
errOption?: MsaExceptionOption;
|
|
28
38
|
}
|
|
29
39
|
/** HttpException과 MsaException을 결합한 타입 */
|
|
30
|
-
export
|
|
40
|
+
export type MsaHttpException = MsaException & HttpException;
|
|
31
41
|
/**
|
|
32
42
|
* @description MsaException 타입 가드
|
|
33
43
|
* @param error 검사할 에러 객체
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isMsaException = void 0;
|
|
3
|
+
exports.isMsaException = exports.MsaExceptionMessageAckBehaviorEnum = void 0;
|
|
4
4
|
const common_1 = require("@nestjs/common");
|
|
5
|
+
var MsaExceptionMessageAckBehaviorEnum;
|
|
6
|
+
(function (MsaExceptionMessageAckBehaviorEnum) {
|
|
7
|
+
MsaExceptionMessageAckBehaviorEnum["ACK"] = "ACK";
|
|
8
|
+
MsaExceptionMessageAckBehaviorEnum["NACK"] = "NACK";
|
|
9
|
+
})(MsaExceptionMessageAckBehaviorEnum || (exports.MsaExceptionMessageAckBehaviorEnum = MsaExceptionMessageAckBehaviorEnum = {}));
|
|
5
10
|
/**
|
|
6
11
|
* @description MsaException 타입 가드
|
|
7
12
|
* @param error 검사할 에러 객체
|
|
@@ -48,7 +48,8 @@ let GracefulShutdownModule = class GracefulShutdownModule {
|
|
|
48
48
|
msaLogger.info('GracefulShutdownService.onModuleDestroy: all interceptors finished, shutting down', { traceId: 'ON_MODULE_DESTROY' });
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
-
GracefulShutdownModule =
|
|
51
|
+
exports.GracefulShutdownModule = GracefulShutdownModule;
|
|
52
|
+
exports.GracefulShutdownModule = GracefulShutdownModule = __decorate([
|
|
52
53
|
(0, common_1.Global)(),
|
|
53
54
|
(0, common_1.Module)({
|
|
54
55
|
providers: [graceful_shutdown_service_1.GracefulShutdownService],
|
|
@@ -56,4 +57,3 @@ GracefulShutdownModule = __decorate([
|
|
|
56
57
|
}),
|
|
57
58
|
__metadata("design:paramtypes", [graceful_shutdown_service_1.GracefulShutdownService])
|
|
58
59
|
], GracefulShutdownModule);
|
|
59
|
-
exports.GracefulShutdownModule = GracefulShutdownModule;
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { ServerRMQ } from '@nestjs/microservices';
|
|
2
|
+
import type { RmqOptions } from '@nestjs/microservices';
|
|
3
|
+
import type { Channel } from 'amqp-connection-manager';
|
|
4
|
+
import type { ConsumeMessage } from 'amqplib';
|
|
5
|
+
export type GracefulRmqOptions = RmqOptions['options'] & {
|
|
5
6
|
waitingEndingHandlersTimeoutMs?: number;
|
|
6
7
|
waitingEndingHandlersIntervalMs?: number;
|
|
7
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* @author jun
|
|
11
|
+
* @description RabbitMQ 연결 식별 정보와 graceful shutdown을 제공하는 RMQ 서버
|
|
12
|
+
* @process 연결 옵션에 추적 가능한 connection_name을 추가한 뒤 메시지 종료를 보장한다.
|
|
13
|
+
*/
|
|
8
14
|
export declare class GracefulServerRMQ extends ServerRMQ {
|
|
9
15
|
protected readonly options: GracefulRmqOptions;
|
|
16
|
+
private readonly observation?;
|
|
10
17
|
protected runningMessages: number;
|
|
11
18
|
protected closing: boolean;
|
|
12
19
|
protected consumerTag: string | null;
|
|
13
20
|
protected readonly waitingEndingHandlersTimeoutMs: number;
|
|
14
21
|
protected readonly waitingEndingHandlersIntervalMs: number;
|
|
15
22
|
constructor(options: GracefulRmqOptions);
|
|
23
|
+
createClient(): any;
|
|
16
24
|
setupChannel(channel: Channel, callback: () => void): Promise<void>;
|
|
17
25
|
protected waitingHandlers(): Promise<void>;
|
|
18
26
|
handleMessage(message: ConsumeMessage, channel: Channel): Promise<void>;
|
|
@@ -1,27 +1,88 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.GracefulServerRMQ = void 0;
|
|
4
7
|
const microservices_1 = require("@nestjs/microservices");
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
5
9
|
const node_util_1 = require("node:util");
|
|
10
|
+
const rmq_diagnostics_function_1 = require("../../functions/rmq-diagnostics.function");
|
|
6
11
|
const sleep = (0, node_util_1.promisify)(setTimeout);
|
|
7
12
|
const RMQ_DEFAULT_WAITING_ENDING_HANDLERS_TIMEOUT_MS = 5000;
|
|
8
13
|
const RMQ_DEFAULT_WAITING_ENDING_HANDLERS_INTERVAL_MS = 500;
|
|
14
|
+
const RMQ_UNKNOWN_CONNECTION_IDENTITY = 'unknown';
|
|
15
|
+
function createRmqConnectionName(queue) {
|
|
16
|
+
const environment = process.env.ENV || process.env.NODE_ENV || RMQ_UNKNOWN_CONNECTION_IDENTITY;
|
|
17
|
+
const owner = process.env.RMQ_OWNER || process.env.USER || process.env.USERNAME || RMQ_UNKNOWN_CONNECTION_IDENTITY;
|
|
18
|
+
return [
|
|
19
|
+
queue || RMQ_UNKNOWN_CONNECTION_IDENTITY,
|
|
20
|
+
environment,
|
|
21
|
+
owner,
|
|
22
|
+
os_1.default.hostname(),
|
|
23
|
+
`pid-${process.pid}`,
|
|
24
|
+
].join('/');
|
|
25
|
+
}
|
|
26
|
+
function addRmqConnectionName(options) {
|
|
27
|
+
var _a;
|
|
28
|
+
const connectionOptions = (_a = options.socketOptions) === null || _a === void 0 ? void 0 : _a.connectionOptions;
|
|
29
|
+
const configuredClientProperties = connectionOptions === null || connectionOptions === void 0 ? void 0 : connectionOptions.clientProperties;
|
|
30
|
+
const clientProperties = configuredClientProperties
|
|
31
|
+
&& typeof configuredClientProperties === 'object'
|
|
32
|
+
&& !Array.isArray(configuredClientProperties)
|
|
33
|
+
? configuredClientProperties
|
|
34
|
+
: {};
|
|
35
|
+
const configuredConnectionName = clientProperties.connection_name;
|
|
36
|
+
const connectionName = typeof configuredConnectionName === 'string' && configuredConnectionName.trim()
|
|
37
|
+
? configuredConnectionName
|
|
38
|
+
: createRmqConnectionName(options.queue);
|
|
39
|
+
return {
|
|
40
|
+
...options,
|
|
41
|
+
socketOptions: {
|
|
42
|
+
...options.socketOptions,
|
|
43
|
+
connectionOptions: {
|
|
44
|
+
...connectionOptions,
|
|
45
|
+
clientProperties: {
|
|
46
|
+
...clientProperties,
|
|
47
|
+
connection_name: connectionName,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* @author jun
|
|
55
|
+
* @description RabbitMQ 연결 식별 정보와 graceful shutdown을 제공하는 RMQ 서버
|
|
56
|
+
* @process 연결 옵션에 추적 가능한 connection_name을 추가한 뒤 메시지 종료를 보장한다.
|
|
57
|
+
*/
|
|
9
58
|
class GracefulServerRMQ extends microservices_1.ServerRMQ {
|
|
10
59
|
constructor(options) {
|
|
11
|
-
|
|
12
|
-
|
|
60
|
+
const observation = (0, rmq_diagnostics_function_1.isRmqDiagnosticsEnabled)() ? (0, rmq_diagnostics_function_1.createRmqObserver)(options, 'consumer') : undefined;
|
|
61
|
+
const optionsWithConnectionName = observation
|
|
62
|
+
? (0, rmq_diagnostics_function_1.withRmqConnectionName)(options, observation) : addRmqConnectionName(options);
|
|
63
|
+
super(optionsWithConnectionName);
|
|
13
64
|
this.runningMessages = 0;
|
|
14
65
|
this.closing = false;
|
|
15
66
|
this.consumerTag = null;
|
|
67
|
+
this.options = optionsWithConnectionName;
|
|
68
|
+
this.observation = observation;
|
|
16
69
|
this.waitingEndingHandlersTimeoutMs = this.getOptionsProp(this.options, 'waitingEndingHandlersTimeoutMs')
|
|
17
70
|
|| RMQ_DEFAULT_WAITING_ENDING_HANDLERS_TIMEOUT_MS;
|
|
18
71
|
this.waitingEndingHandlersIntervalMs = this.getOptionsProp(this.options, 'waitingEndingHandlersIntervalMs')
|
|
19
72
|
|| RMQ_DEFAULT_WAITING_ENDING_HANDLERS_INTERVAL_MS;
|
|
20
73
|
}
|
|
74
|
+
createClient() {
|
|
75
|
+
var _a;
|
|
76
|
+
const client = super.createClient();
|
|
77
|
+
(_a = this.observation) === null || _a === void 0 ? void 0 : _a.observe(client);
|
|
78
|
+
return client;
|
|
79
|
+
}
|
|
21
80
|
async setupChannel(channel, callback) {
|
|
22
81
|
if (this.closing) {
|
|
23
82
|
return;
|
|
24
83
|
}
|
|
84
|
+
if (this.observation)
|
|
85
|
+
(0, rmq_diagnostics_function_1.registerRmqObservation)(channel, this.observation);
|
|
25
86
|
if (!this.queueOptions.noAssert) {
|
|
26
87
|
await channel.assertQueue(this.queue, this.queueOptions);
|
|
27
88
|
}
|
|
@@ -42,18 +103,24 @@ class GracefulServerRMQ extends microservices_1.ServerRMQ {
|
|
|
42
103
|
});
|
|
43
104
|
}
|
|
44
105
|
async close() {
|
|
106
|
+
var _a;
|
|
45
107
|
this.closing = true;
|
|
46
|
-
|
|
47
|
-
|
|
108
|
+
try {
|
|
109
|
+
if (this.channel) {
|
|
110
|
+
await this.channel.removeSetup(undefined, (channel) => channel.cancel(this.consumerTag));
|
|
111
|
+
}
|
|
112
|
+
this.consumerTag = null;
|
|
113
|
+
await Promise.race([
|
|
114
|
+
this.waitingHandlers(),
|
|
115
|
+
this.waitingEndingHandlersTimeoutMs > 0
|
|
116
|
+
&& sleep(this.waitingEndingHandlersTimeoutMs),
|
|
117
|
+
]);
|
|
118
|
+
this.runningMessages = 0;
|
|
119
|
+
await super.close();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
(_a = this.observation) === null || _a === void 0 ? void 0 : _a.close();
|
|
48
123
|
}
|
|
49
|
-
this.consumerTag = null;
|
|
50
|
-
await Promise.race([
|
|
51
|
-
this.waitingHandlers(),
|
|
52
|
-
this.waitingEndingHandlersTimeoutMs > 0
|
|
53
|
-
&& sleep(this.waitingEndingHandlersTimeoutMs),
|
|
54
|
-
]);
|
|
55
|
-
this.runningMessages = 0;
|
|
56
|
-
super.close();
|
|
57
124
|
}
|
|
58
125
|
}
|
|
59
126
|
exports.GracefulServerRMQ = GracefulServerRMQ;
|
|
@@ -133,7 +133,7 @@ let GracefulShutdownService = class GracefulShutdownService {
|
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
};
|
|
136
|
-
GracefulShutdownService =
|
|
136
|
+
exports.GracefulShutdownService = GracefulShutdownService;
|
|
137
|
+
exports.GracefulShutdownService = GracefulShutdownService = __decorate([
|
|
137
138
|
(0, common_1.Injectable)()
|
|
138
139
|
], GracefulShutdownService);
|
|
139
|
-
exports.GracefulShutdownService = GracefulShutdownService;
|
package/modules/redis.module.js
CHANGED
|
@@ -15,10 +15,10 @@ const redisProvider = {
|
|
|
15
15
|
};
|
|
16
16
|
let RedisModule = class RedisModule {
|
|
17
17
|
};
|
|
18
|
-
RedisModule =
|
|
18
|
+
exports.RedisModule = RedisModule;
|
|
19
|
+
exports.RedisModule = RedisModule = __decorate([
|
|
19
20
|
(0, common_1.Module)({
|
|
20
21
|
providers: [redisProvider],
|
|
21
22
|
exports: [redisProvider],
|
|
22
23
|
})
|
|
23
24
|
], RedisModule);
|
|
24
|
-
exports.RedisModule = RedisModule;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolo-croket-dev/core",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"author": "Yolo Co., Ltd.",
|
|
5
5
|
"description": "Yolo commonization module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@yolo-croket-dev/logger": "^1.0.15",
|
|
28
|
-
"@yolo-croket-dev/msa-logger": "
|
|
28
|
+
"@yolo-croket-dev/msa-logger": "3.5.3",
|
|
29
29
|
"@yolo-croket-dev/redis": "^1.0.25",
|
|
30
30
|
"axios": "^1.1.3",
|
|
31
31
|
"randomstring": "^1.2.2",
|
|
@@ -36,8 +36,8 @@ let ValidationPipe = class ValidationPipe {
|
|
|
36
36
|
return value;
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
-
ValidationPipe =
|
|
39
|
+
exports.ValidationPipe = ValidationPipe;
|
|
40
|
+
exports.ValidationPipe = ValidationPipe = __decorate([
|
|
40
41
|
(0, common_1.Injectable)(),
|
|
41
42
|
__metadata("design:paramtypes", [Object])
|
|
42
43
|
], ValidationPipe);
|
|
43
|
-
exports.ValidationPipe = ValidationPipe;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const { Observable } = require('rxjs');
|
|
4
|
+
|
|
5
|
+
const { AmqpHandler, HttpExceptionCustom } = require('../functions/amqp-handler.function.js');
|
|
6
|
+
|
|
7
|
+
test('param.timeout이 지나면 AMQP Observable 구독을 해제한다', async () => {
|
|
8
|
+
let isUnsubscribed = false;
|
|
9
|
+
const client = {
|
|
10
|
+
send() {
|
|
11
|
+
return new Observable(() => () => {
|
|
12
|
+
isUnsubscribed = true;
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
await assert.rejects(
|
|
18
|
+
AmqpHandler(client, 'test.timeout', {
|
|
19
|
+
payload: {},
|
|
20
|
+
param: {
|
|
21
|
+
traceId: 'trace-id',
|
|
22
|
+
timeout: 5,
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
(error) => {
|
|
26
|
+
assert.equal(error instanceof HttpExceptionCustom, true);
|
|
27
|
+
assert.equal(error.errInfo.type, 'TIMEOUT_ERROR');
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
assert.equal(isUnsubscribed, true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('응답이 먼저 완료되면 기존 AMQP 응답을 반환한다', async () => {
|
|
36
|
+
const client = {
|
|
37
|
+
send() {
|
|
38
|
+
return new Observable((subscriber) => {
|
|
39
|
+
subscriber.next({ isError: false, data: { ok: true } });
|
|
40
|
+
subscriber.complete();
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const result = await AmqpHandler(client, 'test.success', { payload: {}, param: { traceId: 'trace-id', timeout: 50 } });
|
|
46
|
+
assert.deepEqual(result, { data: { ok: true } });
|
|
47
|
+
});
|