- 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.
236 lines
7.5 KiB
PHP
236 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Application\LiveComponents\Dashboard\WorkerHealthComponent;
|
|
use App\Application\LiveComponents\Dashboard\WorkerHealthState;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
use App\Framework\Queue\Services\WorkerRegistry;
|
|
use App\Framework\Queue\Entities\Worker;
|
|
use App\Framework\Queue\ValueObjects\WorkerId;
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
|
|
describe('WorkerHealthComponent', function () {
|
|
beforeEach(function () {
|
|
$this->workerRegistry = Mockery::mock(WorkerRegistry::class);
|
|
$this->componentId = ComponentId::create('worker-health', 'test');
|
|
$this->initialState = WorkerHealthState::empty();
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
it('polls and updates state with worker health data', function () {
|
|
$now = Timestamp::now();
|
|
$oneMinuteAgo = $now->sub(Duration::fromSeconds(60));
|
|
|
|
$worker1 = new Worker(
|
|
id: WorkerId::generate(),
|
|
hostname: 'server-01',
|
|
processId: 12345,
|
|
startedAt: $oneMinuteAgo,
|
|
lastHeartbeat: $now,
|
|
status: 'active',
|
|
currentJobs: 2,
|
|
maxJobs: 10,
|
|
memoryUsageMb: 128.5,
|
|
cpuUsage: 45.2
|
|
);
|
|
|
|
$worker2 = new Worker(
|
|
id: WorkerId::generate(),
|
|
hostname: 'server-02',
|
|
processId: 67890,
|
|
startedAt: $oneMinuteAgo,
|
|
lastHeartbeat: $now->sub(Duration::fromMinutes(5)), // Unhealthy - old heartbeat
|
|
status: 'active',
|
|
currentJobs: 0,
|
|
maxJobs: 10,
|
|
memoryUsageMb: 64.0,
|
|
cpuUsage: 98.5 // Unhealthy - high CPU
|
|
);
|
|
|
|
$this->workerRegistry->shouldReceive('findActiveWorkers')
|
|
->once()
|
|
->andReturn([$worker1, $worker2]);
|
|
|
|
$component = new WorkerHealthComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
workerRegistry: $this->workerRegistry
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState)->toBeInstanceOf(WorkerHealthState::class);
|
|
expect($newState->activeWorkers)->toBe(2);
|
|
expect($newState->totalWorkers)->toBe(2);
|
|
expect($newState->jobsInProgress)->toBe(2); // Only worker1 has jobs
|
|
expect($newState->workerDetails)->toHaveCount(2);
|
|
|
|
// Worker 1 should be healthy
|
|
expect($newState->workerDetails[0]['healthy'])->toBeTrue();
|
|
expect($newState->workerDetails[0]['hostname'])->toBe('server-01');
|
|
|
|
// Worker 2 should be unhealthy
|
|
expect($newState->workerDetails[1]['healthy'])->toBeFalse();
|
|
});
|
|
|
|
it('identifies healthy workers correctly', function () {
|
|
$now = Timestamp::now();
|
|
|
|
$healthyWorker = new Worker(
|
|
id: WorkerId::generate(),
|
|
hostname: 'healthy-server',
|
|
processId: 11111,
|
|
startedAt: $now->sub(Duration::fromMinutes(5)),
|
|
lastHeartbeat: $now->sub(Duration::fromSeconds(30)), // Recent heartbeat
|
|
status: 'active',
|
|
currentJobs: 5,
|
|
maxJobs: 10,
|
|
memoryUsageMb: 100.0,
|
|
cpuUsage: 50.0 // Normal CPU
|
|
);
|
|
|
|
$this->workerRegistry->shouldReceive('findActiveWorkers')
|
|
->once()
|
|
->andReturn([$healthyWorker]);
|
|
|
|
$component = new WorkerHealthComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
workerRegistry: $this->workerRegistry
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState->workerDetails[0]['healthy'])->toBeTrue();
|
|
});
|
|
|
|
it('identifies unhealthy workers by stale heartbeat', function () {
|
|
$now = Timestamp::now();
|
|
|
|
$staleWorker = new Worker(
|
|
id: WorkerId::generate(),
|
|
hostname: 'stale-server',
|
|
processId: 22222,
|
|
startedAt: $now->sub(Duration::fromMinutes(10)),
|
|
lastHeartbeat: $now->sub(Duration::fromMinutes(3)), // Stale heartbeat
|
|
status: 'active',
|
|
currentJobs: 2,
|
|
maxJobs: 10,
|
|
memoryUsageMb: 80.0,
|
|
cpuUsage: 30.0
|
|
);
|
|
|
|
$this->workerRegistry->shouldReceive('findActiveWorkers')
|
|
->once()
|
|
->andReturn([$staleWorker]);
|
|
|
|
$component = new WorkerHealthComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
workerRegistry: $this->workerRegistry
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState->workerDetails[0]['healthy'])->toBeFalse();
|
|
});
|
|
|
|
it('identifies unhealthy workers by high CPU', function () {
|
|
$now = Timestamp::now();
|
|
|
|
$highCpuWorker = new Worker(
|
|
id: WorkerId::generate(),
|
|
hostname: 'high-cpu-server',
|
|
processId: 33333,
|
|
startedAt: $now->sub(Duration::fromMinutes(5)),
|
|
lastHeartbeat: $now, // Recent heartbeat
|
|
status: 'active',
|
|
currentJobs: 8,
|
|
maxJobs: 10,
|
|
memoryUsageMb: 200.0,
|
|
cpuUsage: 96.5 // High CPU
|
|
);
|
|
|
|
$this->workerRegistry->shouldReceive('findActiveWorkers')
|
|
->once()
|
|
->andReturn([$highCpuWorker]);
|
|
|
|
$component = new WorkerHealthComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
workerRegistry: $this->workerRegistry
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState->workerDetails[0]['healthy'])->toBeFalse();
|
|
});
|
|
|
|
it('has correct poll interval', function () {
|
|
$component = new WorkerHealthComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
workerRegistry: $this->workerRegistry
|
|
);
|
|
|
|
expect($component->getPollInterval())->toBe(5000);
|
|
});
|
|
|
|
it('returns correct render data', function () {
|
|
$workerDetails = [
|
|
[
|
|
'id' => 'worker-1',
|
|
'hostname' => 'server-01',
|
|
'healthy' => true,
|
|
'jobs' => 5,
|
|
],
|
|
];
|
|
|
|
$state = new WorkerHealthState(
|
|
activeWorkers: 1,
|
|
totalWorkers: 1,
|
|
jobsInProgress: 5,
|
|
workerDetails: $workerDetails,
|
|
lastUpdated: '2024-01-15 12:00:00'
|
|
);
|
|
|
|
$component = new WorkerHealthComponent(
|
|
id: $this->componentId,
|
|
state: $state,
|
|
workerRegistry: $this->workerRegistry
|
|
);
|
|
|
|
$renderData = $component->getRenderData();
|
|
|
|
expect($renderData->templatePath)->toBe('livecomponent-worker-health');
|
|
expect($renderData->data)->toHaveKey('componentId');
|
|
expect($renderData->data)->toHaveKey('pollInterval');
|
|
expect($renderData->data['pollInterval'])->toBe(5000);
|
|
expect($renderData->data['activeWorkers'])->toBe(1);
|
|
expect($renderData->data['jobsInProgress'])->toBe(5);
|
|
});
|
|
|
|
it('handles no active workers', function () {
|
|
$this->workerRegistry->shouldReceive('findActiveWorkers')
|
|
->once()
|
|
->andReturn([]);
|
|
|
|
$component = new WorkerHealthComponent(
|
|
id: $this->componentId,
|
|
state: $this->initialState,
|
|
workerRegistry: $this->workerRegistry
|
|
);
|
|
|
|
$newState = $component->poll();
|
|
|
|
expect($newState->activeWorkers)->toBe(0);
|
|
expect($newState->totalWorkers)->toBe(0);
|
|
expect($newState->jobsInProgress)->toBe(0);
|
|
expect($newState->workerDetails)->toBe([]);
|
|
});
|
|
});
|