Settle late TCP usage asynchronously
This commit is contained in:
@@ -15,6 +15,7 @@ Car는 차량 모뎀/게이트웨이에서 받은 상태를 수집해 DB에 저
|
|||||||
- 허용된 차량 명령 TCP 전송
|
- 허용된 차량 명령 TCP 전송
|
||||||
- monitor 화면과 AJAX 상태 갱신
|
- monitor 화면과 AJAX 상태 갱신
|
||||||
- 사용량/요금 표시와 보정 metadata
|
- 사용량/요금 표시와 보정 metadata
|
||||||
|
- 4.95초 UI timeout과 60초 사용량 정산 분리
|
||||||
- TCP 실패 reason과 마지막 수신 지연 표시
|
- TCP 실패 reason과 마지막 수신 지연 표시
|
||||||
- 도어/경계/시동/공조 상태 해석
|
- 도어/경계/시동/공조 상태 해석
|
||||||
- service worker와 PWA icon 제공
|
- service worker와 PWA icon 제공
|
||||||
@@ -49,6 +50,19 @@ Car는 차량 모뎀/게이트웨이에서 받은 상태를 수집해 DB에 저
|
|||||||
4. 명령 코드를 허용 목록과 대조합니다.
|
4. 명령 코드를 허용 목록과 대조합니다.
|
||||||
5. TCP 게이트웨이로 전송하고 결과와 실패 reason을 반환합니다.
|
5. TCP 게이트웨이로 전송하고 결과와 실패 reason을 반환합니다.
|
||||||
|
|
||||||
|
## TCP timeout과 사용량 정산
|
||||||
|
|
||||||
|
차량 명령 성공/실패 판단은 UI 안정성을 위해 `TCP_TOTAL_TIMEOUT` 기본값인 4.95초를 사용합니다. 이 시간 안에 정상 응답이 없으면 API는 기존처럼 실패를 반환하므로 자동화와 UI는 즉시 재시도 여부를 판단할 수 있습니다.
|
||||||
|
|
||||||
|
단, 실제 모뎀이 4.95초 이후에 늦게 응답할 수 있으므로 사용량 기록은 별도로 정산합니다. 4.95초까지 정상 응답이 없고 송신이 완료된 요청은 `pcntl_fork()`로 자식 프로세스를 만들고, 같은 TCP 소켓을 최대 `TCP_USAGE_SETTLEMENT_TIMEOUT` 기본 60초까지 더 관찰합니다.
|
||||||
|
|
||||||
|
- 60초 안에 정상 응답이 완성되면 `late_response_after_ui_timeout`으로 송신량과 수신량을 함께 기록합니다.
|
||||||
|
- 60초까지 응답이 없으면 `late_total_timeout_60s_sent_only`로 송신 패킷만 사용량에 반영합니다.
|
||||||
|
- 60초 안에 일부 데이터만 오고 정상 포맷이 아니면 `late_invalid_or_partial_response`로 실제 수신된 바이트까지 기록합니다.
|
||||||
|
- 늦은 정상 응답이 차량 상태 포맷이면 상태 DB에도 저장합니다.
|
||||||
|
|
||||||
|
이 구조에서 `request_id`는 UI 응답과 늦은 사용량 정산 기록을 연결하는 기준입니다. 즉 명령 재시도 로직은 4.95초 기준을 유지하고, 데이터 사용량은 최대 60초까지 늦은 수신 패킷을 반영합니다.
|
||||||
|
|
||||||
## 주요 함수/모듈
|
## 주요 함수/모듈
|
||||||
|
|
||||||
- `common.php`: secret 로드와 공통 DB/API 함수
|
- `common.php`: secret 로드와 공통 DB/API 함수
|
||||||
@@ -68,4 +82,4 @@ Car는 차량 모뎀/게이트웨이에서 받은 상태를 수집해 DB에 저
|
|||||||
- 차량 TCP 실패 reason과 마지막 수신 지연을 확인합니다.
|
- 차량 TCP 실패 reason과 마지막 수신 지연을 확인합니다.
|
||||||
- 명령별 rate limit과 감사 로그를 유지합니다.
|
- 명령별 rate limit과 감사 로그를 유지합니다.
|
||||||
- 통신사 기준 데이터 사용량 보정값을 주기적으로 확인합니다.
|
- 통신사 기준 데이터 사용량 보정값을 주기적으로 확인합니다.
|
||||||
|
- 4.95초 timeout 직후에는 사용량 기록이 즉시 생기지 않을 수 있으며, 최대 60초 뒤 늦은 정산 행이 추가될 수 있습니다.
|
||||||
|
|||||||
+156
-4
@@ -15,6 +15,7 @@ define('TCP_HOST', (string)($carTcpConfig['host'] ?? ''));
|
|||||||
define('TCP_PORT', (int)($carTcpConfig['port'] ?? 3400));
|
define('TCP_PORT', (int)($carTcpConfig['port'] ?? 3400));
|
||||||
|
|
||||||
define('TCP_TOTAL_TIMEOUT', (float)($carTcpConfig['total_timeout'] ?? 4.95));
|
define('TCP_TOTAL_TIMEOUT', (float)($carTcpConfig['total_timeout'] ?? 4.95));
|
||||||
|
define('TCP_USAGE_SETTLEMENT_TIMEOUT', (float)($carTcpConfig['usage_settlement_timeout'] ?? 60.0));
|
||||||
|
|
||||||
define('MODEM', (string)($carIdentityConfig['modem'] ?? ''));
|
define('MODEM', (string)($carIdentityConfig['modem'] ?? ''));
|
||||||
define('USER', (string)($carIdentityConfig['user'] ?? ''));
|
define('USER', (string)($carIdentityConfig['user'] ?? ''));
|
||||||
@@ -39,20 +40,25 @@ const CONTROL_CMD = ['ef', 'en', 'hn', 'hf', 'dl', 'du', 'tu'];
|
|||||||
|
|
||||||
const RAW_FULL_REGEX = '/(?<modem>\d{11})\/R:(?<r>[a-z])\/E:(?<E0>[io]{5}\d{3}[io])\/D:(?<D0>[oi]{7})\/L:(?<L0>o{5})\/F:(?<F0>[ots][oi]\d{4}[oi]{4})\/S:(?<S0>[^\/]+)/';
|
const RAW_FULL_REGEX = '/(?<modem>\d{11})\/R:(?<r>[a-z])\/E:(?<E0>[io]{5}\d{3}[io])\/D:(?<D0>[oi]{7})\/L:(?<L0>o{5})\/F:(?<F0>[ots][oi]\d{4}[oi]{4})\/S:(?<S0>[^\/]+)/';
|
||||||
|
|
||||||
function db(): PDO
|
function db(bool $fresh = false): PDO
|
||||||
{
|
{
|
||||||
static $pdo = null;
|
static $pdo = null;
|
||||||
|
|
||||||
if ($pdo instanceof PDO) {
|
if (!$fresh && $pdo instanceof PDO) {
|
||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
|
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
|
||||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
|
$conn = new PDO($dsn, DB_USER, DB_PASS, [
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if ($fresh) {
|
||||||
|
return $conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = $conn;
|
||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +158,137 @@ function record_tcp_usage(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function finalize_late_tcp_usage(
|
||||||
|
$fp,
|
||||||
|
string $cmd,
|
||||||
|
string $usageSource,
|
||||||
|
?string $requestId,
|
||||||
|
int $sentBytes,
|
||||||
|
string $initialResponse,
|
||||||
|
float $deadline
|
||||||
|
): void {
|
||||||
|
ignore_user_abort(true);
|
||||||
|
@set_time_limit((int)ceil(max(5.0, TCP_USAGE_SETTLEMENT_TIMEOUT + 5.0)));
|
||||||
|
|
||||||
|
$response = $initialResponse;
|
||||||
|
$tcpError = '';
|
||||||
|
|
||||||
|
while (microtime(true) < $deadline) {
|
||||||
|
$read = [$fp];
|
||||||
|
$w = null;
|
||||||
|
$e = null;
|
||||||
|
|
||||||
|
$remain = $deadline - microtime(true);
|
||||||
|
if ($remain <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$wait = min($remain, 1.0);
|
||||||
|
$sec = (int)$wait;
|
||||||
|
$usec = (int)(($wait - $sec) * 1000000);
|
||||||
|
|
||||||
|
$sel = @stream_select($read, $w, $e, $sec, $usec);
|
||||||
|
if ($sel === false) {
|
||||||
|
$tcpError = 'late_stream_select_failed';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sel > 0) {
|
||||||
|
$chunk = @fread($fp, 4096);
|
||||||
|
if ($chunk === false) {
|
||||||
|
$tcpError = 'late_read_failed';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($chunk !== '') {
|
||||||
|
$response .= $chunk;
|
||||||
|
|
||||||
|
if (preg_match(RAW_FULL_REGEX, trim($response))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feof($fp)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@fclose($fp);
|
||||||
|
|
||||||
|
$receivedBytes = strlen($response);
|
||||||
|
$trimmed = trim($response);
|
||||||
|
|
||||||
|
if ($trimmed === '') {
|
||||||
|
db_insert_data_usage(db(true), $usageSource, $cmd, $sentBytes, 0, false, 'late_total_timeout_60s_sent_only', $requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match(RAW_FULL_REGEX, $trimmed)) {
|
||||||
|
db_insert_data_usage(
|
||||||
|
db(true),
|
||||||
|
$usageSource,
|
||||||
|
$cmd,
|
||||||
|
$sentBytes,
|
||||||
|
$receivedBytes,
|
||||||
|
false,
|
||||||
|
$tcpError !== '' ? $tcpError : 'late_invalid_or_partial_response',
|
||||||
|
$requestId
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db_insert_data_usage(db(true), $usageSource, $cmd, $sentBytes, $receivedBytes, true, 'late_response_after_ui_timeout', $requestId);
|
||||||
|
|
||||||
|
$trimError = '';
|
||||||
|
$rawTrim = make_trim($trimmed, $trimError);
|
||||||
|
if ($rawTrim === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = parse_trim($rawTrim);
|
||||||
|
if (!is_valid_status_data($data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db_insert_status(db(true), $cmd, $trimmed, $rawTrim, $data);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// 늦은 응답 상태 저장 실패가 사용량 정산을 되돌리면 안 된다.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedule_late_tcp_usage_settlement(
|
||||||
|
$fp,
|
||||||
|
string $cmd,
|
||||||
|
string $usageSource,
|
||||||
|
?string $requestId,
|
||||||
|
int $sentBytes,
|
||||||
|
string $initialResponse = ''
|
||||||
|
): bool {
|
||||||
|
if (!function_exists('pcntl_fork') || $sentBytes <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deadline = microtime(true) + TCP_USAGE_SETTLEMENT_TIMEOUT;
|
||||||
|
$pid = @pcntl_fork();
|
||||||
|
|
||||||
|
if ($pid === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pid > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (function_exists('posix_setsid')) {
|
||||||
|
@posix_setsid();
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize_late_tcp_usage($fp, $cmd, $usageSource, $requestId, $sentBytes, $initialResponse, $deadline);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
function tcp_request(
|
function tcp_request(
|
||||||
string $cmd,
|
string $cmd,
|
||||||
int &$tcpMs,
|
int &$tcpMs,
|
||||||
@@ -261,7 +398,6 @@ function tcp_request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
$readMs = (int)round((microtime(true) - $r0) * 1000);
|
$readMs = (int)round((microtime(true) - $r0) * 1000);
|
||||||
fclose($fp);
|
|
||||||
|
|
||||||
$tcpMs = $connectMs + $readMs;
|
$tcpMs = $connectMs + $readMs;
|
||||||
$receivedBytes = strlen($response);
|
$receivedBytes = strlen($response);
|
||||||
@@ -270,16 +406,32 @@ function tcp_request(
|
|||||||
if ($tcpError === '') {
|
if ($tcpError === '') {
|
||||||
$tcpError = 'total_timeout_4_95s';
|
$tcpError = 'total_timeout_4_95s';
|
||||||
}
|
}
|
||||||
|
$scheduled = $tcpError === 'total_timeout_4_95s'
|
||||||
|
&& schedule_late_tcp_usage_settlement($fp, $cmd, $usageSource, $requestId, $sentBytes, $response);
|
||||||
|
if ($scheduled) {
|
||||||
|
fclose($fp);
|
||||||
|
$tcpError = 'total_timeout_4_95s_late_settlement_pending';
|
||||||
|
} else {
|
||||||
|
fclose($fp);
|
||||||
record_tcp_usage($usageSource, $cmd, $sentBytes, $receivedBytes, false, $tcpError, $requestId);
|
record_tcp_usage($usageSource, $cmd, $sentBytes, $receivedBytes, false, $tcpError, $requestId);
|
||||||
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preg_match(RAW_FULL_REGEX, trim($response))) {
|
if (!preg_match(RAW_FULL_REGEX, trim($response))) {
|
||||||
$tcpError = 'invalid_or_partial_response';
|
$tcpError = 'invalid_or_partial_response';
|
||||||
|
$scheduled = schedule_late_tcp_usage_settlement($fp, $cmd, $usageSource, $requestId, $sentBytes, $response);
|
||||||
|
if ($scheduled) {
|
||||||
|
fclose($fp);
|
||||||
|
$tcpError = 'invalid_or_partial_response_late_settlement_pending';
|
||||||
|
} else {
|
||||||
|
fclose($fp);
|
||||||
record_tcp_usage($usageSource, $cmd, $sentBytes, $receivedBytes, false, $tcpError, $requestId);
|
record_tcp_usage($usageSource, $cmd, $sentBytes, $receivedBytes, false, $tcpError, $requestId);
|
||||||
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fclose($fp);
|
||||||
record_tcp_usage($usageSource, $cmd, $sentBytes, $receivedBytes, true, $tcpError, $requestId);
|
record_tcp_usage($usageSource, $cmd, $sentBytes, $receivedBytes, true, $tcpError, $requestId);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
|
|||||||
Reference in New Issue
Block a user