187 lines
6.2 KiB
PHP
187 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class PartitionManagementService
|
|
{
|
|
/**
|
|
* 파티션 현황 조회
|
|
*/
|
|
public function getPartitions(): array
|
|
{
|
|
$dbName = config('database.connections.mysql.database');
|
|
$retentionDays = $this->getRetentionDays();
|
|
|
|
$partitions = DB::select("
|
|
SELECT PARTITION_NAME, PARTITION_DESCRIPTION, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH
|
|
FROM INFORMATION_SCHEMA.PARTITIONS
|
|
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'trigger_audit_logs'
|
|
AND PARTITION_NAME IS NOT NULL
|
|
ORDER BY PARTITION_ORDINAL_POSITION
|
|
", [$dbName]);
|
|
|
|
// 저장소 전체 크기
|
|
$storageInfo = DB::selectOne("
|
|
SELECT
|
|
ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) as size_mb
|
|
FROM INFORMATION_SCHEMA.TABLES
|
|
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'trigger_audit_logs'
|
|
", [$dbName]);
|
|
|
|
$totalRows = 0;
|
|
$cutoffTs = Carbon::now()->subDays($retentionDays)->timestamp;
|
|
$nowTs = Carbon::now()->timestamp;
|
|
|
|
$items = [];
|
|
foreach ($partitions as $p) {
|
|
$totalRows += $p->TABLE_ROWS;
|
|
|
|
if ($p->PARTITION_DESCRIPTION === 'MAXVALUE') {
|
|
$items[] = [
|
|
'name' => $p->PARTITION_NAME,
|
|
'bound_raw' => 'MAXVALUE',
|
|
'bound_date' => null,
|
|
'rows' => $p->TABLE_ROWS,
|
|
'status' => 'future',
|
|
'can_drop' => false,
|
|
];
|
|
|
|
continue;
|
|
}
|
|
|
|
$boundTs = (int) $p->PARTITION_DESCRIPTION;
|
|
$boundDate = Carbon::createFromTimestamp($boundTs)->format('Y-m-d');
|
|
|
|
// 상태 결정
|
|
if ($boundTs <= $cutoffTs) {
|
|
$status = 'expired'; // 보관기간 초과
|
|
} elseif ($boundTs <= $nowTs) {
|
|
$prevMonthTs = Carbon::now()->startOfMonth()->timestamp;
|
|
$status = $boundTs <= $prevMonthTs ? 'archived' : 'current';
|
|
} else {
|
|
$status = 'upcoming'; // 미래 파티션
|
|
}
|
|
|
|
$items[] = [
|
|
'name' => $p->PARTITION_NAME,
|
|
'bound_raw' => $boundTs,
|
|
'bound_date' => $boundDate,
|
|
'rows' => $p->TABLE_ROWS,
|
|
'status' => $status,
|
|
'can_drop' => $status === 'expired',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'partitions' => $items,
|
|
'total_partitions' => count($items),
|
|
'total_rows' => $totalRows,
|
|
'storage_mb' => $storageInfo->size_mb ?? 0,
|
|
'retention_days' => $retentionDays,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 미래 파티션 추가 (REORGANIZE PARTITION p_future)
|
|
*/
|
|
public function addFuturePartitions(int $months): array
|
|
{
|
|
$dbName = config('database.connections.mysql.database');
|
|
|
|
// 현재 파티션 바운드 조회
|
|
$partitions = DB::select("
|
|
SELECT PARTITION_NAME, PARTITION_DESCRIPTION
|
|
FROM INFORMATION_SCHEMA.PARTITIONS
|
|
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'trigger_audit_logs'
|
|
AND PARTITION_NAME IS NOT NULL
|
|
ORDER BY PARTITION_ORDINAL_POSITION
|
|
", [$dbName]);
|
|
|
|
$existingBounds = [];
|
|
foreach ($partitions as $p) {
|
|
if ($p->PARTITION_DESCRIPTION !== 'MAXVALUE') {
|
|
$existingBounds[] = (int) $p->PARTITION_DESCRIPTION;
|
|
}
|
|
}
|
|
|
|
$added = 0;
|
|
$now = Carbon::now();
|
|
|
|
for ($i = 0; $i <= $months; $i++) {
|
|
$target = $now->copy()->addMonths($i)->startOfMonth()->addMonth();
|
|
$ts = $target->timestamp;
|
|
$name = 'p'.$target->copy()->subMonth()->format('Ym');
|
|
|
|
if (in_array($ts, $existingBounds)) {
|
|
continue;
|
|
}
|
|
|
|
DB::statement("ALTER TABLE trigger_audit_logs REORGANIZE PARTITION p_future INTO (
|
|
PARTITION {$name} VALUES LESS THAN ({$ts}),
|
|
PARTITION p_future VALUES LESS THAN MAXVALUE
|
|
)");
|
|
|
|
$added++;
|
|
}
|
|
|
|
if ($added === 0) {
|
|
return ['success' => true, 'message' => '추가할 파티션이 없습니다 (이미 존재).'];
|
|
}
|
|
|
|
return ['success' => true, 'message' => "파티션 {$added}개 추가 완료."];
|
|
}
|
|
|
|
/**
|
|
* 파티션 삭제 (보관기간 초과 검증 포함)
|
|
*/
|
|
public function dropPartition(string $name): array
|
|
{
|
|
if ($name === 'p_future') {
|
|
return ['success' => false, 'message' => 'p_future 파티션은 삭제할 수 없습니다.'];
|
|
}
|
|
|
|
$dbName = config('database.connections.mysql.database');
|
|
$retentionDays = $this->getRetentionDays();
|
|
|
|
// 파티션 존재 및 바운드 확인
|
|
$partition = DB::selectOne("
|
|
SELECT PARTITION_NAME, PARTITION_DESCRIPTION, TABLE_ROWS
|
|
FROM INFORMATION_SCHEMA.PARTITIONS
|
|
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'trigger_audit_logs'
|
|
AND PARTITION_NAME = ?
|
|
", [$dbName, $name]);
|
|
|
|
if (! $partition) {
|
|
return ['success' => false, 'message' => "파티션 '{$name}'을 찾을 수 없습니다."];
|
|
}
|
|
|
|
if ($partition->PARTITION_DESCRIPTION === 'MAXVALUE') {
|
|
return ['success' => false, 'message' => '이 파티션은 삭제할 수 없습니다.'];
|
|
}
|
|
|
|
// 보관기간 초과 여부 서버측 검증
|
|
$boundTs = (int) $partition->PARTITION_DESCRIPTION;
|
|
$cutoffTs = Carbon::now()->subDays($retentionDays)->timestamp;
|
|
|
|
if ($boundTs > $cutoffTs) {
|
|
return ['success' => false, 'message' => '보관기간이 초과되지 않은 파티션은 삭제할 수 없습니다.'];
|
|
}
|
|
|
|
$rows = $partition->TABLE_ROWS;
|
|
DB::statement("ALTER TABLE trigger_audit_logs DROP PARTITION {$name}");
|
|
|
|
return ['success' => true, 'message' => "파티션 '{$name}' 삭제 완료 ({$rows}행)."];
|
|
}
|
|
|
|
/**
|
|
* 보관기간 (일)
|
|
*/
|
|
public function getRetentionDays(): int
|
|
{
|
|
return (int) env('AUDIT_RETENTION_DAYS', 395);
|
|
}
|
|
}
|