- 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.
384 lines
13 KiB
PHP
384 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Framework\Queue\Performance;
|
|
|
|
use App\Framework\Database\DatabaseManager;
|
|
use App\Framework\Queue\Distribution\JobDistributionService;
|
|
use App\Framework\Queue\Workers\Worker;
|
|
use App\Framework\Queue\Workers\WorkerRegistry;
|
|
use App\Framework\Queue\Workers\WorkerStatus;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class MultiWorkerThroughputTest extends TestCase
|
|
{
|
|
private DatabaseManager $database;
|
|
|
|
private WorkerRegistry $workerRegistry;
|
|
|
|
private JobDistributionService $distributionService;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->database = $this->createTestDatabase();
|
|
$this->workerRegistry = new WorkerRegistry($this->database);
|
|
$this->distributionService = new JobDistributionService(
|
|
$this->database,
|
|
$this->workerRegistry
|
|
);
|
|
|
|
// Clean up any existing test data
|
|
$this->cleanupTestData();
|
|
|
|
// Warm up database connections
|
|
PerformanceTestHelper::warmupDatabase($this->database->getConnection());
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->cleanupTestData();
|
|
}
|
|
|
|
public function testSingleWorkerThroughput(): void
|
|
{
|
|
$workerCount = 1;
|
|
$jobCount = 100;
|
|
$workers = $this->createWorkers($workerCount, 50); // High capacity worker
|
|
|
|
$result = $this->measureThroughput($workers, $jobCount);
|
|
|
|
$this->assertGreaterThan(
|
|
50, // At least 50 jobs/second with single worker
|
|
$result['throughput'],
|
|
'Single worker throughput below expected minimum'
|
|
);
|
|
|
|
echo "\nSingle Worker Throughput Results:\n";
|
|
echo "Throughput: {$result['throughput']} jobs/second\n";
|
|
echo "Total Time: {$result['total_time_ms']}ms\n";
|
|
echo "Average Job Distribution Time: " .
|
|
PerformanceTestHelper::formatStatistics($result['distribution_stats']) . "\n";
|
|
}
|
|
|
|
public function testFiveWorkerThroughput(): void
|
|
{
|
|
$workerCount = 5;
|
|
$jobCount = 500;
|
|
$workers = $this->createWorkers($workerCount, 20);
|
|
|
|
$result = $this->measureThroughput($workers, $jobCount);
|
|
|
|
$this->assertGreaterThan(
|
|
200, // Should achieve at least 200 jobs/second with 5 workers
|
|
$result['throughput'],
|
|
'Five worker throughput below expected minimum'
|
|
);
|
|
|
|
echo "\nFive Worker Throughput Results:\n";
|
|
echo "Throughput: {$result['throughput']} jobs/second\n";
|
|
echo "Total Time: {$result['total_time_ms']}ms\n";
|
|
echo "Average Job Distribution Time: " .
|
|
PerformanceTestHelper::formatStatistics($result['distribution_stats']) . "\n";
|
|
}
|
|
|
|
public function testTenWorkerThroughput(): void
|
|
{
|
|
$workerCount = 10;
|
|
$jobCount = 1000;
|
|
$workers = $this->createWorkers($workerCount, 15);
|
|
|
|
$result = $this->measureThroughput($workers, $jobCount);
|
|
|
|
$this->assertGreaterThan(
|
|
350, // Should achieve at least 350 jobs/second with 10 workers
|
|
$result['throughput'],
|
|
'Ten worker throughput below expected minimum'
|
|
);
|
|
|
|
echo "\nTen Worker Throughput Results:\n";
|
|
echo "Throughput: {$result['throughput']} jobs/second\n";
|
|
echo "Total Time: {$result['total_time_ms']}ms\n";
|
|
echo "Average Job Distribution Time: " .
|
|
PerformanceTestHelper::formatStatistics($result['distribution_stats']) . "\n";
|
|
}
|
|
|
|
public function testTwentyWorkerThroughput(): void
|
|
{
|
|
$workerCount = 20;
|
|
$jobCount = 2000;
|
|
$workers = $this->createWorkers($workerCount, 10);
|
|
|
|
$result = $this->measureThroughput($workers, $jobCount);
|
|
|
|
$this->assertGreaterThan(
|
|
600, // Should achieve at least 600 jobs/second with 20 workers
|
|
$result['throughput'],
|
|
'Twenty worker throughput below expected minimum'
|
|
);
|
|
|
|
echo "\nTwenty Worker Throughput Results:\n";
|
|
echo "Throughput: {$result['throughput']} jobs/second\n";
|
|
echo "Total Time: {$result['total_time_ms']}ms\n";
|
|
echo "Average Job Distribution Time: " .
|
|
PerformanceTestHelper::formatStatistics($result['distribution_stats']) . "\n";
|
|
}
|
|
|
|
public function testThroughputScaling(): void
|
|
{
|
|
$testCases = [
|
|
['workers' => 1, 'jobs' => 50, 'capacity' => 50],
|
|
['workers' => 5, 'jobs' => 250, 'capacity' => 20],
|
|
['workers' => 10, 'jobs' => 500, 'capacity' => 15],
|
|
['workers' => 20, 'jobs' => 1000, 'capacity' => 10],
|
|
];
|
|
|
|
$results = [];
|
|
|
|
foreach ($testCases as $case) {
|
|
$workers = $this->createWorkers($case['workers'], $case['capacity']);
|
|
$result = $this->measureThroughput($workers, $case['jobs']);
|
|
|
|
$results[] = [
|
|
'worker_count' => $case['workers'],
|
|
'throughput' => $result['throughput'],
|
|
'efficiency' => $result['throughput'] / $case['workers'], // Jobs per worker per second
|
|
];
|
|
|
|
$this->cleanupJobs();
|
|
}
|
|
|
|
// Validate scaling efficiency
|
|
for ($i = 1; $i < count($results); $i++) {
|
|
$prev = $results[$i - 1];
|
|
$curr = $results[$i];
|
|
|
|
$scalingFactor = $curr['worker_count'] / $prev['worker_count'];
|
|
$throughputIncrease = $curr['throughput'] / $prev['throughput'];
|
|
|
|
// Throughput should increase with more workers (allow for some overhead)
|
|
$this->assertGreaterThan(
|
|
$scalingFactor * 0.7, // Allow 30% overhead
|
|
$throughputIncrease,
|
|
sprintf(
|
|
'Throughput scaling below expected: %dx workers should achieve at least %.1fx throughput',
|
|
$scalingFactor,
|
|
$scalingFactor * 0.7
|
|
)
|
|
);
|
|
}
|
|
|
|
echo "\nThroughput Scaling Results:\n";
|
|
foreach ($results as $result) {
|
|
echo sprintf(
|
|
"Workers: %2d, Throughput: %6.1f jobs/sec, Efficiency: %5.1f jobs/worker/sec\n",
|
|
$result['worker_count'],
|
|
$result['throughput'],
|
|
$result['efficiency']
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testMixedCapacityThroughput(): void
|
|
{
|
|
$workers = [
|
|
PerformanceTestHelper::createTestWorker('worker_1', 50),
|
|
PerformanceTestHelper::createTestWorker('worker_2', 30),
|
|
PerformanceTestHelper::createTestWorker('worker_3', 20),
|
|
PerformanceTestHelper::createTestWorker('worker_4', 10),
|
|
PerformanceTestHelper::createTestWorker('worker_5', 5),
|
|
];
|
|
|
|
$this->registerWorkers($workers);
|
|
$jobCount = 500;
|
|
|
|
$result = $this->measureThroughput($workers, $jobCount);
|
|
|
|
// Mixed capacity should still achieve good throughput
|
|
$this->assertGreaterThan(
|
|
200, // Reasonable expectation for mixed capacity workers
|
|
$result['throughput'],
|
|
'Mixed capacity worker throughput below expected minimum'
|
|
);
|
|
|
|
echo "\nMixed Capacity Worker Results:\n";
|
|
echo "Worker Capacities: 50, 30, 20, 10, 5\n";
|
|
echo "Throughput: {$result['throughput']} jobs/second\n";
|
|
echo "Average Job Distribution Time: " .
|
|
PerformanceTestHelper::formatStatistics($result['distribution_stats']) . "\n";
|
|
}
|
|
|
|
public function testSustainedLoadThroughput(): void
|
|
{
|
|
$workers = $this->createWorkers(10, 20);
|
|
$duration = 30; // 30 second sustained load test
|
|
$batchSize = 50;
|
|
|
|
$startTime = microtime(true);
|
|
$endTime = $startTime + $duration;
|
|
$totalJobs = 0;
|
|
$distributionTimes = [];
|
|
|
|
while (microtime(true) < $endTime) {
|
|
$jobs = PerformanceTestHelper::createBulkJobs($batchSize);
|
|
|
|
$batchStartTime = microtime(true);
|
|
foreach ($jobs as $job) {
|
|
$measureResult = PerformanceTestHelper::measureTimeWithResult(
|
|
fn () => $this->distributionService->distributeJob($job)
|
|
);
|
|
$distributionTimes[] = $measureResult['time_ms'];
|
|
}
|
|
$batchEndTime = microtime(true);
|
|
|
|
$totalJobs += count($jobs);
|
|
|
|
// Clean up completed jobs to prevent memory issues
|
|
if ($totalJobs % 200 === 0) {
|
|
$this->cleanupJobs();
|
|
}
|
|
|
|
// Brief pause to prevent overwhelming
|
|
usleep(10000); // 10ms
|
|
}
|
|
|
|
$actualDuration = microtime(true) - $startTime;
|
|
$sustainedThroughput = $totalJobs / $actualDuration;
|
|
|
|
$this->assertGreaterThan(
|
|
100, // Should maintain at least 100 jobs/second under sustained load
|
|
$sustainedThroughput,
|
|
'Sustained load throughput below minimum'
|
|
);
|
|
|
|
$distributionStats = PerformanceTestHelper::calculateStatistics($distributionTimes);
|
|
|
|
echo "\nSustained Load Test Results:\n";
|
|
echo "Duration: {$actualDuration} seconds\n";
|
|
echo "Total Jobs: {$totalJobs}\n";
|
|
echo "Sustained Throughput: {$sustainedThroughput} jobs/second\n";
|
|
echo "Distribution Times: " . PerformanceTestHelper::formatStatistics($distributionStats) . "\n";
|
|
|
|
// Distribution times should remain reasonable under sustained load
|
|
$this->assertLessThan(50, $distributionStats['avg'], 'Average distribution time too high under sustained load');
|
|
$this->assertLessThan(100, $distributionStats['p95'], 'P95 distribution time too high under sustained load');
|
|
}
|
|
|
|
private function measureThroughput(array $workers, int $jobCount): array
|
|
{
|
|
$this->registerWorkers($workers);
|
|
|
|
$jobs = PerformanceTestHelper::createBulkJobs($jobCount);
|
|
$distributionTimes = [];
|
|
|
|
$startTime = microtime(true);
|
|
|
|
foreach ($jobs as $job) {
|
|
$measureResult = PerformanceTestHelper::measureTimeWithResult(
|
|
fn () => $this->distributionService->distributeJob($job)
|
|
);
|
|
$distributionTimes[] = $measureResult['time_ms'];
|
|
}
|
|
|
|
$endTime = microtime(true);
|
|
$totalTimeMs = ($endTime - $startTime) * 1000;
|
|
$throughput = $jobCount / ($endTime - $startTime);
|
|
|
|
return [
|
|
'throughput' => round($throughput, 1),
|
|
'total_time_ms' => round($totalTimeMs, 1),
|
|
'distribution_stats' => PerformanceTestHelper::calculateStatistics($distributionTimes),
|
|
'jobs_processed' => $jobCount,
|
|
];
|
|
}
|
|
|
|
private function createWorkers(int $count, int $capacity): array
|
|
{
|
|
$workers = [];
|
|
for ($i = 1; $i <= $count; $i++) {
|
|
$workers[] = PerformanceTestHelper::createTestWorker(
|
|
"perf_worker_{$i}",
|
|
$capacity,
|
|
WorkerStatus::AVAILABLE
|
|
);
|
|
}
|
|
|
|
return $workers;
|
|
}
|
|
|
|
private function registerWorkers(array $workers): void
|
|
{
|
|
foreach ($workers as $worker) {
|
|
$this->workerRegistry->registerWorker($worker);
|
|
}
|
|
}
|
|
|
|
private function createTestDatabase(): DatabaseManager
|
|
{
|
|
// Use in-memory SQLite for performance tests
|
|
$pdo = new \PDO('sqlite::memory:');
|
|
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
|
|
|
// Create required tables
|
|
$pdo->exec('
|
|
CREATE TABLE workers (
|
|
id TEXT PRIMARY KEY,
|
|
queue_names TEXT NOT NULL,
|
|
capacity INTEGER NOT NULL,
|
|
status TEXT NOT NULL,
|
|
last_heartbeat TEXT NOT NULL,
|
|
metadata TEXT
|
|
)
|
|
');
|
|
|
|
$pdo->exec('
|
|
CREATE TABLE jobs (
|
|
id TEXT PRIMARY KEY,
|
|
type TEXT NOT NULL,
|
|
payload TEXT NOT NULL,
|
|
queue_name TEXT NOT NULL,
|
|
priority INTEGER NOT NULL,
|
|
status TEXT NOT NULL,
|
|
worker_id TEXT,
|
|
created_at TEXT NOT NULL,
|
|
started_at TEXT,
|
|
completed_at TEXT,
|
|
attempts INTEGER DEFAULT 0,
|
|
error_message TEXT
|
|
)
|
|
');
|
|
|
|
$pdo->exec('
|
|
CREATE TABLE distributed_locks (
|
|
lock_key TEXT PRIMARY KEY,
|
|
owner_id TEXT NOT NULL,
|
|
acquired_at TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL
|
|
)
|
|
');
|
|
|
|
// Create indexes for performance
|
|
$pdo->exec('CREATE INDEX idx_workers_status ON workers(status)');
|
|
$pdo->exec('CREATE INDEX idx_jobs_status ON jobs(status)');
|
|
$pdo->exec('CREATE INDEX idx_jobs_queue ON jobs(queue_name)');
|
|
$pdo->exec('CREATE INDEX idx_jobs_priority ON jobs(priority)');
|
|
$pdo->exec('CREATE INDEX idx_locks_expires ON distributed_locks(expires_at)');
|
|
|
|
return new DatabaseManager($pdo);
|
|
}
|
|
|
|
private function cleanupTestData(): void
|
|
{
|
|
$pdo = $this->database->getConnection();
|
|
$pdo->exec('DELETE FROM workers WHERE id LIKE "perf_worker_%"');
|
|
$pdo->exec('DELETE FROM jobs WHERE id LIKE "job_%"');
|
|
$pdo->exec('DELETE FROM distributed_locks');
|
|
}
|
|
|
|
private function cleanupJobs(): void
|
|
{
|
|
$pdo = $this->database->getConnection();
|
|
$pdo->exec('DELETE FROM jobs WHERE status IN ("COMPLETED", "FAILED")');
|
|
}
|
|
}
|