docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Commands;
use App\Framework\Console\Attributes\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Queue\Services\DeadLetterManager;
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
/**
* Console commands for Dead Letter Queue management
*/
final readonly class DeadLetterQueueCommands
{
public function __construct(
private DeadLetterManager $deadLetterManager
) {}
#[ConsoleCommand(name: 'queue:failed', description: 'List failed jobs in dead letter queues')]
public function listFailedJobs(ConsoleInput $input, ConsoleOutput $output): int
{
$limit = (int) $input->getOption('limit', 50);
$queue = $input->getOption('queue');
$output->writeLine("📋 Failed Jobs");
$output->writeLine(str_repeat('-', 50));
$failedJobs = $this->deadLetterManager->getFailedJobs(
originalQueue: $queue ? \App\Framework\Queue\ValueObjects\QueueName::fromString($queue) : null,
limit: $limit
);
if (empty($failedJobs)) {
$output->writeLine("✅ No failed jobs found");
return 0;
}
foreach ($failedJobs as $job) {
$output->writeLine("🔴 Job ID: {$job->id}");
$output->writeLine(" Original Job: {$job->originalJobId}");
$output->writeLine(" Queue: {$job->originalQueue}{$job->deadLetterQueue}");
$output->writeLine(" Failed: {$job->failedAt} ({$job->failedAttempts} attempts)");
$output->writeLine(" Reason: {$job->failureReason}");
if ($job->exceptionType) {
$output->writeLine(" Exception: {$job->exceptionType}");
}
$output->writeLine(" Retries: {$job->retryCount}");
$output->writeLine("");
}
$output->writeLine("Total: " . count($failedJobs) . " failed jobs");
return 0;
}
#[ConsoleCommand(name: 'queue:retry', description: 'Retry a failed job from dead letter queue')]
public function retryJob(ConsoleInput $input, ConsoleOutput $output): int
{
$jobId = $input->getArgument('job_id');
if (!$jobId) {
$output->writeLine("❌ Job ID is required");
$output->writeLine("Usage: queue:retry <job_id>");
return 1;
}
$success = $this->deadLetterManager->retryJob($jobId);
if ($success) {
$output->writeLine("✅ Job {$jobId} successfully retried");
} else {
$output->writeLine("❌ Failed to retry job {$jobId}");
return 1;
}
return 0;
}
#[ConsoleCommand(name: 'queue:retry-all', description: 'Retry all jobs in a dead letter queue')]
public function retryAllJobs(ConsoleInput $input, ConsoleOutput $output): int
{
$queueName = $input->getArgument('queue_name');
if (!$queueName) {
$output->writeLine("❌ Queue name is required");
$output->writeLine("Usage: queue:retry-all <queue_name>");
return 1;
}
$deadLetterQueueName = DeadLetterQueueName::fromString($queueName);
$retriedCount = $this->deadLetterManager->retryAllJobs($deadLetterQueueName);
$output->writeLine("✅ Successfully retried {$retriedCount} jobs from queue '{$queueName}'");
return 0;
}
#[ConsoleCommand(name: 'queue:clear-failed', description: 'Clear all jobs from a dead letter queue')]
public function clearDeadLetterQueue(ConsoleInput $input, ConsoleOutput $output): int
{
$queueName = $input->getArgument('queue_name');
if (!$queueName) {
$output->writeLine("❌ Queue name is required");
$output->writeLine("Usage: queue:clear-failed <queue_name>");
return 1;
}
$confirm = $input->getOption('force', false);
if (!$confirm) {
$output->writeLine("⚠️ This will permanently delete all failed jobs in queue '{$queueName}'");
$output->writeLine("Use --force to confirm");
return 1;
}
$deadLetterQueueName = DeadLetterQueueName::fromString($queueName);
$deletedCount = $this->deadLetterManager->clearDeadLetterQueue($deadLetterQueueName);
$output->writeLine("✅ Cleared {$deletedCount} jobs from dead letter queue '{$queueName}'");
return 0;
}
#[ConsoleCommand(name: 'queue:failed-stats', description: 'Show statistics for dead letter queues')]
public function showStats(ConsoleInput $input, ConsoleOutput $output): int
{
$output->writeLine("📊 Dead Letter Queue Statistics");
$output->writeLine(str_repeat('=', 50));
$stats = $this->deadLetterManager->getStatistics();
if (empty($stats)) {
$output->writeLine("✅ No dead letter queues found");
return 0;
}
foreach ($stats as $queueName => $queueStats) {
$output->writeLine("🔴 Queue: {$queueName}");
$output->writeLine(" Total Jobs: {$queueStats['total_jobs']}");
$output->writeLine(" Avg Failed Attempts: {$queueStats['avg_failed_attempts']}");
$output->writeLine(" Max Failed Attempts: {$queueStats['max_failed_attempts']}");
$output->writeLine(" Avg Retries: {$queueStats['avg_retry_count']}");
$output->writeLine(" Max Retries: {$queueStats['max_retry_count']}");
$output->writeLine(" Oldest Job: {$queueStats['oldest_job']}");
$output->writeLine(" Newest Job: {$queueStats['newest_job']}");
$output->writeLine("");
}
return 0;
}
#[ConsoleCommand(name: 'queue:delete-failed', description: 'Delete a specific failed job')]
public function deleteJob(ConsoleInput $input, ConsoleOutput $output): int
{
$jobId = $input->getArgument('job_id');
if (!$jobId) {
$output->writeLine("❌ Job ID is required");
$output->writeLine("Usage: queue:delete-failed <job_id>");
return 1;
}
$confirm = $input->getOption('force', false);
if (!$confirm) {
$output->writeLine("⚠️ This will permanently delete the failed job {$jobId}");
$output->writeLine("Use --force to confirm");
return 1;
}
$success = $this->deadLetterManager->deleteJob($jobId);
if ($success) {
$output->writeLine("✅ Job {$jobId} successfully deleted");
} else {
$output->writeLine("❌ Failed to delete job {$jobId} (job not found)");
return 1;
}
return 0;
}
}

View File

@@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Commands;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Queue\Contracts\JobBatchManagerInterface;
use App\Framework\Queue\ValueObjects\JobBatchStatus;
use App\Framework\Attributes\ConsoleCommand as ConsoleCommandAttribute;
/**
* Console commands for job batch management
*/
final readonly class JobBatchCommands
{
public function __construct(
private JobBatchManagerInterface $batchManager
) {}
#[ConsoleCommandAttribute(name: 'batch:list', description: 'List job batches')]
public function listBatches(string $status = 'all', int $limit = 20): void
{
if ($status === 'all') {
// Get batches for all statuses
$batches = [];
foreach (JobBatchStatus::cases() as $batchStatus) {
$statusBatches = $this->batchManager->getBatchesByStatus($batchStatus, $limit);
$batches = array_merge($batches, $statusBatches);
}
// Sort by created_at desc
usort($batches, fn($a, $b) => $b->createdAt?->getTimestamp() <=> $a->createdAt?->getTimestamp());
$batches = array_slice($batches, 0, $limit);
} else {
try {
$batchStatus = JobBatchStatus::from($status);
$batches = $this->batchManager->getBatchesByStatus($batchStatus, $limit);
} catch (\ValueError) {
echo "❌ Invalid status: {$status}. Valid statuses: " .
implode(', ', array_map(fn($s) => $s->value, JobBatchStatus::cases())) . "\n";
return;
}
}
if (empty($batches)) {
echo "No batches found" . ($status !== 'all' ? " with status: {$status}" : '') . "\n";
return;
}
echo "📋 Job Batches" . ($status !== 'all' ? " ({$status})" : '') . ":\n\n";
foreach ($batches as $batch) {
$icon = $batch->status->getIcon();
$progress = $batch->getProgressPercentage();
$remaining = $batch->getRemainingJobs();
echo "{$icon} {$batch->batchId}\n";
echo " Name: {$batch->name}\n";
echo " Status: {$batch->status->getDisplayName()}\n";
echo " Progress: {$progress}% ({$batch->processedJobs}/{$batch->totalJobs} completed";
if ($batch->failedJobs > 0) {
echo ", {$batch->failedJobs} failed";
}
if ($remaining > 0) {
echo ", {$remaining} remaining";
}
echo ")\n";
echo " Created: {$batch->createdAt?->toRfc3339()}\n";
if ($batch->startedAt) {
echo " Started: {$batch->startedAt->toRfc3339()}\n";
}
if ($batch->completedAt) {
echo " Completed: {$batch->completedAt->toRfc3339()}\n";
}
echo "\n";
}
}
#[ConsoleCommandAttribute(name: 'batch:show', description: 'Show detailed batch information')]
public function showBatch(string $batchId): void
{
$batch = $this->batchManager->getBatch($batchId);
if (!$batch) {
echo "❌ Batch not found: {$batchId}\n";
return;
}
$icon = $batch->status->getIcon();
$progress = $batch->getProgressPercentage();
echo "{$icon} Batch Details\n";
echo "================\n\n";
echo "ID: {$batch->batchId}\n";
echo "Name: {$batch->name}\n";
echo "Status: {$batch->status->getDisplayName()}\n";
echo "Progress: {$progress}%\n\n";
echo "Job Statistics:\n";
echo " Total Jobs: {$batch->totalJobs}\n";
echo " Processed: {$batch->processedJobs}\n";
echo " Failed: {$batch->failedJobs}\n";
echo " Remaining: {$batch->getRemainingJobs()}\n\n";
echo "Timestamps:\n";
echo " Created: {$batch->createdAt?->toRfc3339()}\n";
if ($batch->startedAt) {
echo " Started: {$batch->startedAt->toRfc3339()}\n";
}
if ($batch->completedAt) {
echo " Completed: {$batch->completedAt->toRfc3339()}\n";
}
if ($batch->failedAt) {
echo " Failed: {$batch->failedAt->toRfc3339()}\n";
}
echo "\n";
if (!empty($batch->options)) {
echo "Options:\n";
foreach ($batch->options as $key => $value) {
echo " {$key}: " . (is_array($value) ? json_encode($value) : $value) . "\n";
}
echo "\n";
}
echo "Job IDs:\n";
foreach ($batch->jobIds as $index => $jobId) {
echo " " . ($index + 1) . ". {$jobId}\n";
}
}
#[ConsoleCommandAttribute(name: 'batch:cancel', description: 'Cancel a job batch')]
public function cancelBatch(string $batchId): void
{
$batch = $this->batchManager->getBatch($batchId);
if (!$batch) {
echo "❌ Batch not found: {$batchId}\n";
return;
}
if ($batch->isFinished()) {
echo "❌ Cannot cancel batch: already finished with status {$batch->status->getDisplayName()}\n";
return;
}
$success = $this->batchManager->cancelBatch($batchId);
if ($success) {
echo "✅ Batch cancelled successfully: {$batchId}\n";
} else {
echo "❌ Failed to cancel batch: {$batchId}\n";
}
}
#[ConsoleCommandAttribute(name: 'batch:stats', description: 'Show batch statistics')]
public function showStats(): void
{
$stats = $this->batchManager->getBatchStats();
if (empty($stats)) {
echo "No batch statistics available\n";
return;
}
echo "📊 Batch Statistics\n";
echo "==================\n\n";
$totalBatches = 0;
$totalJobs = 0;
$totalFailed = 0;
foreach ($stats as $status => $data) {
try {
$statusEnum = JobBatchStatus::from($status);
$icon = $statusEnum->getIcon();
$displayName = $statusEnum->getDisplayName();
} catch (\ValueError) {
$icon = '❓';
$displayName = ucfirst($status);
}
echo "{$icon} {$displayName}:\n";
echo " Batches: {$data['count']}\n";
echo " Total Jobs: {$data['total_jobs']}\n";
echo " Failed Jobs: {$data['total_failed']}\n";
echo " Avg Processed: " . number_format($data['avg_processed'], 2) . "\n\n";
$totalBatches += $data['count'];
$totalJobs += $data['total_jobs'];
$totalFailed += $data['total_failed'];
}
echo "Overall:\n";
echo " Total Batches: {$totalBatches}\n";
echo " Total Jobs: {$totalJobs}\n";
echo " Total Failed: {$totalFailed}\n";
if ($totalJobs > 0) {
$failureRate = ($totalFailed / $totalJobs) * 100;
echo " Failure Rate: " . number_format($failureRate, 2) . "%\n";
}
}
#[ConsoleCommandAttribute(name: 'batch:cleanup', description: 'Cleanup old finished batches')]
public function cleanupBatches(int $olderThanDays = 30): void
{
if ($olderThanDays < 1) {
echo "❌ Days must be at least 1\n";
return;
}
echo "🧹 Cleaning up batches older than {$olderThanDays} days...\n";
$deleted = $this->batchManager->cleanupOldBatches($olderThanDays);
if ($deleted > 0) {
echo "✅ Cleaned up {$deleted} old batches\n";
} else {
echo " No old batches to clean up\n";
}
}
#[ConsoleCommandAttribute(name: 'batch:progress', description: 'Show batch progress')]
public function showProgress(string $batchId): void
{
$progress = $this->batchManager->getBatchProgress($batchId);
if (empty($progress)) {
echo "❌ Batch not found: {$batchId}\n";
return;
}
$statusEnum = JobBatchStatus::from($progress['status']);
$icon = $statusEnum->getIcon();
echo "{$icon} {$progress['name']} ({$progress['batch_id']})\n";
echo "Status: {$statusEnum->getDisplayName()}\n";
echo "Progress: {$progress['progress_percentage']}%\n";
// Visual progress bar
$barWidth = 40;
$completedWidth = (int) (($progress['progress_percentage'] / 100) * $barWidth);
$bar = str_repeat('█', $completedWidth) . str_repeat('░', $barWidth - $completedWidth);
echo "[{$bar}] {$progress['processed_jobs']}/{$progress['total_jobs']}\n";
if ($progress['failed_jobs'] > 0) {
echo "❌ Failed: {$progress['failed_jobs']}\n";
}
if ($progress['remaining_jobs'] > 0) {
echo "⏳ Remaining: {$progress['remaining_jobs']}\n";
}
if ($progress['is_finished']) {
echo "✅ Batch completed\n";
}
}
}

View File

@@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Commands;
use App\Framework\Console\Attributes\ConsoleCommand;
use App\Framework\Queue\Contracts\JobChainManagerInterface;
use App\Framework\Queue\ValueObjects\JobChain;
use App\Framework\Queue\ValueObjects\ChainExecutionMode;
use App\Framework\Queue\Services\JobChainExecutionCoordinator;
use App\Framework\Queue\Services\DependencyResolutionEngine;
final readonly class JobChainCommands
{
public function __construct(
private JobChainManagerInterface $chainManager,
private JobChainExecutionCoordinator $coordinator,
private DependencyResolutionEngine $resolutionEngine
) {}
#[ConsoleCommand(name: 'queue:chain:create', description: 'Create a new job chain')]
public function createChain(
string $chainId,
string $name,
string $jobIds,
string $executionMode = 'sequential',
bool $stopOnFailure = true
): void {
$jobIdArray = array_map('trim', explode(',', $jobIds));
$mode = ChainExecutionMode::from($executionMode);
$jobChain = JobChain::create(
chainId: $chainId,
name: $name,
jobIds: $jobIdArray,
executionMode: $mode,
stopOnFailure: $stopOnFailure
);
$this->chainManager->createChain($jobChain);
echo "✅ Job chain created: {$name} ({$chainId})\n";
echo " Jobs: " . implode(' → ', $jobIdArray) . "\n";
echo " Mode: {$executionMode}\n";
echo " Stop on failure: " . ($stopOnFailure ? 'Yes' : 'No') . "\n";
}
#[ConsoleCommand(name: 'queue:chain:start', description: 'Start execution of a job chain')]
public function startChain(string $chainId): void
{
try {
$this->coordinator->startChainExecution($chainId);
echo "🚀 Chain execution started: {$chainId}\n";
} catch (\Exception $e) {
echo "❌ Failed to start chain: {$e->getMessage()}\n";
}
}
#[ConsoleCommand(name: 'queue:chain:status', description: 'Get status of a job chain')]
public function getChainStatus(string $chainId): void
{
try {
$status = $this->coordinator->getChainExecutionStatus($chainId);
echo "📋 Chain Status: {$status['name']} ({$chainId})\n\n";
$statusIcon = match($status['status']) {
'pending' => '⏳',
'running' => '🔄',
'completed' => '✅',
'failed' => '❌',
default => '❓'
};
echo " Status: {$statusIcon} {$status['status']}\n";
echo " Execution Mode: {$status['execution_mode']}\n";
echo " Stop on Failure: " . ($status['progress']['stop_on_failure'] ? 'Yes' : 'No') . "\n";
if ($status['started_at']) {
echo " Started: {$status['started_at']}\n";
}
if ($status['completed_at']) {
echo " Completed: {$status['completed_at']}\n";
}
echo "\n📊 Progress:\n";
echo " {$status['progress']['completed_jobs']}/{$status['progress']['total_jobs']} jobs completed ({$status['progress']['percentage']}%)\n\n";
echo "🔗 Job Status:\n";
foreach ($status['job_statuses'] as $jobStatus) {
$canExecute = $jobStatus['can_execute'] ? '✅' : '⏳';
$depStatus = "{$jobStatus['dependencies_satisfied']}/{$jobStatus['dependencies_total']} deps";
echo " {$canExecute} Job {$jobStatus['position'] + 1}: {$jobStatus['job_id']} ({$depStatus})\n";
}
} catch (\Exception $e) {
echo "❌ Failed to get chain status: {$e->getMessage()}\n";
}
}
#[ConsoleCommand(name: 'queue:chain:list', description: 'List job chains by status')]
public function listChains(string $status = 'all'): void
{
$chains = match($status) {
'active' => $this->chainManager->getActiveChains(),
'pending' => $this->chainManager->getPendingChains(),
default => array_merge(
$this->chainManager->getPendingChains(),
$this->chainManager->getActiveChains()
)
};
echo "📋 Job Chains ({$status}):\n\n";
if (empty($chains)) {
echo " No chains found\n";
return;
}
foreach ($chains as $chain) {
$statusIcon = match($chain->status) {
'pending' => '⏳',
'running' => '🔄',
'completed' => '✅',
'failed' => '❌',
default => '❓'
};
$jobCount = count($chain->getJobIdsArray());
echo " {$statusIcon} {$chain->name} ({$chain->chainId})\n";
echo " Status: {$chain->status} | Jobs: {$jobCount} | Mode: {$chain->executionMode}\n";
if ($chain->startedAt) {
echo " Started: {$chain->startedAt}\n";
}
echo "\n";
}
}
#[ConsoleCommand(name: 'queue:chain:analyze', description: 'Analyze job chains for a specific job')]
public function analyzeChains(string $jobId): void
{
$analysis = $this->resolutionEngine->analyzeChains($jobId);
echo "🔍 Chain Analysis for job: {$jobId}\n\n";
if (empty($analysis['chains'])) {
echo " Job is not part of any chains\n";
return;
}
echo "📊 Total chains: {$analysis['total_chains']}\n\n";
foreach ($analysis['chains'] as $chain) {
$statusIcon = match($chain['status']) {
'pending' => '⏳',
'running' => '🔄',
'completed' => '✅',
'failed' => '❌',
default => '❓'
};
echo "🔗 {$statusIcon} {$chain['name']} ({$chain['chain_id']})\n";
echo " Status: {$chain['status']}\n";
echo " Mode: {$chain['execution_mode']}\n";
echo " Position: {$chain['job_position'] + 1}/{$chain['total_jobs']}\n";
if ($chain['next_job_after_current']) {
echo " Next job: {$chain['next_job_after_current']}\n";
}
echo " Progress: {$chain['progress']['completed_jobs']}/{$chain['progress']['total_jobs']} ({$chain['progress']['percentage']}%)\n\n";
}
}
#[ConsoleCommand(name: 'queue:chain:delete', description: 'Delete a job chain')]
public function deleteChain(string $chainId): void
{
$chain = $this->chainManager->getChain($chainId);
if (!$chain) {
echo "❌ Chain not found: {$chainId}\n";
return;
}
if ($chain->isRunning()) {
echo "❌ Cannot delete running chain. Chain must be completed or failed.\n";
return;
}
$this->chainManager->deleteChain($chainId);
echo "✅ Chain deleted: {$chainId}\n";
}
#[ConsoleCommand(name: 'queue:chain:cleanup', description: 'Clean up old completed chains')]
public function cleanupOldChains(int $olderThanDays = 30): void
{
$deletedCount = $this->chainManager->cleanupOldChains($olderThanDays);
echo "🧹 Cleaned up old chains\n";
echo " Deleted: {$deletedCount} completed/failed chains older than {$olderThanDays} days\n";
}
#[ConsoleCommand(name: 'queue:chain:progress', description: 'Show detailed progress of a chain')]
public function showProgress(string $chainId): void
{
try {
$progress = $this->chainManager->getChainProgress($chainId);
echo "📊 Chain Progress: {$progress['name']} ({$chainId})\n\n";
$statusIcon = match($progress['status']) {
'pending' => '⏳',
'running' => '🔄',
'completed' => '✅',
'failed' => '❌',
default => '❓'
};
echo " Status: {$statusIcon} {$progress['status']}\n";
echo " Mode: {$progress['execution_mode']}\n";
echo " Progress: {$progress['completed_jobs']}/{$progress['total_jobs']} jobs ({$progress['percentage']}%)\n";
if ($progress['started_at']) {
echo " Started: {$progress['started_at']}\n";
}
if ($progress['completed_at']) {
echo " Completed: {$progress['completed_at']}\n";
}
// Progress bar
$barLength = 30;
$filledLength = (int) (($progress['percentage'] / 100) * $barLength);
$bar = str_repeat('█', $filledLength) . str_repeat('░', $barLength - $filledLength);
echo "\n [{$bar}] {$progress['percentage']}%\n";
} catch (\Exception $e) {
echo "❌ Failed to get chain progress: {$e->getMessage()}\n";
}
}
}

View File

@@ -0,0 +1,328 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Commands;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ExitCode;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Queue\Services\JobCleanupService;
use App\Framework\Queue\Services\JobMemoryManager;
use App\Framework\Performance\MemoryMonitor;
final readonly class JobCleanupCommands
{
public function __construct(
private JobCleanupService $cleanupService,
private JobMemoryManager $memoryManager,
private MemoryMonitor $memoryMonitor
) {}
#[ConsoleCommand(name: 'queue:cleanup:all', description: 'Run comprehensive queue cleanup with default retention periods')]
public function runComprehensiveCleanup(): ExitCode
{
echo "🧹 Starting Comprehensive Queue Cleanup\n\n";
try {
// Show current memory status
$memorySummary = $this->memoryMonitor->getSummary();
echo "💾 Memory Status:\n";
echo " Current: {$memorySummary->getCurrentHumanReadable()}\n";
echo " Available: {$memorySummary->getAvailableMemory()->toHumanReadable()}\n";
echo " Usage: {$memorySummary->getUsagePercentageFormatted()}\n\n";
// Show cleanup statistics
echo "📊 Cleanup Statistics:\n";
$stats = $this->cleanupService->getCleanupStatistics();
echo " Eligible for cleanup:\n";
echo " • Completed jobs: " . number_format($stats['eligible_completed_jobs']) . " (>{$stats['retention_days']['completed_jobs']} days)\n";
echo " • Failed jobs: " . number_format($stats['eligible_failed_jobs']) . " (>{$stats['retention_days']['failed_jobs']} days)\n";
echo " • Job metrics: " . number_format($stats['eligible_metrics']) . " (>{$stats['retention_days']['metrics']} days)\n";
echo " • Dead letter jobs: " . number_format($stats['eligible_dead_letter_jobs']) . " (>{$stats['retention_days']['dead_letter_jobs']} days)\n";
echo " • Total eligible: " . number_format($stats['total_eligible']) . "\n";
echo " • Estimated time: ~{$stats['estimated_cleanup_minutes']} minutes\n\n";
if ($stats['total_eligible'] === 0) {
echo "✅ No data eligible for cleanup\n";
return ExitCode::SUCCESS;
}
echo "🔄 Starting cleanup process...\n\n";
// Run comprehensive cleanup
$results = $this->cleanupService->runComprehensiveCleanup();
// Display results
echo "✅ Cleanup Results:\n";
echo " • Completed jobs deleted: " . number_format($results['completed_jobs']) . "\n";
echo " • Failed jobs deleted: " . number_format($results['failed_jobs']) . "\n";
echo " • Job metrics deleted: " . number_format($results['job_metrics']) . "\n";
echo " • Dead letter jobs deleted: " . number_format($results['dead_letter_jobs']) . "\n";
echo " • Total deleted: " . number_format($results['total_deleted']) . "\n\n";
echo "⏱️ Performance:\n";
echo " • Duration: {$results['duration_seconds']} seconds\n";
echo " • Memory used: {$results['memory_used']}\n";
echo " • Peak memory: {$results['memory_peak']}\n\n";
if (!empty($results['errors'])) {
echo "⚠️ Errors occurred:\n";
foreach ($results['errors'] as $error) {
echo "{$error}\n";
}
return ExitCode::GENERAL_ERROR;
}
echo "✅ Cleanup completed successfully!\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:cleanup:completed', description: 'Clean up completed jobs older than specified days')]
public function cleanupCompletedJobs(int $days = 30): ExitCode
{
echo "🧹 Cleaning Completed Jobs\n\n";
try {
if ($days < 1) {
echo "❌ Days must be at least 1\n";
return ExitCode::INVALID_ARGUMENT;
}
echo "📊 Configuration:\n";
echo " • Delete completed jobs older than: {$days} days\n\n";
$duration = Duration::fromDays($days);
$deleted = $this->cleanupService->cleanupCompletedJobs($duration);
echo "✅ Cleanup Results:\n";
echo " • Completed jobs deleted: " . number_format($deleted) . "\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:cleanup:failed', description: 'Clean up failed jobs older than specified days')]
public function cleanupFailedJobs(int $days = 90): ExitCode
{
echo "🧹 Cleaning Failed Jobs\n\n";
try {
if ($days < 1) {
echo "❌ Days must be at least 1\n";
return ExitCode::INVALID_ARGUMENT;
}
echo "📊 Configuration:\n";
echo " • Delete failed jobs older than: {$days} days\n\n";
$duration = Duration::fromDays($days);
$deleted = $this->cleanupService->cleanupFailedJobs($duration);
echo "✅ Cleanup Results:\n";
echo " • Failed jobs deleted: " . number_format($deleted) . "\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:cleanup:metrics', description: 'Clean up job metrics older than specified days')]
public function cleanupMetrics(int $days = 180): ExitCode
{
echo "🧹 Cleaning Job Metrics\n\n";
try {
if ($days < 1) {
echo "❌ Days must be at least 1\n";
return ExitCode::INVALID_ARGUMENT;
}
echo "📊 Configuration:\n";
echo " • Delete metrics older than: {$days} days\n\n";
$duration = Duration::fromDays($days);
$deleted = $this->cleanupService->cleanupJobMetrics($duration);
echo "✅ Cleanup Results:\n";
echo " • Job metrics deleted: " . number_format($deleted) . "\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:cleanup:deadletter', description: 'Clean up dead letter jobs older than specified days')]
public function cleanupDeadLetterJobs(int $days = 365): ExitCode
{
echo "🧹 Cleaning Dead Letter Jobs\n\n";
try {
if ($days < 1) {
echo "❌ Days must be at least 1\n";
return ExitCode::INVALID_ARGUMENT;
}
echo "📊 Configuration:\n";
echo " • Delete dead letter jobs older than: {$days} days\n\n";
$duration = Duration::fromDays($days);
$deleted = $this->cleanupService->cleanupDeadLetterJobs($duration);
echo "✅ Cleanup Results:\n";
echo " • Dead letter jobs deleted: " . number_format($deleted) . "\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:cleanup:stats', description: 'Show cleanup statistics without performing cleanup')]
public function showCleanupStatistics(): ExitCode
{
echo "📊 Queue Cleanup Statistics\n\n";
try {
$stats = $this->cleanupService->getCleanupStatistics();
echo "📋 Retention Periods:\n";
echo " • Completed jobs: {$stats['retention_days']['completed_jobs']} days\n";
echo " • Failed jobs: {$stats['retention_days']['failed_jobs']} days\n";
echo " • Job metrics: {$stats['retention_days']['metrics']} days\n";
echo " • Dead letter jobs: {$stats['retention_days']['dead_letter_jobs']} days\n\n";
echo "🗑️ Eligible for Cleanup:\n";
echo " • Completed jobs: " . number_format($stats['eligible_completed_jobs']) . "\n";
echo " • Failed jobs: " . number_format($stats['eligible_failed_jobs']) . "\n";
echo " • Job metrics: " . number_format($stats['eligible_metrics']) . "\n";
echo " • Dead letter jobs: " . number_format($stats['eligible_dead_letter_jobs']) . "\n";
echo " • Total eligible: " . number_format($stats['total_eligible']) . "\n\n";
echo "⏱️ Estimated Cleanup Time: ~{$stats['estimated_cleanup_minutes']} minutes\n\n";
// Memory recommendations
$recommendations = $this->memoryManager->getMemoryRecommendations();
echo "💾 Memory Recommendations:\n";
echo " • Current usage: {$recommendations['current_usage']}\n";
echo " • Available memory: {$recommendations['available']}\n";
echo " • Priority: {$recommendations['priority']}\n";
foreach ($recommendations['recommendations'] as $rec) {
echo "\n 📌 {$rec['type']}:\n";
echo " {$rec['message']}\n";
if (!empty($rec['actions'])) {
echo " Actions:\n";
foreach ($rec['actions'] as $action) {
echo "{$action}\n";
}
}
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:memory:status', description: 'Show current memory status and recommendations')]
public function showMemoryStatus(): ExitCode
{
echo "💾 Queue Memory Status\n\n";
try {
// Get memory snapshot
$snapshot = $this->memoryManager->getJobMemorySnapshot('memory_status_check');
echo "📊 Current Memory Usage:\n";
echo " • Current: {$snapshot['current']} ({$snapshot['usage_percentage']}%)\n";
echo " • Peak: {$snapshot['peak']}\n";
echo " • Limit: {$snapshot['limit']}\n";
echo " • Available: {$snapshot['available']}\n";
echo " • Status: {$snapshot['status']}\n\n";
// Status indicators
if ($snapshot['is_critical']) {
echo "🔴 CRITICAL: Memory usage is critically high!\n\n";
} elseif ($snapshot['is_warning']) {
echo "🟡 WARNING: Memory usage is elevated.\n\n";
} else {
echo "🟢 NORMAL: Memory usage is within acceptable limits.\n\n";
}
// Get recommendations
$recommendations = $this->memoryManager->getMemoryRecommendations();
echo "📌 Recommendations:\n";
foreach ($recommendations['recommendations'] as $rec) {
echo "\n {$rec['type']}:\n";
echo " {$rec['message']}\n";
if (!empty($rec['actions'])) {
echo " Suggested actions:\n";
foreach ($rec['actions'] as $action) {
echo "{$action}\n";
}
}
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:memory:optimize', description: 'Optimize memory for job processing')]
public function optimizeMemory(): ExitCode
{
echo "🔧 Optimizing Queue Memory\n\n";
try {
echo "📊 Before Optimization:\n";
$beforeSnapshot = $this->memoryManager->getJobMemorySnapshot('before_optimization');
echo " • Memory: {$beforeSnapshot['current']} ({$beforeSnapshot['usage_percentage']}%)\n\n";
echo "🔄 Running optimization...\n";
$optimization = $this->memoryManager->optimizeForJob('manual_optimization');
echo "\n📊 After Optimization:\n";
echo " • Memory: {$optimization['after']}\n";
echo " • Freed: {$optimization['freed']}\n";
echo " • Usage before: {$optimization['usage_before']}\n";
echo " • Usage after: {$optimization['usage_after']}\n\n";
if ($optimization['freed_bytes'] > 0) {
echo "✅ Successfully freed {$optimization['freed']} of memory\n";
} else {
echo " No significant memory was freed (system may have already been optimized)\n";
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
}

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Commands;
use App\Framework\Console\Attributes\ConsoleCommand;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Queue\ValueObjects\JobDependency;
use App\Framework\Queue\ValueObjects\DependencyType;
use App\Framework\Queue\Services\DependencyResolutionEngine;
final readonly class JobDependencyCommands
{
public function __construct(
private JobDependencyManagerInterface $dependencyManager,
private DependencyResolutionEngine $resolutionEngine
) {}
#[ConsoleCommand(name: 'queue:dependency:add', description: 'Add a dependency between two jobs')]
public function addDependency(
string $dependentJobId,
string $dependsOnJobId,
string $type = 'completion',
?string $condition = null
): void {
$dependencyType = DependencyType::from($type);
$dependency = new JobDependency(
dependentJobId: $dependentJobId,
dependsOnJobId: $dependsOnJobId,
type: $dependencyType,
condition: $condition
);
$this->dependencyManager->addDependency($dependency);
echo "✅ Dependency added: {$dependentJobId} depends on {$dependsOnJobId} ({$type})\n";
}
#[ConsoleCommand(name: 'queue:dependency:remove', description: 'Remove a dependency between two jobs')]
public function removeDependency(string $dependentJobId, string $dependsOnJobId): void
{
$this->dependencyManager->removeDependency($dependentJobId, $dependsOnJobId);
echo "✅ Dependency removed: {$dependentJobId} no longer depends on {$dependsOnJobId}\n";
}
#[ConsoleCommand(name: 'queue:dependency:list', description: 'List dependencies for a job')]
public function listDependencies(string $jobId): void
{
$dependencies = $this->dependencyManager->getDependencies($jobId);
$dependents = $this->dependencyManager->getDependents($jobId);
echo "📋 Dependencies for job: {$jobId}\n\n";
echo "🔗 This job depends on:\n";
if (empty($dependencies)) {
echo " No dependencies\n";
} else {
foreach ($dependencies as $dep) {
$status = $dep->isSatisfied ? '✅' : '⏳';
echo " {$status} {$dep->dependsOnJobId} ({$dep->dependencyType})\n";
}
}
echo "\n🔗 Jobs that depend on this one:\n";
if (empty($dependents)) {
echo " No dependents\n";
} else {
foreach ($dependents as $dep) {
$status = $dep->isSatisfied ? '✅' : '⏳';
echo " {$status} {$dep->dependentJobId} ({$dep->dependencyType})\n";
}
}
$canExecute = $this->dependencyManager->canJobBeExecuted($jobId);
$statusIcon = $canExecute ? '✅' : '⏳';
echo "\n{$statusIcon} Can execute: " . ($canExecute ? 'Yes' : 'No') . "\n";
}
#[ConsoleCommand(name: 'queue:dependency:analyze', description: 'Analyze dependencies for a job')]
public function analyzeDependencies(string $jobId): void
{
$analysis = $this->resolutionEngine->analyzeDependencies($jobId);
echo "🔍 Dependency Analysis for job: {$jobId}\n\n";
echo "📊 Statistics:\n";
echo " Total dependencies: {$analysis['statistics']['total_dependencies']}\n";
echo " Satisfied: {$analysis['statistics']['satisfied_dependencies']}\n";
echo " Unsatisfied: {$analysis['statistics']['unsatisfied_dependencies']}\n";
echo " Total dependents: {$analysis['statistics']['total_dependents']}\n";
echo " Chain depth: {$analysis['statistics']['chain_depth']}\n";
$canExecute = $analysis['can_execute'] ? '✅ Yes' : '❌ No';
echo " Can execute: {$canExecute}\n\n";
if (!empty($analysis['unsatisfied_dependencies'])) {
echo "⏳ Unsatisfied Dependencies:\n";
foreach ($analysis['unsatisfied_dependencies'] as $dep) {
echo " - {$dep['depends_on_job_id']} ({$dep['dependency_type']})\n";
}
echo "\n";
}
if (!empty($analysis['full_dependency_chain'])) {
echo "🔗 Full Dependency Chain:\n";
foreach ($analysis['full_dependency_chain'] as $depJobId) {
echo "{$depJobId}\n";
}
}
}
#[ConsoleCommand(name: 'queue:dependency:ready', description: 'List all jobs ready for execution')]
public function listReadyJobs(): void
{
$readyJobs = $this->dependencyManager->getReadyJobs();
echo "🚀 Jobs ready for execution:\n\n";
if (empty($readyJobs)) {
echo " No jobs are currently ready\n";
} else {
foreach ($readyJobs as $jobId) {
echo "{$jobId}\n";
}
}
echo "\nTotal: " . count($readyJobs) . " jobs ready\n";
}
#[ConsoleCommand(name: 'queue:dependency:validate', description: 'Validate dependency graph for circular dependencies')]
public function validateDependencies(string $jobIds): void
{
$jobIdArray = explode(',', $jobIds);
$jobIdArray = array_map('trim', $jobIdArray);
$results = $this->resolutionEngine->validateDependencyGraph($jobIdArray);
echo "🔍 Dependency Graph Validation\n\n";
$hasErrors = false;
foreach ($results as $result) {
$status = $result['valid'] ? '✅' : '❌';
echo "{$status} {$result['job_id']}";
if (!$result['valid']) {
$hasErrors = true;
echo " - {$result['error']}\n";
if (isset($result['dependency_chain'])) {
echo " Chain: " . implode(' → ', $result['dependency_chain']) . "\n";
}
} else {
echo " - Valid\n";
}
}
if ($hasErrors) {
echo "\n❌ Validation failed: Circular dependencies detected\n";
} else {
echo "\n✅ Validation passed: No circular dependencies\n";
}
}
#[ConsoleCommand(name: 'queue:dependency:cleanup', description: 'Clean up old satisfied dependencies')]
public function cleanupOldDependencies(int $olderThanDays = 30): void
{
$deletedCount = $this->dependencyManager->cleanupOldDependencies($olderThanDays);
echo "🧹 Cleaned up old dependencies\n";
echo " Deleted: {$deletedCount} satisfied dependencies older than {$olderThanDays} days\n";
}
#[ConsoleCommand(name: 'queue:dependency:health', description: 'Perform dependency system health check')]
public function healthCheck(): void
{
$health = $this->resolutionEngine->performHealthCheck();
$statusIcon = $health['status'] === 'healthy' ? '✅' : '⚠️';
echo "{$statusIcon} Dependency System Health: {$health['status']}\n\n";
echo "📊 Statistics:\n";
echo " Active chains: {$health['statistics']['active_chains']}\n";
echo " Pending chains: {$health['statistics']['pending_chains']}\n";
echo " Ready jobs: {$health['statistics']['ready_jobs']}\n";
echo " Detected issues: {$health['statistics']['detected_issues']}\n\n";
if (!empty($health['issues'])) {
echo "⚠️ Issues Detected:\n";
foreach ($health['issues'] as $issue) {
echo " - {$issue['type']}: ";
match($issue['type']) {
'stalled_chain' => echo "Chain {$issue['chain_id']} running for {$issue['hours_running']} hours\n",
'many_unsatisfied_dependencies' => echo "Job {$issue['job_id']} has {$issue['unsatisfied_count']} unsatisfied dependencies\n",
default => echo "Unknown issue\n"
};
}
}
}
}

View File

@@ -0,0 +1,373 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Commands;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ExitCode;
use App\Framework\Queue\Services\JobMetricsManagerInterface;
final readonly class JobMetricsCommands
{
public function __construct(
private JobMetricsManagerInterface $metricsManager
) {}
#[ConsoleCommand(name: 'queue:metrics:overview', description: 'Show system-wide queue metrics overview')]
public function systemOverview(): ExitCode
{
try {
$overview = $this->metricsManager->getSystemOverview();
echo "🎯 Queue System Overview\n\n";
// System Health
$healthIcon = $overview['system_health_score'] >= 70 ? '✅' : '⚠️';
echo "📊 System Health: {$healthIcon} {$overview['system_health_score']}%\n";
echo " Total Queues: {$overview['total_queues']}\n";
echo " Healthy Queues: {$overview['healthy_queues']}\n\n";
// Job Statistics
echo "📈 Job Statistics (24h):\n";
echo " Total Jobs: {$overview['total_jobs']}\n";
echo " ✅ Completed: {$overview['completed_jobs']}\n";
echo " ❌ Failed: {$overview['failed_jobs']}\n";
echo " ⏳ Pending: {$overview['pending_jobs']}\n";
echo " 🔄 Running: {$overview['running_jobs']}\n";
echo " Success Rate: {$overview['overall_success_rate']}%\n\n";
// Performance
echo "⚡ Performance:\n";
echo " Avg Execution Time: {$overview['average_execution_time_ms']}ms\n";
echo " Avg Memory Usage: " . round($overview['average_memory_usage_mb'], 2) . "MB\n\n";
// Queue Details
echo "🔗 Queue Details:\n";
foreach ($overview['queue_metrics'] as $queueName => $metrics) {
$healthIcon = $metrics->isHealthy() ? '✅' : '⚠️';
$bottlenecks = $metrics->getBottleneckIndicators();
$bottleneckText = !empty($bottlenecks) ? ' (' . implode(', ', $bottlenecks) . ')' : '';
echo " {$healthIcon} {$queueName}: {$metrics->totalJobs} jobs, {$metrics->successRate->getValue()}% success{$bottleneckText}\n";
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:metrics:queue', description: 'Show detailed metrics for a specific queue')]
public function queueMetrics(string $queueName, string $timeWindow = '1 hour'): ExitCode
{
try {
$metrics = $this->metricsManager->getQueueMetrics($queueName, $timeWindow);
echo "📊 Queue Metrics: {$queueName} (last {$timeWindow})\n\n";
// Health Status
$healthIcon = $metrics->isHealthy() ? '✅' : '⚠️';
echo "🏥 Health: {$healthIcon} {$metrics->getHealthScore()->getValue()}%\n\n";
// Job Counts
echo "📈 Job Statistics:\n";
echo " Total: {$metrics->totalJobs}\n";
echo " ✅ Completed: {$metrics->completedJobs}\n";
echo " ❌ Failed: {$metrics->failedJobs}\n";
echo " ⏳ Pending: {$metrics->pendingJobs}\n";
echo " 🔄 Running: {$metrics->runningJobs}\n";
echo " 💀 Dead Letter: {$metrics->deadLetterJobs}\n\n";
// Success/Failure Rates
echo "📊 Success Metrics:\n";
echo " Success Rate: {$metrics->successRate->getValue()}%\n";
echo " Failure Rate: {$metrics->getFailureRate()->getValue()}%\n\n";
// Performance
echo "⚡ Performance:\n";
echo " Avg Execution Time: {$metrics->averageExecutionTimeMs}ms\n";
echo " Avg Memory Usage: " . round($metrics->averageMemoryUsageMB, 2) . "MB\n";
echo " Throughput: " . round($metrics->throughputJobsPerHour, 2) . " jobs/hour\n\n";
// Bottlenecks
$bottlenecks = $metrics->getBottleneckIndicators();
if (!empty($bottlenecks)) {
echo "⚠️ Bottleneck Indicators:\n";
foreach ($bottlenecks as $bottleneck) {
echo " - " . str_replace('_', ' ', ucfirst($bottleneck)) . "\n";
}
echo "\n";
}
echo "📅 Measured at: {$metrics->measuredAt}\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:metrics:job', description: 'Show metrics for a specific job')]
public function jobMetrics(string $jobId): ExitCode
{
try {
$metrics = $this->metricsManager->getJobMetrics($jobId);
if (!$metrics) {
echo "❌ No metrics found for job: {$jobId}\n";
return ExitCode::GENERAL_ERROR;
}
echo "📊 Job Metrics: {$jobId}\n\n";
// Basic Info
echo " Basic Information:\n";
echo " Queue: {$metrics->queueName}\n";
echo " Status: {$this->getStatusIcon($metrics->status)} {$metrics->status}\n";
echo " Attempts: {$metrics->attempts}/{$metrics->maxAttempts}\n\n";
// Performance
echo "⚡ Performance:\n";
echo " Execution Time: {$metrics->executionTimeMs}ms (" . round($metrics->getExecutionTimeSeconds(), 2) . "s)\n";
echo " Memory Usage: {$metrics->memoryUsageBytes} bytes (" . round($metrics->getMemoryUsageMB(), 2) . "MB)\n";
echo " Success Rate: {$metrics->getSuccessRate()->getValue()}%\n\n";
// Timestamps
echo "📅 Timeline:\n";
echo " Created: {$metrics->createdAt}\n";
if ($metrics->startedAt) {
echo " Started: {$metrics->startedAt}\n";
}
if ($metrics->completedAt) {
echo " Completed: {$metrics->completedAt}\n";
}
if ($metrics->failedAt) {
echo " Failed: {$metrics->failedAt}\n";
}
$duration = $metrics->getDuration();
if ($duration !== null) {
echo " Duration: {$duration} seconds\n";
}
// Error Information
if ($metrics->errorMessage) {
echo "\n❌ Error:\n";
echo " {$metrics->errorMessage}\n";
}
// Metadata
if (!empty($metrics->metadata)) {
echo "\n📝 Metadata:\n";
foreach ($metrics->metadata as $key => $value) {
echo " {$key}: " . json_encode($value) . "\n";
}
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:metrics:slow', description: 'Show slowest jobs')]
public function slowJobs(?string $queueName = null, int $limit = 10): ExitCode
{
try {
$slowJobs = $this->metricsManager->getTopSlowJobs($queueName, $limit);
$title = $queueName ? "Slowest Jobs in '{$queueName}'" : "Slowest Jobs (All Queues)";
echo "🐌 {$title} (Top {$limit})\n\n";
if (empty($slowJobs)) {
echo " No jobs found\n";
return ExitCode::SUCCESS;
}
foreach ($slowJobs as $index => $metrics) {
$rank = $index + 1;
$statusIcon = $this->getStatusIcon($metrics->status);
$executionSeconds = round($metrics->getExecutionTimeSeconds(), 2);
echo " {$rank}. {$statusIcon} {$metrics->jobId} ({$metrics->queueName})\n";
echo " Time: {$metrics->executionTimeMs}ms ({$executionSeconds}s)\n";
echo " Memory: " . round($metrics->getMemoryUsageMB(), 2) . "MB\n";
echo " Status: {$metrics->status}\n\n";
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:metrics:memory', description: 'Show jobs with highest memory usage')]
public function memoryConsumers(?string $queueName = null, int $limit = 10): ExitCode
{
try {
$memoryJobs = $this->metricsManager->getTopMemoryConsumers($queueName, $limit);
$title = $queueName ? "Top Memory Consumers in '{$queueName}'" : "Top Memory Consumers (All Queues)";
echo "🧠 {$title} (Top {$limit})\n\n";
if (empty($memoryJobs)) {
echo " No jobs found\n";
return ExitCode::SUCCESS;
}
foreach ($memoryJobs as $index => $metrics) {
$rank = $index + 1;
$statusIcon = $this->getStatusIcon($metrics->status);
$memoryMB = round($metrics->getMemoryUsageMB(), 2);
echo " {$rank}. {$statusIcon} {$metrics->jobId} ({$metrics->queueName})\n";
echo " Memory: {$metrics->memoryUsageBytes} bytes ({$memoryMB}MB)\n";
echo " Time: {$metrics->executionTimeMs}ms\n";
echo " Status: {$metrics->status}\n\n";
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:metrics:failed', description: 'Show recent failed jobs')]
public function failedJobs(?string $queueName = null, string $timeWindow = '24 hours'): ExitCode
{
try {
$failedJobs = $this->metricsManager->getFailedJobs($queueName, $timeWindow);
$title = $queueName ? "Failed Jobs in '{$queueName}'" : "Failed Jobs (All Queues)";
echo "{$title} (last {$timeWindow})\n\n";
if (empty($failedJobs)) {
echo " No failed jobs found\n";
return ExitCode::SUCCESS;
}
foreach ($failedJobs as $metrics) {
echo " 🔴 {$metrics->jobId} ({$metrics->queueName})\n";
echo " Failed: {$metrics->failedAt}\n";
echo " Attempts: {$metrics->attempts}/{$metrics->maxAttempts}\n";
if ($metrics->errorMessage) {
echo " Error: {$metrics->errorMessage}\n";
}
echo "\n";
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:metrics:performance', description: 'Show performance statistics')]
public function performanceStats(?string $queueName = null, string $timeWindow = '24 hours'): ExitCode
{
try {
$stats = $this->metricsManager->getPerformanceStats($queueName, $timeWindow);
$title = $queueName ? "Performance Stats for '{$queueName}'" : "System Performance Stats";
echo "📈 {$title} (last {$timeWindow})\n\n";
echo "📊 Job Statistics:\n";
echo " Total Jobs: {$stats['total_jobs']}\n";
echo " Completed: {$stats['completed_jobs']}\n";
echo " Failed: {$stats['failed_jobs']}\n";
echo " Success Rate: " . round($stats['success_rate'], 2) . "%\n";
echo " Failure Rate: " . round($stats['failure_rate'], 2) . "%\n\n";
echo "⏱️ Execution Time:\n";
echo " Average: " . round($stats['average_execution_time_ms'], 2) . "ms\n";
echo " Minimum: " . round($stats['min_execution_time_ms'], 2) . "ms\n";
echo " Maximum: " . round($stats['max_execution_time_ms'], 2) . "ms\n\n";
echo "🧠 Memory Usage:\n";
echo " Average: " . round($stats['average_memory_usage_mb'], 2) . "MB\n";
echo " Minimum: " . round($stats['min_memory_usage_bytes'] / (1024 * 1024), 2) . "MB\n";
echo " Maximum: " . round($stats['max_memory_usage_bytes'] / (1024 * 1024), 2) . "MB\n\n";
echo "🔄 Retry Statistics:\n";
echo " Average Attempts: " . round($stats['average_attempts'], 2) . "\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:metrics:throughput', description: 'Show throughput statistics')]
public function throughputStats(?string $queueName = null, string $timeWindow = '24 hours'): ExitCode
{
try {
$stats = $this->metricsManager->getThroughputStats($queueName, $timeWindow);
$title = $queueName ? "Throughput Stats for '{$queueName}'" : "System Throughput Stats";
echo "🚀 {$title} (last {$timeWindow})\n\n";
echo "📊 Overall Throughput:\n";
echo " Total Completed: {$stats['total_completed']} jobs\n";
echo " Average per Hour: " . round($stats['average_throughput_per_hour'], 2) . " jobs/hour\n\n";
echo "📈 Hourly Breakdown:\n";
if (empty($stats['hourly_breakdown'])) {
echo " No data available\n";
} else {
foreach ($stats['hourly_breakdown'] as $hourData) {
$hour = date('H:i', strtotime($hourData['hour']));
$jobs = $hourData['jobs_completed'];
$bar = str_repeat('█', min(50, $jobs)); // Simple bar chart
echo " {$hour}: {$jobs} jobs {$bar}\n";
}
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'queue:metrics:cleanup', description: 'Clean up old metrics data')]
public function cleanupMetrics(int $olderThanDays = 90): ExitCode
{
try {
$deletedCount = $this->metricsManager->cleanupOldMetrics($olderThanDays);
echo "🧹 Metrics Cleanup Completed\n";
echo " Deleted: {$deletedCount} metrics older than {$olderThanDays} days\n";
if ($deletedCount > 0) {
echo " ✅ Cleanup successful\n";
} else {
echo " No old metrics found to clean up\n";
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
private function getStatusIcon(string $status): string
{
return match($status) {
'pending' => '⏳',
'running' => '🔄',
'completed' => '✅',
'failed' => '❌',
'dead_letter' => '💀',
default => '❓'
};
}
}

View File

@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Commands;
use App\Framework\Attributes\ConsoleCommand;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Queue\Services\ProgressManager;
use App\Framework\Queue\Entities\JobProgressEntry;
/**
* Console commands for managing job progress
*/
final readonly class ProgressCommands
{
public function __construct(
private ProgressManager $progressManager
) {}
#[ConsoleCommand(name: 'queue:progress:show', description: 'Show current progress for a job')]
public function showProgress(string $jobId, ConsoleOutput $output): void
{
$progress = $this->progressManager->getJobProgress($jobId);
if (!$progress) {
$output->writeln("<error>No progress found for job: {$jobId}</error>");
return;
}
$output->writeln("<info>Job Progress for: {$jobId}</info>");
$output->writeln("Percentage: {$progress->percentage->format()}");
$output->writeln("Message: {$progress->message}");
if ($progress->metadata) {
$output->writeln("Metadata:");
foreach ($progress->metadata as $key => $value) {
$valueStr = is_array($value) ? json_encode($value) : (string) $value;
$output->writeln(" {$key}: {$valueStr}");
}
}
$output->writeln("Status: " . ($progress->isCompleted() ? 'Completed' : ($progress->isFailed() ? 'Failed' : 'In Progress')));
}
#[ConsoleCommand(name: 'queue:progress:history', description: 'Show progress history for a job')]
public function showHistory(string $jobId, ConsoleOutput $output): void
{
$history = $this->progressManager->getProgressHistory($jobId);
if (empty($history)) {
$output->writeln("<error>No progress history found for job: {$jobId}</error>");
return;
}
$output->writeln("<info>Progress History for: {$jobId}</info>");
$output->writeln(str_repeat('-', 80));
foreach ($history as $entry) {
/** @var JobProgressEntry $entry */
$output->writeln("Time: {$entry->updatedAt}");
$output->writeln("Progress: {$entry->getPercentage()->format()}");
$output->writeln("Message: {$entry->message}");
if ($entry->stepName) {
$output->writeln("Step: {$entry->stepName}");
}
$status = [];
if ($entry->isCompleted) $status[] = 'Completed';
if ($entry->isFailed) $status[] = 'Failed';
if (!empty($status)) {
$output->writeln("Status: " . implode(', ', $status));
}
$output->writeln(str_repeat('-', 40));
}
}
#[ConsoleCommand(name: 'queue:progress:list', description: 'List recent job progress updates')]
public function listRecent(ConsoleOutput $output, ?int $minutes = 60, ?int $limit = 20): void
{
$recentJobs = $this->progressManager->getRecentlyUpdatedJobs($minutes ?? 60, $limit ?? 20);
if (empty($recentJobs)) {
$output->writeln("<info>No recent job progress updates found</info>");
return;
}
$output->writeln("<info>Recent Job Progress Updates (last {$minutes} minutes)</info>");
$output->writeln(str_repeat('=', 80));
foreach ($recentJobs as $entry) {
/** @var JobProgressEntry $entry */
$output->writeln("Job ID: {$entry->jobId}");
$output->writeln("Progress: {$entry->getPercentage()->format()}");
$output->writeln("Message: {$entry->message}");
$output->writeln("Updated: {$entry->updatedAt}");
if ($entry->stepName) {
$output->writeln("Step: {$entry->stepName}");
}
$status = [];
if ($entry->isCompleted) $status[] = 'Completed';
if ($entry->isFailed) $status[] = 'Failed';
if (!empty($status)) {
$output->writeln("Status: " . implode(', ', $status));
}
$output->writeln(str_repeat('-', 40));
}
}
#[ConsoleCommand(name: 'queue:progress:above', description: 'List jobs above a certain progress percentage')]
public function listAboveProgress(float $percentage, ConsoleOutput $output): void
{
if ($percentage < 0 || $percentage > 100) {
$output->writeln("<error>Percentage must be between 0 and 100</error>");
return;
}
$jobs = $this->progressManager->getJobsAboveProgress($percentage);
if (empty($jobs)) {
$output->writeln("<info>No jobs found above {$percentage}% progress</info>");
return;
}
$output->writeln("<info>Jobs above {$percentage}% progress</info>");
$output->writeln(str_repeat('=', 80));
foreach ($jobs as $entry) {
/** @var JobProgressEntry $entry */
$output->writeln("Job ID: {$entry->jobId}");
$output->writeln("Progress: {$entry->getPercentage()->format()}");
$output->writeln("Message: {$entry->message}");
$output->writeln("Updated: {$entry->updatedAt}");
$status = [];
if ($entry->isCompleted) $status[] = 'Completed';
if ($entry->isFailed) $status[] = 'Failed';
if (!empty($status)) {
$output->writeln("Status: " . implode(', ', $status));
}
$output->writeln(str_repeat('-', 40));
}
}
#[ConsoleCommand(name: 'queue:progress:cleanup', description: 'Clean up old progress entries')]
public function cleanup(ConsoleOutput $output, ?int $days = 30): void
{
$deletedCount = $this->progressManager->cleanupOldProgress($days ?? 30);
$output->writeln("<info>Cleaned up {$deletedCount} old progress entries older than {$days} days</info>");
}
#[ConsoleCommand(name: 'queue:progress:multiple', description: 'Show progress for multiple jobs (comma-separated IDs)')]
public function showMultiple(string $jobIds, ConsoleOutput $output): void
{
$idArray = array_map('trim', explode(',', $jobIds));
if (empty($idArray)) {
$output->writeln("<error>No job IDs provided</error>");
return;
}
$progressData = $this->progressManager->getMultipleJobProgress($idArray);
if (empty($progressData)) {
$output->writeln("<error>No progress found for any of the provided job IDs</error>");
return;
}
$output->writeln("<info>Progress for multiple jobs</info>");
$output->writeln(str_repeat('=', 80));
foreach ($idArray as $jobId) {
$output->writeln("Job ID: {$jobId}");
if (isset($progressData[$jobId])) {
$progress = $progressData[$jobId];
$output->writeln(" Progress: {$progress->percentage->format()}");
$output->writeln(" Message: {$progress->message}");
$output->writeln(" Status: " . ($progress->isCompleted() ? 'Completed' : ($progress->isFailed() ? 'Failed' : 'In Progress')));
} else {
$output->writeln(" <comment>No progress data found</comment>");
}
$output->writeln(str_repeat('-', 40));
}
}
}

View File

@@ -0,0 +1,428 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Commands;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ExitCode;
use App\Framework\Queue\Services\WorkerRegistry;
use App\Framework\Queue\Services\WorkerHealthCheckService;
use App\Framework\Queue\Services\JobDistributionService;
use App\Framework\Queue\Services\FailoverRecoveryService;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Core\ValueObjects\Duration;
/**
* Console Commands für Worker Management
*/
final readonly class WorkerCommands
{
public function __construct(
private WorkerRegistry $workerRegistry,
private WorkerHealthCheckService $healthCheckService,
private JobDistributionService $jobDistributionService,
private FailoverRecoveryService $failoverRecoveryService,
private DistributedLockInterface $distributedLock
) {}
#[ConsoleCommand(name: 'worker:list', description: 'List all registered workers with their status')]
public function listWorkers(bool $active = true, bool $detailed = false): ExitCode
{
echo "🔧 Worker Registry Status\n\n";
try {
$workers = $active
? $this->workerRegistry->findActiveWorkers()
: $this->workerRegistry->findActiveWorkers(); // Vereinfacht - könnte erweitert werden
if (empty($workers)) {
echo " No workers found\n";
return ExitCode::SUCCESS;
}
echo "📊 Found " . count($workers) . " worker(s)\n\n";
foreach ($workers as $worker) {
$status = $worker->isHealthy() ? '🟢' : ($worker->isActive ? '🟡' : '🔴');
$load = $worker->getLoadPercentage()->getValue();
echo "{$status} {$worker->hostname}:{$worker->processId}\n";
echo " ID: {$worker->id->toString()}\n";
echo " Load: {$worker->currentJobs}/{$worker->maxJobs} ({$load}%)\n";
echo " Queues: " . implode(', ', array_map(fn($q) => $q->toString(), $worker->queues)) . "\n";
if ($detailed) {
echo " CPU: {$worker->cpuUsage->getValue()}%\n";
echo " Memory: " . round($worker->memoryUsage->toBytes() / 1024 / 1024, 1) . "MB\n";
echo " Last Heartbeat: " . ($worker->lastHeartbeat?->format('Y-m-d H:i:s') ?? 'Never') . "\n";
echo " Capabilities: " . implode(', ', $worker->capabilities) . "\n";
}
echo "\n";
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'worker:health', description: 'Perform health check on all workers')]
public function healthCheck(bool $detailed = false): ExitCode
{
echo "🏥 Worker Health Check\n\n";
try {
$healthReport = $this->healthCheckService->performHealthCheck();
// Overall Status
$overall = $healthReport['overall'];
$statusIcon = match($overall['status']) {
'healthy' => '🟢',
'warning' => '🟡',
'critical' => '🔴',
default => '⚪'
};
echo "{$statusIcon} Overall Status: {$overall['status']} (Score: {$overall['score']})\n";
echo "📊 Workers: " . ($overall['healthy_workers'] ?? 0) . " healthy, " . ($overall['warning_workers'] ?? 0) . " warning, " . ($overall['critical_workers'] ?? 0) . " critical\n\n";
// Worker Details
foreach ($healthReport['workers'] as $workerHealth) {
$statusIcon = match($workerHealth['status']) {
'healthy' => '🟢',
'warning' => '🟡',
'critical' => '🔴',
default => '⚪'
};
echo "{$statusIcon} {$workerHealth['hostname']}:{$workerHealth['process_id']}\n";
echo " Status: {$workerHealth['status']} (Score: {$workerHealth['score']})\n";
if (!empty($workerHealth['issues'])) {
echo " Issues:\n";
foreach ($workerHealth['issues'] as $issue) {
echo "{$issue}\n";
}
}
if (!empty($workerHealth['warnings'])) {
echo " Warnings:\n";
foreach ($workerHealth['warnings'] as $warning) {
echo " ⚠️ {$warning}\n";
}
}
if ($detailed) {
echo " Metrics:\n";
$metrics = $workerHealth['metrics'];
echo " CPU: {$metrics['cpu_usage_percent']}%\n";
echo " Memory: {$metrics['memory_usage_gb']}GB\n";
echo " Load: {$metrics['job_load_percent']}%\n";
echo " Jobs: {$metrics['current_jobs']}/{$metrics['max_jobs']}\n";
}
echo "\n";
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'worker:stats', description: 'Show worker and distribution statistics')]
public function showStatistics(): ExitCode
{
echo "📊 Worker & Distribution Statistics\n\n";
try {
// Worker Statistics
$workerStats = $this->workerRegistry->getWorkerStatistics();
echo "🔧 Worker Overview:\n";
echo " Total Workers: {$workerStats['total_workers']}\n";
echo " Active Workers: {$workerStats['active_workers']}\n";
echo " Healthy Workers: {$workerStats['healthy_workers']}\n";
echo " Unique Hosts: {$workerStats['unique_hosts']}\n";
echo " Total Capacity: {$workerStats['total_capacity']} jobs\n";
echo " Current Load: {$workerStats['current_load']} jobs\n";
echo " Capacity Utilization: {$workerStats['capacity_utilization']}%\n";
echo " Avg CPU Usage: {$workerStats['avg_cpu_usage']}%\n";
echo " Avg Memory Usage: {$workerStats['avg_memory_usage_mb']}MB\n\n";
// Queue Distribution
echo "📋 Queue Distribution:\n";
foreach ($workerStats['queue_distribution'] as $queue => $workerCount) {
echo " {$queue}: {$workerCount} workers\n";
}
echo "\n";
// Distribution Statistics
$distributionStats = $this->jobDistributionService->getDistributionStatistics();
echo "🎯 Job Distribution:\n";
$assignments = $distributionStats['assignments'];
echo " Total Assignments: {$assignments['total']}\n";
echo " Active Workers: {$assignments['active_workers']}\n";
echo " Active Queues: {$assignments['active_queues']}\n";
echo " Avg Assignment Age: {$assignments['avg_assignment_age_seconds']}s\n\n";
// Distribution Health
$health = $distributionStats['distribution_health'];
$statusIcon = match($health['status']) {
'healthy' => '🟢',
'warning' => '🟡',
'degraded' => '🟠',
'critical' => '🔴',
default => '⚪'
};
echo "{$statusIcon} Distribution Health: {$health['status']} (Score: {$health['score']})\n";
echo " Avg Load: " . ($health['avg_load'] ?? 0) . "%\n";
echo " Max Load: " . ($health['max_load'] ?? 0) . "%\n";
echo " Load Variance: " . ($health['load_variance'] ?? 0) . "%\n";
echo " Availability: " . ($health['availability_ratio'] ?? 0) . "%\n";
if (!empty($health['issues'])) {
echo " Issues:\n";
foreach ($health['issues'] as $issue) {
echo "{$issue}\n";
}
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'worker:failover', description: 'Perform failover and recovery process')]
public function performFailover(bool $dryRun = false): ExitCode
{
echo "🔄 " . ($dryRun ? 'Simulating' : 'Performing') . " Failover and Recovery\n\n";
try {
if ($dryRun) {
// Nur fehlgeschlagene Worker erkennen ohne Recovery
$failedWorkers = $this->failoverRecoveryService->detectFailedWorkers();
echo "🔍 Detected Issues:\n";
echo " Failed Workers: " . count($failedWorkers) . "\n";
foreach ($failedWorkers as $worker) {
echo " 🔴 {$worker->hostname}:{$worker->processId} ({$worker->id->toString()})\n";
}
if (empty($failedWorkers)) {
echo " ✅ No failed workers detected\n";
}
echo "\n⚠️ Dry run completed - no recovery actions taken\n";
} else {
$results = $this->failoverRecoveryService->performFailoverRecovery();
echo "📊 Failover Results:\n";
echo " Failed Workers: {$results['failed_workers_detected']}\n";
echo " Jobs Recovered: {$results['jobs_recovered']}\n";
echo " Jobs Reassigned: {$results['jobs_reassigned']}\n";
echo " Workers Cleaned: {$results['workers_cleaned']}\n";
echo " Locks Released: {$results['locks_released']}\n";
if (isset($results['orphaned_jobs_recovered'])) {
echo " Orphaned Jobs Recovered: {$results['orphaned_jobs_recovered']}\n";
}
if (isset($results['expired_locks_cleaned'])) {
echo " Expired Locks Cleaned: {$results['expired_locks_cleaned']}\n";
}
echo " Duration: {$results['duration_seconds']}s\n";
if (!empty($results['errors'])) {
echo "\n❌ Errors:\n";
foreach ($results['errors'] as $error) {
echo "{$error}\n";
}
}
echo "\n✅ Failover process completed\n";
}
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'worker:locks', description: 'Show distributed lock information')]
public function showLocks(bool $detailed = false): ExitCode
{
echo "🔒 Distributed Locks Status\n\n";
try {
$lockStats = $this->distributedLock->getLockStatistics();
echo "📊 Lock Overview:\n";
echo " Total Locks: {$lockStats['total_locks']}\n";
echo " Active Locks: {$lockStats['active_locks']}\n";
echo " Expired Locks: {$lockStats['expired_locks']}\n";
echo " Unique Workers: {$lockStats['unique_workers']}\n";
echo " Avg TTL: {$lockStats['avg_ttl_seconds']}s\n";
echo " Oldest Lock: " . ($lockStats['oldest_lock'] ?? 'None') . "\n";
echo " Newest Lock: " . ($lockStats['newest_lock'] ?? 'None') . "\n\n";
if ($detailed && !empty($lockStats['top_lock_keys'])) {
echo "🔑 Top Lock Keys:\n";
foreach ($lockStats['top_lock_keys'] as $keyInfo) {
echo " {$keyInfo['lock_key']}: {$keyInfo['count']} locks\n";
}
echo "\n";
}
// Cleanup abgelaufene Locks
echo "🧹 Cleaning up expired locks...\n";
$cleaned = $this->distributedLock->cleanupExpiredLocks();
echo " Cleaned {$cleaned} expired locks\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'worker:resilience', description: 'Show system resilience score and recommendations')]
public function showResilience(): ExitCode
{
echo "🛡️ System Resilience Report\n\n";
try {
$resilience = $this->failoverRecoveryService->calculateResilienceScore();
$statusIcon = match($resilience['status']) {
'excellent' => '🟢',
'good' => '🟡',
'fair' => '🟠',
'poor' => '🔴',
'critical' => '💀',
default => '⚪'
};
echo "{$statusIcon} Resilience Score: {$resilience['score']}/100 ({$resilience['status']})\n\n";
echo "📊 Key Metrics:\n";
if (isset($resilience['metrics'])) {
$metrics = $resilience['metrics'];
echo " Worker Availability: {$metrics['worker_availability']}%\n";
echo " Recent Failovers (24h): {$metrics['recent_failovers_24h']}\n";
echo " Active Locks: {$metrics['active_locks']}\n";
echo " Expired Locks: {$metrics['expired_locks']}\n";
}
if (!empty($resilience['factors'])) {
echo "\n⚠️ Resilience Factors:\n";
foreach ($resilience['factors'] as $factor) {
echo "{$factor}\n";
}
}
// Failover Statistics
echo "\n📈 Failover Statistics (7 days):\n";
$failoverStats = $this->failoverRecoveryService->getFailoverStatistics();
$stats = $failoverStats['statistics'];
echo " Total Failovers: {$stats['total_failovers']}\n";
echo " Failed Workers: {$stats['failed_workers']}\n";
echo " Recovery Workers: {$stats['recovery_workers']}\n";
echo " Affected Jobs: {$stats['affected_jobs']}\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'worker:cleanup', description: 'Cleanup inactive workers and old health records')]
public function cleanup(int $inactiveMinutes = 5, int $healthRetentionDays = 7): ExitCode
{
echo "🧹 Worker System Cleanup\n\n";
try {
// Inactive Workers cleanup
echo "🔧 Cleaning up inactive workers...\n";
$inactiveCount = $this->workerRegistry->cleanupInactiveWorkers($inactiveMinutes);
echo " Deactivated {$inactiveCount} workers (inactive > {$inactiveMinutes} min)\n\n";
// Health Records cleanup
echo "🏥 Cleaning up old health records...\n";
$healthCount = $this->healthCheckService->cleanupHealthChecks(
Duration::fromDays($healthRetentionDays)
);
echo " Deleted {$healthCount} health records (older than {$healthRetentionDays} days)\n\n";
// Locks cleanup
echo "🔒 Cleaning up expired locks...\n";
$locksCount = $this->distributedLock->cleanupExpiredLocks();
echo " Cleaned {$locksCount} expired locks\n\n";
echo "✅ Cleanup completed successfully\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand(name: 'worker:deregister', description: 'Forcefully deregister a specific worker')]
public function deregisterWorker(string $workerId): ExitCode
{
echo "🚫 Deregistering Worker\n\n";
try {
$worker = $this->workerRegistry->findById(WorkerId::fromString($workerId));
if (!$worker) {
echo "❌ Worker not found: {$workerId}\n";
return ExitCode::INVALID_ARGUMENT;
}
echo "🔍 Found worker: {$worker->hostname}:{$worker->processId}\n";
echo " Current jobs: {$worker->currentJobs}\n";
echo " Status: " . ($worker->isActive ? 'Active' : 'Inactive') . "\n\n";
// Release all jobs
echo "🎯 Releasing all worker jobs...\n";
$releasedJobs = $this->jobDistributionService->releaseAllWorkerJobs($worker->id);
echo " Released {$releasedJobs} job assignments\n";
// Deregister worker
echo "🚫 Deregistering worker...\n";
$this->workerRegistry->deregister($worker->id);
echo " Worker deregistered successfully\n\n";
echo "✅ Worker {$workerId} has been forcefully deregistered\n";
return ExitCode::SUCCESS;
} catch (\Exception $e) {
echo "❌ Error: {$e->getMessage()}\n";
return ExitCode::GENERAL_ERROR;
}
}
}