- 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.
421 lines
15 KiB
PHP
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');
|
|
});
|
|
});
|