- 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.
417 lines
15 KiB
PHP
417 lines
15 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\Distribution\LoadBalancer;
|
|
use App\Framework\Queue\Jobs\JobPriority;
|
|
use App\Framework\Queue\Queue\QueueName;
|
|
use App\Framework\Queue\Workers\WorkerRegistry;
|
|
use App\Framework\Queue\Workers\WorkerStatus;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class LoadBalancingPerformanceTest extends TestCase
|
|
{
|
|
private DatabaseManager $database;
|
|
|
|
private WorkerRegistry $workerRegistry;
|
|
|
|
private JobDistributionService $distributionService;
|
|
|
|
private LoadBalancer $loadBalancer;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->database = $this->createTestDatabase();
|
|
$this->workerRegistry = new WorkerRegistry($this->database);
|
|
$this->loadBalancer = new LoadBalancer($this->workerRegistry);
|
|
$this->distributionService = new JobDistributionService(
|
|
$this->database,
|
|
$this->workerRegistry
|
|
);
|
|
|
|
$this->cleanupTestData();
|
|
PerformanceTestHelper::warmupDatabase($this->database->getConnection());
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->cleanupTestData();
|
|
}
|
|
|
|
public function testWorkerSelectionLatency(): void
|
|
{
|
|
// Create workers with different loads
|
|
$workers = [
|
|
PerformanceTestHelper::createTestWorker('worker_1', 20),
|
|
PerformanceTestHelper::createTestWorker('worker_2', 15),
|
|
PerformanceTestHelper::createTestWorker('worker_3', 10),
|
|
PerformanceTestHelper::createTestWorker('worker_4', 25),
|
|
PerformanceTestHelper::createTestWorker('worker_5', 30),
|
|
];
|
|
|
|
$this->registerWorkers($workers);
|
|
|
|
$selectionTimes = [];
|
|
$iterations = 1000;
|
|
|
|
for ($i = 0; $i < $iterations; $i++) {
|
|
$time = PerformanceTestHelper::measureTime(function () {
|
|
$this->loadBalancer->selectWorker(new QueueName('test_queue'));
|
|
});
|
|
$selectionTimes[] = $time;
|
|
}
|
|
|
|
$stats = PerformanceTestHelper::calculateStatistics($selectionTimes);
|
|
|
|
// Validate performance benchmarks
|
|
$this->assertLessThan(5.0, $stats['avg'], 'Average worker selection time exceeds 5ms');
|
|
$this->assertLessThan(10.0, $stats['p95'], 'P95 worker selection time exceeds 10ms');
|
|
$this->assertLessThan(20.0, $stats['p99'], 'P99 worker selection time exceeds 20ms');
|
|
|
|
echo "\nWorker Selection Latency Results:\n";
|
|
echo "Iterations: {$iterations}\n";
|
|
echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
|
|
|
|
PerformanceTestHelper::assertPerformance(
|
|
$selectionTimes,
|
|
5.0,
|
|
10.0,
|
|
'Worker selection'
|
|
);
|
|
}
|
|
|
|
public function testJobDistributionLatency(): void
|
|
{
|
|
$workers = $this->createBalancedWorkers(10, 20);
|
|
$this->registerWorkers($workers);
|
|
|
|
$distributionTimes = [];
|
|
$iterations = 500;
|
|
|
|
for ($i = 0; $i < $iterations; $i++) {
|
|
$job = PerformanceTestHelper::createTestJob("dist_job_{$i}");
|
|
|
|
$time = PerformanceTestHelper::measureTime(function () use ($job) {
|
|
$this->distributionService->distributeJob($job);
|
|
});
|
|
|
|
$distributionTimes[] = $time;
|
|
}
|
|
|
|
$stats = PerformanceTestHelper::calculateStatistics($distributionTimes);
|
|
|
|
// Validate performance benchmarks
|
|
$this->assertLessThan(10.0, $stats['avg'], 'Average job distribution time exceeds 10ms');
|
|
$this->assertLessThan(20.0, $stats['p95'], 'P95 job distribution time exceeds 20ms');
|
|
$this->assertLessThan(50.0, $stats['p99'], 'P99 job distribution time exceeds 50ms');
|
|
|
|
echo "\nJob Distribution Latency Results:\n";
|
|
echo "Iterations: {$iterations}\n";
|
|
echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
|
|
|
|
PerformanceTestHelper::assertPerformance(
|
|
$distributionTimes,
|
|
10.0,
|
|
20.0,
|
|
'Job distribution'
|
|
);
|
|
}
|
|
|
|
public function testHighThroughputDistribution(): void
|
|
{
|
|
$workers = $this->createBalancedWorkers(15, 25);
|
|
$this->registerWorkers($workers);
|
|
|
|
$jobsPerSecond = 1000;
|
|
$testDuration = 10; // seconds
|
|
$totalJobs = $jobsPerSecond * $testDuration;
|
|
|
|
echo "\nHigh Throughput Distribution Test:\n";
|
|
echo "Target: {$jobsPerSecond} jobs/second for {$testDuration} seconds\n";
|
|
|
|
$loadResult = PerformanceTestHelper::simulateLoad(
|
|
function ($index) {
|
|
$job = PerformanceTestHelper::createTestJob("load_job_{$index}");
|
|
|
|
return $this->distributionService->distributeJob($job);
|
|
},
|
|
$totalJobs,
|
|
50, // High concurrency
|
|
$testDuration
|
|
);
|
|
|
|
$actualThroughput = $loadResult['throughput_ops_per_sec'];
|
|
$distributionTimes = array_column($loadResult['results'], 'time_ms');
|
|
$stats = PerformanceTestHelper::calculateStatistics($distributionTimes);
|
|
|
|
echo "Actual Throughput: {$actualThroughput} jobs/second\n";
|
|
echo "Distribution Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
|
|
|
|
// Should achieve at least 80% of target throughput
|
|
$this->assertGreaterThan(
|
|
$jobsPerSecond * 0.8,
|
|
$actualThroughput,
|
|
'High throughput distribution below 80% of target'
|
|
);
|
|
|
|
// Distribution times should remain reasonable under high load
|
|
$this->assertLessThan(30.0, $stats['avg'], 'Average distribution time too high under load');
|
|
$this->assertLessThan(100.0, $stats['p95'], 'P95 distribution time too high under load');
|
|
}
|
|
|
|
public function testFairDistributionUnderLoad(): void
|
|
{
|
|
$workers = [
|
|
PerformanceTestHelper::createTestWorker('worker_1', 10),
|
|
PerformanceTestHelper::createTestWorker('worker_2', 20),
|
|
PerformanceTestHelper::createTestWorker('worker_3', 30),
|
|
PerformanceTestHelper::createTestWorker('worker_4', 15),
|
|
PerformanceTestHelper::createTestWorker('worker_5', 25),
|
|
];
|
|
|
|
$this->registerWorkers($workers);
|
|
|
|
$jobCount = 1000;
|
|
$workerAssignments = [];
|
|
|
|
for ($i = 0; $i < $jobCount; $i++) {
|
|
$job = PerformanceTestHelper::createTestJob("fair_job_{$i}");
|
|
$selectedWorker = $this->distributionService->distributeJob($job);
|
|
|
|
if ($selectedWorker) {
|
|
$workerId = $selectedWorker->id->toString();
|
|
$workerAssignments[$workerId] = ($workerAssignments[$workerId] ?? 0) + 1;
|
|
}
|
|
}
|
|
|
|
echo "\nFair Distribution Results:\n";
|
|
$totalCapacity = array_sum(array_map(fn ($w) => $w->capacity->value, $workers));
|
|
|
|
foreach ($workers as $worker) {
|
|
$workerId = $worker->id->toString();
|
|
$assignments = $workerAssignments[$workerId] ?? 0;
|
|
$expectedRatio = $worker->capacity->value / $totalCapacity;
|
|
$actualRatio = $assignments / $jobCount;
|
|
$efficiency = ($actualRatio / $expectedRatio) * 100;
|
|
|
|
echo sprintf(
|
|
"Worker %s: Capacity=%d, Assignments=%d, Expected=%.1f%%, Actual=%.1f%%, Efficiency=%.1f%%\n",
|
|
$workerId,
|
|
$worker->capacity->value,
|
|
$assignments,
|
|
$expectedRatio * 100,
|
|
$actualRatio * 100,
|
|
$efficiency
|
|
);
|
|
|
|
// Each worker should get jobs roughly proportional to their capacity
|
|
// Allow 20% variance for fair distribution
|
|
$this->assertGreaterThan(
|
|
80.0,
|
|
$efficiency,
|
|
"Worker {$workerId} received fewer jobs than expected (efficiency: {$efficiency}%)"
|
|
);
|
|
|
|
$this->assertLessThan(
|
|
120.0,
|
|
$efficiency,
|
|
"Worker {$workerId} received more jobs than expected (efficiency: {$efficiency}%)"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testMixedCapacityLoadBalancing(): void
|
|
{
|
|
// Create workers with very different capacities
|
|
$workers = [
|
|
PerformanceTestHelper::createTestWorker('small_worker', 5),
|
|
PerformanceTestHelper::createTestWorker('medium_worker_1', 15),
|
|
PerformanceTestHelper::createTestWorker('medium_worker_2', 20),
|
|
PerformanceTestHelper::createTestWorker('large_worker', 50),
|
|
PerformanceTestHelper::createTestWorker('xlarge_worker', 100),
|
|
];
|
|
|
|
$this->registerWorkers($workers);
|
|
|
|
$selectionTimes = [];
|
|
$iterations = 500;
|
|
|
|
for ($i = 0; $i < $iterations; $i++) {
|
|
$time = PerformanceTestHelper::measureTime(function () {
|
|
$this->loadBalancer->selectWorker(new QueueName('test_queue'));
|
|
});
|
|
$selectionTimes[] = $time;
|
|
}
|
|
|
|
$stats = PerformanceTestHelper::calculateStatistics($selectionTimes);
|
|
|
|
echo "\nMixed Capacity Load Balancing Results:\n";
|
|
echo "Worker capacities: 5, 15, 20, 50, 100\n";
|
|
echo "Selection performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
|
|
|
|
// Selection should still be fast even with mixed capacities
|
|
$this->assertLessThan(8.0, $stats['avg'], 'Mixed capacity selection average time too high');
|
|
$this->assertLessThan(15.0, $stats['p95'], 'Mixed capacity selection P95 time too high');
|
|
}
|
|
|
|
public function testPriorityJobDistribution(): void
|
|
{
|
|
$workers = $this->createBalancedWorkers(8, 15);
|
|
$this->registerWorkers($workers);
|
|
|
|
$priorities = [JobPriority::LOW, JobPriority::NORMAL, JobPriority::HIGH, JobPriority::CRITICAL];
|
|
$distributionTimes = [];
|
|
|
|
foreach ($priorities as $priority) {
|
|
$iterationsPerPriority = 100;
|
|
|
|
for ($i = 0; $i < $iterationsPerPriority; $i++) {
|
|
$job = PerformanceTestHelper::createTestJob(
|
|
"priority_job_{$priority->value}_{$i}",
|
|
$priority
|
|
);
|
|
|
|
$time = PerformanceTestHelper::measureTime(function () use ($job) {
|
|
$this->distributionService->distributeJob($job);
|
|
});
|
|
|
|
$distributionTimes[$priority->value][] = $time;
|
|
}
|
|
}
|
|
|
|
echo "\nPriority Job Distribution Results:\n";
|
|
|
|
foreach ($priorities as $priority) {
|
|
$stats = PerformanceTestHelper::calculateStatistics($distributionTimes[$priority->value]);
|
|
echo "Priority {$priority->value}: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
|
|
|
|
// All priorities should have similar distribution performance
|
|
$this->assertLessThan(15.0, $stats['avg'], "Priority {$priority->value} distribution too slow");
|
|
}
|
|
}
|
|
|
|
public function testWorkerOverloadHandling(): void
|
|
{
|
|
// Create workers that will quickly become overloaded
|
|
$workers = [
|
|
PerformanceTestHelper::createTestWorker('worker_1', 2),
|
|
PerformanceTestHelper::createTestWorker('worker_2', 3),
|
|
PerformanceTestHelper::createTestWorker('worker_3', 2),
|
|
];
|
|
|
|
$this->registerWorkers($workers);
|
|
|
|
$distributionTimes = [];
|
|
$successfulDistributions = 0;
|
|
$failedDistributions = 0;
|
|
|
|
// Try to distribute more jobs than total worker capacity
|
|
$jobCount = 20; // Total capacity is only 7
|
|
|
|
for ($i = 0; $i < $jobCount; $i++) {
|
|
$job = PerformanceTestHelper::createTestJob("overload_job_{$i}");
|
|
|
|
$result = PerformanceTestHelper::measureTimeWithResult(function () use ($job) {
|
|
return $this->distributionService->distributeJob($job);
|
|
});
|
|
|
|
$distributionTimes[] = $result['time_ms'];
|
|
|
|
if ($result['result'] !== null) {
|
|
$successfulDistributions++;
|
|
} else {
|
|
$failedDistributions++;
|
|
}
|
|
}
|
|
|
|
$stats = PerformanceTestHelper::calculateStatistics($distributionTimes);
|
|
|
|
echo "\nWorker Overload Handling Results:\n";
|
|
echo "Total jobs: {$jobCount}\n";
|
|
echo "Successful distributions: {$successfulDistributions}\n";
|
|
echo "Failed distributions: {$failedDistributions}\n";
|
|
echo "Distribution performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
|
|
|
|
// Should successfully distribute up to total capacity
|
|
$this->assertGreaterThanOrEqual(7, $successfulDistributions, 'Should distribute at least 7 jobs');
|
|
|
|
// Distribution times should remain reasonable even when workers are overloaded
|
|
$this->assertLessThan(20.0, $stats['avg'], 'Distribution time too high during overload');
|
|
}
|
|
|
|
private function createBalancedWorkers(int $count, int $capacity): array
|
|
{
|
|
$workers = [];
|
|
for ($i = 1; $i <= $count; $i++) {
|
|
$workers[] = PerformanceTestHelper::createTestWorker(
|
|
"balanced_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
|
|
{
|
|
$pdo = new \PDO('sqlite::memory:');
|
|
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
|
|
|
// Create required tables with indexes for performance
|
|
$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
|
|
)
|
|
');
|
|
|
|
// Performance-optimized indexes
|
|
$pdo->exec('CREATE INDEX idx_workers_status_capacity ON workers(status, capacity DESC)');
|
|
$pdo->exec('CREATE INDEX idx_workers_queue_status ON workers(queue_names, status)');
|
|
$pdo->exec('CREATE INDEX idx_jobs_worker_status ON jobs(worker_id, status)');
|
|
$pdo->exec('CREATE INDEX idx_jobs_priority_created ON jobs(priority DESC, created_at)');
|
|
|
|
return new DatabaseManager($pdo);
|
|
}
|
|
|
|
private function cleanupTestData(): void
|
|
{
|
|
$pdo = $this->database->getConnection();
|
|
$pdo->exec('DELETE FROM workers');
|
|
$pdo->exec('DELETE FROM jobs');
|
|
}
|
|
}
|