외부 DB 연결 끊김 오류 해결
This commit is contained in:
@@ -113,6 +113,7 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
|
||||
lastUsedAt: Date;
|
||||
activeConnections = 0;
|
||||
maxConnections: number;
|
||||
private isPoolClosed = false;
|
||||
|
||||
constructor(config: ExternalDbConnection) {
|
||||
this.connectionId = config.id!;
|
||||
@@ -131,6 +132,9 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
|
||||
waitForConnections: true,
|
||||
queueLimit: 0,
|
||||
connectTimeout: (config.connection_timeout || 30) * 1000,
|
||||
// 연결 유지 및 자동 재연결 설정
|
||||
enableKeepAlive: true,
|
||||
keepAliveInitialDelay: 10000, // 10초마다 keep-alive 패킷 전송
|
||||
ssl:
|
||||
config.ssl_enabled === "Y" ? { rejectUnauthorized: false } : undefined,
|
||||
});
|
||||
@@ -149,15 +153,46 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
|
||||
`[${this.dbType.toUpperCase()}] 연결 반환 (${this.activeConnections}/${this.maxConnections})`
|
||||
);
|
||||
});
|
||||
|
||||
// 연결 오류 이벤트 처리
|
||||
this.pool.on("error", (err) => {
|
||||
logger.error(`[${this.dbType.toUpperCase()}] 연결 풀 오류:`, err);
|
||||
// 연결이 닫힌 경우 플래그 설정
|
||||
if (err.message.includes("closed state")) {
|
||||
this.isPoolClosed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async query(sql: string, params?: any[]): Promise<any> {
|
||||
this.lastUsedAt = new Date();
|
||||
const [rows] = await this.pool.execute(sql, params);
|
||||
return rows;
|
||||
|
||||
// 연결 풀이 닫힌 상태인지 확인
|
||||
if (this.isPoolClosed) {
|
||||
throw new Error("연결 풀이 닫힌 상태입니다. 재연결이 필요합니다.");
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows] = await this.pool.execute(sql, params);
|
||||
return rows;
|
||||
} catch (error: any) {
|
||||
// 연결 닫힘 오류 감지
|
||||
if (
|
||||
error.message.includes("closed state") ||
|
||||
error.code === "PROTOCOL_CONNECTION_LOST" ||
|
||||
error.code === "ECONNRESET"
|
||||
) {
|
||||
this.isPoolClosed = true;
|
||||
logger.warn(
|
||||
`[${this.dbType.toUpperCase()}] 연결 끊김 감지 (ID: ${this.connectionId})`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
this.isPoolClosed = true;
|
||||
await this.pool.end();
|
||||
logger.info(
|
||||
`[${this.dbType.toUpperCase()}] 연결 풀 종료 (ID: ${this.connectionId})`
|
||||
@@ -165,6 +200,10 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
|
||||
}
|
||||
|
||||
isHealthy(): boolean {
|
||||
// 연결 풀이 닫혔으면 비정상
|
||||
if (this.isPoolClosed) {
|
||||
return false;
|
||||
}
|
||||
return this.activeConnections < this.maxConnections;
|
||||
}
|
||||
}
|
||||
@@ -230,9 +269,11 @@ export class ExternalDbConnectionPoolService {
|
||||
): Promise<ConnectionPoolWrapper> {
|
||||
logger.info(`🔧 새 연결 풀 생성 중 (ID: ${connectionId})...`);
|
||||
|
||||
// DB 연결 정보 조회
|
||||
// DB 연결 정보 조회 (실제 비밀번호 포함)
|
||||
const connectionResult =
|
||||
await ExternalDbConnectionService.getConnectionById(connectionId);
|
||||
await ExternalDbConnectionService.getConnectionByIdWithPassword(
|
||||
connectionId
|
||||
);
|
||||
|
||||
if (!connectionResult.success || !connectionResult.data) {
|
||||
throw new Error(`연결 정보를 찾을 수 없습니다 (ID: ${connectionId})`);
|
||||
@@ -296,16 +337,19 @@ export class ExternalDbConnectionPoolService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿼리 실행 (자동으로 연결 풀 관리)
|
||||
* 쿼리 실행 (자동으로 연결 풀 관리 + 재시도 로직)
|
||||
*/
|
||||
async executeQuery(
|
||||
connectionId: number,
|
||||
sql: string,
|
||||
params?: any[]
|
||||
params?: any[],
|
||||
retryCount = 0
|
||||
): Promise<any> {
|
||||
const pool = await this.getPool(connectionId);
|
||||
const MAX_RETRIES = 2;
|
||||
|
||||
try {
|
||||
const pool = await this.getPool(connectionId);
|
||||
|
||||
logger.debug(
|
||||
`📊 쿼리 실행 (ID: ${connectionId}): ${sql.substring(0, 100)}...`
|
||||
);
|
||||
@@ -314,7 +358,29 @@ export class ExternalDbConnectionPoolService {
|
||||
`✅ 쿼리 완료 (ID: ${connectionId}), 결과: ${result.length}건`
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// 연결 끊김 오류인 경우 재시도
|
||||
const isConnectionError =
|
||||
error.message?.includes("closed state") ||
|
||||
error.message?.includes("연결 풀이 닫힌 상태") ||
|
||||
error.code === "PROTOCOL_CONNECTION_LOST" ||
|
||||
error.code === "ECONNRESET" ||
|
||||
error.code === "ETIMEDOUT";
|
||||
|
||||
if (isConnectionError && retryCount < MAX_RETRIES) {
|
||||
logger.warn(
|
||||
`🔄 연결 오류 감지, 재시도 중... (${retryCount + 1}/${MAX_RETRIES}) (ID: ${connectionId})`
|
||||
);
|
||||
|
||||
// 기존 풀 제거 후 새로 생성
|
||||
await this.removePool(connectionId);
|
||||
|
||||
// 잠시 대기 후 재시도
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
return this.executeQuery(connectionId, sql, params, retryCount + 1);
|
||||
}
|
||||
|
||||
logger.error(`❌ 쿼리 실행 실패 (ID: ${connectionId}):`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user