Use prebuilt route artifacts during hot reload

This commit is contained in:
2026-07-10 17:08:38 +08:00
parent e7d5d1b567
commit 7bc8d04df3
3 changed files with 116 additions and 21 deletions
+101 -13
View File
@@ -8,21 +8,78 @@ class RouteArtifactState
{ {
public function store(string $type, array $artifact): void public function store(string $type, array $artifact): void
{ {
$previous = $this->loadPayload($type);
$payload = [ $payload = [
'timestamp' => time(), 'timestamp' => time(),
'type' => $type, 'type' => $type,
'artifact' => $artifact, 'artifact' => $artifact,
'build' => is_array($previous['build'] ?? null) ? $previous['build'] : null,
]; ];
$directory = dirname($this->getFilePath($type)); $this->writePayload($type, $payload);
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
file_put_contents($this->getFilePath($type), json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
} }
public function load(string $type): array public function load(string $type): array
{
$data = $this->loadPayload($type);
return is_array($data['artifact'] ?? null) ? $data['artifact'] : [];
}
public function markBuild(string $type, array $changedFiles): void
{
$data = $this->loadPayload($type);
if (!is_array($data['artifact'] ?? null)) {
return;
}
$data['build'] = [
'generation' => time(),
'timestamp' => time(),
'changed_files' => $this->normalizeFiles($changedFiles),
];
$this->writePayload($type, $data);
}
public function clearBuild(string $type): void
{
$data = $this->loadPayload($type);
if (empty($data)) {
return;
}
$data['build'] = null;
$this->writePayload($type, $data);
}
public function coversChangedFiles(string $type, array $changedFiles): bool
{
if (empty($changedFiles)) {
return false;
}
$data = $this->loadPayload($type);
$build = is_array($data['build'] ?? null) ? $data['build'] : [];
$builtFiles = is_array($build['changed_files'] ?? null) ? $build['changed_files'] : [];
if (empty($builtFiles)) {
return false;
}
$built = array_fill_keys($this->normalizeFiles($builtFiles), true);
foreach ($this->normalizeFiles($changedFiles) as $file) {
if (!isset($built[$file])) {
return false;
}
}
return true;
}
public function has(string $type): bool
{
return file_exists($this->getFilePath($type));
}
private function loadPayload(string $type): array
{ {
$file = $this->getFilePath($type); $file = $this->getFilePath($type);
if (!file_exists($file)) { if (!file_exists($file)) {
@@ -30,16 +87,47 @@ class RouteArtifactState
} }
$data = json_decode((string)file_get_contents($file), true); $data = json_decode((string)file_get_contents($file), true);
if (!is_array($data)) { return is_array($data) ? $data : [];
return [];
}
return is_array($data['artifact'] ?? null) ? $data['artifact'] : [];
} }
public function has(string $type): bool private function writePayload(string $type, array $payload): void
{ {
return file_exists($this->getFilePath($type)); $file = $this->getFilePath($type);
$directory = dirname($file);
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$tmpFile = $file . '.tmp.' . getmypid();
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
if (file_put_contents($tmpFile, $json, LOCK_EX) === false) {
throw new \RuntimeException('Unable to write route artifact: ' . $tmpFile);
}
if (@rename($tmpFile, $file)) {
return;
}
// Windows rename() cannot replace an existing file.
if (is_file($file) && @unlink($file) && @rename($tmpFile, $file)) {
return;
}
@unlink($tmpFile);
throw new \RuntimeException('Unable to publish route artifact: ' . $file);
}
private function normalizeFiles(array $files): array
{
$normalized = [];
foreach ($files as $file) {
if (!is_string($file) || $file === '') {
continue;
}
$normalized[] = str_replace('\\', '/', realpath($file) ?: $file);
}
return array_values(array_unique($normalized));
} }
private function getFilePath(string $type): string private function getFilePath(string $type): string
+15 -4
View File
@@ -214,6 +214,18 @@ class Router
$changedFiles = $container->get(HotReloadState::class)->consume(); $changedFiles = $container->get(HotReloadState::class)->consume();
$artifactState = $container->get(RouteArtifactState::class);
if (getenv('KIRI_FILE_BUILD') !== '1' && !empty($changedFiles) && $artifactState->coversChangedFiles(static::$type, $changedFiles)) {
$container->get(DataGrip::class)->reset(static::$type);
$router = $container->get(DataGrip::class)->get(static::$type);
if ($router->importArtifact($artifactState->load(static::$type))) {
$this->read_dir_file(APP_PATH . 'routes');
$this->reset($container);
$coordinator->done();
return;
}
}
// Worker 首次启动(无变更文件 + Master 已完成扫描): // Worker 首次启动(无变更文件 + Master 已完成扫描):
// 重新 include 路由文件(Router::get/post 显式注册) + 基于 Master 扫描清单重建注解路由 // 重新 include 路由文件(Router::get/post 显式注册) + 基于 Master 扫描清单重建注解路由
// 避免 opcache_compile_file,仅用 Reflection 重建路由,内存开销极小 // 避免 opcache_compile_file,仅用 Reflection 重建路由,内存开销极小
@@ -232,7 +244,6 @@ class Router
$container->get(DataGrip::class)->reset(static::$type); $container->get(DataGrip::class)->reset(static::$type);
$scanner = $container->get(Kiri\Di\Scanner::class); $scanner = $container->get(Kiri\Di\Scanner::class);
$artifactState = $container->get(RouteArtifactState::class);
$scanConfig = array_merge( $scanConfig = array_merge(
config('servers.reload.scan', []), config('servers.reload.scan', []),
config('site.scanner', []) config('site.scanner', [])
@@ -241,12 +252,12 @@ class Router
$normalizedAppPath = str_replace('\\', '/', realpath(APP_PATH . 'app') ?: APP_PATH . 'app'); $normalizedAppPath = str_replace('\\', '/', realpath(APP_PATH . 'app') ?: APP_PATH . 'app');
$normalizedRoutePath = str_replace('\\', '/', realpath(APP_PATH . 'routes') ?: APP_PATH . 'routes'); $normalizedRoutePath = str_replace('\\', '/', realpath(APP_PATH . 'routes') ?: APP_PATH . 'routes');
$routeChanged = false; $routeChangedFiles = [];
$appChangedFiles = []; $appChangedFiles = [];
foreach ($changedFiles as $changedFile) { foreach ($changedFiles as $changedFile) {
if (str_starts_with($changedFile, $normalizedRoutePath . '/')) { if (str_starts_with($changedFile, $normalizedRoutePath . '/')) {
$routeChanged = true; $routeChangedFiles[] = $changedFile;
continue; continue;
} }
@@ -265,7 +276,7 @@ class Router
if ($canUseArtifact) { if ($canUseArtifact) {
$artifact = $artifactState->load(static::$type); $artifact = $artifactState->load(static::$type);
$router = $container->get(DataGrip::class)->get(static::$type); $router = $container->get(DataGrip::class)->get(static::$type);
$usedArtifact = $router->importArtifact($artifact, $appChangedFiles); $usedArtifact = $router->importArtifact($artifact, array_merge($appChangedFiles, $routeChangedFiles));
} }
// routes 目录中的显式路由文件必须每次重建路由表时重新 include。 // routes 目录中的显式路由文件必须每次重建路由表时重新 include。
-4
View File
@@ -282,10 +282,6 @@ class RouterCollector implements \ArrayAccess, \IteratorAggregate
public function importArtifact(array $artifact, array $excludeSourceFiles = []): bool public function importArtifact(array $artifact, array $excludeSourceFiles = []): bool
{ {
if (($artifact['has_closure_routes'] ?? false) === true) {
return false;
}
$entries = $artifact['entries'] ?? null; $entries = $artifact['entries'] ?? null;
if (!is_array($entries)) { if (!is_array($entries)) {
return false; return false;