This commit is contained in:
biosvos
2026-08-07 17:38:18 +09:00
commit 873193a243
9613 changed files with 2755992 additions and 0 deletions
+512
View File
@@ -0,0 +1,512 @@
#include "GarbageClean.h"
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>
#include <dirent.h>
#include <iterator>
#include "Logger.h"
#include "ProcessConfig.h"
#define SQLITE_DB_LOCK_TIMEOUT 1000 // SQLITE_BUSY 상황 발생 문제 해결을 위해 DB Lock 관련 timeout 설정값. (1 sec)
#define SQLITE_DB_LOCK_MAX_RETRY 60 // SQLITE_BUSY 발생시 재시도 최대 횟수
// meta 에 존재하지 않아.. unlink 여부 판단시... 삭제 방지 day 값
// - unlink 대상인 경우.. 최종 수정일, 최종 access 시간이 본 설정값 이하이면 삭제되지 않도록 처리.
// - 2019-08-29 일감 #32774 에 의해 7일 -> 2일로 변경 처리함.
#define UNLINK_PREVENT_MAX_DAY 2
// unlink slow 모드인 경우.. unlink 후 sleep 시간 (sec)
// - 2 sec sleep : 일 4만건 unlink
// - 1 sec sleep : 일 8만건 unlink
#define UNLINK_SLOW_MODE_SLEEP 1
// 생성자
// - pLocalDb : sqlite memory db 객체에 대한 포인트
CGarbageClean::CGarbageClean( sqlite3 * pLocalDb )
: m_pLocalStmt( NULL )
{
m_pLocalDb = pLocalDb;
}
// 소멸자
CGarbageClean::~CGarbageClean()
{
m_vecTargetDirectory.clear();
// Local DB stamt clear
if( m_pLocalStmt != NULL )
{
sqlite3_finalize( m_pLocalStmt );
m_pLocalStmt = NULL;
}
}
// 객체 초기화 함수
bool CGarbageClean::Init()
{
// 작업대상 폴더 정보를 저장할 변수 초기화.
m_vecTargetDirectory.clear();
// Local sqlite db 관련 변수
int nResult = 0;
int nRetryCount = 0;
// strQuery = "CREATE TABLE meta( resource_id integer, filename_hash text, get_content_length integer, deleted_yn text )";
// 조회 처리를 수행할 stmt 객체 생성.
// - 결과 추출 오류 발생시 sqlite3_column_int() 함수가 0 을 반환하므로.. 오류 와.. 실제 미존재 상태를 구분하기 위해... +1 처리함.
std::string strQuery;
strQuery = "SELECT count( resource_id ) +1 FROM meta WHERE filename_hash = ? ;";
nResult = sqlite3_prepare( m_pLocalDb, strQuery.c_str(), strQuery.size(), &m_pLocalStmt, NULL );
// sqlite3_prepare() 함수 실행시 SQLITE_BUSY 오류가 발생할 수 있으므로.. 재시도 로직을 추가한다.
while( nResult == SQLITE_BUSY && nRetryCount < SQLITE_DB_LOCK_MAX_RETRY )
{
sqlite3_busy_timeout( m_pLocalDb, SQLITE_DB_LOCK_TIMEOUT );
++nRetryCount;
_LOG( LWAR, "Local db prepare sql SQLITE_BUSY. Retry[%d]", nRetryCount );
nResult = sqlite3_prepare( m_pLocalDb, strQuery.c_str(), strQuery.size(), &m_pLocalStmt, NULL );
}
// 최종 실패시...
if( nResult != SQLITE_OK )
{
LOG( LERR, "sqlite3 prepare sql stmt create failed. [%d][%s]", nResult, sqlite3_errmsg( m_pLocalDb ) );
return false;
}
return true;
}
// garbage clean 작업 수행.
// - bGarbageCleanSlow : unlink 수행 후 sleep 를 호출하는 slow 모드로 동작시킬지 여부.
// - bGarbageForceDelete: unlink 처리시 access time 정보를 check 할지 여부
bool CGarbageClean::Start( bool bGarbageCleanSlow, bool bGarbageForceDelete )
{
// 작업 소요 시간 정보 계산을 위한 변수.
// - 시간 동기화로 인한 문제를 해결하기 위해 clock_gettime() 함수 사용.
struct timespec timeStart;
struct timespec timeEnd;
double dTimeInterval = 0.0;
memset( &timeStart, 0x00, sizeof( struct timespec ) );
memset( &timeEnd, 0x00, sizeof( struct timespec ) );
// Meta 에 존재하지 않는 파일 삭제 처리 관련...
// 현재 시간 정보 추출하여.. 정의된 보호 시간을 뺀다.
time_t unlinkPreventMaxTime;
unlinkPreventMaxTime = time( 0 ) - ( UNLINK_PREVENT_MAX_DAY * 24 * 60 * 60 );
// 작업 시작 로깅 및 시작 시간 정보 추출.
_LOG( LINF, "Garbage clean start." );
_LOG( LINF, "- unlink speed: %s", ( bGarbageCleanSlow == true ? "slow" : "normal" ) );
_LOG( LINF, "- unlink modifiy time max prevent : %d day, %ld time.", UNLINK_PREVENT_MAX_DAY, unlinkPreventMaxTime);
if( bGarbageForceDelete == false )
{
_LOG( LINF, "- unlink access time max prevent : %d day, %ld time.", UNLINK_PREVENT_MAX_DAY, unlinkPreventMaxTime );
}
else
{
_LOG( LINF, "- unlink access time not check. force unlink." );
}
clock_gettime( CLOCK_MONOTONIC, &timeStart );
// 통계 정보 저장을 위한 변수
// 각 폴더별 삭제 count
unsigned long long ullDirTotalCount = 0; // 각 폴더별 처리한 count 정보 저장.
unsigned long long ullDirUnlinkCount = 0; // 각 폴더별 삭제한 count 정보 저장.
unsigned long long ullTotalCount = 0; // 누적 처리 count 정보 저장.
unsigned long long ullTotalUnlinkCount = 0; // 누적 삭제 count 정보를 저장
unsigned long long ullTotalUnlinkSize = 0; // 누적 삭제 Size 정보를 저장
// 1. 설정된 대상 경로를 바탕으로.. 실제 최하위 서비스 폴더 목록 list 를 추출한다.
if( GetTargetDir() == false )
{
// 오류 발생시.. 해당 모듈에서 로깅 처리 했으므로.. 실패로 처리.
return false;
}
else
{
_LOG( LINF, "Garbage clean [%s] sub service directory count [%lu]", CProcessConfig::GetInstance()->GetJobRoot(), m_vecTargetDirectory.size() );
}
// 2. 추출된 각 경로에 대해.. 파일 정보를 추출하여.. 검증 수행.
DIR * pDir = NULL;
struct dirent * pDirEntry = NULL;
std::vector< std::string >::iterator it;
// 파일에 대한 정보를 저장할 변수.
std::string strFullFileName;
int nFullFileNameExistCount;
struct stat stFileStat;
// Local sqlite db 관련 변수
int nResult = 0;
int nRetryCount = 0;
for( it = m_vecTargetDirectory.begin(); it != m_vecTargetDirectory.end(); ++it )
{
pDir = opendir( it->c_str() );
if( pDir == NULL )
{
// 폴더 open 실패시...
// - 오류 내역 로깅하고 계속 진행.
LOG( LERR, "directory[%s] open faild. [%d][%s]", it->c_str(), errno, strerror( errno ) );
continue;
}
// 개별 폴더 통계 초기화.
ullDirTotalCount = 0;
ullDirUnlinkCount = 0;
_LOG( LINF, "Garbage clean [%s] start", it->c_str() );
while( ( pDirEntry = readdir( pDir ) ) != NULL )
{
// 만약 하위에 폴더, 링크, FIFO 등이 존재할 수 있으므로...
// 이를 제외하고 only 파일만 비교 처리.
// man 5 dir
// d_type 정의 내역.
// #define DT_DIR 4
// #define DT_REG 8
if( pDirEntry->d_type == DT_REG )
{
// '.'으로 시작하는 숨김 파일 등은 제외 처리. ( '.', '..', '..sujournal' 등 )
if( pDirEntry->d_name[0] == '.' )
continue;
// 진행 사항 로깅.
if( ullDirTotalCount % 10000 == 0 && ullDirTotalCount != 0 )
{
_LOG( LINF, "Garbage checking. [%s] count[%10llu] unlink[%10llu]"
, it->c_str(), ullDirTotalCount, ullDirUnlinkCount );
}
// 통계 정보 증감 처리
++ullDirTotalCount;
// 파일명 전체 경로 정보 조합
strFullFileName = it->c_str();
strFullFileName.append( "/" );
strFullFileName.append( pDirEntry->d_name );
// 이제 실제로 파일이 존재하는지 local DB 에서 확인한다.
// select query 에 변수 bind 처리. ( bind 값을 1부터 시작 )
// - bind 처리시.. SQLITE_TRANSIENT 도 사용할 수 있으나.. 내부 copy 로 속도 느림
// - query 처리 중간에 변수값이 변경될 가능성 없으므로.. SQLITE_STATIC 사용.
nResult = sqlite3_bind_text( m_pLocalStmt, 1, strFullFileName.c_str(), strFullFileName.size(), SQLITE_STATIC );
if( nResult != SQLITE_OK )
{
// bind 처리 실패시....
LOG( LERR, "sqlite3 bind error.[%s] [%d][%s]", strFullFileName.c_str(), nResult, sqlite3_errmsg( m_pLocalDb ) );
// reset 처리 후.. 다음 항목을 진행 처리
sqlite3_reset( m_pLocalStmt );
continue;
}
// Query 실행.
nResult = sqlite3_step( m_pLocalStmt );
nRetryCount = 0;
while( nResult == SQLITE_BUSY && nRetryCount < SQLITE_DB_LOCK_MAX_RETRY )
{
sqlite3_busy_timeout( m_pLocalDb, SQLITE_DB_LOCK_TIMEOUT );
++nRetryCount;
LOG( LWAR, "sqlite3 step SQLITE_BUSY, Retry[%d]", nRetryCount );
// query 재시도
nResult = sqlite3_step( m_pLocalStmt );
}
// SELECT query 수행시....
// - 결과 row 가 2 개 이상인 경우....SQLITE_ROW(100) 을 반환하고.. 더 이상 조회 결과가 없는 경우.....SQLITE_DONE (101) 반환.
// - 결과가 없거나...결과 row 가 1개 인 경우.. SQLITE_DONE (101) 을 반환한다.
// - 따라서 SQLITE_DONE 을 반환한다고 해서.. 결과가 없는 것으로 판단하면 안됨.
// select query 수행시 오류가 발생한 경우...
if( nResult != SQLITE_ROW && nResult != SQLITE_DONE )
{
LOG( LERR, "sqlite3 step error.[%s] [%d][%s]", strFullFileName.c_str(), nResult, sqlite3_errmsg( m_pLocalDb ) );
// 만약 오류 코드가 SQLITE_ERROR, SQLITE_INTERNAL, SQLITE_EMPTY, SQLITE_SCHEMA 등이라면.... DB에 문제 있는 것으로 보고.. 작업 중지.
if( nResult == SQLITE_ERROR || nResult == SQLITE_INTERNAL || nResult == SQLITE_EMPTY || nResult == SQLITE_SCHEMA )
{
LOG( LERR, "sqlite3 db not valid. garbage clean stop.");
closedir( pDir );
pDir = NULL;
sqlite3_finalize( m_pLocalStmt );
m_pLocalStmt = NULL;
return false;
}
// 그 외 오류에 대해서는 무시하고 계속 진행.
}
else
{
// SELECT query 수행시....
// - 결과 row 가 2 개 이상인 경우....SQLITE_ROW(100) 을 반환하고.. 더 이상 조회 결과가 없는 경우.....SQLITE_DONE (101) 반환.
// - 결과가 없거나...결과 row 가 1개 인 경우.. SQLITE_DONE (101) 을 반환한다.
// - 따라서 SQLITE_DONE 을 반환한다고 해서.. 결과가 없는 것으로 판단하면 안됨.
// strQuery = "SELECT count( resource_id ) +1 FROM meta WHERE filename_hash = ? ;";
// - sqlite3_column_int() 호출시 오류가 발생할 경우에 대한 처리를 위해 +1 처리함.
// 조회 결과를 변수에 저장 처리.
nFullFileNameExistCount = sqlite3_column_int( m_pLocalStmt, 0 );
// nFullFileNameExistCount == 0 : sqlite3_column_int() 함수 오류로 default 값이 반환된 경우...
// nFullFileNameExistCount == 1 : Meta 에 해당 파일 정보 미존재시.
// 그 이외에는 Meta 에 해당 파일 정보 존재로 처리.
//_LOG( LINF, "[GARBAGE] Meta SELECT. [%s] count [%d]", strFullFileName.c_str(), nFullFileNameExistCount -1 );
if( nFullFileNameExistCount == 1 )
{
// Meta 에 해당 파일 미존재시.
// 해당 파일 속성 정보 추출.
if( stat( strFullFileName.c_str(), &stFileStat ) == 0 )
{
// 만약 해당 파일이 Meta에 존재하지 않는 경우...
// 최종 수정일 정보를 추출하여... UNLINK_PREVENT_MAX_DAY 일이 지났는지 확인 후.
// UNLINK_PREVENT_MAX_DAY 일 이상된 content 인 경우.. 삭제 처리한다.
// 파일의 최종 수정 시간이 UNLINK_PREVENT_MAX_DAY 일 이상이면 삭제 처리
// - difftime() 함수를 사용해도 되지만.. CPU 사용률이 거의 100% 수준이라... CPU 부하 낮추기 위해 날코딩 처리.
if( stFileStat.st_mtime < unlinkPreventMaxTime )
{
// access time 을 check 하지 않고.. unlink 처리 수행한다.
if( bGarbageForceDelete == true )
{
// 해당 파일 unlink 처리.
unlink( strFullFileName.c_str() );
++ullDirUnlinkCount;
ullTotalUnlinkSize += stFileStat.st_size;
_LOG( LINF, "[GARBAGE] unlink with force[%s] mtime[%ld] size[%jd]"
, strFullFileName.c_str(), stFileStat.st_mtime, stFileStat.st_size );
// unlink slow 모드인 경우.. unlink 처리 후.. 정의된 시간만큼 sleep 처리한다.
if( bGarbageCleanSlow == true )
sleep( UNLINK_SLOW_MODE_SLEEP );
}
else if( stFileStat.st_atime < unlinkPreventMaxTime )
{
// -f 옵션을 사용하지 않은 상태이고 access time 이 2일 이상인 경우
// 삭제 처리를 수행한다.
// 해당 파일 unlink 처리.
unlink( strFullFileName.c_str() );
++ullDirUnlinkCount;
ullTotalUnlinkSize += stFileStat.st_size;
_LOG( LINF, "[GARBAGE] unlink[%s] atime[%ld] mtime[%ld] size[%jd]"
, strFullFileName.c_str(), stFileStat.st_atime, stFileStat.st_mtime, stFileStat.st_size );
// unlink slow 모드인 경우.. unlink 처리 후.. 정의된 시간만큼 sleep 처리한다.
if( bGarbageCleanSlow == true )
sleep( UNLINK_SLOW_MODE_SLEEP );
}
else
{
// 파일의 최종 수정 시간은 UNLINK_PREVENT_MAX_DAY 값을 초과했지만.
// force mode 가 아니고, access 시간이 UNLINK_PREVENT_MAX_DAY 일 이내인 경우..
// - 로깅만 처리.
_LOG( LINF, "[%s] meta not exist, but access occurred within [%d] day. a[%ld] m[%ld] limit[%ld] size[%jd]"
, strFullFileName.c_str(), UNLINK_PREVENT_MAX_DAY, stFileStat.st_atime, stFileStat.st_mtime, unlinkPreventMaxTime, stFileStat.st_size );
}
}
else
{
// 파일의 최종 수정 시간이 UNLINK_PREVENT_MAX_DAY 일 이내인 경우..
// - 로깅만 처리.
_LOG( LINF, "[%s] meta not exist, but modify occurred within [%d] day. a[%ld] m[%ld] limit[%ld] size[%jd]"
, strFullFileName.c_str(), UNLINK_PREVENT_MAX_DAY, stFileStat.st_atime, stFileStat.st_mtime, unlinkPreventMaxTime, stFileStat.st_size );
}
}
else
{
// stat 호출 오류인 경우...
// - multi thread 가 아직 1 process 구조라서.. errno 를 그대로 사용....
// - 만약 multi thread 구조로 변경할 경우.. errno 그대로 사용하면 안됨.
// garbage clean 를 very slow 모드로 동작시킬 경우...
// - 작업 시간이 오래 걸려...
// - 해당 물리 파일이 rc_gcmd 등의 다른 프로세스에 의해 삭제될 가능성이 존재함.
// - 이를 경우... 다음과 같은 오류 발생
// [17:25:42] [ERR ] [GARBAGE] meta not exist. but file stat call error. name[/stg/node0/250/22a753b521dfc43ae468854c2089e8ef] [2][No such file or directory]
// - 따라서.. 다른 프로세스에 의해 삭제된 경우.. 불필요하게 오류 찍을 필요 없다.
if( errno != ENOENT )
{
_LOG( LERR, "[GARBAGE] meta not exist. but file stat call error. name[%s] [%d][%s]", strFullFileName.c_str(), errno, strerror( errno ) );
}
}
}
else if( nFullFileNameExistCount == 0 )
{
//sqlite3_column_int() 함수 오류로 default 값이 반환된 경우...
LOG( LERR, "sqlite3 column get error.[%s] [%d][%s]", strFullFileName.c_str(), nFullFileNameExistCount, sqlite3_errmsg( m_pLocalDb ) );
}
else
{
// Meta 에 해당 파일 정보 존재로 처리.
//_LOG( LINF, "[GARBAGE] Meta OK. [%s] count [%d]", strFullFileName.c_str(), nFullFileNameExistCount - 1 );
}
}
// Query 수행된 경우.. prepared 문을 재사용하기 위해 reset 처리한다.
sqlite3_reset( m_pLocalStmt );
}
} // while( ( pDirEntry = readdir( pDir ) ) != NULL )
// open 처리된 dir 객체를 close 처리한다.
closedir( pDir );
pDir = NULL;
// 통계 저장 처리
ullTotalCount += ullDirTotalCount;
ullTotalUnlinkCount += ullDirUnlinkCount;
_LOG( LINF, "Garbage clean [%s] end. unlink count[ %llu / %llu ] ", it->c_str(), ullDirUnlinkCount, ullDirTotalCount );
} // for( it = m_vecTargetDirectory.begin(); it != m_vecTargetDirectory.end(); ++it )
// 작업 완료 후 stmt 객체 clear
sqlite3_finalize( m_pLocalStmt );
m_pLocalStmt = NULL;
// 작업 완료시 시간 정보 추출 후 소요시간 계산.
clock_gettime( CLOCK_MONOTONIC, &timeEnd );
dTimeInterval = timeEnd.tv_sec - timeStart.tv_sec;
dTimeInterval += ( timeEnd.tv_nsec - timeStart.tv_nsec ) * 1e-9;
_LOG( LINF, "Garbage clean complete. path[%s] unlink mode[%s] elapsed time: %.1lf sec."
, CProcessConfig::GetInstance()->GetJobRoot()
, ( bGarbageCleanSlow == true ? "slow" : "normal" )
, dTimeInterval );
_LOG( LINF, "Garbage clean report: total count[%llu] unlink count[%llu] unlink size[%llu byte]"
, ullTotalCount, ullTotalUnlinkCount, ullTotalUnlinkSize );
return true;
}
// 설정된 기본 경로를 바탕으로.. 작업대상 폴더명 정보를 추출하여.. 멤버변수에 저장 처리.
bool CGarbageClean::GetTargetDir()
{
// 사용자가 입력했거나.. 기본으로 설정된 경로에서...
// 실제 정리 작업을 수행할 서비스 폴더를 추출한다.
// - 서비스 폴더가 아닌 경우.. 운영 목적으로 설정한 파일들이 존재하여..
// - 삭제될 수 있으므로...
// - 서비스 폴더에 대해서만 정리 작업이 수행되도록 한다.
// 추출 방식
// - 직접 코드로 opendir, readdir 함수를 이용하여 추출 가능하지만....
// - 불필요하게 시간이 오래 걸리고.. 버그의 가능성 존재.
// - 따라서 외부 명령어를 이용하여..
// - 하위 폴더 정보를 추출하고.... 서비스 폴더인지 검증 처리.
// 외부 tool
// find /stg/node0 -type d -print
// find /stg/node0 -type d -print | grep '/[1-9]*$'
// 참고사항
// main 모듈에서 root 경로에 대한
// 맨 끝의 '/' 제거 및
// 존재 여부 확인 했으므로... 그냥 사용하면 됨.
char tempBuffer[2048];
std::string strTempPath;
FILE * fd = NULL;
// trim 처리 관련 변수
std::string::size_type pos;
// 명령어 조립
snprintf( tempBuffer, sizeof( tempBuffer ), "find %s -type d -print | grep '/[0-9]*$'", CProcessConfig::GetInstance()->GetJobRoot() );
fd = popen( tempBuffer, "r" );
if( fd == NULL )
{
int errorNum = errno;
LOG( LERR, "service directory search error. popen error. [%d][%s]", errorNum, strerror( errorNum ) );
return false;
}
else
{
// 작업 대상 경로를 저장할 멤버 변수 초기화.
m_vecTargetDirectory.clear();
memset( tempBuffer, 0x00, sizeof( tempBuffer ) );
while( fgets( tempBuffer, sizeof( tempBuffer ) - 1, fd ) != NULL )
{
// 결과 맨 끝에 줄바꿈 문자가 있어.. trim 처리 필요
strTempPath = tempBuffer;
if( strTempPath.length() == 0 )
continue;
pos = strTempPath.find_last_not_of( " \a\b\f\n\r\t\v" );
if( pos != std::string::npos )
strTempPath.erase( pos + 1 );
// 원래는 추출된 전체 경로.. (/stg/node0/290 ) 에서
// 마지막 290 정보 추출하여 숫자인지 비교해야 하지만...
// 귀찬아서.. 외부 명령어의 grep 으로 비교 처리 넣어버림...
// 따라서.. 추출된 폴더 정보를 바로 저장 처리하면 됨.
m_vecTargetDirectory.push_back( strTempPath );
}
pclose( fd );
}
return true;
}