lthn.io/app/Core/Storage/Commands/WarmCacheCommand.php
Claude 41a90cbff8
feat: lthn.io API serving live chain data
Fixed: basePath self→static binding, namespace detection, event wiring,
SQLite cache, file cache driver. All Mod Boot classes converted to
$listens pattern for lifecycle event discovery.

Working endpoints:
- /v1/explorer/info — live chain height, difficulty, aliases
- /v1/explorer/stats — formatted chain statistics
- /v1/names/directory — alias directory grouped by type
- /v1/names/available/{name} — name availability check
- /v1/names/lookup/{name} — name details

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 17:17:42 +01:00

294 lines
9.2 KiB
PHP

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Storage\Commands;
use Core\Storage\CacheWarmer;
use Illuminate\Console\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
/**
* Warm registered cache items.
*
* Pre-populates the cache with frequently accessed data to prevent
* cold cache problems after deployments or cache flushes.
*
* Usage:
* php artisan cache:warm # Warm all registered items
* php artisan cache:warm --stale # Only warm stale/missing items
* php artisan cache:warm --status # Show warming status
* php artisan cache:warm --key=foo # Warm specific key
*
* Scheduling (in app/Console/Kernel.php):
* $schedule->command('cache:warm --stale')->hourly();
*/
class WarmCacheCommand extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'cache:warm
{--stale : Only warm stale (missing/expired) items}
{--key= : Warm a specific key only}
{--status : Show warming status without warming}
{--store= : Use a specific cache store}';
/**
* The console command description.
*/
protected $description = 'Warm registered cache items to prevent cold cache problems';
/**
* Execute the console command.
*/
public function handle(CacheWarmer $warmer): int
{
$this->newLine();
$this->components->info('Cache Warming');
$this->newLine();
// Configure store if specified
if ($store = $this->option('store')) {
$warmer->useStore($store);
$this->components->twoColumnDetail('Using store', "<fg=cyan>{$store}</>");
}
// Status mode
if ($this->option('status')) {
return $this->showStatus($warmer);
}
// Single key mode
if ($key = $this->option('key')) {
return $this->warmSingleKey($warmer, $key);
}
// Check if any items are registered
$registeredKeys = $warmer->getRegisteredKeys();
if (empty($registeredKeys)) {
$this->components->warn('No cache items registered for warming.');
$this->newLine();
$this->components->bulletList([
'Register items in a service provider using CacheWarmer::register()',
'Example: $warmer->register(\'config\', fn() => Config::all());',
]);
$this->newLine();
return self::SUCCESS;
}
$this->components->twoColumnDetail('Registered items', '<fg=cyan>'.count($registeredKeys).'</>');
$this->newLine();
// Warm items
$staleOnly = $this->option('stale');
$results = $staleOnly
? $warmer->warmStale()
: $warmer->warmAll();
// Display results
$this->displayResults($results, $staleOnly);
return self::SUCCESS;
}
/**
* Show warming status without warming.
*/
protected function showStatus(CacheWarmer $warmer): int
{
$status = $warmer->getStatus();
if (empty($status)) {
$this->components->warn('No cache items registered for warming.');
$this->newLine();
return self::SUCCESS;
}
$this->components->twoColumnDetail('<fg=gray;options=bold>Registered Items</>', '');
foreach ($status as $key => $info) {
$cachedStatus = match ($info['cached']) {
true => '<fg=green>cached</>',
false => '<fg=yellow>not cached</>',
null => '<fg=gray>batch</>',
};
$typeLabel = $info['type'] === 'batch'
? "[batch:{$info['batch_size']}]"
: '';
$this->components->twoColumnDetail(
"<fg=cyan>{$key}</> {$typeLabel}",
"{$cachedStatus} (TTL: {$info['ttl']}s)"
);
}
$this->newLine();
// Show stats summary
$stats = $warmer->getStats();
$this->components->twoColumnDetail('<fg=gray;options=bold>Summary</>', '');
$this->components->twoColumnDetail('Registered', "<fg=cyan>{$stats['total_registered']}</>");
$this->components->twoColumnDetail('Cached', "<fg=cyan>{$stats['total_cached']}</>");
$this->components->twoColumnDetail('Cache rate', "<fg=cyan>{$stats['cache_rate']}%</>");
if ($stats['batch_items'] > 0) {
$this->components->twoColumnDetail('Batch items', "<fg=cyan>{$stats['batch_items']}</>");
}
$this->newLine();
return self::SUCCESS;
}
/**
* Warm a single key.
*/
protected function warmSingleKey(CacheWarmer $warmer, string $key): int
{
if (! $warmer->isRegistered($key)) {
$this->components->error("Key '{$key}' is not registered for warming.");
$this->newLine();
// Show available keys
$availableKeys = $warmer->getRegisteredKeys();
if (! empty($availableKeys)) {
$this->components->info('Available keys:');
$this->components->bulletList($availableKeys);
$this->newLine();
}
return self::FAILURE;
}
$this->components->task(
"Warming key: {$key}",
function () use ($warmer, $key) {
return $warmer->warm($key);
}
);
$this->newLine();
$results = $warmer->getLastResults();
if (isset($results[$key])) {
$result = $results[$key];
if ($result['status'] === 'success') {
$this->components->info("Successfully warmed '{$key}' in {$result['duration']}s");
} else {
$this->components->error("Failed to warm '{$key}': {$result['error']}");
return self::FAILURE;
}
}
$this->newLine();
return self::SUCCESS;
}
/**
* Display warming results.
*
* @param array<string, array{status: string, duration: float, error?: string}> $results
*/
protected function displayResults(array $results, bool $staleOnly): void
{
$successes = 0;
$failures = 0;
$skipped = 0;
$totalDuration = 0.0;
$this->components->twoColumnDetail('<fg=gray;options=bold>Results</>', '');
foreach ($results as $key => $result) {
if ($key === '_disabled') {
$this->components->warn('Cache warming is disabled.');
$this->newLine();
return;
}
$statusLabel = match ($result['status']) {
'success' => '<fg=green>warmed</>',
'failed' => '<fg=red>failed</>',
'exists' => '<fg=gray>exists</>',
'skipped' => '<fg=gray>skipped</>',
default => "<fg=yellow>{$result['status']}</>",
};
$duration = $result['duration'] > 0
? sprintf('%.3fs', $result['duration'])
: '';
$this->components->twoColumnDetail(
"<fg=cyan>{$key}</>",
"{$statusLabel} {$duration}"
);
if (isset($result['error'])) {
$this->line(" <fg=red>Error: {$result['error']}</>");
}
match ($result['status']) {
'success' => $successes++,
'failed' => $failures++,
'exists', 'skipped' => $skipped++,
default => null,
};
$totalDuration += $result['duration'];
}
$this->newLine();
// Summary
$this->components->twoColumnDetail('<fg=gray;options=bold>Summary</>', '');
$this->components->twoColumnDetail('Warmed', "<fg=green>{$successes}</>");
if ($failures > 0) {
$this->components->twoColumnDetail('Failed', "<fg=red>{$failures}</>");
}
if ($staleOnly && $skipped > 0) {
$this->components->twoColumnDetail('Already cached', "<fg=gray>{$skipped}</>");
}
$this->components->twoColumnDetail('Total time', sprintf('<fg=cyan>%.3fs</>', $totalDuration));
$this->newLine();
if ($failures > 0) {
$this->components->warn('Some items failed to warm. Check the logs for details.');
$this->newLine();
}
}
/**
* Get shell completion suggestions for options.
*/
public function complete(
CompletionInput $input,
CompletionSuggestions $suggestions
): void {
if ($input->mustSuggestOptionValuesFor('store')) {
// Suggest common cache stores
$suggestions->suggestValues(['file', 'redis', 'database', 'array', 'memcached']);
}
if ($input->mustSuggestOptionValuesFor('key')) {
// Suggest registered keys
try {
$warmer = app(CacheWarmer::class);
$suggestions->suggestValues($warmer->getRegisteredKeys());
} catch (\Throwable) {
// Ignore if container not available
}
}
}
}