- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
703 lines
30 KiB
PHP
703 lines
30 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Queue\ValueObjects\JobId;
|
|
use App\Framework\Queue\ValueObjects\JobMetrics;
|
|
|
|
describe('JobMetrics Value Object', function () {
|
|
|
|
describe('Creation and Basic Properties', function () {
|
|
it('can create job metrics with minimal parameters', function () {
|
|
$jobId = JobId::generate()->toString();
|
|
$queueName = 'email-queue';
|
|
|
|
$metrics = JobMetrics::create($jobId, $queueName);
|
|
|
|
expect($metrics->jobId)->toBe($jobId);
|
|
expect($metrics->queueName)->toBe($queueName);
|
|
expect($metrics->status)->toBe('pending');
|
|
expect($metrics->attempts)->toBe(0);
|
|
expect($metrics->maxAttempts)->toBe(3);
|
|
expect($metrics->executionTimeMs)->toBe(0.0);
|
|
expect($metrics->memoryUsageBytes)->toBe(0);
|
|
expect($metrics->errorMessage)->toBeNull();
|
|
expect($metrics->createdAt)->toBeString();
|
|
expect($metrics->startedAt)->toBeNull();
|
|
expect($metrics->completedAt)->toBeNull();
|
|
expect($metrics->failedAt)->toBeNull();
|
|
expect($metrics->metadata)->toBe([]);
|
|
});
|
|
|
|
it('can create job metrics with custom parameters', function () {
|
|
$jobId = JobId::generate()->toString();
|
|
$queueName = 'report-queue';
|
|
$status = 'running';
|
|
$attempts = 2;
|
|
$maxAttempts = 5;
|
|
|
|
$metrics = JobMetrics::create($jobId, $queueName, $status, $attempts, $maxAttempts);
|
|
|
|
expect($metrics->status)->toBe($status);
|
|
expect($metrics->attempts)->toBe($attempts);
|
|
expect($metrics->maxAttempts)->toBe($maxAttempts);
|
|
});
|
|
|
|
it('is readonly and immutable', function () {
|
|
$metrics = JobMetrics::create('test-job', 'test-queue');
|
|
|
|
$reflection = new ReflectionClass($metrics);
|
|
expect($reflection->isReadOnly())->toBeTrue();
|
|
|
|
// All properties should be readonly
|
|
$properties = ['jobId', 'queueName', 'status', 'attempts', 'maxAttempts',
|
|
'executionTimeMs', 'memoryUsageBytes', 'errorMessage',
|
|
'createdAt', 'startedAt', 'completedAt', 'failedAt', 'metadata'];
|
|
|
|
foreach ($properties as $prop) {
|
|
$property = $reflection->getProperty($prop);
|
|
expect($property->isReadOnly())->toBeTrue("Property {$prop} should be readonly");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Job State Transitions', function () {
|
|
beforeEach(function () {
|
|
$this->baseMetrics = JobMetrics::create('test-job-123', 'email-queue');
|
|
});
|
|
|
|
it('can transition from pending to running', function () {
|
|
$startTime = microtime(true) * 1000; // milliseconds
|
|
$memoryUsage = 1024 * 1024; // 1MB
|
|
|
|
$runningMetrics = $this->baseMetrics->withStarted($startTime, $memoryUsage);
|
|
|
|
expect($runningMetrics->status)->toBe('running');
|
|
expect($runningMetrics->attempts)->toBe(1); // Incremented from 0
|
|
expect($runningMetrics->executionTimeMs)->toBe($startTime);
|
|
expect($runningMetrics->memoryUsageBytes)->toBe($memoryUsage);
|
|
expect($runningMetrics->startedAt)->toBeString();
|
|
expect($runningMetrics->startedAt)->not->toBeNull();
|
|
|
|
// Original should be unchanged
|
|
expect($this->baseMetrics->status)->toBe('pending');
|
|
expect($this->baseMetrics->attempts)->toBe(0);
|
|
});
|
|
|
|
it('can transition from running to completed', function () {
|
|
$startTime = microtime(true) * 1000;
|
|
$runningMetrics = $this->baseMetrics->withStarted($startTime, 1024 * 1024);
|
|
|
|
$totalExecutionTime = 5500.0; // 5.5 seconds in milliseconds
|
|
$peakMemoryUsage = 2 * 1024 * 1024; // 2MB
|
|
|
|
$completedMetrics = $runningMetrics->withCompleted($totalExecutionTime, $peakMemoryUsage);
|
|
|
|
expect($completedMetrics->status)->toBe('completed');
|
|
expect($completedMetrics->executionTimeMs)->toBe($totalExecutionTime);
|
|
expect($completedMetrics->memoryUsageBytes)->toBe($peakMemoryUsage);
|
|
expect($completedMetrics->completedAt)->toBeString();
|
|
expect($completedMetrics->completedAt)->not->toBeNull();
|
|
expect($completedMetrics->attempts)->toBe(1); // Same as running state
|
|
});
|
|
|
|
it('can transition from running to failed', function () {
|
|
$startTime = microtime(true) * 1000;
|
|
$runningMetrics = $this->baseMetrics->withStarted($startTime, 1024 * 1024);
|
|
|
|
$errorMessage = 'Database connection timeout';
|
|
$executionTime = 2500.0; // 2.5 seconds
|
|
$memoryUsage = 1.5 * 1024 * 1024; // 1.5MB
|
|
|
|
$failedMetrics = $runningMetrics->withFailed($errorMessage, $executionTime, $memoryUsage);
|
|
|
|
expect($failedMetrics->status)->toBe('failed');
|
|
expect($failedMetrics->errorMessage)->toBe($errorMessage);
|
|
expect($failedMetrics->executionTimeMs)->toBe($executionTime);
|
|
expect($failedMetrics->memoryUsageBytes)->toBe($memoryUsage);
|
|
expect($failedMetrics->failedAt)->toBeString();
|
|
expect($failedMetrics->failedAt)->not->toBeNull();
|
|
});
|
|
|
|
it('preserves metadata across state transitions', function () {
|
|
$originalMetadata = ['batch_id' => 123, 'priority' => 'high'];
|
|
$metricsWithMetadata = $this->baseMetrics->withMetadata($originalMetadata);
|
|
|
|
$runningMetrics = $metricsWithMetadata->withStarted(1000.0, 1024 * 1024);
|
|
expect($runningMetrics->metadata)->toBe($originalMetadata);
|
|
|
|
$completedMetrics = $runningMetrics->withCompleted(5000.0, 2 * 1024 * 1024);
|
|
expect($completedMetrics->metadata)->toBe($originalMetadata);
|
|
});
|
|
});
|
|
|
|
describe('Metadata Management', function () {
|
|
beforeEach(function () {
|
|
$this->baseMetrics = JobMetrics::create('test-job', 'test-queue');
|
|
});
|
|
|
|
it('can add metadata to metrics', function () {
|
|
$metadata = [
|
|
'user_id' => 12345,
|
|
'email_template' => 'newsletter',
|
|
'batch_size' => 1000,
|
|
];
|
|
|
|
$metricsWithMetadata = $this->baseMetrics->withMetadata($metadata);
|
|
|
|
expect($metricsWithMetadata->metadata)->toBe($metadata);
|
|
expect($this->baseMetrics->metadata)->toBe([]); // Original unchanged
|
|
});
|
|
|
|
it('merges metadata when called multiple times', function () {
|
|
$firstMetadata = ['user_id' => 123, 'type' => 'email'];
|
|
$secondMetadata = ['priority' => 'high', 'retry_count' => 2];
|
|
|
|
$metrics = $this->baseMetrics
|
|
->withMetadata($firstMetadata)
|
|
->withMetadata($secondMetadata);
|
|
|
|
expect($metrics->metadata)->toBe([
|
|
'user_id' => 123,
|
|
'type' => 'email',
|
|
'priority' => 'high',
|
|
'retry_count' => 2,
|
|
]);
|
|
});
|
|
|
|
it('overwrites existing metadata keys', function () {
|
|
$firstMetadata = ['priority' => 'low', 'attempts' => 1];
|
|
$secondMetadata = ['priority' => 'high']; // Overwrites priority
|
|
|
|
$metrics = $this->baseMetrics
|
|
->withMetadata($firstMetadata)
|
|
->withMetadata($secondMetadata);
|
|
|
|
expect($metrics->metadata['priority'])->toBe('high');
|
|
expect($metrics->metadata['attempts'])->toBe(1); // Preserved
|
|
});
|
|
});
|
|
|
|
describe('Status Check Methods', function () {
|
|
beforeEach(function () {
|
|
$this->jobId = 'status-test-job';
|
|
$this->queueName = 'status-queue';
|
|
});
|
|
|
|
it('correctly identifies pending status', function () {
|
|
$metrics = JobMetrics::create($this->jobId, $this->queueName, 'pending');
|
|
|
|
expect($metrics->isPending())->toBeTrue();
|
|
expect($metrics->isRunning())->toBeFalse();
|
|
expect($metrics->isCompleted())->toBeFalse();
|
|
expect($metrics->isFailed())->toBeFalse();
|
|
});
|
|
|
|
it('correctly identifies running status', function () {
|
|
$metrics = JobMetrics::create($this->jobId, $this->queueName, 'running');
|
|
|
|
expect($metrics->isRunning())->toBeTrue();
|
|
expect($metrics->isPending())->toBeFalse();
|
|
expect($metrics->isCompleted())->toBeFalse();
|
|
expect($metrics->isFailed())->toBeFalse();
|
|
});
|
|
|
|
it('correctly identifies completed status', function () {
|
|
$metrics = JobMetrics::create($this->jobId, $this->queueName, 'completed');
|
|
|
|
expect($metrics->isCompleted())->toBeTrue();
|
|
expect($metrics->isPending())->toBeFalse();
|
|
expect($metrics->isRunning())->toBeFalse();
|
|
expect($metrics->isFailed())->toBeFalse();
|
|
});
|
|
|
|
it('correctly identifies failed status', function () {
|
|
$metrics = JobMetrics::create($this->jobId, $this->queueName, 'failed');
|
|
|
|
expect($metrics->isFailed())->toBeTrue();
|
|
expect($metrics->isPending())->toBeFalse();
|
|
expect($metrics->isRunning())->toBeFalse();
|
|
expect($metrics->isCompleted())->toBeFalse();
|
|
});
|
|
|
|
it('correctly checks max attempts', function () {
|
|
$metrics = JobMetrics::create($this->jobId, $this->queueName, 'running', 2, 3);
|
|
expect($metrics->hasMaxAttempts())->toBeFalse();
|
|
|
|
$maxedMetrics = JobMetrics::create($this->jobId, $this->queueName, 'failed', 3, 3);
|
|
expect($maxedMetrics->hasMaxAttempts())->toBeTrue();
|
|
|
|
$exceededMetrics = JobMetrics::create($this->jobId, $this->queueName, 'failed', 5, 3);
|
|
expect($exceededMetrics->hasMaxAttempts())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Calculation Methods', function () {
|
|
it('calculates success rate correctly', function () {
|
|
// Job not started yet
|
|
$pendingMetrics = JobMetrics::create('job', 'queue', 'pending', 0);
|
|
expect($pendingMetrics->getSuccessRate()->getValue())->toBe(100.0);
|
|
|
|
// Failed job with attempts
|
|
$failedMetrics = JobMetrics::create('job', 'queue', 'failed', 2);
|
|
expect($failedMetrics->getSuccessRate()->getValue())->toBe(0.0);
|
|
|
|
// Completed job
|
|
$completedMetrics = JobMetrics::create('job', 'queue', 'completed', 1);
|
|
expect($completedMetrics->getSuccessRate()->getValue())->toBe(100.0);
|
|
});
|
|
|
|
it('converts execution time from milliseconds to seconds', function () {
|
|
$metrics = JobMetrics::create('job', 'queue')
|
|
->withCompleted(5500.0, 1024 * 1024); // 5.5 seconds
|
|
|
|
expect($metrics->getExecutionTimeSeconds())->toBe(5.5);
|
|
});
|
|
|
|
it('converts memory usage from bytes to MB', function () {
|
|
$metrics = JobMetrics::create('job', 'queue')
|
|
->withCompleted(1000.0, 2.5 * 1024 * 1024); // 2.5 MB
|
|
|
|
expect($metrics->getMemoryUsageMB())->toBe(2.5);
|
|
});
|
|
|
|
it('calculates duration correctly', function () {
|
|
$metrics = JobMetrics::create('job', 'queue');
|
|
|
|
// No duration for pending job
|
|
expect($metrics->getDuration())->toBeNull();
|
|
|
|
// Mock started/completed times
|
|
$startedMetrics = new JobMetrics(
|
|
jobId: 'job',
|
|
queueName: 'queue',
|
|
status: 'completed',
|
|
attempts: 1,
|
|
maxAttempts: 3,
|
|
executionTimeMs: 1000.0,
|
|
memoryUsageBytes: 1024,
|
|
errorMessage: null,
|
|
createdAt: '2024-01-01 10:00:00',
|
|
startedAt: '2024-01-01 10:00:05',
|
|
completedAt: '2024-01-01 10:00:15',
|
|
failedAt: null
|
|
);
|
|
|
|
$duration = $startedMetrics->getDuration();
|
|
expect($duration)->toBe(10); // 10 seconds difference
|
|
});
|
|
|
|
it('calculates duration for failed jobs', function () {
|
|
$failedMetrics = new JobMetrics(
|
|
jobId: 'job',
|
|
queueName: 'queue',
|
|
status: 'failed',
|
|
attempts: 2,
|
|
maxAttempts: 3,
|
|
executionTimeMs: 500.0,
|
|
memoryUsageBytes: 1024,
|
|
errorMessage: 'Error occurred',
|
|
createdAt: '2024-01-01 10:00:00',
|
|
startedAt: '2024-01-01 10:00:05',
|
|
completedAt: null,
|
|
failedAt: '2024-01-01 10:00:08'
|
|
);
|
|
|
|
$duration = $failedMetrics->getDuration();
|
|
expect($duration)->toBe(3); // 3 seconds from start to failure
|
|
});
|
|
});
|
|
|
|
describe('Array Conversion', function () {
|
|
it('provides comprehensive metrics information', function () {
|
|
$metrics = JobMetrics::create('test-job-456', 'email-queue', 'completed', 1, 3)
|
|
->withCompleted(3750.0, 1.5 * 1024 * 1024)
|
|
->withMetadata(['batch_id' => 789, 'template' => 'welcome']);
|
|
|
|
$array = $metrics->toArray();
|
|
|
|
// Verify all expected keys exist
|
|
$expectedKeys = [
|
|
'job_id', 'queue_name', 'status', 'attempts', 'max_attempts',
|
|
'execution_time_ms', 'execution_time_seconds', 'memory_usage_bytes',
|
|
'memory_usage_mb', 'success_rate', 'duration_seconds', 'error_message',
|
|
'created_at', 'started_at', 'completed_at', 'failed_at', 'metadata',
|
|
];
|
|
|
|
foreach ($expectedKeys as $key) {
|
|
expect($array)->toHaveKey($key);
|
|
}
|
|
|
|
// Verify calculated values
|
|
expect($array['job_id'])->toBe('test-job-456');
|
|
expect($array['queue_name'])->toBe('email-queue');
|
|
expect($array['status'])->toBe('completed');
|
|
expect($array['execution_time_seconds'])->toBe(3.75);
|
|
expect($array['memory_usage_mb'])->toBe(1.5);
|
|
expect($array['success_rate'])->toBe(100.0);
|
|
expect($array['metadata'])->toBe(['batch_id' => 789, 'template' => 'welcome']);
|
|
});
|
|
|
|
it('handles null values correctly in array conversion', function () {
|
|
$pendingMetrics = JobMetrics::create('pending-job', 'test-queue');
|
|
$array = $pendingMetrics->toArray();
|
|
|
|
expect($array['error_message'])->toBeNull();
|
|
expect($array['started_at'])->toBeNull();
|
|
expect($array['completed_at'])->toBeNull();
|
|
expect($array['failed_at'])->toBeNull();
|
|
expect($array['duration_seconds'])->toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases and Error Handling', function () {
|
|
it('handles zero execution time', function () {
|
|
$metrics = JobMetrics::create('instant-job', 'fast-queue')
|
|
->withCompleted(0.0, 1024);
|
|
|
|
expect($metrics->getExecutionTimeSeconds())->toBe(0.0);
|
|
});
|
|
|
|
it('handles zero memory usage', function () {
|
|
$metrics = JobMetrics::create('no-memory-job', 'efficient-queue')
|
|
->withCompleted(1000.0, 0);
|
|
|
|
expect($metrics->getMemoryUsageMB())->toBe(0.0);
|
|
});
|
|
|
|
it('handles very large execution times', function () {
|
|
$largeTime = 3600000.0; // 1 hour in milliseconds
|
|
$metrics = JobMetrics::create('long-job', 'slow-queue')
|
|
->withCompleted($largeTime, 1024);
|
|
|
|
expect($metrics->getExecutionTimeSeconds())->toBe(3600.0); // 1 hour
|
|
});
|
|
|
|
it('handles very large memory usage', function () {
|
|
$largeMemory = 10 * 1024 * 1024 * 1024; // 10GB
|
|
$metrics = JobMetrics::create('memory-intensive-job', 'heavy-queue')
|
|
->withCompleted(1000.0, $largeMemory);
|
|
|
|
expect($metrics->getMemoryUsageMB())->toBe(10240.0); // 10GB in MB
|
|
});
|
|
|
|
it('handles empty metadata gracefully', function () {
|
|
$metrics = JobMetrics::create('no-metadata-job', 'simple-queue')
|
|
->withMetadata([]);
|
|
|
|
expect($metrics->metadata)->toBe([]);
|
|
});
|
|
|
|
it('handles complex metadata structures', function () {
|
|
$complexMetadata = [
|
|
'nested' => ['level1' => ['level2' => 'value']],
|
|
'array' => [1, 2, 3, 4, 5],
|
|
'mixed' => ['string', 42, true, null],
|
|
];
|
|
|
|
$metrics = JobMetrics::create('complex-job', 'data-queue')
|
|
->withMetadata($complexMetadata);
|
|
|
|
expect($metrics->metadata)->toBe($complexMetadata);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Job Metrics Collection Mock System', function () {
|
|
|
|
beforeEach(function () {
|
|
// Create a mock metrics manager for testing
|
|
$this->metricsManager = new class () {
|
|
private array $jobMetrics = [];
|
|
|
|
private array $queueStats = [];
|
|
|
|
public function recordJobExecution(string $jobId, float $executionTimeMs, int $memoryUsage): void
|
|
{
|
|
if (! isset($this->jobMetrics[$jobId])) {
|
|
$this->jobMetrics[$jobId] = JobMetrics::create($jobId, 'default-queue');
|
|
}
|
|
|
|
$this->jobMetrics[$jobId] = $this->jobMetrics[$jobId]
|
|
->withCompleted($executionTimeMs, $memoryUsage);
|
|
}
|
|
|
|
public function recordJobFailure(string $jobId, string $errorMessage, float $executionTimeMs, int $memoryUsage): void
|
|
{
|
|
if (! isset($this->jobMetrics[$jobId])) {
|
|
$this->jobMetrics[$jobId] = JobMetrics::create($jobId, 'default-queue');
|
|
}
|
|
|
|
$this->jobMetrics[$jobId] = $this->jobMetrics[$jobId]
|
|
->withFailed($errorMessage, $executionTimeMs, $memoryUsage);
|
|
}
|
|
|
|
public function getJobMetrics(string $jobId): ?JobMetrics
|
|
{
|
|
return $this->jobMetrics[$jobId] ?? null;
|
|
}
|
|
|
|
public function getQueueMetrics(string $queueName): array
|
|
{
|
|
$jobs = array_filter($this->jobMetrics, fn ($metrics) => $metrics->queueName === $queueName);
|
|
|
|
if (empty($jobs)) {
|
|
return [
|
|
'queue_name' => $queueName,
|
|
'total_jobs' => 0,
|
|
'completed_jobs' => 0,
|
|
'failed_jobs' => 0,
|
|
'average_execution_time_ms' => 0.0,
|
|
'average_memory_usage_mb' => 0.0,
|
|
'success_rate' => 100.0,
|
|
];
|
|
}
|
|
|
|
$totalJobs = count($jobs);
|
|
$completedJobs = count(array_filter($jobs, fn ($m) => $m->isCompleted()));
|
|
$failedJobs = count(array_filter($jobs, fn ($m) => $m->isFailed()));
|
|
$avgExecutionTime = array_sum(array_map(fn ($m) => $m->executionTimeMs, $jobs)) / $totalJobs;
|
|
$avgMemoryUsage = array_sum(array_map(fn ($m) => $m->getMemoryUsageMB(), $jobs)) / $totalJobs;
|
|
$successRate = ($completedJobs / $totalJobs) * 100;
|
|
|
|
return [
|
|
'queue_name' => $queueName,
|
|
'total_jobs' => $totalJobs,
|
|
'completed_jobs' => $completedJobs,
|
|
'failed_jobs' => $failedJobs,
|
|
'average_execution_time_ms' => round($avgExecutionTime, 2),
|
|
'average_memory_usage_mb' => round($avgMemoryUsage, 2),
|
|
'success_rate' => round($successRate, 2),
|
|
];
|
|
}
|
|
|
|
public function getSystemMetrics(): array
|
|
{
|
|
if (empty($this->jobMetrics)) {
|
|
return [
|
|
'total_jobs' => 0,
|
|
'completed_jobs' => 0,
|
|
'failed_jobs' => 0,
|
|
'running_jobs' => 0,
|
|
'overall_success_rate' => 100.0,
|
|
'average_execution_time_ms' => 0.0,
|
|
'peak_memory_usage_mb' => 0.0,
|
|
];
|
|
}
|
|
|
|
$totalJobs = count($this->jobMetrics);
|
|
$completedJobs = count(array_filter($this->jobMetrics, fn ($m) => $m->isCompleted()));
|
|
$failedJobs = count(array_filter($this->jobMetrics, fn ($m) => $m->isFailed()));
|
|
$runningJobs = count(array_filter($this->jobMetrics, fn ($m) => $m->isRunning()));
|
|
$overallSuccessRate = ($completedJobs / $totalJobs) * 100;
|
|
$avgExecutionTime = array_sum(array_map(fn ($m) => $m->executionTimeMs, $this->jobMetrics)) / $totalJobs;
|
|
$peakMemoryUsage = max(array_map(fn ($m) => $m->getMemoryUsageMB(), $this->jobMetrics));
|
|
|
|
return [
|
|
'total_jobs' => $totalJobs,
|
|
'completed_jobs' => $completedJobs,
|
|
'failed_jobs' => $failedJobs,
|
|
'running_jobs' => $runningJobs,
|
|
'overall_success_rate' => round($overallSuccessRate, 2),
|
|
'average_execution_time_ms' => round($avgExecutionTime, 2),
|
|
'peak_memory_usage_mb' => round($peakMemoryUsage, 2),
|
|
];
|
|
}
|
|
|
|
public function getTopSlowJobs(int $limit = 10): array
|
|
{
|
|
$jobs = $this->jobMetrics;
|
|
usort($jobs, fn ($a, $b) => $b->executionTimeMs <=> $a->executionTimeMs);
|
|
|
|
return array_slice($jobs, 0, $limit);
|
|
}
|
|
|
|
public function getTopMemoryJobs(int $limit = 10): array
|
|
{
|
|
$jobs = $this->jobMetrics;
|
|
usort($jobs, fn ($a, $b) => $b->memoryUsageBytes <=> $a->memoryUsageBytes);
|
|
|
|
return array_slice($jobs, 0, $limit);
|
|
}
|
|
|
|
public function getJobsByQueue(string $queueName): array
|
|
{
|
|
return array_filter($this->jobMetrics, fn ($metrics) => $metrics->queueName === $queueName);
|
|
}
|
|
};
|
|
});
|
|
|
|
describe('Job Metrics Recording', function () {
|
|
it('can record successful job execution', function () {
|
|
$jobId = 'success-job-123';
|
|
$executionTime = 2500.0; // 2.5 seconds
|
|
$memoryUsage = 3 * 1024 * 1024; // 3MB
|
|
|
|
$this->metricsManager->recordJobExecution($jobId, $executionTime, $memoryUsage);
|
|
|
|
$metrics = $this->metricsManager->getJobMetrics($jobId);
|
|
expect($metrics)->not->toBeNull();
|
|
expect($metrics->isCompleted())->toBeTrue();
|
|
expect($metrics->executionTimeMs)->toBe($executionTime);
|
|
expect($metrics->memoryUsageBytes)->toBe($memoryUsage);
|
|
});
|
|
|
|
it('can record failed job execution', function () {
|
|
$jobId = 'failed-job-456';
|
|
$errorMessage = 'Database connection timeout';
|
|
$executionTime = 1200.0; // 1.2 seconds
|
|
$memoryUsage = 1.5 * 1024 * 1024; // 1.5MB
|
|
|
|
$this->metricsManager->recordJobFailure($jobId, $errorMessage, $executionTime, $memoryUsage);
|
|
|
|
$metrics = $this->metricsManager->getJobMetrics($jobId);
|
|
expect($metrics)->not->toBeNull();
|
|
expect($metrics->isFailed())->toBeTrue();
|
|
expect($metrics->errorMessage)->toBe($errorMessage);
|
|
expect($metrics->executionTimeMs)->toBe($executionTime);
|
|
expect($metrics->memoryUsageBytes)->toBe($memoryUsage);
|
|
});
|
|
|
|
it('handles non-existent job metrics', function () {
|
|
$metrics = $this->metricsManager->getJobMetrics('non-existent-job');
|
|
expect($metrics)->toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Queue-Level Metrics', function () {
|
|
beforeEach(function () {
|
|
// Set up test data with different queue metrics
|
|
// Email queue jobs
|
|
$this->metricsManager->recordJobExecution('email-1', 1500.0, 2 * 1024 * 1024);
|
|
$this->metricsManager->recordJobExecution('email-2', 2000.0, 2.5 * 1024 * 1024);
|
|
$this->metricsManager->recordJobFailure('email-3', 'SMTP error', 800.0, 1 * 1024 * 1024);
|
|
|
|
// Report queue jobs
|
|
$this->metricsManager->recordJobExecution('report-1', 5000.0, 10 * 1024 * 1024);
|
|
$this->metricsManager->recordJobExecution('report-2', 7500.0, 15 * 1024 * 1024);
|
|
});
|
|
|
|
it('calculates queue metrics correctly', function () {
|
|
$emailMetrics = $this->metricsManager->getQueueMetrics('email-queue');
|
|
|
|
expect($emailMetrics['queue_name'])->toBe('email-queue');
|
|
expect($emailMetrics['total_jobs'])->toBe(3);
|
|
expect($emailMetrics['completed_jobs'])->toBe(2);
|
|
expect($emailMetrics['failed_jobs'])->toBe(1);
|
|
expect($emailMetrics['success_rate'])->toBe(66.67); // 2/3 * 100
|
|
});
|
|
|
|
it('handles empty queue metrics', function () {
|
|
$emptyMetrics = $this->metricsManager->getQueueMetrics('empty-queue');
|
|
|
|
expect($emptyMetrics['total_jobs'])->toBe(0);
|
|
expect($emptyMetrics['completed_jobs'])->toBe(0);
|
|
expect($emptyMetrics['failed_jobs'])->toBe(0);
|
|
expect($emptyMetrics['success_rate'])->toBe(100.0);
|
|
expect($emptyMetrics['average_execution_time_ms'])->toBe(0.0);
|
|
});
|
|
});
|
|
|
|
describe('System-Level Metrics', function () {
|
|
beforeEach(function () {
|
|
// Add various jobs across different queues
|
|
$this->metricsManager->recordJobExecution('job-1', 1000.0, 1 * 1024 * 1024);
|
|
$this->metricsManager->recordJobExecution('job-2', 2000.0, 2 * 1024 * 1024);
|
|
$this->metricsManager->recordJobExecution('job-3', 3000.0, 4 * 1024 * 1024);
|
|
$this->metricsManager->recordJobFailure('job-4', 'Error', 500.0, 0.5 * 1024 * 1024);
|
|
$this->metricsManager->recordJobFailure('job-5', 'Error', 750.0, 1 * 1024 * 1024);
|
|
});
|
|
|
|
it('calculates system-wide metrics correctly', function () {
|
|
$systemMetrics = $this->metricsManager->getSystemMetrics();
|
|
|
|
expect($systemMetrics['total_jobs'])->toBe(5);
|
|
expect($systemMetrics['completed_jobs'])->toBe(3);
|
|
expect($systemMetrics['failed_jobs'])->toBe(2);
|
|
expect($systemMetrics['overall_success_rate'])->toBe(60.0); // 3/5 * 100
|
|
expect($systemMetrics['average_execution_time_ms'])->toBe(1450.0); // (1000+2000+3000+500+750)/5
|
|
expect($systemMetrics['peak_memory_usage_mb'])->toBe(4.0); // Highest from job-3
|
|
});
|
|
|
|
it('handles empty system gracefully', function () {
|
|
$emptyManager = new class () {
|
|
private array $jobMetrics = [];
|
|
|
|
public function getSystemMetrics(): array
|
|
{
|
|
return [
|
|
'total_jobs' => 0,
|
|
'completed_jobs' => 0,
|
|
'failed_jobs' => 0,
|
|
'running_jobs' => 0,
|
|
'overall_success_rate' => 100.0,
|
|
'average_execution_time_ms' => 0.0,
|
|
'peak_memory_usage_mb' => 0.0,
|
|
];
|
|
}
|
|
};
|
|
|
|
$systemMetrics = $emptyManager->getSystemMetrics();
|
|
expect($systemMetrics['total_jobs'])->toBe(0);
|
|
expect($systemMetrics['overall_success_rate'])->toBe(100.0);
|
|
});
|
|
});
|
|
|
|
describe('Performance Analysis', function () {
|
|
beforeEach(function () {
|
|
// Add jobs with varying performance characteristics
|
|
$this->metricsManager->recordJobExecution('fast-job', 100.0, 0.5 * 1024 * 1024);
|
|
$this->metricsManager->recordJobExecution('medium-job', 2500.0, 2 * 1024 * 1024);
|
|
$this->metricsManager->recordJobExecution('slow-job', 10000.0, 1 * 1024 * 1024);
|
|
$this->metricsManager->recordJobExecution('memory-heavy', 1500.0, 50 * 1024 * 1024);
|
|
$this->metricsManager->recordJobExecution('balanced-job', 3000.0, 3 * 1024 * 1024);
|
|
});
|
|
|
|
it('identifies slowest jobs correctly', function () {
|
|
$slowJobs = $this->metricsManager->getTopSlowJobs(3);
|
|
|
|
expect(count($slowJobs))->toBe(3);
|
|
expect($slowJobs[0]->jobId)->toBe('slow-job');
|
|
expect($slowJobs[1]->jobId)->toBe('balanced-job');
|
|
expect($slowJobs[2]->jobId)->toBe('medium-job');
|
|
});
|
|
|
|
it('identifies memory-intensive jobs correctly', function () {
|
|
$memoryJobs = $this->metricsManager->getTopMemoryJobs(3);
|
|
|
|
expect(count($memoryJobs))->toBe(3);
|
|
expect($memoryJobs[0]->jobId)->toBe('memory-heavy');
|
|
expect($memoryJobs[1]->jobId)->toBe('balanced-job');
|
|
expect($memoryJobs[2]->jobId)->toBe('medium-job');
|
|
});
|
|
|
|
it('respects limit parameter for top jobs', function () {
|
|
$limitedSlowJobs = $this->metricsManager->getTopSlowJobs(2);
|
|
expect(count($limitedSlowJobs))->toBe(2);
|
|
|
|
$limitedMemoryJobs = $this->metricsManager->getTopMemoryJobs(1);
|
|
expect(count($limitedMemoryJobs))->toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Queue Filtering and Analysis', function () {
|
|
beforeEach(function () {
|
|
// This would require modifying the mock to support different queues
|
|
// For now, we'll test the interface
|
|
});
|
|
|
|
it('can retrieve jobs by queue', function () {
|
|
$this->metricsManager->recordJobExecution('email-1', 1000.0, 1024 * 1024);
|
|
$this->metricsManager->recordJobExecution('email-2', 2000.0, 2048 * 1024);
|
|
|
|
$queueJobs = $this->metricsManager->getJobsByQueue('default-queue');
|
|
expect(count($queueJobs))->toBe(2);
|
|
});
|
|
|
|
it('handles non-existent queue filtering', function () {
|
|
$emptyQueue = $this->metricsManager->getJobsByQueue('non-existent-queue');
|
|
expect($emptyQueue)->toBe([]);
|
|
});
|
|
});
|
|
});
|