feat(Production): Complete production deployment infrastructure

- 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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -24,6 +24,7 @@ test('basic queue system initialization', function () {
echo "✅ All queue system services resolved successfully\n";
} catch (\Throwable $e) {
echo "❌ Error resolving queue services: " . $e->getMessage() . "\n";
throw $e;
}
});
});

View File

@@ -2,13 +2,11 @@
declare(strict_types=1);
use App\Framework\Queue\Exceptions\InvalidDeadLetterQueueNameException;
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueuePriority;
use App\Framework\Queue\ValueObjects\FailureReason;
use App\Framework\Queue\Exceptions\InvalidDeadLetterQueueNameException;
use App\Framework\Queue\ValueObjects\QueueName;
describe('DeadLetterQueueName Value Object', function () {
@@ -21,7 +19,7 @@ describe('DeadLetterQueueName Value Object', function () {
'user.registration.failed',
'queue_123_failed',
'a', // minimum length
str_repeat('a', 100) // maximum length
str_repeat('a', 100), // maximum length
];
foreach ($validNames as $name) {
@@ -57,11 +55,11 @@ describe('DeadLetterQueueName Value Object', function () {
'queue?invalid',
'queue/invalid',
'queue~invalid',
'queue`invalid'
'queue`invalid',
];
foreach ($invalidNames as $name) {
expect(fn() => DeadLetterQueueName::fromString($name))
expect(fn () => DeadLetterQueueName::fromString($name))
->toThrow(InvalidDeadLetterQueueNameException::class);
}
});
@@ -166,53 +164,66 @@ describe('Dead Letter Queue Mock Implementation', function () {
beforeEach(function () {
// Create a mock dead letter queue for testing
$this->mockDlq = new class {
$this->mockDlq = new class () {
private array $jobs = [];
private array $stats = [];
public function addFailedJob(array $jobData): void {
public function addFailedJob(array $jobData): void
{
$this->jobs[] = $jobData;
}
public function getJobs(DeadLetterQueueName $queueName, int $limit = 100): array {
public function getJobs(DeadLetterQueueName $queueName, int $limit = 100): array
{
return array_slice(
array_filter($this->jobs, fn($job) => $job['dlq_name'] === $queueName->toString()),
array_filter($this->jobs, fn ($job) => $job['dlq_name'] === $queueName->toString()),
0,
$limit
);
}
public function retryJob(string $jobId): bool {
public function retryJob(string $jobId): bool
{
foreach ($this->jobs as $index => $job) {
if ($job['id'] === $jobId) {
unset($this->jobs[$index]);
return true;
}
}
return false;
}
public function deleteJob(string $jobId): bool {
public function deleteJob(string $jobId): bool
{
foreach ($this->jobs as $index => $job) {
if ($job['id'] === $jobId) {
unset($this->jobs[$index]);
return true;
}
}
return false;
}
public function clearQueue(DeadLetterQueueName $queueName): int {
public function clearQueue(DeadLetterQueueName $queueName): int
{
$initialCount = count($this->jobs);
$this->jobs = array_filter(
$this->jobs,
fn($job) => $job['dlq_name'] !== $queueName->toString()
fn ($job) => $job['dlq_name'] !== $queueName->toString()
);
return $initialCount - count($this->jobs);
}
public function getQueueStats(DeadLetterQueueName $queueName): array {
public function getQueueStats(DeadLetterQueueName $queueName): array
{
$jobs = $this->getJobs($queueName);
return [
'queue_name' => $queueName->toString(),
'total_jobs' => count($jobs),
@@ -221,13 +232,15 @@ describe('Dead Letter Queue Mock Implementation', function () {
'avg_retry_count' => count($jobs) > 0 ? array_sum(array_column($jobs, 'retry_count')) / count($jobs) : 0,
'max_retry_count' => count($jobs) > 0 ? max(array_column($jobs, 'retry_count')) : 0,
'oldest_job' => count($jobs) > 0 ? min(array_column($jobs, 'failed_at')) : null,
'newest_job' => count($jobs) > 0 ? max(array_column($jobs, 'failed_at')) : null
'newest_job' => count($jobs) > 0 ? max(array_column($jobs, 'failed_at')) : null,
];
}
public function getAvailableQueues(): array {
public function getAvailableQueues(): array
{
$queueNames = array_unique(array_column($this->jobs, 'dlq_name'));
return array_map(fn($name) => DeadLetterQueueName::fromString($name), $queueNames);
return array_map(fn ($name) => DeadLetterQueueName::fromString($name), $queueNames);
}
};
});
@@ -248,7 +261,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => 'Stack trace here...',
'failed_attempts' => 3,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
];
$this->mockDlq->addFailedJob($jobData);
@@ -274,7 +287,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => 'trace1',
'failed_attempts' => 1,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
$this->mockDlq->addFailedJob([
@@ -288,7 +301,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => 'trace2',
'failed_attempts' => 2,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
$emailJobs = $this->mockDlq->getJobs($emailDlq);
@@ -315,7 +328,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => 'retry_trace',
'failed_attempts' => 1,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
expect(count($this->mockDlq->getJobs($dlqName)))->toBe(1);
@@ -342,7 +355,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => 'delete_trace',
'failed_attempts' => 5,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
expect(count($this->mockDlq->getJobs($dlqName)))->toBe(1);
@@ -369,7 +382,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => "trace_{$i}",
'failed_attempts' => $i,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
}
@@ -405,7 +418,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => "trace_{$index}",
'failed_attempts' => $jobStats['failed_attempts'],
'failed_at' => date('Y-m-d H:i:s', time() - $index * 3600), // Different times
'retry_count' => $jobStats['retry_count']
'retry_count' => $jobStats['retry_count'],
]);
}
@@ -439,7 +452,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
$queues = [
DeadLetterQueueName::fromString('email_failed'),
DeadLetterQueueName::fromString('report_failed'),
DeadLetterQueueName::fromString('background_failed')
DeadLetterQueueName::fromString('background_failed'),
];
foreach ($queues as $index => $queue) {
@@ -454,7 +467,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => "trace_{$index}",
'failed_attempts' => 1,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
}
@@ -462,7 +475,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
expect(count($availableQueues))->toBe(3);
$queueNames = array_map(fn($q) => $q->toString(), $availableQueues);
$queueNames = array_map(fn ($q) => $q->toString(), $availableQueues);
expect($queueNames)->toContain('email_failed');
expect($queueNames)->toContain('report_failed');
expect($queueNames)->toContain('background_failed');
@@ -502,7 +515,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => "trace_{$i}",
'failed_attempts' => 1,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
}
@@ -518,17 +531,20 @@ describe('Dead Letter Queue Mock Implementation', function () {
describe('Dead Letter Queue Integration Scenarios', function () {
beforeEach(function () {
$this->testJob = new class {
$this->testJob = new class () {
public function __construct(
public string $email = 'test@example.com',
public string $subject = 'Test Email'
) {}
) {
}
public function handle(): bool {
public function handle(): bool
{
// Simulate processing that might fail
if ($this->email === 'invalid@test.com') {
throw new \Exception('Invalid email address');
}
return true;
}
};
@@ -537,23 +553,23 @@ describe('Dead Letter Queue Integration Scenarios', function () {
'network_timeout' => [
'reason' => 'Network connection timeout',
'exception' => 'NetworkTimeoutException',
'retryable' => true
'retryable' => true,
],
'invalid_data' => [
'reason' => 'Invalid email format',
'exception' => 'ValidationException',
'retryable' => false
'retryable' => false,
],
'service_unavailable' => [
'reason' => 'External service unavailable',
'exception' => 'ServiceUnavailableException',
'retryable' => true
'retryable' => true,
],
'permission_denied' => [
'reason' => 'Insufficient permissions',
'exception' => 'PermissionException',
'retryable' => false
]
'retryable' => false,
],
];
});
@@ -576,12 +592,12 @@ describe('Dead Letter Queue Integration Scenarios', function () {
it('demonstrates retry strategies for different failure types', function () {
$retryableFailures = array_filter(
$this->failureScenarios,
fn($scenario) => $scenario['retryable']
fn ($scenario) => $scenario['retryable']
);
$nonRetryableFailures = array_filter(
$this->failureScenarios,
fn($scenario) => !$scenario['retryable']
fn ($scenario) => ! $scenario['retryable']
);
expect(count($retryableFailures))->toBeGreaterThan(0);
@@ -602,7 +618,7 @@ describe('Dead Letter Queue Integration Scenarios', function () {
$queueTypes = [
'email' => DeadLetterQueueName::forQueue(QueueName::fromString('email')),
'reports' => DeadLetterQueueName::forQueue(QueueName::fromString('reports')),
'background' => DeadLetterQueueName::forQueue(QueueName::fromString('background'))
'background' => DeadLetterQueueName::forQueue(QueueName::fromString('background')),
];
foreach ($queueTypes as $originalQueue => $dlqName) {
@@ -610,8 +626,8 @@ describe('Dead Letter Queue Integration Scenarios', function () {
}
// Each queue type should have its own DLQ
$queueNames = array_map(fn($dlq) => $dlq->toString(), $queueTypes);
$queueNames = array_map(fn ($dlq) => $dlq->toString(), $queueTypes);
$uniqueNames = array_unique($queueNames);
expect(count($uniqueNames))->toBe(count($queueTypes));
});
});
});

View File

@@ -2,11 +2,11 @@
declare(strict_types=1);
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;
describe('LockKey Value Object', function () {
@@ -20,7 +20,7 @@ describe('LockKey Value Object', function () {
'resource.database_connection',
'batch.monthly_report_2024',
'a', // minimum length
str_repeat('a', 255) // maximum length
str_repeat('a', 255), // maximum length
];
foreach ($validKeys as $key) {
@@ -56,11 +56,11 @@ describe('LockKey Value Object', function () {
'lock?invalid',
'lock/invalid',
'lock~invalid',
'lock`invalid'
'lock`invalid',
];
foreach ($invalidKeys as $key) {
expect(fn() => LockKey::fromString($key))
expect(fn () => LockKey::fromString($key))
->toThrow(\InvalidArgumentException::class);
}
});
@@ -191,7 +191,7 @@ describe('LockKey Value Object', function () {
expect(strlen($withShortPrefix->toString()))->toBe(252); // 'x' + '.' + 250
// This should fail (would exceed 255 chars)
expect(fn() => $baseKey->withPrefix('toolong'))
expect(fn () => $baseKey->withPrefix('toolong'))
->toThrow(\InvalidArgumentException::class);
});
@@ -210,10 +210,11 @@ describe('Distributed Lock Mock Implementation', function () {
beforeEach(function () {
// Create a mock distributed lock for testing
$this->distributedLock = new class {
$this->distributedLock = new class () {
private array $locks = [];
public function acquire(LockKey $key, WorkerId $workerId, Duration $ttl): bool {
public function acquire(LockKey $key, WorkerId $workerId, Duration $ttl): bool
{
$keyStr = $key->toString();
$now = time();
@@ -230,17 +231,18 @@ describe('Distributed Lock Mock Implementation', function () {
'worker_id' => $workerId->toString(),
'acquired_at' => $now,
'expires_at' => $now + $ttl->toSeconds(),
'ttl' => $ttl->toSeconds()
'ttl' => $ttl->toSeconds(),
];
return true;
}
public function extend(LockKey $key, WorkerId $workerId, Duration $ttl): bool {
public function extend(LockKey $key, WorkerId $workerId, Duration $ttl): bool
{
$keyStr = $key->toString();
$now = time();
if (!isset($this->locks[$keyStr])) {
if (! isset($this->locks[$keyStr])) {
return false; // Lock doesn't exist
}
@@ -258,10 +260,11 @@ describe('Distributed Lock Mock Implementation', function () {
return true;
}
public function release(LockKey $key, WorkerId $workerId): bool {
public function release(LockKey $key, WorkerId $workerId): bool
{
$keyStr = $key->toString();
if (!isset($this->locks[$keyStr])) {
if (! isset($this->locks[$keyStr])) {
return false; // Lock doesn't exist
}
@@ -273,26 +276,30 @@ describe('Distributed Lock Mock Implementation', function () {
}
unset($this->locks[$keyStr]);
return true;
}
public function exists(LockKey $key): bool {
public function exists(LockKey $key): bool
{
$keyStr = $key->toString();
$now = time();
if (!isset($this->locks[$keyStr])) {
if (! isset($this->locks[$keyStr])) {
return false;
}
$lock = $this->locks[$keyStr];
return $lock['expires_at'] > $now;
}
public function getLockInfo(LockKey $key): ?array {
public function getLockInfo(LockKey $key): ?array
{
$keyStr = $key->toString();
$now = time();
if (!isset($this->locks[$keyStr])) {
if (! isset($this->locks[$keyStr])) {
return null;
}
@@ -307,11 +314,12 @@ describe('Distributed Lock Mock Implementation', function () {
'worker_id' => $lock['worker_id'],
'acquired_at' => date('Y-m-d H:i:s', $lock['acquired_at']),
'expires_at' => date('Y-m-d H:i:s', $lock['expires_at']),
'ttl_seconds' => $lock['expires_at'] - $now
'ttl_seconds' => $lock['expires_at'] - $now,
];
}
public function acquireWithTimeout(LockKey $key, WorkerId $workerId, Duration $ttl, Duration $timeout): bool {
public function acquireWithTimeout(LockKey $key, WorkerId $workerId, Duration $ttl, Duration $timeout): bool
{
$startTime = microtime(true);
$timeoutSeconds = $timeout->toSeconds();
@@ -325,7 +333,8 @@ describe('Distributed Lock Mock Implementation', function () {
return false;
}
public function releaseAllWorkerLocks(WorkerId $workerId): int {
public function releaseAllWorkerLocks(WorkerId $workerId): int
{
$workerIdStr = $workerId->toString();
$released = 0;
@@ -339,7 +348,8 @@ describe('Distributed Lock Mock Implementation', function () {
return $released;
}
public function cleanupExpiredLocks(): int {
public function cleanupExpiredLocks(): int
{
$now = time();
$cleaned = 0;
@@ -353,7 +363,8 @@ describe('Distributed Lock Mock Implementation', function () {
return $cleaned;
}
public function getLockStatistics(): array {
public function getLockStatistics(): array
{
$now = time();
$activeLocks = 0;
$expiredLocks = 0;
@@ -374,8 +385,8 @@ describe('Distributed Lock Mock Implementation', function () {
'expired_locks' => $expiredLocks,
'unique_workers' => count($workers),
'avg_ttl_seconds' => $activeLocks > 0 ?
array_sum(array_map(fn($lock) => $lock['ttl'], array_filter($this->locks, fn($lock) => $lock['expires_at'] > $now))) / $activeLocks
: 0
array_sum(array_map(fn ($lock) => $lock['ttl'], array_filter($this->locks, fn ($lock) => $lock['expires_at'] > $now))) / $activeLocks
: 0,
];
}
};
@@ -644,9 +655,11 @@ describe('Distributed Lock Mock Implementation', function () {
describe('Distributed Lock Integration Scenarios', function () {
beforeEach(function () {
$this->distributedLock = new class {
$this->distributedLock = new class () {
private array $locks = [];
public function acquire(LockKey $key, WorkerId $workerId, Duration $ttl): bool {
public function acquire(LockKey $key, WorkerId $workerId, Duration $ttl): bool
{
$keyStr = $key->toString();
$now = time();
if (isset($this->locks[$keyStr]) && $this->locks[$keyStr]['expires_at'] > $now) {
@@ -655,36 +668,45 @@ describe('Distributed Lock Integration Scenarios', function () {
$this->locks[$keyStr] = [
'worker_id' => $workerId->toString(),
'acquired_at' => $now,
'expires_at' => $now + $ttl->toSeconds()
'expires_at' => $now + $ttl->toSeconds(),
];
return true;
}
public function release(LockKey $key, WorkerId $workerId): bool {
public function release(LockKey $key, WorkerId $workerId): bool
{
$keyStr = $key->toString();
if (!isset($this->locks[$keyStr]) || $this->locks[$keyStr]['worker_id'] !== $workerId->toString()) {
if (! isset($this->locks[$keyStr]) || $this->locks[$keyStr]['worker_id'] !== $workerId->toString()) {
return false;
}
unset($this->locks[$keyStr]);
return true;
}
public function exists(LockKey $key): bool {
public function exists(LockKey $key): bool
{
$keyStr = $key->toString();
return isset($this->locks[$keyStr]) && $this->locks[$keyStr]['expires_at'] > time();
}
};
$this->emailJob = new class {
$this->emailJob = new class () {
public function __construct(
public string $batchId = 'email-batch-123',
public int $recipientCount = 1000
) {}
) {
}
};
$this->reportJob = new class {
$this->reportJob = new class () {
public function __construct(
public string $reportId = 'monthly-sales-2024',
public string $resourceType = 'database'
) {}
) {
}
};
});
@@ -810,4 +832,4 @@ describe('Distributed Lock Integration Scenarios', function () {
$queueNowAcquired = $this->distributedLock->acquire($queueLock, $queueWorker, Duration::fromMinutes(30));
expect($queueNowAcquired)->toBeTrue();
});
});
});

View File

@@ -2,18 +2,18 @@
declare(strict_types=1);
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;
describe('Worker Entity', function () {
beforeEach(function () {
$this->workerId = WorkerId::generate();
$this->queues = [
QueueName::defaultQueue(),
QueueName::emailQueue()
QueueName::emailQueue(),
];
$this->capabilities = ['email', 'pdf-generation', 'image-processing'];
});
@@ -40,7 +40,7 @@ describe('Worker Entity', function () {
it('validates worker construction constraints', function () {
// Empty queues
expect(fn() => Worker::register(
expect(fn () => Worker::register(
hostname: 'test-host',
processId: 1001,
queues: [], // Invalid
@@ -48,14 +48,14 @@ describe('Worker Entity', function () {
))->toThrow(\InvalidArgumentException::class, 'Worker must handle at least one queue');
// Invalid max jobs
expect(fn() => Worker::register(
expect(fn () => Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 0 // Invalid
))->toThrow(\InvalidArgumentException::class, 'Max jobs must be greater than 0');
expect(fn() => Worker::register(
expect(fn () => Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
@@ -72,7 +72,7 @@ describe('Worker Entity', function () {
);
// Negative current jobs should fail during construction
expect(fn() => new Worker(
expect(fn () => new Worker(
id: $worker->id,
hostname: $worker->hostname,
processId: $worker->processId,
@@ -83,7 +83,7 @@ describe('Worker Entity', function () {
))->toThrow(\InvalidArgumentException::class, 'Current jobs cannot be negative');
// Current jobs exceeding max jobs should fail
expect(fn() => new Worker(
expect(fn () => new Worker(
id: $worker->id,
hostname: $worker->hostname,
processId: $worker->processId,
@@ -171,7 +171,7 @@ describe('Worker Entity', function () {
processId: 1001,
queues: [
QueueName::defaultQueue(),
QueueName::emailQueue()
QueueName::emailQueue(),
],
maxJobs: 10
);
@@ -401,7 +401,7 @@ describe('Worker Entity', function () {
'queues' => '["default"]',
'max_jobs' => 5,
'registered_at' => '2024-01-01 12:00:00',
'is_active' => 1
'is_active' => 1,
];
$worker = Worker::fromArray($minimalData);
@@ -417,4 +417,4 @@ describe('Worker Entity', function () {
expect($worker->capabilities)->toBe([]);
expect($worker->version)->toBe('1.0.0');
});
});
});

View File

@@ -2,23 +2,19 @@
declare(strict_types=1);
use App\Framework\Queue\Contracts\QueueInterface;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Database\EntityManagerInterface;
use App\Framework\DI\Container;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Contracts\JobChainManagerInterface;
use App\Framework\Queue\Services\JobMetricsManager;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Queue\Contracts\QueueInterface;
use App\Framework\Queue\Services\DependencyResolutionEngine;
use App\Framework\Queue\Services\JobChainExecutionCoordinator;
use App\Framework\Queue\ValueObjects\JobDependency;
use App\Framework\Queue\ValueObjects\JobChain;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Queue\Services\JobMetricsManager;
use App\Framework\Queue\ValueObjects\ChainExecutionMode;
use App\Framework\Queue\Entities\JobProgressEntry;
use App\Framework\Queue\Entities\JobProgressStep;
use App\Framework\Database\EntityManagerInterface;
use App\Framework\Logging\Logger;
use App\Framework\Core\Application;
use App\Framework\DI\Container;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Queue\ValueObjects\JobChain;
use App\Framework\Queue\ValueObjects\JobDependency;
use App\Framework\Queue\ValueObjects\JobMetrics;
beforeEach(function () {
// Set up test container
@@ -37,11 +33,12 @@ beforeEach(function () {
function createTestJob(string $id, string $data): object
{
return new class($id, $data) {
return new class ($id, $data) {
public function __construct(
public readonly string $id,
public readonly string $data
) {}
) {
}
};
}
@@ -112,7 +109,7 @@ test('job chain execution with sequential mode', function () {
$jobs = [
createTestJob('chain-job-1', 'Chain Job 1'),
createTestJob('chain-job-2', 'Chain Job 2'),
createTestJob('chain-job-3', 'Chain Job 3')
createTestJob('chain-job-3', 'Chain Job 3'),
];
// 2. Create job chain
@@ -137,7 +134,7 @@ test('job chain failure handling', function () {
$jobs = [
createTestJob('fail-job-1', 'Job 1'),
createTestJob('fail-job-2', 'Job 2 (will fail)'),
createTestJob('fail-job-3', 'Job 3')
createTestJob('fail-job-3', 'Job 3'),
];
// 2. Create job chain with stop on failure
@@ -181,7 +178,7 @@ test('circular dependency detection', function () {
$this->dependencyManager->addDependency($depB);
// 3. Adding the third dependency should throw an exception or be handled
expect(fn() => $this->dependencyManager->addDependency($depC))
expect(fn () => $this->dependencyManager->addDependency($depC))
->toThrow(\InvalidArgumentException::class);
});
@@ -265,7 +262,7 @@ test('queue metrics calculation', function () {
completedAt: null,
failedAt: date('Y-m-d H:i:s'),
metadata: []
)
),
];
// 2. Record all metrics
@@ -307,7 +304,7 @@ test('dead letter queue functionality', function () {
// 3. Verify dead letter detection
$failedJobs = $this->metricsManager->getFailedJobs('default', '1 hour');
$deadLetterJob = array_filter($failedJobs, fn($job) => $job->jobId === 'dead-letter-job')[0] ?? null;
$deadLetterJob = array_filter($failedJobs, fn ($job) => $job->jobId === 'dead-letter-job')[0] ?? null;
expect($deadLetterJob)->not()->toBeNull()
->and($deadLetterJob->attempts)->toBe(3)
@@ -323,7 +320,7 @@ test('system health monitoring', function () {
new JobMetrics('health-2', 'health-queue', 'completed', 1, 3, 75.0, 1024, null, date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), null, []),
new JobMetrics('health-3', 'health-queue', 'completed', 1, 3, 100.0, 1024, null, date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), null, []),
// One failed job
new JobMetrics('health-4', 'health-queue', 'failed', 2, 3, 25.0, 1024, 'Health test failure', date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), null, date('Y-m-d H:i:s'), [])
new JobMetrics('health-4', 'health-queue', 'failed', 2, 3, 25.0, 1024, 'Health test failure', date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), null, date('Y-m-d H:i:s'), []),
];
// 2. Record all metrics
@@ -345,7 +342,7 @@ test('performance and throughput metrics', function () {
$performanceMetrics = [
new JobMetrics('perf-1', 'perf-queue', 'completed', 1, 3, 50.0, 1024 * 1024, null, date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), null, []),
new JobMetrics('perf-2', 'perf-queue', 'completed', 1, 3, 150.0, 2 * 1024 * 1024, null, date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), null, []),
new JobMetrics('perf-3', 'perf-queue', 'completed', 1, 3, 300.0, 4 * 1024 * 1024, null, date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), null, [])
new JobMetrics('perf-3', 'perf-queue', 'completed', 1, 3, 300.0, 4 * 1024 * 1024, null, date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), null, []),
];
// 2. Record performance metrics
@@ -369,4 +366,4 @@ test('performance and throughput metrics', function () {
expect($throughputStats)->toHaveKey('total_completed')
->and($throughputStats['total_completed'])->toBe(3)
->and($throughputStats)->toHaveKey('average_throughput_per_hour');
});
});

View File

@@ -2,21 +2,18 @@
declare(strict_types=1);
use App\Framework\Queue\Services\WorkerRegistry;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\Services\DatabaseDistributedLock;
use App\Framework\Queue\Services\JobDistributionService;
use App\Framework\Queue\Services\WorkerHealthCheckService;
use App\Framework\Queue\Services\FailoverRecoveryService;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\Services\WorkerRegistry;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Logging\Logger;
/**
* Real-world scenario tests for the Distributed Processing System
@@ -64,7 +61,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::emailQueue()],
maxJobs: 20,
capabilities: ['email', 'newsletter', 'notifications']
)
),
];
$imageWorkers = [
@@ -74,7 +71,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('image-processing')],
maxJobs: 5, // Resource intensive
capabilities: ['image-resize', 'thumbnail', 'watermark']
)
),
];
$generalWorkers = [
@@ -83,7 +80,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
processId: 1001,
queues: [
QueueName::defaultQueue(),
QueueName::fromString('reports')
QueueName::fromString('reports'),
],
maxJobs: 15,
capabilities: ['pdf-generation', 'reporting', 'exports']
@@ -93,11 +90,11 @@ describe('Distributed Processing Real-World Scenarios', function () {
processId: 1002,
queues: [
QueueName::defaultQueue(),
QueueName::fromString('reports')
QueueName::fromString('reports'),
],
maxJobs: 15,
capabilities: ['pdf-generation', 'reporting', 'exports']
)
),
];
$allWorkers = array_merge($emailWorkers, $imageWorkers, $generalWorkers);
@@ -123,7 +120,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
// General processing jobs
['id' => JobId::generate(), 'queue' => QueueName::defaultQueue(), 'type' => 'invoice-generation'],
['id' => JobId::generate(), 'queue' => QueueName::fromString('reports'), 'type' => 'sales-report']
['id' => JobId::generate(), 'queue' => QueueName::fromString('reports'), 'type' => 'sales-report'],
];
// Mock job distribution
@@ -222,7 +219,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('video-processing')],
maxJobs: 2,
capabilities: ['video-encode', 'gpu-acceleration', 'h264', 'h265']
)
),
];
// CPU workers for audio processing
@@ -233,7 +230,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('audio-processing')],
maxJobs: 8,
capabilities: ['audio-encode', 'mp3', 'aac', 'flac']
)
),
];
// Thumbnail generation workers
@@ -251,7 +248,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('thumbnail-generation')],
maxJobs: 10,
capabilities: ['image-resize', 'ffmpeg', 'thumbnail']
)
),
];
$allWorkers = array_merge($videoWorkers, $audioWorkers, $thumbnailWorkers);
@@ -311,8 +308,8 @@ describe('Distributed Processing Real-World Scenarios', function () {
'required_capabilities' => ['gpu-acceleration', 'h264'],
'resource_requirements' => [
'gpu_memory' => '4GB',
'encoding_quality' => 'high'
]
'encoding_quality' => 'high',
],
];
// Mock worker scoring (would normally be done by JobDistributionService)
@@ -341,7 +338,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('transactions')],
maxJobs: 50,
capabilities: ['payment-processing', 'fraud-detection', 'pci-compliant']
)
),
];
// Simulate concurrent transaction processing
@@ -423,7 +420,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('cache-warming')],
maxJobs: 25,
capabilities: ['cdn-management', 'us-east-region', 'edge-caching']
)
),
];
$europeWorkers = [
@@ -433,7 +430,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('cache-warming')],
maxJobs: 20,
capabilities: ['cdn-management', 'eu-west-region', 'edge-caching']
)
),
];
$asiaWorkers = [
@@ -443,18 +440,24 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('cache-warming')],
maxJobs: 15,
capabilities: ['cdn-management', 'asia-pacific-region', 'edge-caching']
)
),
];
$allCdnWorkers = array_merge($usEastWorkers, $europeWorkers, $asiaWorkers);
// Verify regional distribution
$usEastCount = count(array_filter($allCdnWorkers,
fn($w) => $w->hasCapability('us-east-region')));
$europeCount = count(array_filter($allCdnWorkers,
fn($w) => $w->hasCapability('eu-west-region')));
$asiaCount = count(array_filter($allCdnWorkers,
fn($w) => $w->hasCapability('asia-pacific-region')));
$usEastCount = count(array_filter(
$allCdnWorkers,
fn ($w) => $w->hasCapability('us-east-region')
));
$europeCount = count(array_filter(
$allCdnWorkers,
fn ($w) => $w->hasCapability('eu-west-region')
));
$asiaCount = count(array_filter(
$allCdnWorkers,
fn ($w) => $w->hasCapability('asia-pacific-region')
));
expect($usEastCount)->toBe(2);
expect($europeCount)->toBe(1);
@@ -492,7 +495,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('content-delivery')],
maxJobs: 50,
capabilities: ['backup-region', 'medium-capacity']
)
),
];
// Simulate primary region failure
@@ -505,7 +508,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
}
// Total backup capacity should handle reduced load
$totalBackupCapacity = array_sum(array_map(fn($w) => $w->maxJobs, $backupWorkers));
$totalBackupCapacity = array_sum(array_map(fn ($w) => $w->maxJobs, $backupWorkers));
expect($totalBackupCapacity)->toBe(100); // Same as primary capacity
});
});
@@ -535,7 +538,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('data-preprocessing')],
maxJobs: 10,
capabilities: ['data-cleaning', 'feature-engineering', 'pandas', 'numpy']
)
),
];
$inferenceWorkers = [
@@ -545,7 +548,7 @@ describe('Distributed Processing Real-World Scenarios', function () {
queues: [QueueName::fromString('ml-inference')],
maxJobs: 50, // High throughput for inference
capabilities: ['model-serving', 'tensorflow-lite', 'onnx']
)
),
];
// Simulate GPU worker under heavy load
@@ -580,4 +583,4 @@ describe('Distributed Processing Real-World Scenarios', function () {
expect($inferenceWorkerIdle->getLoadPercentage()->getValue())->toBe(20.0); // Light load
});
});
});
});

View File

@@ -2,21 +2,21 @@
declare(strict_types=1);
use App\Framework\Queue\Services\WorkerRegistry;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\Services\DatabaseDistributedLock;
use App\Framework\Queue\Services\FailoverRecoveryService;
use App\Framework\Queue\Services\JobDistributionService;
use App\Framework\Queue\Services\WorkerHealthCheckService;
use App\Framework\Queue\Services\FailoverRecoveryService;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\Services\WorkerRegistry;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Logging\Logger;
use App\Framework\Queue\ValueObjects\WorkerId;
/**
* Comprehensive integration tests for the Distributed Processing System
@@ -56,7 +56,7 @@ describe('Distributed Processing System', function () {
processId: 1001,
queues: [
QueueName::emailQueue(),
QueueName::defaultQueue()
QueueName::defaultQueue(),
],
maxJobs: 10,
capabilities: ['email', 'pdf-generation']
@@ -67,7 +67,7 @@ describe('Distributed Processing System', function () {
processId: 1002,
queues: [
QueueName::defaultQueue(),
QueueName::fromString('high-priority')
QueueName::fromString('high-priority'),
],
maxJobs: 5,
capabilities: ['image-processing', 'pdf-generation']
@@ -77,7 +77,7 @@ describe('Distributed Processing System', function () {
hostname: 'app-server-3',
processId: 1003,
queues: [
QueueName::emailQueue()
QueueName::emailQueue(),
],
maxJobs: 15,
capabilities: ['email', 'notifications']
@@ -330,7 +330,7 @@ describe('Distributed Processing System', function () {
it('calculates worker scores based on load and capabilities', function () {
$jobData = [
'required_capabilities' => ['email', 'pdf-generation']
'required_capabilities' => ['email', 'pdf-generation'],
];
$bestWorker = $this->jobDistribution->findBestWorkerForJob(
@@ -558,7 +558,7 @@ describe('Distributed Processing System', function () {
JobId::generate(),
JobId::generate(),
JobId::generate(),
JobId::generate()
JobId::generate(),
];
// Mock successful distribution for all concurrent jobs
@@ -597,7 +597,7 @@ describe('Distributed Processing System', function () {
$workers = [
WorkerId::generate(),
WorkerId::generate(),
WorkerId::generate()
WorkerId::generate(),
];
// Simulate lock contention - only first worker succeeds
@@ -643,7 +643,7 @@ describe('Distributed Processing System', function () {
$this->logger->shouldReceive('error')->andReturn(null);
// System should handle gracefully and throw appropriate exception
expect(fn() => $this->workerRegistry->findActiveWorkers())
expect(fn () => $this->workerRegistry->findActiveWorkers())
->toThrow(\PDOException::class);
});
@@ -659,7 +659,7 @@ describe('Distributed Processing System', function () {
'total_capacity' => 30,
'current_load' => 10,
'avg_cpu_usage' => 45.5,
'avg_memory_usage' => 819200000 // ~800MB in bytes
'avg_memory_usage' => 819200000, // ~800MB in bytes
]);
$queueStmt = mock(\PDOStatement::class);
@@ -671,7 +671,8 @@ describe('Distributed Processing System', function () {
);
$this->connection->shouldReceive('prepare')->andReturn(
$statsStmt, $queueStmt
$statsStmt,
$queueStmt
);
$this->logger->shouldReceive('error')->never();
@@ -744,21 +745,21 @@ describe('Distributed Processing System', function () {
describe('Edge Cases and Error Scenarios', function () {
it('handles worker registration with invalid data gracefully', function () {
expect(fn() => Worker::register(
expect(fn () => Worker::register(
hostname: '', // Invalid empty hostname
processId: 1001,
queues: [QueueName::defaultQueue()],
maxJobs: 10
))->toThrow(\InvalidArgumentException::class);
expect(fn() => Worker::register(
expect(fn () => Worker::register(
hostname: 'valid-host',
processId: 1001,
queues: [], // Invalid empty queues
maxJobs: 10
))->toThrow(\InvalidArgumentException::class);
expect(fn() => Worker::register(
expect(fn () => Worker::register(
hostname: 'valid-host',
processId: 1001,
queues: [QueueName::defaultQueue()],
@@ -767,13 +768,13 @@ describe('Distributed Processing System', function () {
});
it('handles lock key validation properly', function () {
expect(fn() => LockKey::fromString(''))
expect(fn () => LockKey::fromString(''))
->toThrow(\InvalidArgumentException::class);
expect(fn() => LockKey::fromString(str_repeat('a', 256))) // Too long
expect(fn () => LockKey::fromString(str_repeat('a', 256))) // Too long
->toThrow(\InvalidArgumentException::class);
expect(fn() => LockKey::fromString('invalid@key!')) // Invalid characters
expect(fn () => LockKey::fromString('invalid@key!')) // Invalid characters
->toThrow(\InvalidArgumentException::class);
});
@@ -817,4 +818,4 @@ describe('Distributed Processing System', function () {
expect($result)->toBeFalse();
});
});
});
});

View File

@@ -2,10 +2,8 @@
declare(strict_types=1);
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Queue\ValueObjects\JobMetrics;
describe('JobMetrics Value Object', function () {
@@ -142,7 +140,7 @@ describe('JobMetrics Value Object', function () {
$metadata = [
'user_id' => 12345,
'email_template' => 'newsletter',
'batch_size' => 1000
'batch_size' => 1000,
];
$metricsWithMetadata = $this->baseMetrics->withMetadata($metadata);
@@ -163,7 +161,7 @@ describe('JobMetrics Value Object', function () {
'user_id' => 123,
'type' => 'email',
'priority' => 'high',
'retry_count' => 2
'retry_count' => 2,
]);
});
@@ -323,7 +321,7 @@ describe('JobMetrics Value Object', function () {
'job_id', 'queue_name', 'status', 'attempts', 'max_attempts',
'execution_time_ms', 'execution_time_seconds', 'memory_usage_bytes',
'memory_usage_mb', 'success_rate', 'duration_seconds', 'error_message',
'created_at', 'started_at', 'completed_at', 'failed_at', 'metadata'
'created_at', 'started_at', 'completed_at', 'failed_at', 'metadata',
];
foreach ($expectedKeys as $key) {
@@ -394,7 +392,7 @@ describe('JobMetrics Value Object', function () {
$complexMetadata = [
'nested' => ['level1' => ['level2' => 'value']],
'array' => [1, 2, 3, 4, 5],
'mixed' => ['string', 42, true, null]
'mixed' => ['string', 42, true, null],
];
$metrics = JobMetrics::create('complex-job', 'data-queue')
@@ -409,12 +407,14 @@ describe('Job Metrics Collection Mock System', function () {
beforeEach(function () {
// Create a mock metrics manager for testing
$this->metricsManager = new class {
$this->metricsManager = new class () {
private array $jobMetrics = [];
private array $queueStats = [];
public function recordJobExecution(string $jobId, float $executionTimeMs, int $memoryUsage): void {
if (!isset($this->jobMetrics[$jobId])) {
public function recordJobExecution(string $jobId, float $executionTimeMs, int $memoryUsage): void
{
if (! isset($this->jobMetrics[$jobId])) {
$this->jobMetrics[$jobId] = JobMetrics::create($jobId, 'default-queue');
}
@@ -422,8 +422,9 @@ describe('Job Metrics Collection Mock System', function () {
->withCompleted($executionTimeMs, $memoryUsage);
}
public function recordJobFailure(string $jobId, string $errorMessage, float $executionTimeMs, int $memoryUsage): void {
if (!isset($this->jobMetrics[$jobId])) {
public function recordJobFailure(string $jobId, string $errorMessage, float $executionTimeMs, int $memoryUsage): void
{
if (! isset($this->jobMetrics[$jobId])) {
$this->jobMetrics[$jobId] = JobMetrics::create($jobId, 'default-queue');
}
@@ -431,12 +432,14 @@ describe('Job Metrics Collection Mock System', function () {
->withFailed($errorMessage, $executionTimeMs, $memoryUsage);
}
public function getJobMetrics(string $jobId): ?JobMetrics {
public function getJobMetrics(string $jobId): ?JobMetrics
{
return $this->jobMetrics[$jobId] ?? null;
}
public function getQueueMetrics(string $queueName): array {
$jobs = array_filter($this->jobMetrics, fn($metrics) => $metrics->queueName === $queueName);
public function getQueueMetrics(string $queueName): array
{
$jobs = array_filter($this->jobMetrics, fn ($metrics) => $metrics->queueName === $queueName);
if (empty($jobs)) {
return [
@@ -446,15 +449,15 @@ describe('Job Metrics Collection Mock System', function () {
'failed_jobs' => 0,
'average_execution_time_ms' => 0.0,
'average_memory_usage_mb' => 0.0,
'success_rate' => 100.0
'success_rate' => 100.0,
];
}
$totalJobs = count($jobs);
$completedJobs = count(array_filter($jobs, fn($m) => $m->isCompleted()));
$failedJobs = count(array_filter($jobs, fn($m) => $m->isFailed()));
$avgExecutionTime = array_sum(array_map(fn($m) => $m->executionTimeMs, $jobs)) / $totalJobs;
$avgMemoryUsage = array_sum(array_map(fn($m) => $m->getMemoryUsageMB(), $jobs)) / $totalJobs;
$completedJobs = count(array_filter($jobs, fn ($m) => $m->isCompleted()));
$failedJobs = count(array_filter($jobs, fn ($m) => $m->isFailed()));
$avgExecutionTime = array_sum(array_map(fn ($m) => $m->executionTimeMs, $jobs)) / $totalJobs;
$avgMemoryUsage = array_sum(array_map(fn ($m) => $m->getMemoryUsageMB(), $jobs)) / $totalJobs;
$successRate = ($completedJobs / $totalJobs) * 100;
return [
@@ -464,11 +467,12 @@ describe('Job Metrics Collection Mock System', function () {
'failed_jobs' => $failedJobs,
'average_execution_time_ms' => round($avgExecutionTime, 2),
'average_memory_usage_mb' => round($avgMemoryUsage, 2),
'success_rate' => round($successRate, 2)
'success_rate' => round($successRate, 2),
];
}
public function getSystemMetrics(): array {
public function getSystemMetrics(): array
{
if (empty($this->jobMetrics)) {
return [
'total_jobs' => 0,
@@ -477,17 +481,17 @@ describe('Job Metrics Collection Mock System', function () {
'running_jobs' => 0,
'overall_success_rate' => 100.0,
'average_execution_time_ms' => 0.0,
'peak_memory_usage_mb' => 0.0
'peak_memory_usage_mb' => 0.0,
];
}
$totalJobs = count($this->jobMetrics);
$completedJobs = count(array_filter($this->jobMetrics, fn($m) => $m->isCompleted()));
$failedJobs = count(array_filter($this->jobMetrics, fn($m) => $m->isFailed()));
$runningJobs = count(array_filter($this->jobMetrics, fn($m) => $m->isRunning()));
$completedJobs = count(array_filter($this->jobMetrics, fn ($m) => $m->isCompleted()));
$failedJobs = count(array_filter($this->jobMetrics, fn ($m) => $m->isFailed()));
$runningJobs = count(array_filter($this->jobMetrics, fn ($m) => $m->isRunning()));
$overallSuccessRate = ($completedJobs / $totalJobs) * 100;
$avgExecutionTime = array_sum(array_map(fn($m) => $m->executionTimeMs, $this->jobMetrics)) / $totalJobs;
$peakMemoryUsage = max(array_map(fn($m) => $m->getMemoryUsageMB(), $this->jobMetrics));
$avgExecutionTime = array_sum(array_map(fn ($m) => $m->executionTimeMs, $this->jobMetrics)) / $totalJobs;
$peakMemoryUsage = max(array_map(fn ($m) => $m->getMemoryUsageMB(), $this->jobMetrics));
return [
'total_jobs' => $totalJobs,
@@ -496,24 +500,29 @@ describe('Job Metrics Collection Mock System', function () {
'running_jobs' => $runningJobs,
'overall_success_rate' => round($overallSuccessRate, 2),
'average_execution_time_ms' => round($avgExecutionTime, 2),
'peak_memory_usage_mb' => round($peakMemoryUsage, 2)
'peak_memory_usage_mb' => round($peakMemoryUsage, 2),
];
}
public function getTopSlowJobs(int $limit = 10): array {
public function getTopSlowJobs(int $limit = 10): array
{
$jobs = $this->jobMetrics;
usort($jobs, fn($a, $b) => $b->executionTimeMs <=> $a->executionTimeMs);
usort($jobs, fn ($a, $b) => $b->executionTimeMs <=> $a->executionTimeMs);
return array_slice($jobs, 0, $limit);
}
public function getTopMemoryJobs(int $limit = 10): array {
public function getTopMemoryJobs(int $limit = 10): array
{
$jobs = $this->jobMetrics;
usort($jobs, fn($a, $b) => $b->memoryUsageBytes <=> $a->memoryUsageBytes);
usort($jobs, fn ($a, $b) => $b->memoryUsageBytes <=> $a->memoryUsageBytes);
return array_slice($jobs, 0, $limit);
}
public function getJobsByQueue(string $queueName): array {
return array_filter($this->jobMetrics, fn($metrics) => $metrics->queueName === $queueName);
public function getJobsByQueue(string $queueName): array
{
return array_filter($this->jobMetrics, fn ($metrics) => $metrics->queueName === $queueName);
}
};
});
@@ -611,9 +620,11 @@ describe('Job Metrics Collection Mock System', function () {
});
it('handles empty system gracefully', function () {
$emptyManager = new class {
$emptyManager = new class () {
private array $jobMetrics = [];
public function getSystemMetrics(): array {
public function getSystemMetrics(): array
{
return [
'total_jobs' => 0,
'completed_jobs' => 0,
@@ -621,7 +632,7 @@ describe('Job Metrics Collection Mock System', function () {
'running_jobs' => 0,
'overall_success_rate' => 100.0,
'average_execution_time_ms' => 0.0,
'peak_memory_usage_mb' => 0.0
'peak_memory_usage_mb' => 0.0,
];
}
};
@@ -688,4 +699,4 @@ describe('Job Metrics Collection Mock System', function () {
expect($emptyQueue)->toBe([]);
});
});
});
});

View File

@@ -2,10 +2,9 @@
declare(strict_types=1);
use App\Framework\Queue\ValueObjects\JobProgress;
use App\Framework\Queue\ValueObjects\ProgressStep;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\JobProgress;
describe('JobProgress Value Object', function () {
@@ -23,10 +22,10 @@ describe('JobProgress Value Object', function () {
});
it('rejects empty progress messages', function () {
expect(fn() => JobProgress::withPercentage(Percentage::zero(), ''))
expect(fn () => JobProgress::withPercentage(Percentage::zero(), ''))
->toThrow(\InvalidArgumentException::class, 'Progress message cannot be empty');
expect(fn() => JobProgress::withPercentage(Percentage::zero(), ' '))
expect(fn () => JobProgress::withPercentage(Percentage::zero(), ' '))
->toThrow(\InvalidArgumentException::class, 'Progress message cannot be empty');
});
@@ -285,16 +284,19 @@ describe('JobProgress Value Object', function () {
expect($array['is_starting'])->toBeTrue();
expect($array['is_completed'])->toBeFalse();
expect($array['is_failed'])->toBeFalse();
break;
case 'completed':
expect($array['is_completed'])->toBeTrue();
expect($array['is_starting'])->toBeFalse();
expect($array['is_failed'])->toBeFalse();
break;
case 'failed':
expect($array['is_failed'])->toBeTrue();
expect($array['is_starting'])->toBeFalse();
expect($array['is_completed'])->toBeFalse();
break;
}
}
@@ -306,36 +308,42 @@ describe('Job Progress Tracking System Mock', function () {
beforeEach(function () {
// Create a mock progress tracker for testing
$this->progressTracker = new class {
$this->progressTracker = new class () {
private array $progressEntries = [];
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void {
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void
{
$this->progressEntries[$jobId][] = [
'progress' => $progress,
'step_name' => $stepName,
'timestamp' => time(),
'id' => uniqid()
'id' => uniqid(),
];
}
public function getCurrentProgress(string $jobId): ?JobProgress {
if (!isset($this->progressEntries[$jobId]) || empty($this->progressEntries[$jobId])) {
public function getCurrentProgress(string $jobId): ?JobProgress
{
if (! isset($this->progressEntries[$jobId]) || empty($this->progressEntries[$jobId])) {
return null;
}
$entries = $this->progressEntries[$jobId];
return end($entries)['progress'];
}
public function getProgressHistory(string $jobId): array {
public function getProgressHistory(string $jobId): array
{
return $this->progressEntries[$jobId] ?? [];
}
public function markJobCompleted(string $jobId, string $message = 'Job completed successfully'): void {
public function markJobCompleted(string $jobId, string $message = 'Job completed successfully'): void
{
$this->updateProgress($jobId, JobProgress::completed($message));
}
public function markJobFailed(string $jobId, string $message = 'Job failed', ?\Throwable $exception = null): void {
public function markJobFailed(string $jobId, string $message = 'Job failed', ?\Throwable $exception = null): void
{
$metadata = [];
if ($exception) {
$metadata['exception_type'] = get_class($exception);
@@ -346,7 +354,8 @@ describe('Job Progress Tracking System Mock', function () {
$this->updateProgress($jobId, $progress);
}
public function getProgressForJobs(array $jobIds): array {
public function getProgressForJobs(array $jobIds): array
{
$result = [];
foreach ($jobIds as $jobId) {
$current = $this->getCurrentProgress($jobId);
@@ -354,10 +363,12 @@ describe('Job Progress Tracking System Mock', function () {
$result[$jobId] = $current;
}
}
return $result;
}
public function getJobsAboveProgress(float $minPercentage): array {
public function getJobsAboveProgress(float $minPercentage): array
{
$result = [];
foreach ($this->progressEntries as $jobId => $entries) {
$current = end($entries)['progress'];
@@ -365,6 +376,7 @@ describe('Job Progress Tracking System Mock', function () {
$result[] = ['job_id' => $jobId, 'progress' => $current];
}
}
return $result;
}
};
@@ -396,7 +408,7 @@ describe('Job Progress Tracking System Mock', function () {
'validation' => 'Validating input data',
'processing' => 'Processing records',
'notification' => 'Sending notifications',
'cleanup' => 'Cleaning up temporary files'
'cleanup' => 'Cleaning up temporary files',
];
foreach ($steps as $stepName => $message) {
@@ -411,7 +423,7 @@ describe('Job Progress Tracking System Mock', function () {
expect(count($history))->toBe(4);
// Check step names are tracked
$stepNames = array_map(fn($entry) => $entry['step_name'], $history);
$stepNames = array_map(fn ($entry) => $entry['step_name'], $history);
expect($stepNames)->toBe(['validation', 'processing', 'notification', 'cleanup']);
});
@@ -538,14 +550,14 @@ describe('Job Progress Tracking System Mock', function () {
}
$history = $this->progressTracker->getProgressHistory($jobId);
$messages = array_map(fn($entry) => $entry['progress']->message, $history);
$messages = array_map(fn ($entry) => $entry['progress']->message, $history);
expect($messages)->toBe([
'Starting',
'Quarter done',
'Half done',
'Three quarters done',
'Completed'
'Completed',
]);
});
@@ -590,32 +602,36 @@ describe('Job Progress Tracking System Mock', function () {
describe('Job Progress Integration Scenarios', function () {
beforeEach(function () {
$this->emailJob = new class {
$this->emailJob = new class () {
public function __construct(
public array $recipients = ['test@example.com'],
public string $subject = 'Test Email',
public string $template = 'newsletter'
) {}
) {
}
public function getRecipientCount(): int {
public function getRecipientCount(): int
{
return count($this->recipients);
}
};
$this->reportJob = new class {
$this->reportJob = new class () {
public function __construct(
public string $reportType = 'sales',
public array $criteria = ['period' => 'monthly'],
public int $totalSteps = 5
) {}
) {
}
public function getSteps(): array {
public function getSteps(): array
{
return [
'data_collection' => 'Collecting data from database',
'data_processing' => 'Processing and aggregating data',
'chart_generation' => 'Generating charts and graphs',
'pdf_creation' => 'Creating PDF document',
'distribution' => 'Distributing report to stakeholders'
'distribution' => 'Distributing report to stakeholders',
];
}
};
@@ -623,13 +639,20 @@ describe('Job Progress Integration Scenarios', function () {
it('demonstrates email job progress tracking', function () {
$jobId = JobId::generate()->toString();
$progressTracker = new class {
$progressTracker = new class () {
private array $progressEntries = [];
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void {
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void
{
$this->progressEntries[$jobId][] = ['progress' => $progress, 'step_name' => $stepName];
}
public function getCurrentProgress(string $jobId): ?JobProgress {
if (!isset($this->progressEntries[$jobId])) return null;
public function getCurrentProgress(string $jobId): ?JobProgress
{
if (! isset($this->progressEntries[$jobId])) {
return null;
}
return end($this->progressEntries[$jobId])['progress'];
}
};
@@ -678,12 +701,16 @@ describe('Job Progress Integration Scenarios', function () {
it('demonstrates report generation progress tracking', function () {
$jobId = JobId::generate()->toString();
$progressTracker = new class {
$progressTracker = new class () {
private array $progressEntries = [];
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void {
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void
{
$this->progressEntries[$jobId][] = ['progress' => $progress, 'step_name' => $stepName];
}
public function getProgressHistory(string $jobId): array {
public function getProgressHistory(string $jobId): array
{
return $this->progressEntries[$jobId] ?? [];
}
};
@@ -705,23 +732,30 @@ describe('Job Progress Integration Scenarios', function () {
expect(count($history))->toBe($totalSteps);
// Verify step progression
$stepNames = array_map(fn($entry) => $entry['step_name'], $history);
$stepNames = array_map(fn ($entry) => $entry['step_name'], $history);
expect($stepNames)->toBe(array_keys($steps));
// Verify progress percentages
$percentages = array_map(fn($entry) => $entry['progress']->percentage->getValue(), $history);
$percentages = array_map(fn ($entry) => $entry['progress']->percentage->getValue(), $history);
expect($percentages)->toBe([20.0, 40.0, 60.0, 80.0, 100.0]);
});
it('demonstrates error handling with progress tracking', function () {
$jobId = JobId::generate()->toString();
$progressTracker = new class {
$progressTracker = new class () {
private array $progressEntries = [];
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void {
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void
{
$this->progressEntries[$jobId][] = ['progress' => $progress, 'step_name' => $stepName];
}
public function getCurrentProgress(string $jobId): ?JobProgress {
if (!isset($this->progressEntries[$jobId])) return null;
public function getCurrentProgress(string $jobId): ?JobProgress
{
if (! isset($this->progressEntries[$jobId])) {
return null;
}
return end($this->progressEntries[$jobId])['progress'];
}
};
@@ -747,7 +781,7 @@ describe('Job Progress Integration Scenarios', function () {
'exception_message' => $exception->getMessage(),
'failed_at_step' => 'data_processing',
'items_processed' => 150,
'total_items' => 500
'total_items' => 500,
]);
$progressTracker->updateProgress($jobId, $failedProgress, 'data_processing');
@@ -757,4 +791,4 @@ describe('Job Progress Integration Scenarios', function () {
expect($currentProgress->metadata['items_processed'])->toBe(150);
expect($currentProgress->metadata['exception_type'])->toBe('RuntimeException');
});
});
});

View File

@@ -14,7 +14,9 @@ use PHPUnit\Framework\TestCase;
final class DatabasePerformanceTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private JobDistributionService $distributionService;
protected function setUp(): void
@@ -55,7 +57,7 @@ final class DatabasePerformanceTest extends TestCase
$randomWorker = $workers[array_rand($workers)];
$workerId = $randomWorker->id->toString();
$time = PerformanceTestHelper::measureTime(function() use ($workerId) {
$time = PerformanceTestHelper::measureTime(function () use ($workerId) {
return $this->workerRegistry->getWorker($workerId);
});
@@ -68,7 +70,7 @@ final class DatabasePerformanceTest extends TestCase
// Test getting available workers
$availableWorkerTimes = [];
for ($i = 0; $i < 100; $i++) {
$time = PerformanceTestHelper::measureTime(function() {
$time = PerformanceTestHelper::measureTime(function () {
return $this->workerRegistry->getAvailableWorkers();
});
$availableWorkerTimes[] = $time;
@@ -83,7 +85,7 @@ final class DatabasePerformanceTest extends TestCase
$randomWorker = $workers[array_rand($workers)];
$workerId = $randomWorker->id->toString();
$time = PerformanceTestHelper::measureTime(function() use ($workerId) {
$time = PerformanceTestHelper::measureTime(function () use ($workerId) {
$this->updateWorkerStatus($workerId, WorkerStatus::BUSY);
$this->updateWorkerStatus($workerId, WorkerStatus::AVAILABLE);
});
@@ -136,7 +138,7 @@ final class DatabasePerformanceTest extends TestCase
$randomJob = $jobs[array_rand($jobs)];
$jobId = $randomJob->id;
$time = PerformanceTestHelper::measureTime(function() use ($jobId) {
$time = PerformanceTestHelper::measureTime(function () use ($jobId) {
return $this->getJobById($jobId);
});
@@ -152,7 +154,7 @@ final class DatabasePerformanceTest extends TestCase
foreach ($statuses as $status) {
for ($i = 0; $i < 25; $i++) {
$time = PerformanceTestHelper::measureTime(function() use ($status) {
$time = PerformanceTestHelper::measureTime(function () use ($status) {
return $this->getJobsByStatus($status);
});
$statusQueryTimes[] = $time;
@@ -168,7 +170,7 @@ final class DatabasePerformanceTest extends TestCase
$randomWorker = $workers[array_rand($workers)];
$workerId = $randomWorker->id->toString();
$time = PerformanceTestHelper::measureTime(function() use ($workerId) {
$time = PerformanceTestHelper::measureTime(function () use ($workerId) {
return $this->getJobsByWorker($workerId);
});
@@ -199,21 +201,21 @@ final class DatabasePerformanceTest extends TestCase
// Test batch worker registration
$workers = $this->createWorkers($batchSize, 20);
$batchRegisterTime = PerformanceTestHelper::measureTime(function() use ($workers) {
$batchRegisterTime = PerformanceTestHelper::measureTime(function () use ($workers) {
$this->registerWorkers($workers);
});
// Test batch job distribution
$jobs = PerformanceTestHelper::createBulkJobs($batchSize);
$batchDistributeTime = PerformanceTestHelper::measureTime(function() use ($jobs) {
$batchDistributeTime = PerformanceTestHelper::measureTime(function () use ($jobs) {
foreach ($jobs as $job) {
$this->distributionService->distributeJob($job);
}
});
// Test batch job status updates
$batchUpdateTime = PerformanceTestHelper::measureTime(function() use ($jobs) {
$batchUpdateTime = PerformanceTestHelper::measureTime(function () use ($jobs) {
foreach ($jobs as $job) {
$this->updateJobStatus($job->id, JobStatus::COMPLETED);
}
@@ -262,22 +264,23 @@ final class DatabasePerformanceTest extends TestCase
// Test indexed queries performance
$indexTests = [
'worker_by_id' => function() use ($workers) {
'worker_by_id' => function () use ($workers) {
$randomWorker = $workers[array_rand($workers)];
return $this->workerRegistry->getWorker($randomWorker->id->toString());
},
'workers_by_status' => function() {
'workers_by_status' => function () {
return $this->workerRegistry->getWorkersByStatus(WorkerStatus::AVAILABLE);
},
'jobs_by_status' => function() {
'jobs_by_status' => function () {
return $this->getJobsByStatus(JobStatus::PENDING);
},
'jobs_by_priority' => function() {
'jobs_by_priority' => function () {
return $this->getJobsByPriority(\App\Framework\Queue\Jobs\JobPriority::HIGH);
},
'jobs_by_queue' => function() {
'jobs_by_queue' => function () {
return $this->getJobsByQueue('test_queue');
}
},
];
foreach ($indexTests as $testName => $testFunction) {
@@ -303,17 +306,19 @@ final class DatabasePerformanceTest extends TestCase
// Simulate multiple concurrent database operations
$operationTypes = [
'worker_lookup' => function() {
'worker_lookup' => function () {
$workerId = 'worker_' . rand(1, 100);
return $this->workerRegistry->getWorker($workerId);
},
'job_insertion' => function() {
'job_insertion' => function () {
$job = PerformanceTestHelper::createTestJob('pool_test_' . uniqid());
return $this->distributionService->distributeJob($job);
},
'status_query' => function() {
'status_query' => function () {
return $this->getJobsByStatus(JobStatus::PENDING);
}
},
];
$concurrencyLevels = [1, 5, 10, 20];
@@ -361,18 +366,18 @@ final class DatabasePerformanceTest extends TestCase
// Test complex queries that should benefit from optimization
$complexQueries = [
'workers_with_capacity_filter' => function() {
'workers_with_capacity_filter' => function () {
return $this->getWorkersByCapacityRange(15, 25);
},
'jobs_with_multiple_filters' => function() {
'jobs_with_multiple_filters' => function () {
return $this->getJobsWithFilters(JobStatus::PENDING, \App\Framework\Queue\Jobs\JobPriority::NORMAL);
},
'job_count_aggregation' => function() {
'job_count_aggregation' => function () {
return $this->getJobCountsByStatus();
},
'worker_utilization_stats' => function() {
'worker_utilization_stats' => function () {
return $this->getWorkerUtilizationStats();
}
},
];
foreach ($complexQueries as $queryName => $queryFunction) {
@@ -406,7 +411,7 @@ final class DatabasePerformanceTest extends TestCase
$transactionTimes = [];
for ($iteration = 0; $iteration < 20; $iteration++) {
$time = PerformanceTestHelper::measureTime(function() use ($size, $iteration) {
$time = PerformanceTestHelper::measureTime(function () use ($size, $iteration) {
$pdo = $this->database->getConnection();
try {
@@ -420,6 +425,7 @@ final class DatabasePerformanceTest extends TestCase
$pdo->commit();
} catch (\Exception $e) {
$pdo->rollBack();
throw $e;
}
});
@@ -454,6 +460,7 @@ final class DatabasePerformanceTest extends TestCase
$status
);
}
return $workers;
}
@@ -483,6 +490,7 @@ final class DatabasePerformanceTest extends TestCase
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE id = ?');
$stmt->execute([$jobId]);
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
}
@@ -491,6 +499,7 @@ final class DatabasePerformanceTest extends TestCase
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE status = ? LIMIT 100');
$stmt->execute([$status->value]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
@@ -499,6 +508,7 @@ final class DatabasePerformanceTest extends TestCase
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE worker_id = ? LIMIT 100');
$stmt->execute([$workerId]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
@@ -507,6 +517,7 @@ final class DatabasePerformanceTest extends TestCase
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE priority = ? LIMIT 100');
$stmt->execute([$priority->value]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
@@ -515,6 +526,7 @@ final class DatabasePerformanceTest extends TestCase
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE queue_name = ? LIMIT 100');
$stmt->execute([$queueName]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
@@ -523,6 +535,7 @@ final class DatabasePerformanceTest extends TestCase
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM workers WHERE capacity BETWEEN ? AND ?');
$stmt->execute([$minCapacity, $maxCapacity]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
@@ -531,6 +544,7 @@ final class DatabasePerformanceTest extends TestCase
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT * FROM jobs WHERE status = ? AND priority = ? LIMIT 100');
$stmt->execute([$status->value, $priority->value]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
@@ -538,6 +552,7 @@ final class DatabasePerformanceTest extends TestCase
{
$pdo = $this->database->getConnection();
$stmt = $pdo->query('SELECT status, COUNT(*) as count FROM jobs GROUP BY status');
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
@@ -554,6 +569,7 @@ final class DatabasePerformanceTest extends TestCase
LEFT JOIN jobs j ON w.id = j.worker_id
GROUP BY w.status
');
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
@@ -619,4 +635,4 @@ final class DatabasePerformanceTest extends TestCase
$pdo->exec('DELETE FROM workers');
$pdo->exec('DELETE FROM jobs');
}
}
}

View File

@@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase;
final class DistributedLockPerformanceTest extends TestCase
{
private DatabaseManager $database;
private DatabaseDistributedLock $lockService;
protected function setUp(): void
@@ -38,11 +39,12 @@ final class DistributedLockPerformanceTest extends TestCase
$lockKey = new LockKey("test_lock_{$i}");
$owner = new LockOwner("owner_{$i}");
$time = PerformanceTestHelper::measureTime(function() use ($lockKey, $owner) {
$time = PerformanceTestHelper::measureTime(function () use ($lockKey, $owner) {
$acquired = $this->lockService->acquire($lockKey, $owner, 30);
if ($acquired) {
$this->lockService->release($lockKey, $owner);
}
return $acquired;
});
@@ -87,7 +89,7 @@ final class DistributedLockPerformanceTest extends TestCase
// Measure release times
foreach ($locks as $lock) {
$time = PerformanceTestHelper::measureTime(function() use ($lock) {
$time = PerformanceTestHelper::measureTime(function () use ($lock) {
return $this->lockService->release($lock['key'], $lock['owner']);
});
$releaseTimes[] = $time;
@@ -130,14 +132,16 @@ final class DistributedLockPerformanceTest extends TestCase
$failures = 0;
for ($attempt = 0; $attempt < $attemptsPerWorker; $attempt++) {
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($lockKey, $owner) {
$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;
});
@@ -194,7 +198,7 @@ final class DistributedLockPerformanceTest extends TestCase
// Second owner repeatedly tries to acquire with short timeouts
for ($i = 0; $i < $iterations; $i++) {
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($lockKey, $owner2) {
$result = PerformanceTestHelper::measureTimeWithResult(function () use ($lockKey, $owner2) {
return $this->lockService->acquire($lockKey, $owner2, 0.1); // 100ms timeout
});
@@ -251,7 +255,7 @@ final class DistributedLockPerformanceTest extends TestCase
}
// Measure cleanup performance
$cleanupTime = PerformanceTestHelper::measureTime(function() {
$cleanupTime = PerformanceTestHelper::measureTime(function () {
$this->lockService->cleanupExpiredLocks();
});
@@ -282,7 +286,7 @@ final class DistributedLockPerformanceTest extends TestCase
));
}
$largeCleanupTime = PerformanceTestHelper::measureTime(function() {
$largeCleanupTime = PerformanceTestHelper::measureTime(function () {
$this->lockService->cleanupExpiredLocks();
});
@@ -301,7 +305,7 @@ final class DistributedLockPerformanceTest extends TestCase
echo "Target: {$operationsPerSecond} operations/second for {$testDuration} seconds\n";
$loadResult = PerformanceTestHelper::simulateLoad(
function($index) {
function ($index) {
$lockKey = new LockKey("throughput_lock_{$index}");
$owner = new LockOwner("owner_{$index}");
@@ -309,8 +313,10 @@ final class DistributedLockPerformanceTest extends TestCase
$acquired = $this->lockService->acquire($lockKey, $owner, 5);
if ($acquired) {
$this->lockService->release($lockKey, $owner);
return true;
}
return false;
},
$totalOperations,
@@ -324,7 +330,7 @@ final class DistributedLockPerformanceTest extends TestCase
$successfulOperations = count(array_filter(
array_column($loadResult['results'], 'result'),
fn($result) => $result['result'] === true
fn ($result) => $result['result'] === true
));
echo "Actual Throughput: {$actualThroughput} operations/second\n";
@@ -371,18 +377,19 @@ final class DistributedLockPerformanceTest extends TestCase
$this->lockService->release($lockKey, $owner1);
}
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($lockKey, $owner2) {
$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']
'success' => $result['result'],
];
if ($result['result']) {
// Successfully acquired, release it and stop
$this->lockService->release($lockKey, $owner2);
break;
}
@@ -432,4 +439,4 @@ final class DistributedLockPerformanceTest extends TestCase
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM distributed_locks');
}
}
}

View File

@@ -16,9 +16,13 @@ use PHPUnit\Framework\TestCase;
final class FailoverPerformanceTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private WorkerHealthCheckService $healthCheckService;
private FailoverRecoveryService $failoverService;
private JobDistributionService $distributionService;
protected function setUp(): void
@@ -70,7 +74,7 @@ final class FailoverPerformanceTest extends TestCase
$iterations = 10;
for ($i = 0; $i < $iterations; $i++) {
$time = PerformanceTestHelper::measureTime(function() {
$time = PerformanceTestHelper::measureTime(function () {
return $this->healthCheckService->checkAllWorkers();
});
$detectionTimes[] = $time;
@@ -83,7 +87,7 @@ final class FailoverPerformanceTest extends TestCase
// Verify failed workers were detected
$failedWorkers = $this->workerRegistry->getWorkersByStatus(WorkerStatus::FAILED);
$detectedFailures = array_map(fn($w) => $w->id->toString(), $failedWorkers);
$detectedFailures = array_map(fn ($w) => $w->id->toString(), $failedWorkers);
echo "\nWorker Failure Detection Results:\n";
echo "Workers created: " . count($workers) . "\n";
@@ -123,7 +127,7 @@ final class FailoverPerformanceTest extends TestCase
if ($assignedWorker) {
$assignedJobs[] = [
'job' => $job,
'worker_id' => $assignedWorker->id->toString()
'worker_id' => $assignedWorker->id->toString(),
];
}
}
@@ -135,7 +139,7 @@ final class FailoverPerformanceTest extends TestCase
// Find jobs assigned to failed worker
$jobsToReassign = array_filter(
$assignedJobs,
fn($item) => $item['worker_id'] === $failedWorkerId
fn ($item) => $item['worker_id'] === $failedWorkerId
);
echo "\nJob Reassignment Test:\n";
@@ -143,7 +147,7 @@ final class FailoverPerformanceTest extends TestCase
echo "Jobs to reassign: " . count($jobsToReassign) . "\n";
// Measure job reassignment performance
$reassignmentTime = PerformanceTestHelper::measureTime(function() use ($failedWorkerId) {
$reassignmentTime = PerformanceTestHelper::measureTime(function () use ($failedWorkerId) {
return $this->failoverService->reassignFailedWorkerJobs($failedWorkerId);
});
@@ -184,7 +188,7 @@ final class FailoverPerformanceTest extends TestCase
$failedWorkerIds = [
$workers[0]->id->toString(),
$workers[1]->id->toString(),
$workers[2]->id->toString()
$workers[2]->id->toString(),
];
foreach ($failedWorkerIds as $workerId) {
@@ -197,7 +201,7 @@ final class FailoverPerformanceTest extends TestCase
echo "Total jobs: " . count($jobs) . "\n";
// Measure full system recovery time
$recoveryTime = PerformanceTestHelper::measureTime(function() {
$recoveryTime = PerformanceTestHelper::measureTime(function () {
return $this->failoverService->performFullSystemRecovery();
});
@@ -231,7 +235,7 @@ final class FailoverPerformanceTest extends TestCase
PerformanceTestHelper::createTestWorker('medium_capacity_1', 20),
PerformanceTestHelper::createTestWorker('medium_capacity_2', 20),
PerformanceTestHelper::createTestWorker('low_capacity_1', 10),
PerformanceTestHelper::createTestWorker('low_capacity_2', 10)
PerformanceTestHelper::createTestWorker('low_capacity_2', 10),
];
$this->registerWorkers($workers);
@@ -289,7 +293,7 @@ final class FailoverPerformanceTest extends TestCase
while (microtime(true) < $endTime) {
$job = PerformanceTestHelper::createTestJob("load_job_{$jobsDistributed}");
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($job) {
$result = PerformanceTestHelper::measureTimeWithResult(function () use ($job) {
try {
return $this->distributionService->distributeJob($job);
} catch (\Exception $e) {
@@ -352,7 +356,7 @@ final class FailoverPerformanceTest extends TestCase
$failedWorkerIds = [
$workers[0]->id->toString(),
$workers[1]->id->toString(),
$workers[2]->id->toString()
$workers[2]->id->toString(),
];
foreach ($failedWorkerIds as $workerId) {
@@ -369,7 +373,7 @@ final class FailoverPerformanceTest extends TestCase
// Update heartbeat to simulate worker recovery
$this->updateWorkerHeartbeat($workerId, new \DateTimeImmutable());
$recoveryTime = PerformanceTestHelper::measureTime(function() use ($workerId) {
$recoveryTime = PerformanceTestHelper::measureTime(function () use ($workerId) {
// Simulate health check detecting recovery
$this->healthCheckService->checkWorker($workerId);
@@ -434,6 +438,7 @@ final class FailoverPerformanceTest extends TestCase
WorkerStatus::AVAILABLE
);
}
return $workers;
}
@@ -463,6 +468,7 @@ final class FailoverPerformanceTest extends TestCase
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT COUNT(*) FROM jobs WHERE worker_id != ? AND worker_id IS NOT NULL');
$stmt->execute([$failedWorkerId]);
return (int) $stmt->fetchColumn();
}
@@ -471,6 +477,7 @@ final class FailoverPerformanceTest extends TestCase
$pdo = $this->database->getConnection();
$stmt = $pdo->prepare('SELECT COUNT(*) FROM jobs WHERE status = ?');
$stmt->execute([$status->value]);
return (int) $stmt->fetchColumn();
}
@@ -521,4 +528,4 @@ final class FailoverPerformanceTest extends TestCase
$pdo->exec('DELETE FROM workers');
$pdo->exec('DELETE FROM jobs');
}
}
}

View File

@@ -16,8 +16,11 @@ 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
@@ -47,7 +50,7 @@ final class LoadBalancingPerformanceTest extends TestCase
PerformanceTestHelper::createTestWorker('worker_2', 15),
PerformanceTestHelper::createTestWorker('worker_3', 10),
PerformanceTestHelper::createTestWorker('worker_4', 25),
PerformanceTestHelper::createTestWorker('worker_5', 30)
PerformanceTestHelper::createTestWorker('worker_5', 30),
];
$this->registerWorkers($workers);
@@ -56,7 +59,7 @@ final class LoadBalancingPerformanceTest extends TestCase
$iterations = 1000;
for ($i = 0; $i < $iterations; $i++) {
$time = PerformanceTestHelper::measureTime(function() {
$time = PerformanceTestHelper::measureTime(function () {
$this->loadBalancer->selectWorker(new QueueName('test_queue'));
});
$selectionTimes[] = $time;
@@ -92,7 +95,7 @@ final class LoadBalancingPerformanceTest extends TestCase
for ($i = 0; $i < $iterations; $i++) {
$job = PerformanceTestHelper::createTestJob("dist_job_{$i}");
$time = PerformanceTestHelper::measureTime(function() use ($job) {
$time = PerformanceTestHelper::measureTime(function () use ($job) {
$this->distributionService->distributeJob($job);
});
@@ -131,8 +134,9 @@ final class LoadBalancingPerformanceTest extends TestCase
echo "Target: {$jobsPerSecond} jobs/second for {$testDuration} seconds\n";
$loadResult = PerformanceTestHelper::simulateLoad(
function($index) {
function ($index) {
$job = PerformanceTestHelper::createTestJob("load_job_{$index}");
return $this->distributionService->distributeJob($job);
},
$totalJobs,
@@ -166,7 +170,7 @@ final class LoadBalancingPerformanceTest extends TestCase
PerformanceTestHelper::createTestWorker('worker_2', 20),
PerformanceTestHelper::createTestWorker('worker_3', 30),
PerformanceTestHelper::createTestWorker('worker_4', 15),
PerformanceTestHelper::createTestWorker('worker_5', 25)
PerformanceTestHelper::createTestWorker('worker_5', 25),
];
$this->registerWorkers($workers);
@@ -185,7 +189,7 @@ final class LoadBalancingPerformanceTest extends TestCase
}
echo "\nFair Distribution Results:\n";
$totalCapacity = array_sum(array_map(fn($w) => $w->capacity->value, $workers));
$totalCapacity = array_sum(array_map(fn ($w) => $w->capacity->value, $workers));
foreach ($workers as $worker) {
$workerId = $worker->id->toString();
@@ -228,7 +232,7 @@ final class LoadBalancingPerformanceTest extends TestCase
PerformanceTestHelper::createTestWorker('medium_worker_1', 15),
PerformanceTestHelper::createTestWorker('medium_worker_2', 20),
PerformanceTestHelper::createTestWorker('large_worker', 50),
PerformanceTestHelper::createTestWorker('xlarge_worker', 100)
PerformanceTestHelper::createTestWorker('xlarge_worker', 100),
];
$this->registerWorkers($workers);
@@ -237,7 +241,7 @@ final class LoadBalancingPerformanceTest extends TestCase
$iterations = 500;
for ($i = 0; $i < $iterations; $i++) {
$time = PerformanceTestHelper::measureTime(function() {
$time = PerformanceTestHelper::measureTime(function () {
$this->loadBalancer->selectWorker(new QueueName('test_queue'));
});
$selectionTimes[] = $time;
@@ -271,7 +275,7 @@ final class LoadBalancingPerformanceTest extends TestCase
$priority
);
$time = PerformanceTestHelper::measureTime(function() use ($job) {
$time = PerformanceTestHelper::measureTime(function () use ($job) {
$this->distributionService->distributeJob($job);
});
@@ -296,7 +300,7 @@ final class LoadBalancingPerformanceTest extends TestCase
$workers = [
PerformanceTestHelper::createTestWorker('worker_1', 2),
PerformanceTestHelper::createTestWorker('worker_2', 3),
PerformanceTestHelper::createTestWorker('worker_3', 2)
PerformanceTestHelper::createTestWorker('worker_3', 2),
];
$this->registerWorkers($workers);
@@ -311,7 +315,7 @@ final class LoadBalancingPerformanceTest extends TestCase
for ($i = 0; $i < $jobCount; $i++) {
$job = PerformanceTestHelper::createTestJob("overload_job_{$i}");
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($job) {
$result = PerformanceTestHelper::measureTimeWithResult(function () use ($job) {
return $this->distributionService->distributeJob($job);
});
@@ -349,6 +353,7 @@ final class LoadBalancingPerformanceTest extends TestCase
WorkerStatus::AVAILABLE
);
}
return $workers;
}
@@ -408,4 +413,4 @@ final class LoadBalancingPerformanceTest extends TestCase
$pdo->exec('DELETE FROM workers');
$pdo->exec('DELETE FROM jobs');
}
}
}

View File

@@ -6,13 +6,7 @@ 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;
@@ -20,7 +14,9 @@ use PHPUnit\Framework\TestCase;
final class MultiWorkerThroughputTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private JobDistributionService $distributionService;
protected function setUp(): void
@@ -134,7 +130,7 @@ final class MultiWorkerThroughputTest extends TestCase
['workers' => 1, 'jobs' => 50, 'capacity' => 50],
['workers' => 5, 'jobs' => 250, 'capacity' => 20],
['workers' => 10, 'jobs' => 500, 'capacity' => 15],
['workers' => 20, 'jobs' => 1000, 'capacity' => 10]
['workers' => 20, 'jobs' => 1000, 'capacity' => 10],
];
$results = [];
@@ -146,7 +142,7 @@ final class MultiWorkerThroughputTest extends TestCase
$results[] = [
'worker_count' => $case['workers'],
'throughput' => $result['throughput'],
'efficiency' => $result['throughput'] / $case['workers'] // Jobs per worker per second
'efficiency' => $result['throughput'] / $case['workers'], // Jobs per worker per second
];
$this->cleanupJobs();
@@ -190,7 +186,7 @@ final class MultiWorkerThroughputTest extends TestCase
PerformanceTestHelper::createTestWorker('worker_2', 30),
PerformanceTestHelper::createTestWorker('worker_3', 20),
PerformanceTestHelper::createTestWorker('worker_4', 10),
PerformanceTestHelper::createTestWorker('worker_5', 5)
PerformanceTestHelper::createTestWorker('worker_5', 5),
];
$this->registerWorkers($workers);
@@ -229,7 +225,7 @@ final class MultiWorkerThroughputTest extends TestCase
$batchStartTime = microtime(true);
foreach ($jobs as $job) {
$measureResult = PerformanceTestHelper::measureTimeWithResult(
fn() => $this->distributionService->distributeJob($job)
fn () => $this->distributionService->distributeJob($job)
);
$distributionTimes[] = $measureResult['time_ms'];
}
@@ -279,7 +275,7 @@ final class MultiWorkerThroughputTest extends TestCase
foreach ($jobs as $job) {
$measureResult = PerformanceTestHelper::measureTimeWithResult(
fn() => $this->distributionService->distributeJob($job)
fn () => $this->distributionService->distributeJob($job)
);
$distributionTimes[] = $measureResult['time_ms'];
}
@@ -292,7 +288,7 @@ final class MultiWorkerThroughputTest extends TestCase
'throughput' => round($throughput, 1),
'total_time_ms' => round($totalTimeMs, 1),
'distribution_stats' => PerformanceTestHelper::calculateStatistics($distributionTimes),
'jobs_processed' => $jobCount
'jobs_processed' => $jobCount,
];
}
@@ -306,6 +302,7 @@ final class MultiWorkerThroughputTest extends TestCase
WorkerStatus::AVAILABLE
);
}
return $workers;
}
@@ -383,4 +380,4 @@ final class MultiWorkerThroughputTest extends TestCase
$pdo = $this->database->getConnection();
$pdo->exec('DELETE FROM jobs WHERE status IN ("COMPLETED", "FAILED")');
}
}
}

View File

@@ -6,14 +6,13 @@ namespace Tests\Framework\Queue\Performance;
use App\Framework\Queue\Jobs\Job;
use App\Framework\Queue\Jobs\JobRequest;
use App\Framework\Queue\Jobs\JobResult;
use App\Framework\Queue\Jobs\JobStatus;
use App\Framework\Queue\Queue\JobPriority;
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\WorkerStatus;
use App\Framework\Queue\Queue\QueueName;
use App\Framework\Queue\Queue\JobPriority;
final readonly class PerformanceTestHelper
{
@@ -61,6 +60,7 @@ final readonly class PerformanceTestHelper
payload: ['batch_id' => $i, 'data' => str_repeat('x', 100)]
);
}
return $jobs;
}
@@ -81,7 +81,7 @@ final readonly class PerformanceTestHelper
return [
'result' => $result,
'time_ms' => ($end - $start) * 1000
'time_ms' => ($end - $start) * 1000,
];
}
@@ -96,7 +96,7 @@ final readonly class PerformanceTestHelper
'median' => 0,
'p95' => 0,
'p99' => 0,
'stddev' => 0
'stddev' => 0,
];
}
@@ -132,7 +132,7 @@ final readonly class PerformanceTestHelper
'median' => round($median, 3),
'p95' => round($p95, 3),
'p99' => round($p99, 3),
'stddev' => round($stddev, 3)
'stddev' => round($stddev, 3),
];
}
@@ -141,12 +141,18 @@ final readonly class PerformanceTestHelper
return sprintf(
"Count: %d, Min: %.3f%s, Max: %.3f%s, Avg: %.3f%s, P95: %.3f%s, P99: %.3f%s, StdDev: %.3f%s",
$stats['count'],
$stats['min'], $unit,
$stats['max'], $unit,
$stats['avg'], $unit,
$stats['p95'], $unit,
$stats['p99'], $unit,
$stats['stddev'], $unit
$stats['min'],
$unit,
$stats['max'],
$unit,
$stats['avg'],
$unit,
$stats['p95'],
$unit,
$stats['p99'],
$unit,
$stats['stddev'],
$unit
);
}
@@ -187,7 +193,7 @@ final readonly class PerformanceTestHelper
'current_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
'peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
'current_real_mb' => round(memory_get_usage(false) / 1024 / 1024, 2),
'peak_real_mb' => round(memory_get_peak_usage(false) / 1024 / 1024, 2)
'peak_real_mb' => round(memory_get_peak_usage(false) / 1024 / 1024, 2),
];
}
@@ -204,7 +210,7 @@ final readonly class PerformanceTestHelper
$operations = [];
for ($i = 0; $i < $concurrency; $i++) {
$operations[] = function() use ($operation, $i) {
$operations[] = function () use ($operation, $i) {
return $operation($i);
};
}
@@ -233,7 +239,7 @@ final readonly class PerformanceTestHelper
// Execute concurrent operations
for ($i = 0; $i < $batchSize; $i++) {
$result = self::measureTimeWithResult(function() use ($operation, $batch, $i) {
$result = self::measureTimeWithResult(function () use ($operation, $batch, $i) {
return $operation($batch * $concurrency + $i);
});
$batchResults[] = $result;
@@ -253,7 +259,7 @@ final readonly class PerformanceTestHelper
'results' => $results,
'operations_completed' => $operationsCompleted,
'duration_seconds' => microtime(true) - $startTime,
'throughput_ops_per_sec' => $operationsCompleted / (microtime(true) - $startTime)
'throughput_ops_per_sec' => $operationsCompleted / (microtime(true) - $startTime),
];
}
@@ -292,4 +298,4 @@ final readonly class PerformanceTestHelper
return $report;
}
}
}

View File

@@ -9,7 +9,6 @@ use App\Framework\Queue\Distribution\JobDistributionService;
use App\Framework\Queue\Failover\FailoverRecoveryService;
use App\Framework\Queue\Health\WorkerHealthCheckService;
use App\Framework\Queue\Jobs\JobPriority;
use App\Framework\Queue\Jobs\JobStatus;
use App\Framework\Queue\Workers\WorkerRegistry;
use App\Framework\Queue\Workers\WorkerStatus;
use PHPUnit\Framework\TestCase;
@@ -17,9 +16,13 @@ use PHPUnit\Framework\TestCase;
final class RealisticLoadScenariosTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private JobDistributionService $distributionService;
private WorkerHealthCheckService $healthCheckService;
private FailoverRecoveryService $failoverService;
protected function setUp(): void
@@ -60,7 +63,7 @@ final class RealisticLoadScenariosTest extends TestCase
// Medium-capacity workers for inventory updates
...$this->createWorkers(12, 30, 'inventory_worker'),
// Lower-capacity workers for email notifications
...$this->createWorkers(20, 15, 'notification_worker')
...$this->createWorkers(20, 15, 'notification_worker'),
];
$this->registerWorkers($workers);
@@ -105,7 +108,7 @@ final class RealisticLoadScenariosTest extends TestCase
// Medium workers for image processing
...$this->createWorkers(8, 50, 'image_processor'),
// Light workers for metadata extraction
...$this->createWorkers(12, 25, 'metadata_worker')
...$this->createWorkers(12, 25, 'metadata_worker'),
];
$this->registerWorkers($workers);
@@ -150,7 +153,7 @@ final class RealisticLoadScenariosTest extends TestCase
// Fraud detection workers
...$this->createWorkers(10, 15, 'fraud_detector'),
// Settlement workers
...$this->createWorkers(5, 30, 'settlement_worker')
...$this->createWorkers(5, 30, 'settlement_worker'),
];
$this->registerWorkers($workers);
@@ -197,7 +200,7 @@ final class RealisticLoadScenariosTest extends TestCase
// Data validation workers
...$this->createWorkers(8, 75, 'validator'),
// Report generation workers
...$this->createWorkers(4, 150, 'report_generator')
...$this->createWorkers(4, 150, 'report_generator'),
];
$this->registerWorkers($workers);
@@ -244,7 +247,7 @@ final class RealisticLoadScenariosTest extends TestCase
// Heavy computation workers
...$this->createWorkers(5, 80, 'compute_worker'),
// Notification workers
...$this->createWorkers(20, 10, 'notification_worker')
...$this->createWorkers(20, 10, 'notification_worker'),
];
$this->registerWorkers($workers);
@@ -254,7 +257,7 @@ final class RealisticLoadScenariosTest extends TestCase
['duration' => 60, 'rate' => 20, 'mix' => 'normal'],
['duration' => 120, 'rate' => 50, 'mix' => 'peak'],
['duration' => 60, 'rate' => 15, 'mix' => 'background'],
['duration' => 90, 'rate' => 35, 'mix' => 'mixed']
['duration' => 90, 'rate' => 35, 'mix' => 'mixed'],
];
$overallResults = [];
@@ -293,7 +296,7 @@ final class RealisticLoadScenariosTest extends TestCase
$workers = [
...$this->createWorkers(12, 25, 'primary_worker'),
...$this->createWorkers(8, 30, 'secondary_worker'),
...$this->createWorkers(6, 20, 'backup_worker')
...$this->createWorkers(6, 20, 'backup_worker'),
];
$this->registerWorkers($workers);
@@ -312,7 +315,7 @@ final class RealisticLoadScenariosTest extends TestCase
'jobs_processed' => 0,
'jobs_failed' => 0,
'response_times' => [],
'failover_events' => []
'failover_events' => [],
];
$failoverTriggered = false;
@@ -321,7 +324,7 @@ final class RealisticLoadScenariosTest extends TestCase
$cycleStart = microtime(true);
// Trigger failover at 1/3 of test duration
if (!$failoverTriggered && (microtime(true) - $startTime) > ($testDuration / 3)) {
if (! $failoverTriggered && (microtime(true) - $startTime) > ($testDuration / 3)) {
echo "\nTriggering failover scenario...\n";
// Fail primary workers
@@ -329,13 +332,13 @@ final class RealisticLoadScenariosTest extends TestCase
$this->updateWorkerStatus("primary_worker_{$i}", WorkerStatus::FAILED);
}
$failoverTime = PerformanceTestHelper::measureTime(function() {
$failoverTime = PerformanceTestHelper::measureTime(function () {
$this->failoverService->performFullSystemRecovery();
});
$metrics['failover_events'][] = [
'time' => microtime(true) - $startTime,
'recovery_time' => $failoverTime
'recovery_time' => $failoverTime,
];
echo "Failover completed in {$failoverTime}ms\n";
@@ -346,7 +349,7 @@ final class RealisticLoadScenariosTest extends TestCase
for ($i = 0; $i < $baseJobRate; $i++) {
$job = PerformanceTestHelper::createTestJob("realworld_job_{$metrics['jobs_processed']}");
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($job) {
$result = PerformanceTestHelper::measureTimeWithResult(function () use ($job) {
try {
return $this->distributionService->distributeJob($job);
} catch (\Exception $e) {
@@ -382,7 +385,7 @@ final class RealisticLoadScenariosTest extends TestCase
echo "Success rate: {$successRate}%\n";
echo "Response times: " . PerformanceTestHelper::formatStatistics($responseStats) . "\n";
if (!empty($metrics['failover_events'])) {
if (! empty($metrics['failover_events'])) {
echo "Failover recovery time: {$metrics['failover_events'][0]['recovery_time']}ms\n";
}
@@ -408,7 +411,7 @@ final class RealisticLoadScenariosTest extends TestCase
'response_times' => [],
'memory_snapshots' => [],
'start_memory' => null,
'end_memory' => null
'end_memory' => null,
];
if ($enableResourceMonitoring) {
@@ -426,7 +429,7 @@ final class RealisticLoadScenariosTest extends TestCase
$jobType = $this->selectJobType($jobMix);
$job = $this->createJobForType($jobType, $jobCounter);
$result = PerformanceTestHelper::measureTimeWithResult(function() use ($job) {
$result = PerformanceTestHelper::measureTimeWithResult(function () use ($job) {
try {
return $this->distributionService->distributeJob($job);
} catch (\Exception $e) {
@@ -448,7 +451,7 @@ final class RealisticLoadScenariosTest extends TestCase
if ($enableResourceMonitoring && microtime(true) >= $nextSnapshotTime) {
$metrics['memory_snapshots'][] = [
'time' => microtime(true) - $startTime,
'memory' => PerformanceTestHelper::getMemoryUsage()
'memory' => PerformanceTestHelper::getMemoryUsage(),
];
$nextSnapshotTime += $snapshotInterval;
}
@@ -488,7 +491,7 @@ final class RealisticLoadScenariosTest extends TestCase
'success_rate' => round($successRate, 2),
'avg_response_time' => $responseStats['avg'],
'p95_response_time' => $responseStats['p95'],
'p99_response_time' => $responseStats['p99']
'p99_response_time' => $responseStats['p99'],
];
if ($includeResourceMetrics && isset($metrics['start_memory'], $metrics['end_memory'])) {
@@ -513,7 +516,7 @@ final class RealisticLoadScenariosTest extends TestCase
'inventory_update' => 25,
'payment_processing' => 20,
'email_notification' => 10,
'user_analytics' => 5
'user_analytics' => 5,
];
}
@@ -523,7 +526,7 @@ final class RealisticLoadScenariosTest extends TestCase
'video_transcode' => 30,
'image_resize' => 40,
'thumbnail_generation' => 20,
'metadata_extraction' => 10
'metadata_extraction' => 10,
];
}
@@ -533,7 +536,7 @@ final class RealisticLoadScenariosTest extends TestCase
'payment_processing' => 50,
'fraud_detection' => 25,
'account_verification' => 15,
'transaction_logging' => 10
'transaction_logging' => 10,
];
}
@@ -543,7 +546,7 @@ final class RealisticLoadScenariosTest extends TestCase
'data_transformation' => 40,
'data_validation' => 30,
'report_generation' => 20,
'data_archival' => 10
'data_archival' => 10,
];
}
@@ -553,24 +556,24 @@ final class RealisticLoadScenariosTest extends TestCase
'normal' => [
'web_request' => 50,
'background_task' => 30,
'notification' => 20
'notification' => 20,
],
'peak' => [
'web_request' => 60,
'background_task' => 20,
'notification' => 15,
'compute_task' => 5
'compute_task' => 5,
],
'background' => [
'background_task' => 60,
'compute_task' => 30,
'notification' => 10
'notification' => 10,
],
'mixed' => [
'web_request' => 35,
'background_task' => 25,
'compute_task' => 25,
'notification' => 15
'notification' => 15,
],
default => ['web_request' => 100]
};
@@ -638,7 +641,8 @@ final class RealisticLoadScenariosTest extends TestCase
private function calculateStandardDeviation(array $values): float
{
$mean = array_sum($values) / count($values);
$sumSquaredDiffs = array_sum(array_map(fn($v) => pow($v - $mean, 2), $values));
$sumSquaredDiffs = array_sum(array_map(fn ($v) => pow($v - $mean, 2), $values));
return sqrt($sumSquaredDiffs / count($values));
}
@@ -652,6 +656,7 @@ final class RealisticLoadScenariosTest extends TestCase
WorkerStatus::AVAILABLE
);
}
return $workers;
}
@@ -717,4 +722,4 @@ final class RealisticLoadScenariosTest extends TestCase
$pdo->exec('DELETE FROM workers');
$pdo->exec('DELETE FROM jobs');
}
}
}

View File

@@ -6,14 +6,15 @@ namespace Tests\Framework\Queue\Performance;
use App\Framework\Database\DatabaseManager;
use App\Framework\Queue\Distribution\JobDistributionService;
use App\Framework\Queue\Jobs\JobPriority;
use App\Framework\Queue\Workers\WorkerRegistry;
use PHPUnit\Framework\TestCase;
final class SystemResourcesTest extends TestCase
{
private DatabaseManager $database;
private WorkerRegistry $workerRegistry;
private JobDistributionService $distributionService;
protected function setUp(): void
@@ -96,7 +97,7 @@ final class SystemResourcesTest extends TestCase
['batch_size' => 50, 'batches' => 10],
['batch_size' => 100, 'batches' => 10],
['batch_size' => 500, 'batches' => 5],
['batch_size' => 1000, 'batches' => 3]
['batch_size' => 1000, 'batches' => 3],
];
foreach ($testCases as $case) {
@@ -110,7 +111,7 @@ final class SystemResourcesTest extends TestCase
for ($batch = 0; $batch < $batchCount; $batch++) {
$jobs = PerformanceTestHelper::createBulkJobs($batchSize);
$batchTime = PerformanceTestHelper::measureTime(function() use ($jobs) {
$batchTime = PerformanceTestHelper::measureTime(function () use ($jobs) {
foreach ($jobs as $job) {
$this->distributionService->distributeJob($job);
}
@@ -168,7 +169,7 @@ final class SystemResourcesTest extends TestCase
for ($i = 0; $i < $iterations; $i++) {
$job = PerformanceTestHelper::createTestJob("gc_test_job_{$i}");
$operationTime = PerformanceTestHelper::measureTime(function() use ($job) {
$operationTime = PerformanceTestHelper::measureTime(function () use ($job) {
return $this->distributionService->distributeJob($job);
});
@@ -179,7 +180,7 @@ final class SystemResourcesTest extends TestCase
$gcStats[] = [
'operation' => $i,
'memory' => PerformanceTestHelper::getMemoryUsage(),
'gc_stats' => gc_status()
'gc_stats' => gc_status(),
];
}
}
@@ -305,7 +306,7 @@ final class SystemResourcesTest extends TestCase
$memorySnapshots[] = [
'time' => $elapsed,
'operations' => $operationCount,
'memory' => $memory
'memory' => $memory,
];
echo sprintf(
@@ -384,7 +385,7 @@ final class SystemResourcesTest extends TestCase
echo "Memory after distribution: {$afterDistribution['current_mb']}MB\n";
// Measure cleanup time
$cleanupTime = PerformanceTestHelper::measureTime(function() {
$cleanupTime = PerformanceTestHelper::measureTime(function () {
$this->cleanupCompletedJobs();
});
@@ -426,7 +427,7 @@ final class SystemResourcesTest extends TestCase
$distributionTimes = [];
foreach ($jobs as $job) {
$time = PerformanceTestHelper::measureTime(function() use ($job) {
$time = PerformanceTestHelper::measureTime(function () use ($job) {
return $this->distributionService->distributeJob($job);
});
$distributionTimes[] = $time;
@@ -487,7 +488,7 @@ final class SystemResourcesTest extends TestCase
for ($op = 0; $op < $operationsPerWorker; $op++) {
$job = PerformanceTestHelper::createTestJob("concurrent_job_{$worker}_{$op}");
$time = PerformanceTestHelper::measureTime(function() use ($job) {
$time = PerformanceTestHelper::measureTime(function () use ($job) {
return $this->distributionService->distributeJob($job);
});
@@ -501,7 +502,7 @@ final class SystemResourcesTest extends TestCase
return [
'times' => $times,
'total_operations' => $actualOperations,
'duration' => $endTime - $startTime
'duration' => $endTime - $startTime,
];
}
@@ -556,6 +557,7 @@ final class SystemResourcesTest extends TestCase
$capacity
);
}
return $workers;
}
@@ -614,4 +616,4 @@ final class SystemResourcesTest extends TestCase
$pdo->exec('DELETE FROM workers');
$pdo->exec('DELETE FROM jobs');
}
}
}

View File

@@ -2,38 +2,34 @@
declare(strict_types=1);
use App\Framework\Database\EntityManagerInterface;
use App\Framework\DI\Container;
use App\Framework\DI\DefaultContainer;
use App\Framework\Queue\Queue;
use App\Framework\Queue\QueueInitializer;
use App\Framework\Queue\QueueDependencyInitializer;
// Queue service interfaces
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Queue\Contracts\JobProgressTrackerInterface;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Queue\Contracts\DeadLetterQueueInterface;
use App\Framework\Queue\Contracts\JobChainManagerInterface;
use App\Framework\Queue\Services\JobMetricsManagerInterface;
// Concrete implementations
use App\Framework\Queue\Services\DatabaseDistributedLock;
use App\Framework\Queue\Services\DatabaseJobProgressTracker;
use App\Framework\Queue\Services\DatabaseJobDependencyManager;
use App\Framework\Queue\Services\DatabaseDeadLetterQueue;
use App\Framework\Queue\Services\DatabaseJobChainManager;
use App\Framework\Queue\Services\JobMetricsManager;
// Additional services
use App\Framework\Queue\Services\WorkerRegistry;
use App\Framework\Queue\Services\JobDistributionService;
use App\Framework\Queue\Services\WorkerHealthCheckService;
use App\Framework\Queue\Services\FailoverRecoveryService;
use App\Framework\Queue\Services\DependencyResolutionEngine;
// Framework dependencies
use App\Framework\Database\EntityManagerInterface;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Contracts\DeadLetterQueueInterface;
// Queue service interfaces
use App\Framework\Queue\Contracts\JobChainManagerInterface;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Queue\Contracts\JobProgressTrackerInterface;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Queue\Queue;
use App\Framework\Queue\QueueDependencyInitializer;
// Concrete implementations
use App\Framework\Queue\QueueInitializer;
use App\Framework\Queue\Services\DatabaseDeadLetterQueue;
use App\Framework\Queue\Services\DatabaseDistributedLock;
use App\Framework\Queue\Services\DatabaseJobChainManager;
use App\Framework\Queue\Services\DatabaseJobDependencyManager;
use App\Framework\Queue\Services\DatabaseJobProgressTracker;
// Additional services
use App\Framework\Queue\Services\DependencyResolutionEngine;
use App\Framework\Queue\Services\FailoverRecoveryService;
use App\Framework\Queue\Services\JobDistributionService;
use App\Framework\Queue\Services\JobMetricsManager;
use App\Framework\Queue\Services\JobMetricsManagerInterface;
// Framework dependencies
use App\Framework\Queue\Services\WorkerHealthCheckService;
use App\Framework\Queue\Services\WorkerRegistry;
describe('Queue Service Registration', function () {
@@ -41,33 +37,105 @@ describe('Queue Service Registration', function () {
$this->container = new DefaultContainer();
// Mock essential framework dependencies
$this->mockEntityManager = new class implements EntityManagerInterface {
public function persist(object $entity): void {}
public function find(string $className, mixed $id): ?object { return null; }
public function flush(): void {}
public function remove(object $entity): void {}
public function clear(): void {}
public function detach(object $entity): void {}
public function contains(object $entity): bool { return false; }
public function refresh(object $entity): void {}
public function createQueryBuilder(): object { return new stdClass(); }
public function getRepository(string $className): object { return new stdClass(); }
public function beginTransaction(): void {}
public function commit(): void {}
public function rollback(): void {}
public function isTransactionActive(): bool { return false; }
$this->mockEntityManager = new class () implements EntityManagerInterface {
public function persist(object $entity): void
{
}
public function find(string $className, mixed $id): ?object
{
return null;
}
public function flush(): void
{
}
public function remove(object $entity): void
{
}
public function clear(): void
{
}
public function detach(object $entity): void
{
}
public function contains(object $entity): bool
{
return false;
}
public function refresh(object $entity): void
{
}
public function createQueryBuilder(): object
{
return new stdClass();
}
public function getRepository(string $className): object
{
return new stdClass();
}
public function beginTransaction(): void
{
}
public function commit(): void
{
}
public function rollback(): void
{
}
public function isTransactionActive(): bool
{
return false;
}
};
$this->mockLogger = new class implements Logger {
public function emergency(string $message, array $context = []): void {}
public function alert(string $message, array $context = []): void {}
public function critical(string $message, array $context = []): void {}
public function error(string $message, array $context = []): void {}
public function warning(string $message, array $context = []): void {}
public function notice(string $message, array $context = []): void {}
public function info(string $message, array $context = []): void {}
public function debug(string $message, array $context = []): void {}
public function log(string $level, string $message, array $context = []): void {}
$this->mockLogger = new class () implements Logger {
public function emergency(string $message, array $context = []): void
{
}
public function alert(string $message, array $context = []): void
{
}
public function critical(string $message, array $context = []): void
{
}
public function error(string $message, array $context = []): void
{
}
public function warning(string $message, array $context = []): void
{
}
public function notice(string $message, array $context = []): void
{
}
public function info(string $message, array $context = []): void
{
}
public function debug(string $message, array $context = []): void
{
}
public function log(string $level, string $message, array $context = []): void
{
}
};
// Register mocked dependencies
@@ -81,8 +149,9 @@ describe('Queue Service Registration', function () {
// Note: This will fallback to FileQueue since Redis is not available in tests
$queueInitializer = new QueueInitializer(
pathProvider: new class {
public function resolvePath(string $path): string {
pathProvider: new class () {
public function resolvePath(string $path): string
{
return '/home/michael/dev/michaelschiemer/tests/tmp/queue/';
}
}
@@ -96,7 +165,7 @@ describe('Queue Service Registration', function () {
it('Queue service is accessible from container after registration', function () {
// Register queue manually for testing
$this->container->singleton(Queue::class, function() {
$this->container->singleton(Queue::class, function () {
return new \App\Framework\Queue\InMemoryQueue();
});
@@ -111,19 +180,34 @@ describe('Queue Service Registration', function () {
$this->dependencyInitializer = new QueueDependencyInitializer();
// Register a basic queue interface for the dependencies
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function() {
return new class implements \App\Framework\Queue\Contracts\QueueInterface {
public function push(mixed $job): void {}
public function pop(): mixed { return null; }
public function size(): int { return 0; }
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function () {
return new class () implements \App\Framework\Queue\Contracts\QueueInterface {
public function push(mixed $job): void
{
}
public function pop(): mixed
{
return null;
}
public function size(): int
{
return 0;
}
};
});
// Register EventDispatcher mock
$this->container->singleton(\App\Framework\Core\Events\EventDispatcherInterface::class, function() {
return new class implements \App\Framework\Core\Events\EventDispatcherInterface {
public function dispatch(object $event): void {}
public function listen(string $event, callable $listener): void {}
$this->container->singleton(\App\Framework\Core\Events\EventDispatcherInterface::class, function () {
return new class () implements \App\Framework\Core\Events\EventDispatcherInterface {
public function dispatch(object $event): void
{
}
public function listen(string $event, callable $listener): void
{
}
};
});
});
@@ -228,11 +312,21 @@ describe('Queue Service Registration', function () {
$this->mockLogger
));
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function() {
return new class implements \App\Framework\Queue\Contracts\QueueInterface {
public function push(mixed $job): void {}
public function pop(): mixed { return null; }
public function size(): int { return 0; }
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function () {
return new class () implements \App\Framework\Queue\Contracts\QueueInterface {
public function push(mixed $job): void
{
}
public function pop(): mixed
{
return null;
}
public function size(): int
{
return 0;
}
};
});
@@ -264,23 +358,23 @@ describe('Queue Service Registration', function () {
expect($resolutionEngine)->toBeInstanceOf(DependencyResolutionEngine::class);
// These services should be functional (not throw errors)
expect(fn() => $dependencyManager)->not->toThrow();
expect(fn() => $chainManager)->not->toThrow();
expect(fn() => $resolutionEngine)->not->toThrow();
expect(fn () => $dependencyManager)->not->toThrow();
expect(fn () => $chainManager)->not->toThrow();
expect(fn () => $resolutionEngine)->not->toThrow();
});
it('can resolve complex dependency graph', function () {
$this->dependencyInitializer->__invoke($this->container);
// Add additional services
$this->container->singleton(DistributedLockInterface::class, function() {
$this->container->singleton(DistributedLockInterface::class, function () {
return new DatabaseDistributedLock(
$this->mockEntityManager,
$this->mockLogger
);
});
$this->container->singleton(JobProgressTrackerInterface::class, function() {
$this->container->singleton(JobProgressTrackerInterface::class, function () {
return new DatabaseJobProgressTracker(
$this->mockEntityManager,
$this->mockLogger
@@ -323,11 +417,22 @@ describe('Queue Service Registration', function () {
$original = $this->container->get(JobMetricsManager::class);
// Create mock replacement
$mock = new class implements JobMetricsManagerInterface {
public function recordJobExecution(\App\Framework\Queue\ValueObjects\JobId $jobId, float $executionTime): void {}
public function recordJobFailure(\App\Framework\Queue\ValueObjects\JobId $jobId, string $errorMessage): void {}
public function getJobMetrics(\App\Framework\Queue\ValueObjects\JobId $jobId): ?\App\Framework\Queue\ValueObjects\JobMetrics { return null; }
public function getQueueMetrics(\App\Framework\Queue\ValueObjects\QueueName $queueName): \App\Framework\Queue\ValueObjects\QueueMetrics {
$mock = new class () implements JobMetricsManagerInterface {
public function recordJobExecution(\App\Framework\Queue\ValueObjects\JobId $jobId, float $executionTime): void
{
}
public function recordJobFailure(\App\Framework\Queue\ValueObjects\JobId $jobId, string $errorMessage): void
{
}
public function getJobMetrics(\App\Framework\Queue\ValueObjects\JobId $jobId): ?\App\Framework\Queue\ValueObjects\JobMetrics
{
return null;
}
public function getQueueMetrics(\App\Framework\Queue\ValueObjects\QueueName $queueName): \App\Framework\Queue\ValueObjects\QueueMetrics
{
return new \App\Framework\Queue\ValueObjects\QueueMetrics(
queueName: $queueName,
totalJobs: 0,
@@ -336,7 +441,11 @@ describe('Queue Service Registration', function () {
averageExecutionTime: 0.0
);
}
public function getSystemMetrics(): array { return []; }
public function getSystemMetrics(): array
{
return [];
}
};
// Replace with mock
@@ -357,7 +466,7 @@ describe('Queue Service Registration', function () {
$dependencyInitializer = new QueueDependencyInitializer();
// This should fail due to missing dependencies
expect(fn() => $dependencyInitializer->__invoke($this->container))
expect(fn () => $dependencyInitializer->__invoke($this->container))
->toThrow();
});
});
@@ -369,47 +478,134 @@ describe('Queue Service Integration Test', function () {
$this->container = new DefaultContainer();
// Register all required mocks
$this->container->instance(EntityManagerInterface::class, new class implements EntityManagerInterface {
public function persist(object $entity): void {}
public function find(string $className, mixed $id): ?object { return null; }
public function flush(): void {}
public function remove(object $entity): void {}
public function clear(): void {}
public function detach(object $entity): void {}
public function contains(object $entity): bool { return false; }
public function refresh(object $entity): void {}
public function createQueryBuilder(): object { return new stdClass(); }
public function getRepository(string $className): object { return new stdClass(); }
public function beginTransaction(): void {}
public function commit(): void {}
public function rollback(): void {}
public function isTransactionActive(): bool { return false; }
$this->container->instance(EntityManagerInterface::class, new class () implements EntityManagerInterface {
public function persist(object $entity): void
{
}
public function find(string $className, mixed $id): ?object
{
return null;
}
public function flush(): void
{
}
public function remove(object $entity): void
{
}
public function clear(): void
{
}
public function detach(object $entity): void
{
}
public function contains(object $entity): bool
{
return false;
}
public function refresh(object $entity): void
{
}
public function createQueryBuilder(): object
{
return new stdClass();
}
public function getRepository(string $className): object
{
return new stdClass();
}
public function beginTransaction(): void
{
}
public function commit(): void
{
}
public function rollback(): void
{
}
public function isTransactionActive(): bool
{
return false;
}
});
$this->container->instance(Logger::class, new class implements Logger {
public function emergency(string $message, array $context = []): void {}
public function alert(string $message, array $context = []): void {}
public function critical(string $message, array $context = []): void {}
public function error(string $message, array $context = []): void {}
public function warning(string $message, array $context = []): void {}
public function notice(string $message, array $context = []): void {}
public function info(string $message, array $context = []): void {}
public function debug(string $message, array $context = []): void {}
public function log(string $level, string $message, array $context = []): void {}
$this->container->instance(Logger::class, new class () implements Logger {
public function emergency(string $message, array $context = []): void
{
}
public function alert(string $message, array $context = []): void
{
}
public function critical(string $message, array $context = []): void
{
}
public function error(string $message, array $context = []): void
{
}
public function warning(string $message, array $context = []): void
{
}
public function notice(string $message, array $context = []): void
{
}
public function info(string $message, array $context = []): void
{
}
public function debug(string $message, array $context = []): void
{
}
public function log(string $level, string $message, array $context = []): void
{
}
});
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function() {
return new class implements \App\Framework\Queue\Contracts\QueueInterface {
public function push(mixed $job): void {}
public function pop(): mixed { return null; }
public function size(): int { return 0; }
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function () {
return new class () implements \App\Framework\Queue\Contracts\QueueInterface {
public function push(mixed $job): void
{
}
public function pop(): mixed
{
return null;
}
public function size(): int
{
return 0;
}
};
});
$this->container->singleton(\App\Framework\Core\Events\EventDispatcherInterface::class, function() {
return new class implements \App\Framework\Core\Events\EventDispatcherInterface {
public function dispatch(object $event): void {}
public function listen(string $event, callable $listener): void {}
$this->container->singleton(\App\Framework\Core\Events\EventDispatcherInterface::class, function () {
return new class () implements \App\Framework\Core\Events\EventDispatcherInterface {
public function dispatch(object $event): void
{
}
public function listen(string $event, callable $listener): void
{
}
};
});
});
@@ -420,28 +616,28 @@ describe('Queue Service Integration Test', function () {
$dependencyInitializer->__invoke($this->container);
// Register additional services that would normally be auto-registered
$this->container->singleton(DistributedLockInterface::class, function($container) {
$this->container->singleton(DistributedLockInterface::class, function ($container) {
return new DatabaseDistributedLock(
$container->get(EntityManagerInterface::class),
$container->get(Logger::class)
);
});
$this->container->singleton(JobProgressTrackerInterface::class, function($container) {
$this->container->singleton(JobProgressTrackerInterface::class, function ($container) {
return new DatabaseJobProgressTracker(
$container->get(EntityManagerInterface::class),
$container->get(Logger::class)
);
});
$this->container->singleton(DeadLetterQueueInterface::class, function($container) {
$this->container->singleton(DeadLetterQueueInterface::class, function ($container) {
return new DatabaseDeadLetterQueue(
$container->get(EntityManagerInterface::class),
$container->get(Logger::class)
);
});
$this->container->singleton(WorkerRegistry::class, function($container) {
$this->container->singleton(WorkerRegistry::class, function ($container) {
return new WorkerRegistry(
$container->get(EntityManagerInterface::class),
$container->get(Logger::class)
@@ -486,8 +682,8 @@ describe('Queue Service Integration Test', function () {
$metricsManager = $this->container->get(JobMetricsManager::class);
// Basic interaction tests (should not throw)
expect(fn() => $dependencyManager)->not->toThrow();
expect(fn() => $chainManager)->not->toThrow();
expect(fn() => $metricsManager)->not->toThrow();
expect(fn () => $dependencyManager)->not->toThrow();
expect(fn () => $chainManager)->not->toThrow();
expect(fn () => $metricsManager)->not->toThrow();
});
});
});

View File

@@ -2,16 +2,16 @@
declare(strict_types=1);
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Queue\InMemoryQueue;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueuePriority;
use App\Framework\Core\ValueObjects\Duration;
describe('Queue Interface Basic Operations', function () {
beforeEach(function () {
$this->queue = new InMemoryQueue();
$this->testJob = new class {
$this->testJob = new class () {
public function handle(): string
{
return 'test job executed';
@@ -82,8 +82,12 @@ describe('Queue Interface Basic Operations', function () {
});
it('processes FIFO for same priority jobs', function () {
$job1 = new class { public $id = 1; };
$job2 = new class { public $id = 2; };
$job1 = new class () {
public $id = 1;
};
$job2 = new class () {
public $id = 2;
};
$payload1 = JobPayload::create($job1, QueuePriority::normal());
$payload2 = JobPayload::create($job2, QueuePriority::normal());
@@ -230,11 +234,21 @@ describe('Queue Priority Processing', function () {
$jobs = [];
// Create jobs with different priorities
$jobs['low'] = JobPayload::create(new class { public $type = 'low'; }, QueuePriority::low());
$jobs['deferred'] = JobPayload::create(new class { public $type = 'deferred'; }, QueuePriority::deferred());
$jobs['normal'] = JobPayload::create(new class { public $type = 'normal'; }, QueuePriority::normal());
$jobs['high'] = JobPayload::create(new class { public $type = 'high'; }, QueuePriority::high());
$jobs['critical'] = JobPayload::create(new class { public $type = 'critical'; }, QueuePriority::critical());
$jobs['low'] = JobPayload::create(new class () {
public $type = 'low';
}, QueuePriority::low());
$jobs['deferred'] = JobPayload::create(new class () {
public $type = 'deferred';
}, QueuePriority::deferred());
$jobs['normal'] = JobPayload::create(new class () {
public $type = 'normal';
}, QueuePriority::normal());
$jobs['high'] = JobPayload::create(new class () {
public $type = 'high';
}, QueuePriority::high());
$jobs['critical'] = JobPayload::create(new class () {
public $type = 'critical';
}, QueuePriority::critical());
// Push in random order
$this->queue->push($jobs['normal']);
@@ -253,9 +267,15 @@ describe('Queue Priority Processing', function () {
});
it('handles custom priority values correctly', function () {
$customHigh = JobPayload::create(new class { public $id = 'custom_high'; }, new QueuePriority(500));
$customLow = JobPayload::create(new class { public $id = 'custom_low'; }, new QueuePriority(-50));
$standardHigh = JobPayload::create(new class { public $id = 'standard_high'; }, QueuePriority::high());
$customHigh = JobPayload::create(new class () {
public $id = 'custom_high';
}, new QueuePriority(500));
$customLow = JobPayload::create(new class () {
public $id = 'custom_low';
}, new QueuePriority(-50));
$standardHigh = JobPayload::create(new class () {
public $id = 'standard_high';
}, QueuePriority::high());
$this->queue->push($customLow);
$this->queue->push($standardHigh);
@@ -289,7 +309,9 @@ describe('Queue Edge Cases', function () {
});
it('maintains integrity after mixed operations', function () {
$job = new class { public $data = 'test'; };
$job = new class () {
public $data = 'test';
};
// Complex sequence of operations
$this->queue->push(JobPayload::create($job));
@@ -316,8 +338,10 @@ describe('Queue Edge Cases', function () {
// Add 1000 jobs
for ($i = 0; $i < 1000; $i++) {
$job = new class {
public function __construct(public int $id) {}
$job = new class () {
public function __construct(public int $id)
{
}
};
$payload = JobPayload::create(new $job($i), QueuePriority::normal());
$this->queue->push($payload);
@@ -337,4 +361,4 @@ describe('Queue Edge Cases', function () {
$elapsed = microtime(true) - $start;
expect($elapsed)->toBeLessThan(1.0); // Should complete within 1 second
});
});
});

View File

@@ -2,10 +2,9 @@
declare(strict_types=1);
use App\Framework\Queue\Services\ProgressManager;
use App\Framework\Queue\Contracts\JobProgressTrackerInterface;
use App\Framework\Queue\Services\ProgressManager;
use App\Framework\Queue\ValueObjects\JobProgress;
use App\Framework\Core\ValueObjects\Percentage;
beforeEach(function () {
// Mock the progress tracker interface instead of final classes
@@ -62,7 +61,7 @@ it('can create step tracker', function () {
$jobId = 'test-job-123';
$steps = [
['name' => 'step1', 'description' => 'First step'],
['name' => 'step2', 'description' => 'Second step']
['name' => 'step2', 'description' => 'Second step'],
];
$stepTracker = $this->progressManager->createStepTracker($jobId, $steps);
@@ -77,7 +76,7 @@ it('throws exception for empty steps array', function () {
$jobId = 'test-job-123';
$steps = [];
expect(fn() => $this->progressManager->createStepTracker($jobId, $steps))
expect(fn () => $this->progressManager->createStepTracker($jobId, $steps))
->toThrow(\InvalidArgumentException::class, 'Steps array cannot be empty');
});
@@ -87,6 +86,6 @@ it('throws exception for invalid step structure', function () {
['name' => 'step1'], // Missing description
];
expect(fn() => $this->progressManager->createStepTracker($jobId, $steps))
expect(fn () => $this->progressManager->createStepTracker($jobId, $steps))
->toThrow(\InvalidArgumentException::class);
});
});

View File

@@ -34,19 +34,19 @@ describe('JobId Value Object', function () {
});
it('rejects empty JobId', function () {
expect(fn() => JobId::fromString(''))
expect(fn () => JobId::fromString(''))
->toThrow(\InvalidArgumentException::class, 'JobId cannot be empty');
});
it('validates JobId format correctly', function () {
// Valid formats should work
expect(fn() => JobId::fromString('job_12345'))->not->toThrow();
expect(fn() => JobId::fromString('01FXYZ0123456789ABCDEF1234'))->not->toThrow(); // ULID format
expect(fn() => JobId::fromString('simple-id'))->not->toThrow();
expect(fn () => JobId::fromString('job_12345'))->not->toThrow();
expect(fn () => JobId::fromString('01FXYZ0123456789ABCDEF1234'))->not->toThrow(); // ULID format
expect(fn () => JobId::fromString('simple-id'))->not->toThrow();
// Any non-empty string is currently accepted
expect(fn() => JobId::fromString('a'))->not->toThrow();
expect(fn() => JobId::fromString('very-long-job-identifier-12345'))->not->toThrow();
expect(fn () => JobId::fromString('a'))->not->toThrow();
expect(fn () => JobId::fromString('very-long-job-identifier-12345'))->not->toThrow();
});
it('is readonly and immutable', function () {
@@ -207,7 +207,7 @@ describe('JobId Value Object', function () {
$nonUlidJobId = JobId::fromString('not-a-ulid-format');
// This should throw an exception since it's not a valid ULID
expect(fn() => $nonUlidJobId->toUlid())
expect(fn () => $nonUlidJobId->toUlid())
->toThrow();
});
@@ -215,7 +215,7 @@ describe('JobId Value Object', function () {
$nonUlidJobId = JobId::fromString('simple-job-id');
// This should throw an exception since it's not a valid ULID
expect(fn() => $nonUlidJobId->getTimestamp())
expect(fn () => $nonUlidJobId->getTimestamp())
->toThrow();
});
});
@@ -301,7 +301,7 @@ describe('JobId in Queue Context', function () {
$completedJobs[$jobId->toString()] = [
'started_at' => $processingJobs[$jobId->toString()],
'completed_at' => time(),
'status' => 'success'
'status' => 'success',
];
unset($processingJobs[$jobId->toString()]);
@@ -342,4 +342,4 @@ describe('JobId in Queue Context', function () {
expect($normalTime > $urgentTime)->toBeTrue();
});
});
});

View File

@@ -2,29 +2,32 @@
declare(strict_types=1);
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Queue\ValueObjects\JobMetadata;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueuePriority;
use App\Framework\Queue\ValueObjects\JobMetadata;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Retry\Strategies\ExponentialBackoffStrategy;
use App\Framework\Retry\Strategies\FixedDelayStrategy;
describe('JobPayload Value Object', function () {
beforeEach(function () {
$this->simpleJob = new class {
public function handle(): string {
$this->simpleJob = new class () {
public function handle(): string
{
return 'executed';
}
};
$this->complexJob = new class {
$this->complexJob = new class () {
public function __construct(
public string $id = 'test-123',
public array $data = ['key' => 'value']
) {}
) {
}
public function process(): array {
public function process(): array
{
return $this->data;
}
};
@@ -354,19 +357,22 @@ describe('JobPayload Value Object', function () {
});
it('handles complex job objects with dependencies', function () {
$complexJob = new class {
$complexJob = new class () {
public array $config;
public \DateTime $created;
public function __construct() {
public function __construct()
{
$this->config = ['timeout' => 30, 'retries' => 3];
$this->created = new \DateTime();
}
public function getData(): array {
public function getData(): array
{
return [
'config' => $this->config,
'created' => $this->created->format('Y-m-d H:i:s')
'created' => $this->created->format('Y-m-d H:i:s'),
];
}
};
@@ -391,26 +397,30 @@ describe('JobPayload Value Object', function () {
describe('JobPayload Integration Scenarios', function () {
beforeEach(function () {
$this->emailJob = new class {
$this->emailJob = new class () {
public function __construct(
public string $to = 'test@example.com',
public string $subject = 'Test Email',
public string $body = 'Hello World'
) {}
) {
}
public function send(): bool {
public function send(): bool
{
// Simulate email sending
return true;
}
};
$this->reportJob = new class {
$this->reportJob = new class () {
public function __construct(
public array $criteria = ['period' => 'monthly'],
public string $format = 'pdf'
) {}
) {
}
public function generate(): string {
public function generate(): string
{
return "Report generated with format: {$this->format}";
}
};
@@ -442,7 +452,7 @@ describe('JobPayload Integration Scenarios', function () {
$metadata = JobMetadata::create([
'user_id' => 123,
'report_type' => 'financial',
'department' => 'accounting'
'department' => 'accounting',
]);
$customReport = $monthlyReport->withMetadata($metadata);
@@ -481,4 +491,4 @@ describe('JobPayload Integration Scenarios', function () {
expect($rateLimitedPayload->retryStrategy->getMaxAttempts())->toBe(5);
expect($rateLimitedPayload->retryStrategy)->toBeInstanceOf(ExponentialBackoffStrategy::class);
});
});
});

View File

@@ -2,10 +2,10 @@
declare(strict_types=1);
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;
describe('LockKey Value Object', function () {
it('can create lock keys from strings', function () {
@@ -18,18 +18,18 @@ describe('LockKey Value Object', function () {
it('validates lock key constraints', function () {
// Empty key
expect(fn() => LockKey::fromString(''))
expect(fn () => LockKey::fromString(''))
->toThrow(\InvalidArgumentException::class, 'Lock key cannot be empty');
// Too long
expect(fn() => LockKey::fromString(str_repeat('a', 256)))
expect(fn () => LockKey::fromString(str_repeat('a', 256)))
->toThrow(\InvalidArgumentException::class, 'Lock key cannot exceed 255 characters');
// Invalid characters
expect(fn() => LockKey::fromString('invalid@key!'))
expect(fn () => LockKey::fromString('invalid@key!'))
->toThrow(\InvalidArgumentException::class, 'Lock key contains invalid characters');
expect(fn() => LockKey::fromString('key with spaces'))
expect(fn () => LockKey::fromString('key with spaces'))
->toThrow(\InvalidArgumentException::class, 'Lock key contains invalid characters');
});
@@ -40,7 +40,7 @@ describe('LockKey Value Object', function () {
'key.with.dots',
'key123',
'UPPERCASE-key',
'mixed-Key_123.test'
'mixed-Key_123.test',
];
foreach ($validKeys as $key) {
@@ -152,4 +152,4 @@ describe('LockKey Value Object', function () {
expect($rowLock->matches('database.users.*'))->toBeTrue();
expect($rowLock->matches('*.row-123'))->toBeTrue();
});
});
});

View File

@@ -34,7 +34,7 @@ describe('WorkerId Value Object', function () {
});
it('validates worker ID is not empty', function () {
expect(fn() => WorkerId::fromString(''))
expect(fn () => WorkerId::fromString(''))
->toThrow(\InvalidArgumentException::class, 'WorkerId cannot be empty');
});
@@ -64,4 +64,4 @@ describe('WorkerId Value Object', function () {
expect($workerId->jsonSerialize())->toBe($id);
expect(json_encode($workerId))->toBe('"' . $id . '"');
});
});
});

View File

@@ -2,30 +2,33 @@
declare(strict_types=1);
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\Services\WorkerRegistry;
use App\Framework\Queue\Services\WorkerHealthCheckService;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\Services\WorkerHealthCheckService;
use App\Framework\Queue\Services\WorkerRegistry;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;
describe('Worker Management System', function () {
beforeEach(function () {
// Mock database connection for testing
$this->mockConnection = new class {
$this->mockConnection = new class () {
private array $data = [];
private int $lastInsertId = 0;
private int $rowCount = 0;
public function prepare(string $sql): object
{
return new class($sql, $this) {
return new class ($sql, $this) {
public function __construct(
private string $sql,
private object $connection
) {}
) {
}
public function execute(array $params = []): bool
{
@@ -55,6 +58,7 @@ describe('Worker Management System', function () {
$this->connection->data['health_checks'][] = $params;
$this->connection->rowCount = 1;
}
return true;
}
@@ -62,6 +66,7 @@ describe('Worker Management System', function () {
{
if (str_contains($this->sql, 'SELECT * FROM queue_workers WHERE id = :id')) {
$id = func_get_args()[0]['id'] ?? null;
return $this->connection->data['workers'][$id] ?? false;
}
@@ -72,6 +77,7 @@ describe('Worker Management System', function () {
return $worker;
}
}
return false;
}
@@ -79,13 +85,13 @@ describe('Worker Management System', function () {
if (str_contains($this->sql, 'COUNT(*) as total_workers')) {
return [
'total_workers' => count($this->connection->data['workers'] ?? []),
'active_workers' => count(array_filter($this->connection->data['workers'] ?? [], fn($w) => $w['is_active'])),
'healthy_workers' => count(array_filter($this->connection->data['workers'] ?? [], fn($w) => $w['is_active'])),
'active_workers' => count(array_filter($this->connection->data['workers'] ?? [], fn ($w) => $w['is_active'])),
'healthy_workers' => count(array_filter($this->connection->data['workers'] ?? [], fn ($w) => $w['is_active'])),
'total_capacity' => 100,
'current_load' => 50,
'avg_cpu_usage' => 25.5,
'avg_memory_usage' => 1024 * 1024 * 512, // 512MB
'unique_hosts' => 2
'unique_hosts' => 2,
];
}
@@ -103,10 +109,11 @@ describe('Worker Management System', function () {
'total_checks' => 10,
'healthy_count' => 8,
'warning_count' => 2,
'critical_count' => 0
]
'critical_count' => 0,
],
];
}
return [];
}
@@ -124,7 +131,7 @@ describe('Worker Management System', function () {
};
// Mock logger for testing
$this->mockLogger = new class {
$this->mockLogger = new class () {
public array $logs = [];
public function info(string $message, array $context = []): void
@@ -179,7 +186,7 @@ describe('Worker Management System', function () {
});
it('throws exception for empty worker ID', function () {
expect(fn() => WorkerId::fromString(''))
expect(fn () => WorkerId::fromString(''))
->toThrow(\InvalidArgumentException::class, 'WorkerId cannot be empty');
});
@@ -226,7 +233,7 @@ describe('Worker Management System', function () {
});
it('throws exception when no queues provided', function () {
expect(fn() => new Worker(
expect(fn () => new Worker(
id: WorkerId::generate(),
hostname: 'test',
processId: 1234,
@@ -237,7 +244,7 @@ describe('Worker Management System', function () {
});
it('validates job constraints during construction', function () {
$baseWorker = fn($maxJobs, $currentJobs) => new Worker(
$baseWorker = fn ($maxJobs, $currentJobs) => new Worker(
id: WorkerId::generate(),
hostname: 'test',
processId: 1234,
@@ -248,15 +255,15 @@ describe('Worker Management System', function () {
);
// Invalid max jobs
expect(fn() => $baseWorker(0, 0))
expect(fn () => $baseWorker(0, 0))
->toThrow(\InvalidArgumentException::class, 'Max jobs must be greater than 0');
expect(fn() => $baseWorker(-1, 0))
expect(fn () => $baseWorker(-1, 0))
->toThrow(\InvalidArgumentException::class, 'Max jobs must be greater than 0');
// Invalid current jobs
expect(fn() => $baseWorker(5, -1))
expect(fn () => $baseWorker(5, -1))
->toThrow(\InvalidArgumentException::class, 'Current jobs cannot be negative');
expect(fn() => $baseWorker(5, 10))
expect(fn () => $baseWorker(5, 10))
->toThrow(\InvalidArgumentException::class, 'Current jobs cannot exceed max jobs');
});
@@ -525,14 +532,14 @@ describe('Worker Management System', function () {
'hostname' => 'test-host',
'process_id' => 1234,
'queues' => [QueueName::default()],
'max_jobs' => 10
]
'max_jobs' => 10,
],
]);
expect($this->mockLogger->logs)->toContain([
'level' => 'debug',
'message' => 'Worker registered successfully',
'context' => ['worker_id' => $worker->id->toString()]
'context' => ['worker_id' => $worker->id->toString()],
]);
});
@@ -543,8 +550,8 @@ describe('Worker Management System', function () {
$this->mockConnection->setWorkerData([
$workerId->toString() => [
'id' => $workerId->toString(),
'is_active' => 1
]
'is_active' => 1,
],
]);
$this->workerRegistry->deregister($workerId);
@@ -552,7 +559,7 @@ describe('Worker Management System', function () {
expect($this->mockLogger->logs)->toContain([
'level' => 'info',
'message' => 'Deregistering worker',
'context' => ['worker_id' => $workerId->toString()]
'context' => ['worker_id' => $workerId->toString()],
]);
});
@@ -566,14 +573,14 @@ describe('Worker Management System', function () {
$this->mockConnection->setWorkerData([
$workerId->toString() => [
'id' => $workerId->toString(),
'is_active' => 1
]
'is_active' => 1,
],
]);
$this->workerRegistry->updateHeartbeat($workerId, $cpuUsage, $memoryUsage, $currentJobs);
// Should not log warnings when worker found and updated
$warningLogs = array_filter($this->mockLogger->logs, fn($log) => $log['level'] === 'warning');
$warningLogs = array_filter($this->mockLogger->logs, fn ($log) => $log['level'] === 'warning');
expect($warningLogs)->toBeEmpty();
});
@@ -586,7 +593,7 @@ describe('Worker Management System', function () {
$this->workerRegistry->updateHeartbeat($workerId, $cpuUsage, $memoryUsage, 2);
$warningLogs = array_filter($this->mockLogger->logs, fn($log) => $log['level'] === 'warning');
$warningLogs = array_filter($this->mockLogger->logs, fn ($log) => $log['level'] === 'warning');
expect($warningLogs)->not()->toBeEmpty();
});
@@ -605,7 +612,7 @@ describe('Worker Management System', function () {
'registered_at' => '2024-01-01 12:00:00',
'last_heartbeat' => '2024-01-01 12:05:00',
'capabilities' => json_encode(['pdf']),
'version' => '1.0.0'
'version' => '1.0.0',
];
$this->mockConnection->setWorkerData([$workerId->toString() => $workerData]);
@@ -651,7 +658,7 @@ describe('Worker Management System', function () {
expect($this->mockLogger->logs)->toContain([
'level' => 'info',
'message' => 'Starting cleanup of inactive workers',
'context' => ['inactive_minutes' => 5]
'context' => ['inactive_minutes' => 5],
]);
});
});
@@ -777,8 +784,8 @@ describe('Worker Management System', function () {
'registered_at' => '2024-01-01 12:00:00',
'last_heartbeat' => date('Y-m-d H:i:s'),
'capabilities' => json_encode([]),
'version' => '1.0.0'
]
'version' => '1.0.0',
],
]);
$report = $this->healthCheckService->generateSystemHealthReport();
@@ -802,8 +809,8 @@ describe('Worker Management System', function () {
'message' => 'Health check cleanup completed',
'context' => [
'deleted_records' => $deletedCount,
'retention_days' => 7.0
]
'retention_days' => 7.0,
],
]);
});
});
@@ -857,7 +864,7 @@ describe('Worker Management System', function () {
);
// Should log warning
$warningLogs = array_filter($this->mockLogger->logs, fn($log) => $log['level'] === 'warning');
$warningLogs = array_filter($this->mockLogger->logs, fn ($log) => $log['level'] === 'warning');
expect($warningLogs)->not()->toBeEmpty();
});
@@ -890,4 +897,4 @@ describe('Worker Management System', function () {
expect($inactiveWorker->isActive)->toBeFalse(); // New instance inactive
});
});
});
});