Files
michaelschiemer/tests/Framework/Queue/Performance/DistributedLockPerformanceTest.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

435 lines
16 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Framework\Queue\Performance;
use App\Framework\Database\DatabaseManager;
use App\Framework\Queue\Locks\DatabaseDistributedLock;
use App\Framework\Queue\Locks\LockKey;
use App\Framework\Queue\Locks\LockOwner;
use PHPUnit\Framework\TestCase;
final class DistributedLockPerformanceTest extends TestCase
{
private DatabaseManager $database;
private DatabaseDistributedLock $lockService;
protected function setUp(): void
{
$this->database = $this->createTestDatabase();
$this->lockService = new DatabaseDistributedLock($this->database);
$this->cleanupTestData();
PerformanceTestHelper::warmupDatabase($this->database->getConnection());
}
protected function tearDown(): void
{
$this->cleanupTestData();
}
public function testLockAcquisitionLatency(): void
{
$acquisitionTimes = [];
$iterations = 1000;
for ($i = 0; $i < $iterations; $i++) {
$lockKey = new LockKey("test_lock_{$i}");
$owner = new LockOwner("owner_{$i}");
$time = PerformanceTestHelper::measureTime(function() use ($lockKey, $owner) {
$acquired = $this->lockService->acquire($lockKey, $owner, 30);
if ($acquired) {
$this->lockService->release($lockKey, $owner);
}
return $acquired;
});
$acquisitionTimes[] = $time;
}
$stats = PerformanceTestHelper::calculateStatistics($acquisitionTimes);
// Validate performance benchmarks
$this->assertLessThan(2.0, $stats['avg'], 'Average lock acquisition time exceeds 2ms');
$this->assertLessThan(5.0, $stats['p95'], 'P95 lock acquisition time exceeds 5ms');
$this->assertLessThan(10.0, $stats['p99'], 'P99 lock acquisition time exceeds 10ms');
echo "\nLock Acquisition Latency Results:\n";
echo "Iterations: {$iterations}\n";
echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
PerformanceTestHelper::assertPerformance(
$acquisitionTimes,
2.0,
5.0,
'Lock acquisition'
);
}
public function testLockReleaseLatency(): void
{
$releaseTimes = [];
$iterations = 1000;
// Pre-acquire locks
$locks = [];
for ($i = 0; $i < $iterations; $i++) {
$lockKey = new LockKey("release_lock_{$i}");
$owner = new LockOwner("owner_{$i}");
$acquired = $this->lockService->acquire($lockKey, $owner, 60);
if ($acquired) {
$locks[] = ['key' => $lockKey, 'owner' => $owner];
}
}
// Measure release times
foreach ($locks as $lock) {
$time = PerformanceTestHelper::measureTime(function() use ($lock) {
return $this->lockService->release($lock['key'], $lock['owner']);
});
$releaseTimes[] = $time;
}
$stats = PerformanceTestHelper::calculateStatistics($releaseTimes);
// Validate performance benchmarks
$this->assertLessThan(1.5, $stats['avg'], 'Average lock release time exceeds 1.5ms');
$this->assertLessThan(3.0, $stats['p95'], 'P95 lock release time exceeds 3ms');
$this->assertLessThan(8.0, $stats['p99'], 'P99 lock release time exceeds 8ms');
echo "\nLock Release Latency Results:\n";
echo "Locks released: " . count($releaseTimes) . "\n";
echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
PerformanceTestHelper::assertPerformance(
$releaseTimes,
1.5,
3.0,
'Lock release'
);
}
public function testLockContentionPerformance(): void
{
$lockKey = new LockKey('contended_lock');
$concurrentAttempts = 20;
$attemptsPerWorker = 50;
$allResults = [];
$successCounts = [];
$failureCounts = [];
// Simulate concurrent lock acquisition attempts
for ($worker = 0; $worker < $concurrentAttempts; $worker++) {
$owner = new LockOwner("worker_{$worker}");
$workerResults = [];
$successes = 0;
$failures = 0;
for ($attempt = 0; $attempt < $attemptsPerWorker; $attempt++) {
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($lockKey, $owner) {
$acquired = $this->lockService->acquire($lockKey, $owner, 1); // 1 second timeout
if ($acquired) {
// Hold lock briefly then release
usleep(100); // 0.1ms
$this->lockService->release($lockKey, $owner);
return true;
}
return false;
});
$workerResults[] = $result['time_ms'];
if ($result['result']) {
$successes++;
} else {
$failures++;
}
// Brief pause between attempts
usleep(50); // 0.05ms
}
$allResults = array_merge($allResults, $workerResults);
$successCounts[$worker] = $successes;
$failureCounts[$worker] = $failures;
}
$stats = PerformanceTestHelper::calculateStatistics($allResults);
$totalSuccesses = array_sum($successCounts);
$totalFailures = array_sum($failureCounts);
$successRate = $totalSuccesses / ($totalSuccesses + $totalFailures) * 100;
echo "\nLock Contention Performance Results:\n";
echo "Concurrent workers: {$concurrentAttempts}\n";
echo "Total attempts: " . ($totalSuccesses + $totalFailures) . "\n";
echo "Successful acquisitions: {$totalSuccesses}\n";
echo "Failed acquisitions: {$totalFailures}\n";
echo "Success rate: {$successRate}%\n";
echo "Attempt performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Under contention, some failures are expected but most should succeed
$this->assertGreaterThan(50.0, $successRate, 'Lock success rate too low under contention');
// Performance should degrade gracefully under contention
$this->assertLessThan(50.0, $stats['avg'], 'Average lock time too high under contention');
$this->assertLessThan(200.0, $stats['p95'], 'P95 lock time too high under contention');
}
public function testLockTimeoutPerformance(): void
{
$lockKey = new LockKey('timeout_lock');
$owner1 = new LockOwner('owner_1');
$owner2 = new LockOwner('owner_2');
// First owner acquires the lock
$acquired = $this->lockService->acquire($lockKey, $owner1, 60);
$this->assertTrue($acquired, 'Initial lock acquisition should succeed');
$timeoutResults = [];
$iterations = 100;
// Second owner repeatedly tries to acquire with short timeouts
for ($i = 0; $i < $iterations; $i++) {
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($lockKey, $owner2) {
return $this->lockService->acquire($lockKey, $owner2, 0.1); // 100ms timeout
});
$timeoutResults[] = $result['time_ms'];
$this->assertFalse($result['result'], 'Lock acquisition should fail due to timeout');
}
// Release the lock
$this->lockService->release($lockKey, $owner1);
$stats = PerformanceTestHelper::calculateStatistics($timeoutResults);
echo "\nLock Timeout Performance Results:\n";
echo "Timeout attempts: {$iterations}\n";
echo "Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Timeout should be close to requested timeout (100ms) but not much longer
$this->assertGreaterThan(90.0, $stats['avg'], 'Timeout too fast - should wait approximately 100ms');
$this->assertLessThan(150.0, $stats['avg'], 'Timeout too slow - should be close to 100ms');
// Timeouts should be consistent
$this->assertLessThan(30.0, $stats['stddev'], 'Timeout timing too inconsistent');
}
public function testLockCleanupPerformance(): void
{
$expiredLockCount = 500;
$validLockCount = 100;
// Create expired locks
$pdo = $this->database->getConnection();
$expiredTime = (new \DateTimeImmutable())->modify('-1 hour');
for ($i = 0; $i < $expiredLockCount; $i++) {
$pdo->exec(sprintf(
"INSERT INTO distributed_locks (lock_key, owner_id, acquired_at, expires_at) VALUES ('expired_%d', 'owner_%d', '%s', '%s')",
$i,
$i,
$expiredTime->format('Y-m-d H:i:s'),
$expiredTime->format('Y-m-d H:i:s')
));
}
// Create valid locks
$validTime = (new \DateTimeImmutable())->modify('+1 hour');
for ($i = 0; $i < $validLockCount; $i++) {
$pdo->exec(sprintf(
"INSERT INTO distributed_locks (lock_key, owner_id, acquired_at, expires_at) VALUES ('valid_%d', 'owner_%d', '%s', '%s')",
$i,
$i,
$validTime->format('Y-m-d H:i:s'),
$validTime->format('Y-m-d H:i:s')
));
}
// Measure cleanup performance
$cleanupTime = PerformanceTestHelper::measureTime(function() {
$this->lockService->cleanupExpiredLocks();
});
// Verify cleanup results
$remaining = $pdo->query('SELECT COUNT(*) FROM distributed_locks')->fetchColumn();
echo "\nLock Cleanup Performance Results:\n";
echo "Expired locks created: {$expiredLockCount}\n";
echo "Valid locks created: {$validLockCount}\n";
echo "Locks remaining after cleanup: {$remaining}\n";
echo "Cleanup time: {$cleanupTime}ms\n";
$this->assertEquals($validLockCount, $remaining, 'Should only clean up expired locks');
$this->assertLessThan(100.0, $cleanupTime, 'Lock cleanup should complete within 100ms');
// Test cleanup performance with larger dataset
$this->cleanupTestData();
// Create many more expired locks
$largeExpiredCount = 5000;
for ($i = 0; $i < $largeExpiredCount; $i++) {
$pdo->exec(sprintf(
"INSERT INTO distributed_locks (lock_key, owner_id, acquired_at, expires_at) VALUES ('large_expired_%d', 'owner_%d', '%s', '%s')",
$i,
$i,
$expiredTime->format('Y-m-d H:i:s'),
$expiredTime->format('Y-m-d H:i:s')
));
}
$largeCleanupTime = PerformanceTestHelper::measureTime(function() {
$this->lockService->cleanupExpiredLocks();
});
echo "Large cleanup ({$largeExpiredCount} locks): {$largeCleanupTime}ms\n";
$this->assertLessThan(500.0, $largeCleanupTime, 'Large cleanup should complete within 500ms');
}
public function testHighThroughputLockOperations(): void
{
$operationsPerSecond = 500;
$testDuration = 10; // seconds
$totalOperations = $operationsPerSecond * $testDuration;
echo "\nHigh Throughput Lock Operations Test:\n";
echo "Target: {$operationsPerSecond} operations/second for {$testDuration} seconds\n";
$loadResult = PerformanceTestHelper::simulateLoad(
function($index) {
$lockKey = new LockKey("throughput_lock_{$index}");
$owner = new LockOwner("owner_{$index}");
// Acquire and immediately release
$acquired = $this->lockService->acquire($lockKey, $owner, 5);
if ($acquired) {
$this->lockService->release($lockKey, $owner);
return true;
}
return false;
},
$totalOperations,
25, // Moderate concurrency
$testDuration
);
$actualThroughput = $loadResult['throughput_ops_per_sec'];
$operationTimes = array_column($loadResult['results'], 'time_ms');
$stats = PerformanceTestHelper::calculateStatistics($operationTimes);
$successfulOperations = count(array_filter(
array_column($loadResult['results'], 'result'),
fn($result) => $result['result'] === true
));
echo "Actual Throughput: {$actualThroughput} operations/second\n";
echo "Successful operations: {$successfulOperations}\n";
echo "Operation Performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Should achieve at least 70% of target throughput
$this->assertGreaterThan(
$operationsPerSecond * 0.7,
$actualThroughput,
'High throughput lock operations below 70% of target'
);
// Most operations should succeed
$successRate = $successfulOperations / $loadResult['operations_completed'] * 100;
$this->assertGreaterThan(95.0, $successRate, 'Lock operation success rate too low');
// Operation times should remain reasonable
$this->assertLessThan(20.0, $stats['avg'], 'Average operation time too high under load');
$this->assertLessThan(100.0, $stats['p95'], 'P95 operation time too high under load');
}
public function testLockRetryPerformance(): void
{
$lockKey = new LockKey('retry_lock');
$owner1 = new LockOwner('blocking_owner');
$owner2 = new LockOwner('retry_owner');
// First owner acquires lock for a short time
$this->lockService->acquire($lockKey, $owner1, 60);
// Schedule lock release after 200ms
$releaseTime = microtime(true) + 0.2;
$retryAttempts = [];
$maxRetries = 50;
$retryDelay = 50; // 50ms between retries
for ($i = 0; $i < $maxRetries; $i++) {
$startTime = microtime(true);
// Release the lock when it's time
if ($startTime >= $releaseTime && $this->lockService->isLocked($lockKey)) {
$this->lockService->release($lockKey, $owner1);
}
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($lockKey, $owner2) {
return $this->lockService->acquire($lockKey, $owner2, 0.01); // 10ms timeout
});
$retryAttempts[] = [
'time_ms' => $result['time_ms'],
'success' => $result['result']
];
if ($result['result']) {
// Successfully acquired, release it and stop
$this->lockService->release($lockKey, $owner2);
break;
}
usleep($retryDelay * 1000); // Convert to microseconds
}
$retryTimes = array_column($retryAttempts, 'time_ms');
$stats = PerformanceTestHelper::calculateStatistics($retryTimes);
$successfulAttempt = array_search(true, array_column($retryAttempts, 'success'));
$attemptsUntilSuccess = $successfulAttempt !== false ? $successfulAttempt + 1 : $maxRetries;
echo "\nLock Retry Performance Results:\n";
echo "Attempts until success: {$attemptsUntilSuccess}\n";
echo "Retry performance: " . PerformanceTestHelper::formatStatistics($stats) . "\n";
// Should eventually succeed
$this->assertNotFalse($successfulAttempt, 'Lock retry should eventually succeed');
// Retry attempts should be fast (mostly just timeout delays)
$this->assertLessThan(20.0, $stats['avg'], 'Retry attempts taking too long');
}
private function createTestDatabase(): DatabaseManager
{
$pdo = new \PDO('sqlite::memory:');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$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
)
');
// Performance-optimized indexes
$pdo->exec('CREATE INDEX idx_locks_expires ON distributed_locks(expires_at)');
$pdo->exec('CREATE INDEX idx_locks_owner ON distributed_locks(owner_id)');
return new DatabaseManager($pdo);
}
private function cleanupTestData(): void
{
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM distributed_locks');
}
}