Files
michaelschiemer/tests/Framework/Queue/Entities/WorkerTest.php
Michael Schiemer fc3d7e6357 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.
2025-10-25 19:18:37 +02:00

421 lines
15 KiB
PHP

<?php
declare(strict_types=1);
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(),
];
$this->capabilities = ['email', 'pdf-generation', 'image-processing'];
});
it('can register a new worker with valid parameters', function () {
$worker = Worker::register(
hostname: 'app-server-1',
processId: 1001,
queues: $this->queues,
maxJobs: 10,
capabilities: $this->capabilities
);
expect($worker->hostname)->toBe('app-server-1');
expect($worker->processId)->toBe(1001);
expect($worker->queues)->toHaveCount(2);
expect($worker->maxJobs)->toBe(10);
expect($worker->currentJobs)->toBe(0);
expect($worker->isActive)->toBeTrue();
expect($worker->capabilities)->toBe($this->capabilities);
expect($worker->registeredAt)->toBeInstanceOf(\DateTimeImmutable::class);
expect($worker->lastHeartbeat)->toBeInstanceOf(\DateTimeImmutable::class);
});
it('validates worker construction constraints', function () {
// Empty queues
expect(fn () => Worker::register(
hostname: 'test-host',
processId: 1001,
queues: [], // Invalid
maxJobs: 10
))->toThrow(\InvalidArgumentException::class, 'Worker must handle at least one queue');
// Invalid max jobs
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(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: -5 // Invalid
))->toThrow(\InvalidArgumentException::class, 'Max jobs must be greater than 0');
});
it('validates current jobs constraints', function () {
$worker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 5
);
// Negative current jobs should fail during construction
expect(fn () => new Worker(
id: $worker->id,
hostname: $worker->hostname,
processId: $worker->processId,
queues: $worker->queues,
maxJobs: $worker->maxJobs,
registeredAt: $worker->registeredAt,
currentJobs: -1 // Invalid
))->toThrow(\InvalidArgumentException::class, 'Current jobs cannot be negative');
// Current jobs exceeding max jobs should fail
expect(fn () => new Worker(
id: $worker->id,
hostname: $worker->hostname,
processId: $worker->processId,
queues: $worker->queues,
maxJobs: $worker->maxJobs,
registeredAt: $worker->registeredAt,
currentJobs: 10 // Exceeds maxJobs of 5
))->toThrow(\InvalidArgumentException::class, 'Current jobs cannot exceed max jobs');
});
it('can update heartbeat with resource usage', function () {
$worker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 10
);
$updatedWorker = $worker->updateHeartbeat(
cpuUsage: new Percentage(45),
memoryUsage: Byte::fromMegabytes(800),
currentJobs: 3
);
expect($updatedWorker->cpuUsage->getValue())->toBe(45.0);
expect($updatedWorker->memoryUsage->toMegabytes())->toBe(800.0);
expect($updatedWorker->currentJobs)->toBe(3);
expect($updatedWorker->isActive)->toBeTrue();
expect($updatedWorker->lastHeartbeat)->toBeInstanceOf(\DateTimeImmutable::class);
// Original worker should be unchanged (immutable)
expect($worker->cpuUsage->getValue())->toBe(0.0);
expect($worker->currentJobs)->toBe(0);
});
it('can mark worker as inactive', function () {
$worker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 10
);
$inactiveWorker = $worker->markInactive();
expect($inactiveWorker->isActive)->toBeFalse();
expect($worker->isActive)->toBeTrue(); // Original unchanged
});
it('correctly determines worker availability', function () {
$worker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 5
);
// Fresh worker should be available
expect($worker->isAvailableForJobs())->toBeTrue();
// Worker at capacity should not be available
$atCapacityWorker = $worker->updateHeartbeat(
new Percentage(30),
Byte::fromMegabytes(500),
5 // At max capacity
);
expect($atCapacityWorker->isAvailableForJobs())->toBeFalse();
// Inactive worker should not be available
$inactiveWorker = $worker->markInactive();
expect($inactiveWorker->isAvailableForJobs())->toBeFalse();
// Unhealthy worker should not be available
$unhealthyWorker = $worker->updateHeartbeat(
new Percentage(95), // Critical CPU
Byte::fromGigabytes(3), // Over memory limit
2
);
expect($unhealthyWorker->isAvailableForJobs())->toBeFalse();
});
it('can check if worker handles specific queues', function () {
$worker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: [
QueueName::defaultQueue(),
QueueName::emailQueue(),
],
maxJobs: 10
);
expect($worker->handlesQueue(QueueName::defaultQueue()))->toBeTrue();
expect($worker->handlesQueue(QueueName::emailQueue()))->toBeTrue();
expect($worker->handlesQueue(QueueName::fromString('unknown-queue')))->toBeFalse();
});
it('correctly determines worker health status', function () {
$worker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 10
);
// Fresh worker should be healthy
expect($worker->isHealthy())->toBeTrue();
// Worker with high CPU should be unhealthy
$highCpuWorker = $worker->updateHeartbeat(
new Percentage(95), // Over 90% threshold
Byte::fromMegabytes(500),
3
);
expect($highCpuWorker->isHealthy())->toBeFalse();
// Worker with excessive memory should be unhealthy
$highMemoryWorker = $worker->updateHeartbeat(
new Percentage(30),
Byte::fromGigabytes(3), // Over 2GB threshold
3
);
expect($highMemoryWorker->isHealthy())->toBeFalse();
// Inactive worker should be unhealthy
$inactiveWorker = $worker->markInactive();
expect($inactiveWorker->isHealthy())->toBeFalse();
// Worker with stale heartbeat should be unhealthy
$staleWorker = new Worker(
id: $worker->id,
hostname: $worker->hostname,
processId: $worker->processId,
queues: $worker->queues,
maxJobs: $worker->maxJobs,
registeredAt: $worker->registeredAt,
lastHeartbeat: new \DateTimeImmutable('-2 minutes'), // Stale
isActive: true,
cpuUsage: new Percentage(30),
memoryUsage: Byte::fromMegabytes(500),
currentJobs: 3
);
expect($staleWorker->isHealthy())->toBeFalse();
// Worker with no heartbeat should be unhealthy
$noHeartbeatWorker = new Worker(
id: $worker->id,
hostname: $worker->hostname,
processId: $worker->processId,
queues: $worker->queues,
maxJobs: $worker->maxJobs,
registeredAt: $worker->registeredAt,
lastHeartbeat: null, // No heartbeat
isActive: true
);
expect($noHeartbeatWorker->isHealthy())->toBeFalse();
});
it('calculates load percentage correctly', function () {
$worker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 10
);
// Test job-based load
$jobLoadWorker = $worker->updateHeartbeat(
new Percentage(20), // 20% CPU
Byte::fromMegabytes(500),
3 // 3/10 = 30% job load
);
expect($jobLoadWorker->getLoadPercentage()->getValue())->toBe(30.0); // Higher of 20% CPU or 30% jobs
// Test CPU-based load
$cpuLoadWorker = $worker->updateHeartbeat(
new Percentage(75), // 75% CPU
Byte::fromMegabytes(500),
2 // 2/10 = 20% job load
);
expect($cpuLoadWorker->getLoadPercentage()->getValue())->toBe(75.0); // Higher of 75% CPU or 20% jobs
// Test worker with zero max jobs
$zeroJobsWorker = new Worker(
id: $worker->id,
hostname: $worker->hostname,
processId: $worker->processId,
queues: $worker->queues,
maxJobs: 0, // Special case
registeredAt: $worker->registeredAt,
lastHeartbeat: new \DateTimeImmutable(),
isActive: true,
currentJobs: 0
);
expect($zeroJobsWorker->getLoadPercentage()->getValue())->toBe(100.0);
});
it('can check worker capabilities', function () {
$worker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 10,
capabilities: ['email', 'pdf-generation', 'image-processing']
);
expect($worker->hasCapability('email'))->toBeTrue();
expect($worker->hasCapability('pdf-generation'))->toBeTrue();
expect($worker->hasCapability('image-processing'))->toBeTrue();
expect($worker->hasCapability('video-processing'))->toBeFalse();
expect($worker->hasCapability(''))->toBeFalse();
});
it('provides comprehensive monitoring data', function () {
$worker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 10,
capabilities: $this->capabilities
)->updateHeartbeat(
new Percentage(45),
Byte::fromMegabytes(800),
3
);
$monitoringData = $worker->toMonitoringArray();
expect($monitoringData)->toHaveKey('id');
expect($monitoringData)->toHaveKey('hostname');
expect($monitoringData)->toHaveKey('process_id');
expect($monitoringData)->toHaveKey('queues');
expect($monitoringData)->toHaveKey('max_jobs');
expect($monitoringData)->toHaveKey('current_jobs');
expect($monitoringData)->toHaveKey('is_active');
expect($monitoringData)->toHaveKey('is_healthy');
expect($monitoringData)->toHaveKey('is_available');
expect($monitoringData)->toHaveKey('load_percentage');
expect($monitoringData)->toHaveKey('cpu_usage');
expect($monitoringData)->toHaveKey('memory_usage_mb');
expect($monitoringData)->toHaveKey('capabilities');
expect($monitoringData['hostname'])->toBe('test-host');
expect($monitoringData['process_id'])->toBe(1001);
expect($monitoringData['max_jobs'])->toBe(10);
expect($monitoringData['current_jobs'])->toBe(3);
expect($monitoringData['is_active'])->toBeTrue();
expect($monitoringData['load_percentage'])->toBe(45.0);
expect($monitoringData['cpu_usage'])->toBe(45.0);
expect($monitoringData['memory_usage_mb'])->toBe(800.0);
expect($monitoringData['capabilities'])->toBe($this->capabilities);
});
it('can be serialized to array for persistence', function () {
$worker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 10,
capabilities: $this->capabilities
);
$array = $worker->toArray();
expect($array)->toHaveKey('id');
expect($array)->toHaveKey('hostname');
expect($array)->toHaveKey('process_id');
expect($array)->toHaveKey('queues');
expect($array)->toHaveKey('max_jobs');
expect($array)->toHaveKey('current_jobs');
expect($array)->toHaveKey('is_active');
expect($array)->toHaveKey('cpu_usage');
expect($array)->toHaveKey('memory_usage_bytes');
expect($array)->toHaveKey('registered_at');
expect($array)->toHaveKey('last_heartbeat');
expect($array)->toHaveKey('capabilities');
expect($array)->toHaveKey('version');
// Queues should be JSON encoded
$queues = json_decode($array['queues'], true);
expect($queues)->toBeArray();
expect($queues)->toHaveCount(2);
// Capabilities should be JSON encoded
$capabilities = json_decode($array['capabilities'], true);
expect($capabilities)->toBe($this->capabilities);
});
it('can be reconstructed from array data', function () {
$originalWorker = Worker::register(
hostname: 'test-host',
processId: 1001,
queues: $this->queues,
maxJobs: 10,
capabilities: $this->capabilities
);
$array = $originalWorker->toArray();
$reconstructedWorker = Worker::fromArray($array);
expect($reconstructedWorker->hostname)->toBe($originalWorker->hostname);
expect($reconstructedWorker->processId)->toBe($originalWorker->processId);
expect($reconstructedWorker->maxJobs)->toBe($originalWorker->maxJobs);
expect($reconstructedWorker->currentJobs)->toBe($originalWorker->currentJobs);
expect($reconstructedWorker->isActive)->toBe($originalWorker->isActive);
expect($reconstructedWorker->capabilities)->toBe($originalWorker->capabilities);
expect($reconstructedWorker->version)->toBe($originalWorker->version);
});
it('handles edge cases in array reconstruction', function () {
$minimalData = [
'id' => 'test-worker-id',
'hostname' => 'test-host',
'process_id' => 1001,
'queues' => '["default"]',
'max_jobs' => 5,
'registered_at' => '2024-01-01 12:00:00',
'is_active' => 1,
];
$worker = Worker::fromArray($minimalData);
expect($worker->hostname)->toBe('test-host');
expect($worker->processId)->toBe(1001);
expect($worker->maxJobs)->toBe(5);
expect($worker->currentJobs)->toBe(0); // Default value
expect($worker->isActive)->toBeTrue();
expect($worker->lastHeartbeat)->toBeNull();
expect($worker->cpuUsage->getValue())->toBe(0.0);
expect($worker->memoryUsage->toBytes())->toBe(0);
expect($worker->capabilities)->toBe([]);
expect($worker->version)->toBe('1.0.0');
});
});