Files
michaelschiemer/tests/Framework/Queue/WorkerManagementTest.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

901 lines
37 KiB
PHP

<?php
declare(strict_types=1);
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 () {
private array $data = [];
private int $lastInsertId = 0;
private int $rowCount = 0;
public function prepare(string $sql): object
{
return new class ($sql, $this) {
public function __construct(
private string $sql,
private object $connection
) {
}
public function execute(array $params = []): bool
{
// Simulate different SQL operations
if (str_contains($this->sql, 'INSERT INTO queue_workers')) {
$this->connection->rowCount = 1;
$this->connection->data['workers'][$params['id']] = $params;
} elseif (str_contains($this->sql, 'UPDATE queue_workers')) {
if (str_contains($this->sql, 'is_active = 0')) {
// Deregister operation
if (isset($this->connection->data['workers'][$params['id']])) {
$this->connection->data['workers'][$params['id']]['is_active'] = 0;
$this->connection->rowCount = 1;
}
} else {
// Heartbeat update
if (isset($this->connection->data['workers'][$params['id']])) {
$worker = &$this->connection->data['workers'][$params['id']];
$worker['cpu_usage'] = $params['cpu_usage'];
$worker['memory_usage_bytes'] = $params['memory_usage_bytes'];
$worker['current_jobs'] = $params['current_jobs'];
$worker['last_heartbeat'] = date('Y-m-d H:i:s');
$this->connection->rowCount = 1;
}
}
} elseif (str_contains($this->sql, 'INSERT INTO worker_health_checks')) {
$this->connection->data['health_checks'][] = $params;
$this->connection->rowCount = 1;
}
return true;
}
public function fetch(): array|false
{
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;
}
if (str_contains($this->sql, 'SELECT * FROM queue_workers') && str_contains($this->sql, 'is_active = 1')) {
// Return first active worker for testing
foreach ($this->connection->data['workers'] ?? [] as $worker) {
if ($worker['is_active']) {
return $worker;
}
}
return false;
}
// Statistics query
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'])),
'total_capacity' => 100,
'current_load' => 50,
'avg_cpu_usage' => 25.5,
'avg_memory_usage' => 1024 * 1024 * 512, // 512MB
'unique_hosts' => 2,
];
}
return false;
}
public function fetchAll(): array
{
if (str_contains($this->sql, 'worker_health_checks') && str_contains($this->sql, 'GROUP BY')) {
return [
[
'check_date' => '2024-01-01',
'check_hour' => '14',
'avg_score' => 85.5,
'total_checks' => 10,
'healthy_count' => 8,
'warning_count' => 2,
'critical_count' => 0,
],
];
}
return [];
}
public function rowCount(): int
{
return $this->connection->rowCount;
}
};
}
public function setWorkerData(array $workers): void
{
$this->data['workers'] = $workers;
}
};
// Mock logger for testing
$this->mockLogger = new class () {
public array $logs = [];
public function info(string $message, array $context = []): void
{
$this->logs[] = ['level' => 'info', 'message' => $message, 'context' => $context];
}
public function debug(string $message, array $context = []): void
{
$this->logs[] = ['level' => 'debug', 'message' => $message, 'context' => $context];
}
public function error(string $message, array $context = []): void
{
$this->logs[] = ['level' => 'error', 'message' => $message, 'context' => $context];
}
public function warning(string $message, array $context = []): void
{
$this->logs[] = ['level' => 'warning', 'message' => $message, 'context' => $context];
}
};
$this->workerRegistry = new WorkerRegistry($this->mockConnection, $this->mockLogger);
$this->healthCheckService = new WorkerHealthCheckService($this->workerRegistry, $this->mockConnection, $this->mockLogger);
});
describe('WorkerId Value Object', function () {
it('can generate unique worker IDs', function () {
$id1 = WorkerId::generate();
$id2 = WorkerId::generate();
expect($id1->toString())->not()->toBe($id2->toString());
expect($id1->getValue())->not()->toBeEmpty();
});
it('can create deterministic IDs for host and process', function () {
$id1 = WorkerId::forHost('localhost', 1234);
$id2 = WorkerId::forHost('localhost', 1234);
expect($id1->toString())->toBe($id2->toString());
expect($id1->getValue())->toHaveLength(16);
});
it('can create from string and validate', function () {
$original = 'worker_123_test';
$id = WorkerId::fromString($original);
expect($id->toString())->toBe($original);
expect($id->getValue())->toBe($original);
expect((string) $id)->toBe($original);
});
it('throws exception for empty worker ID', function () {
expect(fn () => WorkerId::fromString(''))
->toThrow(\InvalidArgumentException::class, 'WorkerId cannot be empty');
});
it('can compare worker IDs for equality', function () {
$id1 = WorkerId::fromString('worker_123');
$id2 = WorkerId::fromString('worker_123');
$id3 = WorkerId::fromString('worker_456');
expect($id1->equals($id2))->toBeTrue();
expect($id1->equals($id3))->toBeFalse();
});
it('supports JSON serialization', function () {
$id = WorkerId::fromString('worker_test_123');
$json = json_encode($id);
expect($json)->toBe('"worker_test_123"');
});
});
describe('Worker Entity', function () {
it('can register a new worker with valid parameters', function () {
$queues = [QueueName::default(), QueueName::high()];
$capabilities = ['pdf_processing', 'email_sending'];
$worker = Worker::register(
hostname: 'worker-01',
processId: 1234,
queues: $queues,
maxJobs: 5,
capabilities: $capabilities
);
expect($worker->hostname)->toBe('worker-01');
expect($worker->processId)->toBe(1234);
expect($worker->queues)->toBe($queues);
expect($worker->maxJobs)->toBe(5);
expect($worker->isActive)->toBeTrue();
expect($worker->capabilities)->toBe($capabilities);
expect($worker->currentJobs)->toBe(0);
expect($worker->version)->toBe('1.0.0');
expect($worker->registeredAt)->toBeInstanceOf(\DateTimeImmutable::class);
expect($worker->lastHeartbeat)->toBeInstanceOf(\DateTimeImmutable::class);
});
it('throws exception when no queues provided', function () {
expect(fn () => new Worker(
id: WorkerId::generate(),
hostname: 'test',
processId: 1234,
queues: [], // Empty queues
maxJobs: 5,
registeredAt: new \DateTimeImmutable()
))->toThrow(\InvalidArgumentException::class, 'Worker must handle at least one queue');
});
it('validates job constraints during construction', function () {
$baseWorker = fn ($maxJobs, $currentJobs) => new Worker(
id: WorkerId::generate(),
hostname: 'test',
processId: 1234,
queues: [QueueName::default()],
maxJobs: $maxJobs,
registeredAt: new \DateTimeImmutable(),
currentJobs: $currentJobs
);
// Invalid max jobs
expect(fn () => $baseWorker(0, 0))
->toThrow(\InvalidArgumentException::class, 'Max jobs must be greater than 0');
expect(fn () => $baseWorker(-1, 0))
->toThrow(\InvalidArgumentException::class, 'Max jobs must be greater than 0');
// Invalid current jobs
expect(fn () => $baseWorker(5, -1))
->toThrow(\InvalidArgumentException::class, 'Current jobs cannot be negative');
expect(fn () => $baseWorker(5, 10))
->toThrow(\InvalidArgumentException::class, 'Current jobs cannot exceed max jobs');
});
it('can update heartbeat with new metrics', function () {
$worker = Worker::register('worker-01', 1234, [QueueName::default()]);
$newCpuUsage = new Percentage(75.5);
$newMemoryUsage = Byte::fromMegabytes(512);
$newCurrentJobs = 3;
$updatedWorker = $worker->updateHeartbeat($newCpuUsage, $newMemoryUsage, $newCurrentJobs);
expect($updatedWorker->cpuUsage)->toBe($newCpuUsage);
expect($updatedWorker->memoryUsage)->toBe($newMemoryUsage);
expect($updatedWorker->currentJobs)->toBe($newCurrentJobs);
expect($updatedWorker->isActive)->toBeTrue();
expect($updatedWorker->lastHeartbeat->getTimestamp())->toBeGreaterThan($worker->lastHeartbeat->getTimestamp());
});
it('can mark worker as inactive', function () {
$worker = Worker::register('worker-01', 1234, [QueueName::default()]);
$inactiveWorker = $worker->markInactive();
expect($inactiveWorker->isActive)->toBeFalse();
expect($inactiveWorker->id)->toBe($worker->id);
expect($inactiveWorker->hostname)->toBe($worker->hostname);
});
it('correctly determines worker availability for jobs', function () {
$queues = [QueueName::default()];
// Healthy active worker with capacity
$healthyWorker = new Worker(
id: WorkerId::generate(),
hostname: 'healthy-worker',
processId: 1234,
queues: $queues,
maxJobs: 5,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable(),
isActive: true,
cpuUsage: new Percentage(50),
memoryUsage: Byte::fromMegabytes(500),
currentJobs: 2
);
expect($healthyWorker->isAvailableForJobs())->toBeTrue();
// Inactive worker
$inactiveWorker = $healthyWorker->markInactive();
expect($inactiveWorker->isAvailableForJobs())->toBeFalse();
// Worker at max capacity
$maxCapacityWorker = new Worker(
id: WorkerId::generate(),
hostname: 'maxed-worker',
processId: 1234,
queues: $queues,
maxJobs: 5,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable(),
isActive: true,
currentJobs: 5 // At max capacity
);
expect($maxCapacityWorker->isAvailableForJobs())->toBeFalse();
// Unhealthy worker (high CPU)
$unhealthyWorker = new Worker(
id: WorkerId::generate(),
hostname: 'unhealthy-worker',
processId: 1234,
queues: $queues,
maxJobs: 5,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable(),
isActive: true,
cpuUsage: new Percentage(95), // Too high
currentJobs: 2
);
expect($unhealthyWorker->isAvailableForJobs())->toBeFalse();
});
it('can check queue handling capability', function () {
$defaultQueue = QueueName::default();
$highQueue = QueueName::high();
$lowQueue = QueueName::low();
$worker = Worker::register('worker-01', 1234, [$defaultQueue, $highQueue]);
expect($worker->handlesQueue($defaultQueue))->toBeTrue();
expect($worker->handlesQueue($highQueue))->toBeTrue();
expect($worker->handlesQueue($lowQueue))->toBeFalse();
});
it('determines health status based on multiple factors', function () {
$queues = [QueueName::default()];
// Healthy worker
$healthyWorker = new Worker(
id: WorkerId::generate(),
hostname: 'healthy',
processId: 1234,
queues: $queues,
maxJobs: 5,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable(), // Recent heartbeat
isActive: true,
cpuUsage: new Percentage(50),
memoryUsage: Byte::fromMegabytes(500) // 500MB < 2GB limit
);
expect($healthyWorker->isHealthy())->toBeTrue();
// Worker with old heartbeat
$staleWorker = new Worker(
id: WorkerId::generate(),
hostname: 'stale',
processId: 1234,
queues: $queues,
maxJobs: 5,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable('-2 minutes'), // Too old
isActive: true
);
expect($staleWorker->isHealthy())->toBeFalse();
// Worker with high CPU
$highCpuWorker = new Worker(
id: WorkerId::generate(),
hostname: 'high-cpu',
processId: 1234,
queues: $queues,
maxJobs: 5,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable(),
isActive: true,
cpuUsage: new Percentage(95) // Too high
);
expect($highCpuWorker->isHealthy())->toBeFalse();
// Worker with high memory
$highMemoryWorker = new Worker(
id: WorkerId::generate(),
hostname: 'high-memory',
processId: 1234,
queues: $queues,
maxJobs: 5,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable(),
isActive: true,
memoryUsage: Byte::fromGigabytes(3) // > 2GB limit
);
expect($highMemoryWorker->isHealthy())->toBeFalse();
});
it('calculates load percentage correctly', function () {
$queues = [QueueName::default()];
// Job load higher than CPU load
$jobLoadWorker = new Worker(
id: WorkerId::generate(),
hostname: 'job-load',
processId: 1234,
queues: $queues,
maxJobs: 10,
registeredAt: new \DateTimeImmutable(),
currentJobs: 8, // 80% job load
cpuUsage: new Percentage(30) // 30% CPU load
);
expect($jobLoadWorker->getLoadPercentage()->getValue())->toBe(80.0);
// CPU load higher than job load
$cpuLoadWorker = new Worker(
id: WorkerId::generate(),
hostname: 'cpu-load',
processId: 1234,
queues: $queues,
maxJobs: 10,
registeredAt: new \DateTimeImmutable(),
currentJobs: 3, // 30% job load
cpuUsage: new Percentage(70) // 70% CPU load
);
expect($cpuLoadWorker->getLoadPercentage()->getValue())->toBe(70.0);
// Zero max jobs edge case
$zeroMaxWorker = new Worker(
id: WorkerId::generate(),
hostname: 'zero-max',
processId: 1234,
queues: $queues,
maxJobs: 1,
registeredAt: new \DateTimeImmutable(),
currentJobs: 1 // 100% job load
);
expect($zeroMaxWorker->getLoadPercentage()->getValue())->toBe(100.0);
});
it('can check capabilities', function () {
$worker = Worker::register('worker-01', 1234, [QueueName::default()], 5, ['pdf', 'email', 'resize']);
expect($worker->hasCapability('pdf'))->toBeTrue();
expect($worker->hasCapability('email'))->toBeTrue();
expect($worker->hasCapability('video'))->toBeFalse();
});
it('can convert to monitoring array format', function () {
$worker = Worker::register('worker-01', 1234, [QueueName::default()], 5, ['pdf']);
$monitoring = $worker->toMonitoringArray();
expect($monitoring)->toHaveKey('id');
expect($monitoring)->toHaveKey('hostname');
expect($monitoring)->toHaveKey('process_id');
expect($monitoring)->toHaveKey('queues');
expect($monitoring)->toHaveKey('is_healthy');
expect($monitoring)->toHaveKey('is_available');
expect($monitoring)->toHaveKey('load_percentage');
expect($monitoring)->toHaveKey('capabilities');
expect($monitoring['hostname'])->toBe('worker-01');
expect($monitoring['process_id'])->toBe(1234);
expect($monitoring['capabilities'])->toBe(['pdf']);
});
it('can serialize to and from array', function () {
$queues = [QueueName::default(), QueueName::high()];
$capabilities = ['pdf', 'email'];
$worker = Worker::register('worker-01', 1234, $queues, 5, $capabilities);
$array = $worker->toArray();
expect($array)->toHaveKey('id');
expect($array)->toHaveKey('hostname');
expect($array)->toHaveKey('queues');
expect($array)->toHaveKey('capabilities');
// Test that queues and capabilities are JSON encoded
expect($array['queues'])->toBeString();
expect($array['capabilities'])->toBeString();
// Note: fromArray() is simplified in the implementation
// In real testing, you'd want to test full serialization/deserialization
$restoredWorker = Worker::fromArray($array);
expect($restoredWorker->hostname)->toBe('worker-01');
expect($restoredWorker->processId)->toBe(1234);
});
});
describe('WorkerRegistry Service', function () {
it('can register a worker successfully', function () {
$worker = Worker::register('test-host', 1234, [QueueName::default()]);
$this->workerRegistry->register($worker);
expect($this->mockLogger->logs)->toContain([
'level' => 'info',
'message' => 'Registering worker',
'context' => [
'worker_id' => $worker->id->toString(),
'hostname' => 'test-host',
'process_id' => 1234,
'queues' => [QueueName::default()],
'max_jobs' => 10,
],
]);
expect($this->mockLogger->logs)->toContain([
'level' => 'debug',
'message' => 'Worker registered successfully',
'context' => ['worker_id' => $worker->id->toString()],
]);
});
it('can deregister a worker', function () {
$workerId = WorkerId::generate();
// Setup worker data in mock
$this->mockConnection->setWorkerData([
$workerId->toString() => [
'id' => $workerId->toString(),
'is_active' => 1,
],
]);
$this->workerRegistry->deregister($workerId);
expect($this->mockLogger->logs)->toContain([
'level' => 'info',
'message' => 'Deregistering worker',
'context' => ['worker_id' => $workerId->toString()],
]);
});
it('can update worker heartbeat', function () {
$workerId = WorkerId::generate();
$cpuUsage = new Percentage(45.5);
$memoryUsage = Byte::fromMegabytes(256);
$currentJobs = 3;
// Setup worker data in mock
$this->mockConnection->setWorkerData([
$workerId->toString() => [
'id' => $workerId->toString(),
'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');
expect($warningLogs)->toBeEmpty();
});
it('logs warning when heartbeat update fails for non-existent worker', function () {
$workerId = WorkerId::generate();
$cpuUsage = new Percentage(50);
$memoryUsage = Byte::fromMegabytes(512);
// No worker data setup - worker doesn't exist
$this->workerRegistry->updateHeartbeat($workerId, $cpuUsage, $memoryUsage, 2);
$warningLogs = array_filter($this->mockLogger->logs, fn ($log) => $log['level'] === 'warning');
expect($warningLogs)->not()->toBeEmpty();
});
it('can find worker by ID', function () {
$workerId = WorkerId::generate();
$workerData = [
'id' => $workerId->toString(),
'hostname' => 'test-worker',
'process_id' => 1234,
'queues' => json_encode(['default']),
'max_jobs' => 5,
'current_jobs' => 2,
'is_active' => 1,
'cpu_usage' => 50,
'memory_usage_bytes' => 512 * 1024 * 1024,
'registered_at' => '2024-01-01 12:00:00',
'last_heartbeat' => '2024-01-01 12:05:00',
'capabilities' => json_encode(['pdf']),
'version' => '1.0.0',
];
$this->mockConnection->setWorkerData([$workerId->toString() => $workerData]);
$foundWorker = $this->workerRegistry->findById($workerId);
expect($foundWorker)->toBeInstanceOf(Worker::class);
expect($foundWorker->hostname)->toBe('test-worker');
expect($foundWorker->processId)->toBe(1234);
});
it('returns null when worker not found by ID', function () {
$workerId = WorkerId::generate();
$foundWorker = $this->workerRegistry->findById($workerId);
expect($foundWorker)->toBeNull();
});
it('can get worker statistics', function () {
$stats = $this->workerRegistry->getWorkerStatistics();
expect($stats)->toHaveKey('total_workers');
expect($stats)->toHaveKey('active_workers');
expect($stats)->toHaveKey('healthy_workers');
expect($stats)->toHaveKey('total_capacity');
expect($stats)->toHaveKey('current_load');
expect($stats)->toHaveKey('capacity_utilization');
expect($stats)->toHaveKey('avg_cpu_usage');
expect($stats)->toHaveKey('avg_memory_usage_mb');
expect($stats)->toHaveKey('unique_hosts');
expect($stats)->toHaveKey('queue_distribution');
expect($stats['capacity_utilization'])->toBe(50.0);
expect($stats['avg_memory_usage_mb'])->toBe(512.0);
});
it('can cleanup inactive workers', function () {
$cleanedCount = $this->workerRegistry->cleanupInactiveWorkers(5);
expect($cleanedCount)->toBeInt();
expect($this->mockLogger->logs)->toContain([
'level' => 'info',
'message' => 'Starting cleanup of inactive workers',
'context' => ['inactive_minutes' => 5],
]);
});
});
describe('WorkerHealthCheckService', function () {
it('can perform health check on individual worker', function () {
$worker = new Worker(
id: WorkerId::generate(),
hostname: 'test-worker',
processId: 1234,
queues: [QueueName::default()],
maxJobs: 10,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable(),
isActive: true,
cpuUsage: new Percentage(45),
memoryUsage: Byte::fromMegabytes(800),
currentJobs: 5
);
$health = $this->healthCheckService->checkWorkerHealth($worker);
expect($health)->toHaveKey('worker_id');
expect($health)->toHaveKey('hostname');
expect($health)->toHaveKey('status');
expect($health)->toHaveKey('score');
expect($health)->toHaveKey('metrics');
expect($health)->toHaveKey('issues');
expect($health)->toHaveKey('warnings');
expect($health)->toHaveKey('checked_at');
expect($health['worker_id'])->toBe($worker->id->toString());
expect($health['hostname'])->toBe('test-worker');
expect($health['status'])->toBe('healthy');
expect($health['score'])->toBeGreaterThanOrEqual(80);
expect($health['issues'])->toBeEmpty();
});
it('detects critical health issues', function () {
$criticalWorker = new Worker(
id: WorkerId::generate(),
hostname: 'critical-worker',
processId: 1234,
queues: [QueueName::default()],
maxJobs: 5,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable('-5 minutes'), // Very old heartbeat
isActive: true,
cpuUsage: new Percentage(95), // Critical CPU
memoryUsage: Byte::fromGigabytes(3), // Critical memory
currentJobs: 5 // Overloaded
);
$health = $this->healthCheckService->checkWorkerHealth($criticalWorker);
expect($health['status'])->toBe('critical');
expect($health['score'])->toBeLessThan(50);
expect($health['issues'])->not()->toBeEmpty();
expect($health['issues'])->toContain('No heartbeat for 300 seconds');
expect($health['issues'])->toContain('Critical CPU usage: 95%');
expect($health['issues'])->toContain('Critical memory usage: 3.000GB');
expect($health['issues'])->toContain('Worker overloaded: 95%');
});
it('detects warning conditions', function () {
$warningWorker = new Worker(
id: WorkerId::generate(),
hostname: 'warning-worker',
processId: 1234,
queues: [QueueName::default()],
maxJobs: 10,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable('-70 seconds'), // Slightly delayed
isActive: true,
cpuUsage: new Percentage(80), // Warning level CPU
memoryUsage: Byte::fromGigabytes(1.8), // Warning level memory
currentJobs: 9 // High load
);
$health = $this->healthCheckService->checkWorkerHealth($warningWorker);
expect($health['status'])->toBe('warning');
expect($health['score'])->toBeGreaterThan(30);
expect($health['score'])->toBeLessThan(80);
expect($health['warnings'])->not()->toBeEmpty();
expect($health['warnings'])->toContain('Heartbeat delayed (70s)');
expect($health['warnings'])->toContain('High CPU usage: 80%');
expect($health['warnings'])->toContain('High memory usage: 1.800GB');
expect($health['warnings'])->toContain('High worker load: 90%');
});
it('handles inactive workers correctly', function () {
$inactiveWorker = new Worker(
id: WorkerId::generate(),
hostname: 'inactive-worker',
processId: 1234,
queues: [QueueName::default()],
maxJobs: 5,
registeredAt: new \DateTimeImmutable(),
isActive: false // Inactive
);
$health = $this->healthCheckService->checkWorkerHealth($inactiveWorker);
expect($health['status'])->toBe('critical');
expect($health['score'])->toBe(0);
expect($health['issues'])->toContain('Worker marked as inactive');
});
it('can generate system health report', function () {
// Setup some test workers in the mock
$this->mockConnection->setWorkerData([
'worker1' => [
'id' => 'worker1',
'hostname' => 'test1',
'process_id' => 1234,
'queues' => json_encode(['default']),
'max_jobs' => 5,
'current_jobs' => 2,
'is_active' => 1,
'cpu_usage' => 50,
'memory_usage_bytes' => 512 * 1024 * 1024,
'registered_at' => '2024-01-01 12:00:00',
'last_heartbeat' => date('Y-m-d H:i:s'),
'capabilities' => json_encode([]),
'version' => '1.0.0',
],
]);
$report = $this->healthCheckService->generateSystemHealthReport();
expect($report)->toHaveKey('current_health');
expect($report)->toHaveKey('trends_24h');
expect($report)->toHaveKey('top_issues_24h');
expect($report)->toHaveKey('generated_at');
expect($report['current_health'])->toHaveKey('workers');
expect($report['current_health'])->toHaveKey('overall');
});
it('can cleanup old health check records', function () {
$deletedCount = $this->healthCheckService->cleanupHealthChecks(Duration::fromDays(7));
expect($deletedCount)->toBeInt();
expect($this->mockLogger->logs)->toContain([
'level' => 'info',
'message' => 'Health check cleanup completed',
'context' => [
'deleted_records' => $deletedCount,
'retention_days' => 7.0,
],
]);
});
});
describe('Integration Testing', function () {
it('can perform full worker lifecycle management', function () {
// 1. Register worker
$worker = Worker::register('integration-host', 9999, [QueueName::default(), QueueName::high()], 8, ['pdf']);
$this->workerRegistry->register($worker);
// 2. Update heartbeat
$this->workerRegistry->updateHeartbeat(
$worker->id,
new Percentage(65),
Byte::fromMegabytes(1024),
3
);
// 3. Perform health check
$health = $this->healthCheckService->checkWorkerHealth($worker);
expect($health['status'])->toBeIn(['healthy', 'warning']);
// 4. Find worker
$foundWorker = $this->workerRegistry->findById($worker->id);
expect($foundWorker)->not()->toBeNull();
// 5. Get statistics
$stats = $this->workerRegistry->getWorkerStatistics();
expect($stats['total_workers'])->toBeGreaterThanOrEqual(1);
// 6. Deregister worker
$this->workerRegistry->deregister($worker->id);
// Verify all operations logged appropriately
$logCount = count($this->mockLogger->logs);
expect($logCount)->toBeGreaterThan(0);
});
it('handles edge cases and error conditions gracefully', function () {
// Test with non-existent worker ID
$nonExistentId = WorkerId::generate();
$foundWorker = $this->workerRegistry->findById($nonExistentId);
expect($foundWorker)->toBeNull();
// Test heartbeat update for non-existent worker
$this->workerRegistry->updateHeartbeat(
$nonExistentId,
new Percentage(50),
Byte::fromMegabytes(512),
1
);
// Should log warning
$warningLogs = array_filter($this->mockLogger->logs, fn ($log) => $log['level'] === 'warning');
expect($warningLogs)->not()->toBeEmpty();
});
it('maintains data consistency across operations', function () {
$worker = Worker::register('consistency-test', 7777, [QueueName::default()]);
// Test immutability - operations should return new instances
$originalId = $worker->id;
$originalHostname = $worker->hostname;
$updatedWorker = $worker->updateHeartbeat(
new Percentage(30),
Byte::fromMegabytes(256),
1
);
// Original worker unchanged
expect($worker->id)->toBe($originalId);
expect($worker->hostname)->toBe($originalHostname);
expect($worker->currentJobs)->toBe(0);
// Updated worker has new values
expect($updatedWorker->id)->toBe($originalId); // Same ID
expect($updatedWorker->hostname)->toBe($originalHostname); // Same hostname
expect($updatedWorker->currentJobs)->toBe(1); // Updated jobs
expect($updatedWorker->cpuUsage->getValue())->toBe(30.0);
$inactiveWorker = $updatedWorker->markInactive();
expect($updatedWorker->isActive)->toBeTrue(); // Original still active
expect($inactiveWorker->isActive)->toBeFalse(); // New instance inactive
});
});
});