Files
michaelschiemer/tests/Framework/Queue/Performance/MultiWorkerThroughputTest.php
Michael Schiemer 5050c7d73a 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
2025-10-05 11:05:04 +02:00

386 lines
14 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\Jobs\Job;
use App\Framework\Queue\Jobs\JobPriority;
use App\Framework\Queue\Jobs\JobStatus;
use App\Framework\Queue\Queue\QueueName;
use App\Framework\Queue\Workers\Worker;
use App\Framework\Queue\Workers\WorkerCapacity;
use App\Framework\Queue\Workers\WorkerId;
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")');
}
}