Files
CLOAK/lib/Cloak/VisitorLogQueue.php
T

219 lines
5.8 KiB
PHP
Raw Normal View History

2026-06-14 14:00:24 +08:00
<?php
/**
* 访客日志异步写入队列(Redis 优先,文件降级)
*/
class VisitorLogQueue
{
const REDIS_KEY = 'cloak:visitor_log_queue';
const FILE_DIR = 'visitor_log_queue';
/** @var bool|null */
private static $redisAvailable = null;
/**
* @param array $job 需含 type: insert_full|enrich
* @return bool
*/
public static function push(array $job)
{
$job['queued_at'] = date('Y-m-d H:i:s');
$payload = json_encode($job, JSON_UNESCAPED_UNICODE);
if ($payload === false) {
return false;
}
$redis = self::redisClient();
if ($redis) {
try {
return (bool) $redis->lPush(self::REDIS_KEY, $payload);
} catch (Throwable $e) {
self::logError('Redis push failed: ' . $e->getMessage());
}
}
return self::pushFile($payload);
}
/**
* @return array<int, array>
*/
public static function popBatch(int $limit = 100)
{
$limit = max(1, min(500, $limit));
$jobs = [];
$redis = self::redisClient();
if ($redis) {
try {
for ($i = 0; $i < $limit; $i++) {
$raw = $redis->rPop(self::REDIS_KEY);
if ($raw === false || $raw === null) {
break;
}
$decoded = json_decode((string) $raw, true);
if (is_array($decoded)) {
$jobs[] = $decoded;
}
}
if ($jobs !== []) {
return $jobs;
}
} catch (Throwable $e) {
self::logError('Redis pop failed: ' . $e->getMessage());
}
}
return self::popFileBatch($limit);
}
/**
* 请求 shutdown 时轻量 drain(最多 $limit 条)
*/
public static function drain(int $limit = 5)
{
if (!class_exists('VisitorLogWorker', false)) {
require_once __DIR__ . '/VisitorLogWorker.php';
}
VisitorLogWorker::processBatch($limit);
}
/**
* @return Redis|null
*/
private static function redisClient()
{
if (self::$redisAvailable === false) {
return null;
}
if (!extension_loaded('redis') || !defined('REDIS_ENABLED') || strtoupper(REDIS_ENABLED) !== 'ON') {
self::$redisAvailable = false;
return null;
}
try {
$r = new Redis();
$timeout = defined('REDIS_TIMEOUT') ? (float) REDIS_TIMEOUT : 1.0;
if (!@$r->connect(REDIS_HOST, (int) REDIS_PORT, $timeout)) {
self::$redisAvailable = false;
return null;
}
if (defined('REDIS_PASSWORD') && REDIS_PASSWORD !== '') {
$r->auth(REDIS_PASSWORD);
}
self::$redisAvailable = true;
return $r;
} catch (Throwable $e) {
self::$redisAvailable = false;
return null;
}
}
private static function queueDir()
{
$dir = dirname(__DIR__, 2) . '/storage/' . self::FILE_DIR;
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
return $dir;
}
private static function pushFile($payload)
{
$dir = self::queueDir();
$file = $dir . '/pending.ndjson';
$fp = @fopen($file, 'ab');
if (!$fp) {
return false;
}
if (flock($fp, LOCK_EX)) {
fwrite($fp, $payload . "\n");
fflush($fp);
flock($fp, LOCK_UN);
}
fclose($fp);
return true;
}
/**
* @return array<int, array>
*/
private static function popFileBatch(int $limit)
{
$dir = self::queueDir();
$file = $dir . '/pending.ndjson';
if (!is_file($file)) {
return [];
}
$fp = @fopen($file, 'c+');
if (!$fp) {
return [];
}
$jobs = [];
if (!flock($fp, LOCK_EX)) {
fclose($fp);
return [];
}
$lines = [];
while (($line = fgets($fp)) !== false) {
$line = trim($line);
if ($line !== '') {
$lines[] = $line;
}
}
if ($lines === []) {
flock($fp, LOCK_UN);
fclose($fp);
return [];
}
$take = array_slice($lines, 0, $limit);
$remain = array_slice($lines, $limit);
ftruncate($fp, 0);
rewind($fp);
foreach ($remain as $line) {
fwrite($fp, $line . "\n");
}
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
foreach ($take as $line) {
$decoded = json_decode($line, true);
if (is_array($decoded)) {
$jobs[] = $decoded;
}
}
return $jobs;
}
/**
* @param array $job
* @param string $error
*/
public static function moveToDead(array $job, $error)
{
$dir = self::queueDir() . '/dead';
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
$job['dead_at'] = date('Y-m-d H:i:s');
$job['dead_error'] = (string) $error;
$name = $dir . '/' . date('Ymd_His') . '_' . substr(md5(json_encode($job)), 0, 8) . '.json';
@file_put_contents($name, json_encode($job, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
self::logError('Dead letter: ' . $error);
}
public static function logError($message)
{
$handle = @fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
if ($handle) {
fwrite($handle, date('Y-m-d H:i:s') . ' | VisitorLogQueue | ' . $message . "\n");
fclose($handle);
}
}
}