- 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.
690 lines
24 KiB
PHP
690 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Database\EntityManagerInterface;
|
|
use App\Framework\DI\Container;
|
|
use App\Framework\DI\DefaultContainer;
|
|
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 () {
|
|
|
|
beforeEach(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->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
|
|
$this->container->instance(EntityManagerInterface::class, $this->mockEntityManager);
|
|
$this->container->instance(Logger::class, $this->mockLogger);
|
|
});
|
|
|
|
describe('Core Queue Service', function () {
|
|
it('registers Queue service correctly', function () {
|
|
// This test verifies that the QueueInitializer properly registers a Queue
|
|
// 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
|
|
{
|
|
return '/home/michael/dev/michaelschiemer/tests/tmp/queue/';
|
|
}
|
|
}
|
|
);
|
|
|
|
$queue = $queueInitializer($this->mockLogger);
|
|
|
|
expect($queue)->toBeInstanceOf(Queue::class);
|
|
expect($queue)->not->toBeNull();
|
|
});
|
|
|
|
it('Queue service is accessible from container after registration', function () {
|
|
// Register queue manually for testing
|
|
$this->container->singleton(Queue::class, function () {
|
|
return new \App\Framework\Queue\InMemoryQueue();
|
|
});
|
|
|
|
$queue = $this->container->get(Queue::class);
|
|
expect($queue)->toBeInstanceOf(Queue::class);
|
|
});
|
|
});
|
|
|
|
describe('Queue Dependencies Registration', function () {
|
|
beforeEach(function () {
|
|
// Initialize the queue dependency system
|
|
$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;
|
|
}
|
|
};
|
|
});
|
|
|
|
// 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
|
|
{
|
|
}
|
|
};
|
|
});
|
|
});
|
|
|
|
it('registers JobDependencyManagerInterface', function () {
|
|
$dependencyManager = $this->dependencyInitializer->__invoke($this->container);
|
|
|
|
expect($dependencyManager)->toBeInstanceOf(JobDependencyManagerInterface::class);
|
|
expect($dependencyManager)->toBeInstanceOf(DatabaseJobDependencyManager::class);
|
|
|
|
// Should be accessible from container
|
|
$retrieved = $this->container->get(JobDependencyManagerInterface::class);
|
|
expect($retrieved)->toBe($dependencyManager);
|
|
});
|
|
|
|
it('registers JobChainManagerInterface', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
$chainManager = $this->container->get(JobChainManagerInterface::class);
|
|
expect($chainManager)->toBeInstanceOf(JobChainManagerInterface::class);
|
|
expect($chainManager)->toBeInstanceOf(DatabaseJobChainManager::class);
|
|
});
|
|
|
|
it('registers DependencyResolutionEngine as singleton', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
$engine1 = $this->container->get(DependencyResolutionEngine::class);
|
|
$engine2 = $this->container->get(DependencyResolutionEngine::class);
|
|
|
|
expect($engine1)->toBeInstanceOf(DependencyResolutionEngine::class);
|
|
expect($engine1)->toBe($engine2); // Should be same instance (singleton)
|
|
});
|
|
|
|
it('registers JobMetricsManager as singleton', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
$metrics1 = $this->container->get(JobMetricsManager::class);
|
|
$metrics2 = $this->container->get(JobMetricsManager::class);
|
|
|
|
expect($metrics1)->toBeInstanceOf(JobMetricsManager::class);
|
|
expect($metrics1)->toBe($metrics2); // Should be same instance (singleton)
|
|
});
|
|
});
|
|
|
|
describe('Individual Service Registration', function () {
|
|
it('can register DistributedLockInterface service', function () {
|
|
$lockService = new DatabaseDistributedLock(
|
|
entityManager: $this->mockEntityManager,
|
|
logger: $this->mockLogger
|
|
);
|
|
|
|
$this->container->singleton(DistributedLockInterface::class, $lockService);
|
|
|
|
$retrieved = $this->container->get(DistributedLockInterface::class);
|
|
expect($retrieved)->toBe($lockService);
|
|
expect($retrieved)->toBeInstanceOf(DistributedLockInterface::class);
|
|
});
|
|
|
|
it('can register JobProgressTrackerInterface service', function () {
|
|
$progressTracker = new DatabaseJobProgressTracker(
|
|
entityManager: $this->mockEntityManager,
|
|
logger: $this->mockLogger
|
|
);
|
|
|
|
$this->container->singleton(JobProgressTrackerInterface::class, $progressTracker);
|
|
|
|
$retrieved = $this->container->get(JobProgressTrackerInterface::class);
|
|
expect($retrieved)->toBe($progressTracker);
|
|
expect($retrieved)->toBeInstanceOf(JobProgressTrackerInterface::class);
|
|
});
|
|
|
|
it('can register DeadLetterQueueInterface service', function () {
|
|
$deadLetterQueue = new DatabaseDeadLetterQueue(
|
|
entityManager: $this->mockEntityManager,
|
|
logger: $this->mockLogger
|
|
);
|
|
|
|
$this->container->singleton(DeadLetterQueueInterface::class, $deadLetterQueue);
|
|
|
|
$retrieved = $this->container->get(DeadLetterQueueInterface::class);
|
|
expect($retrieved)->toBe($deadLetterQueue);
|
|
expect($retrieved)->toBeInstanceOf(DeadLetterQueueInterface::class);
|
|
});
|
|
|
|
it('can register WorkerRegistry service', function () {
|
|
$workerRegistry = new WorkerRegistry(
|
|
entityManager: $this->mockEntityManager,
|
|
logger: $this->mockLogger
|
|
);
|
|
|
|
$this->container->singleton(WorkerRegistry::class, $workerRegistry);
|
|
|
|
$retrieved = $this->container->get(WorkerRegistry::class);
|
|
expect($retrieved)->toBe($workerRegistry);
|
|
expect($retrieved)->toBeInstanceOf(WorkerRegistry::class);
|
|
});
|
|
|
|
it('can register JobDistributionService', function () {
|
|
// First register dependencies
|
|
$this->container->singleton(WorkerRegistry::class, new WorkerRegistry(
|
|
$this->mockEntityManager,
|
|
$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;
|
|
}
|
|
};
|
|
});
|
|
|
|
$distributionService = new JobDistributionService(
|
|
workerRegistry: $this->container->get(WorkerRegistry::class),
|
|
queue: $this->container->get(\App\Framework\Queue\Contracts\QueueInterface::class),
|
|
logger: $this->mockLogger
|
|
);
|
|
|
|
$this->container->singleton(JobDistributionService::class, $distributionService);
|
|
|
|
$retrieved = $this->container->get(JobDistributionService::class);
|
|
expect($retrieved)->toBe($distributionService);
|
|
expect($retrieved)->toBeInstanceOf(JobDistributionService::class);
|
|
});
|
|
});
|
|
|
|
describe('Service Dependencies and Integration', function () {
|
|
it('services have proper dependencies injected', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
$dependencyManager = $this->container->get(JobDependencyManagerInterface::class);
|
|
$chainManager = $this->container->get(JobChainManagerInterface::class);
|
|
$resolutionEngine = $this->container->get(DependencyResolutionEngine::class);
|
|
|
|
// Verify dependencies are properly injected
|
|
expect($dependencyManager)->toBeInstanceOf(DatabaseJobDependencyManager::class);
|
|
expect($chainManager)->toBeInstanceOf(DatabaseJobChainManager::class);
|
|
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();
|
|
});
|
|
|
|
it('can resolve complex dependency graph', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
// Add additional services
|
|
$this->container->singleton(DistributedLockInterface::class, function () {
|
|
return new DatabaseDistributedLock(
|
|
$this->mockEntityManager,
|
|
$this->mockLogger
|
|
);
|
|
});
|
|
|
|
$this->container->singleton(JobProgressTrackerInterface::class, function () {
|
|
return new DatabaseJobProgressTracker(
|
|
$this->mockEntityManager,
|
|
$this->mockLogger
|
|
);
|
|
});
|
|
|
|
// All services should be resolvable
|
|
$services = [
|
|
JobDependencyManagerInterface::class,
|
|
JobChainManagerInterface::class,
|
|
DependencyResolutionEngine::class,
|
|
JobMetricsManager::class,
|
|
DistributedLockInterface::class,
|
|
JobProgressTrackerInterface::class,
|
|
];
|
|
|
|
foreach ($services as $serviceInterface) {
|
|
$service = $this->container->get($serviceInterface);
|
|
expect($service)->not->toBeNull();
|
|
expect($service)->toBeObject();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Service Lifecycle Management', function () {
|
|
it('singleton services maintain state across requests', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
$metrics1 = $this->container->get(JobMetricsManager::class);
|
|
$metrics2 = $this->container->get(JobMetricsManager::class);
|
|
|
|
// Should be exact same instance
|
|
expect($metrics1)->toBe($metrics2);
|
|
});
|
|
|
|
it('services can be replaced for testing', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
// Get original service
|
|
$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
|
|
{
|
|
return new \App\Framework\Queue\ValueObjects\QueueMetrics(
|
|
queueName: $queueName,
|
|
totalJobs: 0,
|
|
completedJobs: 0,
|
|
failedJobs: 0,
|
|
averageExecutionTime: 0.0
|
|
);
|
|
}
|
|
|
|
public function getSystemMetrics(): array
|
|
{
|
|
return [];
|
|
}
|
|
};
|
|
|
|
// Replace with mock
|
|
$this->container->instance(JobMetricsManagerInterface::class, $mock);
|
|
|
|
$replaced = $this->container->get(JobMetricsManagerInterface::class);
|
|
expect($replaced)->toBe($mock);
|
|
expect($replaced)->not->toBe($original);
|
|
});
|
|
|
|
it('handles missing dependencies gracefully', function () {
|
|
// Don't register EventDispatcher - this should cause failure
|
|
unset($this->container);
|
|
$this->container = new DefaultContainer();
|
|
$this->container->instance(EntityManagerInterface::class, $this->mockEntityManager);
|
|
$this->container->instance(Logger::class, $this->mockLogger);
|
|
|
|
$dependencyInitializer = new QueueDependencyInitializer();
|
|
|
|
// This should fail due to missing dependencies
|
|
expect(fn () => $dependencyInitializer->__invoke($this->container))
|
|
->toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Queue Service Integration Test', function () {
|
|
|
|
beforeEach(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(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\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
|
|
{
|
|
}
|
|
};
|
|
});
|
|
});
|
|
|
|
it('can initialize complete queue system', function () {
|
|
// Initialize all queue services
|
|
$dependencyInitializer = new QueueDependencyInitializer();
|
|
$dependencyInitializer->__invoke($this->container);
|
|
|
|
// Register additional services that would normally be auto-registered
|
|
$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) {
|
|
return new DatabaseJobProgressTracker(
|
|
$container->get(EntityManagerInterface::class),
|
|
$container->get(Logger::class)
|
|
);
|
|
});
|
|
|
|
$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) {
|
|
return new WorkerRegistry(
|
|
$container->get(EntityManagerInterface::class),
|
|
$container->get(Logger::class)
|
|
);
|
|
});
|
|
|
|
// Verify all 9 expected queue services are registered
|
|
$expectedServices = [
|
|
DistributedLockInterface::class,
|
|
WorkerRegistry::class,
|
|
JobDistributionService::class, // This might not be auto-registered
|
|
WorkerHealthCheckService::class, // This might not be auto-registered
|
|
FailoverRecoveryService::class, // This might not be auto-registered
|
|
JobProgressTrackerInterface::class,
|
|
DeadLetterQueueInterface::class,
|
|
JobMetricsManagerInterface::class,
|
|
JobDependencyManagerInterface::class,
|
|
];
|
|
|
|
$registeredCount = 0;
|
|
foreach ($expectedServices as $service) {
|
|
try {
|
|
$instance = $this->container->get($service);
|
|
if ($instance !== null) {
|
|
$registeredCount++;
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Service not registered, which is expected for some
|
|
}
|
|
}
|
|
|
|
// At least the core services should be registered
|
|
expect($registeredCount)->toBeGreaterThan(4);
|
|
});
|
|
|
|
it('services can interact without errors', function () {
|
|
$dependencyInitializer = new QueueDependencyInitializer();
|
|
$dependencyInitializer->__invoke($this->container);
|
|
|
|
$dependencyManager = $this->container->get(JobDependencyManagerInterface::class);
|
|
$chainManager = $this->container->get(JobChainManagerInterface::class);
|
|
$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();
|
|
});
|
|
});
|