Files
sam-manage/app/Services/PartitionManagementService.php
2026-02-25 11:45:01 +09:00

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);
}
}