Settle late TCP usage asynchronously

This commit is contained in:
seo
2026-06-15 11:00:07 +09:00
parent 7b9b41f144
commit f3abb812a4
2 changed files with 186 additions and 20 deletions
+171 -19
View File
@@ -15,6 +15,7 @@ define('TCP_HOST', (string)($carTcpConfig['host'] ?? ''));
define('TCP_PORT', (int)($carTcpConfig['port'] ?? 3400));
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('USER', (string)($carIdentityConfig['user'] ?? ''));
@@ -39,22 +40,27 @@ 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>[^\/]+)/';
function db(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
return $pdo;
}
function db(bool $fresh = false): PDO
{
static $pdo = null;
if (!$fresh && $pdo instanceof PDO) {
return $pdo;
}
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$conn = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
if ($fresh) {
return $conn;
}
$pdo = $conn;
return $pdo;
}
function b(string $ch): int
{
@@ -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(
string $cmd,
int &$tcpMs,
@@ -261,7 +398,6 @@ function tcp_request(
}
$readMs = (int)round((microtime(true) - $r0) * 1000);
fclose($fp);
$tcpMs = $connectMs + $readMs;
$receivedBytes = strlen($response);
@@ -270,16 +406,32 @@ function tcp_request(
if ($tcpError === '') {
$tcpError = 'total_timeout_4_95s';
}
record_tcp_usage($usageSource, $cmd, $sentBytes, $receivedBytes, false, $tcpError, $requestId);
$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);
}
return '';
}
if (!preg_match(RAW_FULL_REGEX, trim($response))) {
$tcpError = 'invalid_or_partial_response';
record_tcp_usage($usageSource, $cmd, $sentBytes, $receivedBytes, false, $tcpError, $requestId);
$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);
}
return '';
}
fclose($fp);
record_tcp_usage($usageSource, $cmd, $sentBytes, $receivedBytes, true, $tcpError, $requestId);
return $response;