- 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
435 lines
16 KiB
PHP
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');
|
|
}
|
|
} |