JSON 상태 저장소 DB 전환

This commit is contained in:
seo
2026-07-23 02:29:13 +09:00
parent 8cea4105eb
commit 1947b31c87
3 changed files with 106 additions and 52 deletions
+3 -4
View File
@@ -72,8 +72,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
- `control_state`: 팬 모드와 PWM 상태
- `sensor_logs`: 온도, RPM, PWM, load, memory, disk, uptime. 과거 배터리 컬럼은 호환을 위해 유지하지만 UPS 제거 상태에서는 새 배터리값을 기록하지 않습니다.
- `/tmp/control-low-voltage-state.json`: `vcgencmd get_throttled`반 저전압 감지 상태, 최근 지속시간, 최근 episode 이력
- `/tmp/control-throttling-state.json`: `vcgencmd get_throttled` 기반 스로틀링 감지 상태, 최근 지속시간, 최근 episode 이력
- `app_json_store`:존 JSON 파일 저장소를 대체하는 DB 기반 JSON 상태/캐시 저장 테이블. 저전압/스로틀링 episode 상태, 배터리 프로파일 캐시, 사용자 서비스 캐시를 저장합니다.
- `system_notice_state`: notice 활성 상태와 기준값
- `system_notice_logs`: notice 발생 이력
- `wifi_observed_sessions`: 5G WiFi/LAN client의 최초/마지막 감지 시간
@@ -187,11 +186,11 @@ control-wifi-observe.service
## 주요 함수/모듈
- `collect_snapshot()`: 센서와 fan 상태 snapshot 생성
- `custom_systemd_services()`: `/etc/systemd/system/*.service` 단위를 조회해 사용자 생성 서비스 상태와 최근 journal 로그를 구성
- `custom_systemd_services()`: `/etc/systemd/system/*.service` 단위를 조회해 사용자 생성 서비스 상태와 최근 journal 로그를 구성하고 `app_json_store`에 짧은 TTL 캐시로 저장
- `systemd_service_logs()`: 서비스별 최근 journal 로그 조회
- `read_throttled_flags()`: `vcgencmd get_throttled`를 직접 조회하고, 권한 문제로 실패하면 sudo 경유 조회를 시도
- `throttled_event_status()`: 저전압/스로틀링 episode 이력, 복구 중/반복 발생 판정, 최근 10분 통계를 계산
- `throttled_statuses()`: 라즈베리파이 throttled flag를 읽고 저전압/스로틀링 감지 이력을 상태 파일로 추적
- `throttled_statuses()`: 라즈베리파이 throttled flag를 읽고 저전압/스로틀링 감지 이력을 `app_json_store`로 추적
- `apply_fan_policy()`: 팬 목표값 계산과 적용
- `fan_control_temp()`: CPU/RP1 혼합 팬 판단 온도 계산
- `json_out()`: API JSON 응답 표준화
+15
View File
@@ -426,6 +426,21 @@ function bootstrap_db(): void
COLLATE=utf8mb4_unicode_ci
");
$pdo->exec("
CREATE TABLE IF NOT EXISTS app_json_store (
store_key VARCHAR(128) NOT NULL PRIMARY KEY,
payload LONGTEXT NOT NULL,
expires_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_expires_at (expires_at),
INDEX idx_updated_at (updated_at)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci
");
$pdo->exec("
CREATE TABLE IF NOT EXISTS wifi_client_aliases (
mac VARCHAR(17) NOT NULL PRIMARY KEY,
+74 -34
View File
@@ -121,6 +121,62 @@ function dmesg_log(): array
];
}
function json_store_key_from_path(string $path): string
{
$name = preg_replace('/[^a-zA-Z0-9_.-]+/', '-', basename($path, '.json')) ?: md5($path);
return 'file:' . $name;
}
function json_store_read(string $key, ?string $legacyPath = null): ?array
{
try {
$stmt = db()->prepare("
SELECT payload, expires_at
FROM app_json_store
WHERE store_key = :store_key
AND (expires_at IS NULL OR expires_at >= NOW())
LIMIT 1
");
$stmt->execute([':store_key' => $key]);
$row = $stmt->fetch();
if ($row) {
$decoded = json_decode((string)$row['payload'], true);
if (is_array($decoded)) {
return $decoded;
}
}
} catch (Throwable) {
}
if ($legacyPath !== null && is_readable($legacyPath)) {
$decoded = json_decode((string)@file_get_contents($legacyPath), true);
if (is_array($decoded)) {
json_store_write($key, $decoded);
return $decoded;
}
}
return null;
}
function json_store_write(string $key, array $payload, ?int $ttlSeconds = null): void
{
$expiresAt = $ttlSeconds !== null ? date('Y-m-d H:i:s', time() + max(1, $ttlSeconds)) : null;
$stmt = db()->prepare("
INSERT INTO app_json_store (store_key, payload, expires_at)
VALUES (:store_key, :payload, :expires_at)
ON DUPLICATE KEY UPDATE
payload = VALUES(payload),
expires_at = VALUES(expires_at),
updated_at = CURRENT_TIMESTAMP
");
$stmt->execute([
':store_key' => $key,
':payload' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
':expires_at' => $expiresAt,
]);
}
function throttled_event_status(?int $flags, string $raw, int $activeBit, int $seenBit, string $statePath): array
{
$now = time();
@@ -129,10 +185,6 @@ function throttled_event_status(?int $flags, string $raw, int $activeBit, int $s
$active = $flags !== null && (bool)($flags & $activeBit);
$seen = $flags !== null && (bool)($flags & $seenBit);
if (is_file($statePath) && !is_writable($statePath)) {
@chmod($statePath, 0666);
}
$state = [
'active' => false,
'active_since' => null,
@@ -143,12 +195,8 @@ function throttled_event_status(?int $flags, string $raw, int $activeBit, int $s
'events' => [],
];
$fp = @fopen($statePath, 'c+');
if ($fp !== false) {
@chmod($statePath, 0666);
@flock($fp, LOCK_EX);
$saved = stream_get_contents($fp);
$decoded = json_decode((string)$saved, true);
try {
$decoded = json_store_read(json_store_key_from_path($statePath), $statePath);
if (is_array($decoded)) {
$state = array_merge($state, array_intersect_key($decoded, $state));
}
@@ -204,15 +252,9 @@ function throttled_event_status(?int $flags, string $raw, int $activeBit, int $s
)), -200);
$state['updated_at'] = $now;
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($state, JSON_UNESCAPED_SLASHES));
fflush($fp);
@chmod($statePath, 0666);
json_store_write(json_store_key_from_path($statePath), $state);
}
@flock($fp, LOCK_UN);
fclose($fp);
} catch (Throwable) {
}
$duration = 0;
@@ -885,7 +927,8 @@ function battery_profile_history(int $days = 45): array
static $cache = [];
$cacheKey = (string)$days;
$now = time();
$cacheFile = sys_get_temp_dir() . '/control-battery-profile-' . $days . 'd.json';
$storeKey = 'battery_profile:' . $days . 'd';
$legacyCacheFile = sys_get_temp_dir() . '/control-battery-profile-' . $days . 'd.json';
if (
isset($cache[$cacheKey])
@@ -895,9 +938,7 @@ function battery_profile_history(int $days = 45): array
return $cache[$cacheKey]['rows'];
}
if (is_readable($cacheFile)) {
$raw = @file_get_contents($cacheFile);
$decoded = is_string($raw) ? json_decode($raw, true) : null;
$decoded = json_store_read($storeKey, $legacyCacheFile);
if (
is_array($decoded)
&& isset($decoded['created_at'], $decoded['rows'])
@@ -910,7 +951,6 @@ function battery_profile_history(int $days = 45): array
];
return $decoded['rows'];
}
}
$maxId = (int)db()->query("SELECT MAX(id) FROM sensor_logs")->fetchColumn();
$minId = max(1, $maxId - 3000000);
@@ -952,14 +992,11 @@ function battery_profile_history(int $days = 45): array
'time' => $now,
'rows' => $rows,
];
@file_put_contents(
$cacheFile,
json_encode([
json_store_write($storeKey, [
'created_at' => $now,
'days' => $days,
'rows' => $rows,
], JSON_UNESCAPED_UNICODE)
);
], 21600);
return $rows;
}
@@ -3405,12 +3442,12 @@ function systemd_service_logs(string $unit, int $limit = 12): array
function custom_systemd_services(): array
{
$cachePath = '/tmp/control-custom-services.json';
$cacheSeconds = (int)setting_value('display.custom_service_cache_seconds');
if (is_file($cachePath) && filemtime($cachePath) >= time() - $cacheSeconds) {
$cached = json_decode((string)@file_get_contents($cachePath), true);
if (is_array($cached)) {
return $cached;
$cacheKey = 'custom_services';
$cached = json_store_read($cacheKey, '/tmp/control-custom-services.json');
if (is_array($cached) && isset($cached['created_at'], $cached['rows']) && is_array($cached['rows'])) {
if (time() - (int)$cached['created_at'] <= $cacheSeconds) {
return $cached['rows'];
}
}
@@ -3473,7 +3510,10 @@ function custom_systemd_services(): array
];
}
@file_put_contents($cachePath, json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
json_store_write($cacheKey, [
'created_at' => time(),
'rows' => $rows,
], $cacheSeconds);
return $rows;
}