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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -8,10 +8,9 @@ use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\EntityManager;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Queue\Contracts\DeadLetterQueueInterface;
use App\Framework\Queue\Contracts\QueueInterface;
use App\Framework\Queue\Queue;
use App\Framework\Queue\Entities\DeadLetterJob;
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\QueuePriority;
@@ -23,8 +22,9 @@ final readonly class DatabaseDeadLetterQueue implements DeadLetterQueueInterface
public function __construct(
private ConnectionInterface $connection,
private EntityManager $entityManager,
private QueueInterface $originalQueue
) {}
private Queue $originalQueue
) {
}
public function addFailedJob(DeadLetterJob $deadLetterJob): void
{
@@ -69,7 +69,7 @@ final readonly class DatabaseDeadLetterQueue implements DeadLetterQueueInterface
try {
// Get the dead letter job
$deadLetterJob = $this->findDeadLetterJob($deadLetterJobId);
if (!$deadLetterJob) {
if (! $deadLetterJob) {
return false;
}
@@ -96,6 +96,7 @@ final readonly class DatabaseDeadLetterQueue implements DeadLetterQueueInterface
return true;
} catch (\Throwable $e) {
$this->connection->rollback();
throw $e;
}
}
@@ -122,6 +123,7 @@ final readonly class DatabaseDeadLetterQueue implements DeadLetterQueueInterface
public function clearQueue(DeadLetterQueueName $deadLetterQueueName): int
{
$sql = "DELETE FROM dead_letter_jobs WHERE dead_letter_queue = ?";
return $this->connection->execute(SqlQuery::create($sql, [$deadLetterQueueName->toString()]));
}
@@ -150,7 +152,7 @@ final readonly class DatabaseDeadLetterQueue implements DeadLetterQueueInterface
'avg_retry_count' => round((float) $stats['avg_retry_count'], 2),
'max_retry_count' => (int) $stats['max_retry_count'],
'oldest_job' => $stats['oldest_job'],
'newest_job' => $stats['newest_job']
'newest_job' => $stats['newest_job'],
];
}
@@ -177,6 +179,7 @@ final readonly class DatabaseDeadLetterQueue implements DeadLetterQueueInterface
private function deleteJobById(string $deadLetterJobId): bool
{
$sql = "DELETE FROM dead_letter_jobs WHERE id = ?";
return $this->connection->execute(SqlQuery::create($sql, [$deadLetterJobId])) > 0;
}
@@ -198,4 +201,4 @@ final readonly class DatabaseDeadLetterQueue implements DeadLetterQueueInterface
lastRetryAt: $row['last_retry_at']
);
}
}
}

View File

@@ -4,14 +4,14 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\WorkerId;
/**
* Database-basierte Distributed Lock Implementation
@@ -25,7 +25,8 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
public function __construct(
private ConnectionInterface $connection,
private Logger $logger
) {}
) {
}
/**
* Lock erwerben
@@ -35,7 +36,7 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
$this->logger->debug('Attempting to acquire lock', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'ttl_seconds' => $ttl->toSeconds()
'ttl_seconds' => $ttl->toSeconds(),
]));
try {
@@ -51,14 +52,15 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
$affectedRows = $this->connection->execute(SqlQuery::create($sql, [
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'expires_at' => $expiresAt->format('Y-m-d H:i:s')
'expires_at' => $expiresAt->format('Y-m-d H:i:s'),
]));
if ($affectedRows > 0) {
$this->logger->info('Lock acquired successfully', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
return true;
}
@@ -69,15 +71,16 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
if ($this->isDuplicateKeyError($e)) {
$this->logger->debug('Lock already exists', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
return false;
}
$this->logger->error('Failed to acquire lock', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
@@ -92,7 +95,7 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
$this->logger->debug('Attempting to extend lock', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'ttl_seconds' => $ttl->toSeconds()
'ttl_seconds' => $ttl->toSeconds(),
]));
try {
@@ -107,7 +110,7 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
$affectedRows = $this->connection->execute(SqlQuery::create($sql, [
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'expires_at' => $expiresAt->format('Y-m-d H:i:s')
'expires_at' => $expiresAt->format('Y-m-d H:i:s'),
]));
$success = $affectedRows > 0;
@@ -115,12 +118,12 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
if ($success) {
$this->logger->debug('Lock extended successfully', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
} else {
$this->logger->warning('Failed to extend lock - not owned or expired', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
}
@@ -130,8 +133,9 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
$this->logger->error('Failed to extend lock', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -143,7 +147,7 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
{
$this->logger->debug('Attempting to release lock', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
try {
@@ -153,7 +157,7 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
$affectedRows = $this->connection->execute(SqlQuery::create($sql, [
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
$success = $affectedRows > 0;
@@ -161,12 +165,12 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
if ($success) {
$this->logger->info('Lock released successfully', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
} else {
$this->logger->warning('Lock not found or not owned by worker', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
}
@@ -176,8 +180,9 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
$this->logger->error('Failed to release lock', LogContext::withData([
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -193,13 +198,15 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
AND expires_at > NOW()";
$result = $this->connection->queryOne(SqlQuery::create($sql, ['lock_key' => $key->toString()]));
return (int) $result['count'] > 0;
} catch (\Exception $e) {
$this->logger->error('Failed to check lock existence', LogContext::withData([
'lock_key' => $key->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -215,7 +222,7 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
AND expires_at > NOW()";
$data = $this->connection->queryOne(SqlQuery::create($sql, ['lock_key' => $key->toString()]));
if (!$data) {
if (! $data) {
return null;
}
@@ -224,14 +231,15 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
'worker_id' => $data['worker_id'],
'acquired_at' => $data['acquired_at'],
'expires_at' => $data['expires_at'],
'ttl_seconds' => strtotime($data['expires_at']) - time()
'ttl_seconds' => strtotime($data['expires_at']) - time(),
];
} catch (\Exception $e) {
$this->logger->error('Failed to get lock info', LogContext::withData([
'lock_key' => $key->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -245,7 +253,7 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'ttl_seconds' => $ttl->toSeconds(),
'timeout_seconds' => $timeout->toSeconds()
'timeout_seconds' => $timeout->toSeconds(),
]));
$startTime = microtime(true);
@@ -260,8 +268,9 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'attempts' => $attempts,
'elapsed_seconds' => microtime(true) - $startTime
'elapsed_seconds' => microtime(true) - $startTime,
]));
break;
}
@@ -271,8 +280,9 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'attempts' => $attempts + 1,
'elapsed_seconds' => microtime(true) - $startTime
'elapsed_seconds' => microtime(true) - $startTime,
]));
return true;
}
@@ -286,7 +296,7 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
'lock_key' => $key->toString(),
'worker_id' => $workerId->toString(),
'attempts' => $attempts,
'timeout_seconds' => $timeoutSeconds
'timeout_seconds' => $timeoutSeconds,
]));
return false;
@@ -298,7 +308,7 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
public function releaseAllWorkerLocks(WorkerId $workerId): int
{
$this->logger->info('Releasing all locks for worker', LogContext::withData([
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
try {
@@ -309,7 +319,7 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
$this->logger->info('Released all worker locks', LogContext::withData([
'worker_id' => $workerId->toString(),
'released_count' => $count
'released_count' => $count,
]));
return $count;
@@ -317,8 +327,9 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
} catch (\Exception $e) {
$this->logger->error('Failed to release worker locks', LogContext::withData([
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -337,15 +348,16 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
$count = $this->connection->execute(SqlQuery::create($sql));
$this->logger->info('Cleaned up expired locks', LogContext::withData([
'cleaned_count' => $count
'cleaned_count' => $count,
]));
return $count;
} catch (\Exception $e) {
$this->logger->error('Failed to cleanup expired locks', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -387,13 +399,14 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
'avg_ttl_seconds' => round((float) $stats['avg_ttl_seconds'], 2),
'oldest_lock' => $stats['oldest_lock'],
'newest_lock' => $stats['newest_lock'],
'top_lock_keys' => $topKeys
'top_lock_keys' => $topKeys,
];
} catch (\Exception $e) {
$this->logger->error('Failed to get lock statistics', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -404,8 +417,9 @@ final readonly class DatabaseDistributedLock implements DistributedLockInterface
private function isDuplicateKeyError(\Exception $e): bool
{
$message = strtolower($e->getMessage());
return strpos($message, 'duplicate') !== false
|| strpos($message, 'unique') !== false
|| $e->getCode() === 23000; // SQLSTATE für Constraint Violation
}
}
}

View File

@@ -4,14 +4,14 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Queue\Contracts\JobBatchManagerInterface;
use App\Framework\Queue\ValueObjects\JobBatch;
use App\Framework\Queue\ValueObjects\JobBatchStatus;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
/**
* Database-based job batch manager
@@ -21,7 +21,8 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
public function __construct(
private ConnectionInterface $connection,
private Logger $logger
) {}
) {
}
public function createBatch(string $name, array $jobIds, array $options = []): JobBatch
{
@@ -42,13 +43,13 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
$batch->processedJobs,
$batch->failedJobs,
json_encode($batch->options),
$batch->createdAt->toSqlString()
$batch->createdAt->toSqlString(),
]));
$this->logger->info('Job batch created', LogContext::withData([
'batch_id' => $batch->batchId,
'name' => $batch->name,
'total_jobs' => $batch->totalJobs
'total_jobs' => $batch->totalJobs,
]));
return $batch;
@@ -59,7 +60,7 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
$sql = "SELECT * FROM job_batches WHERE batch_id = ?";
$row = $this->connection->queryOne(SqlQuery::create($sql, [$batchId]));
if (!$row) {
if (! $row) {
return null;
}
@@ -69,7 +70,7 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
public function recordJobCompleted(string $batchId, string $jobId): void
{
$batch = $this->getBatch($batchId);
if (!$batch) {
if (! $batch) {
return;
}
@@ -79,14 +80,14 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
$this->logger->debug('Job batch progress updated', LogContext::withData([
'batch_id' => $batchId,
'job_id' => $jobId,
'progress' => $updatedBatch->getProgressPercentage()
'progress' => $updatedBatch->getProgressPercentage(),
]));
}
public function recordJobFailed(string $batchId, string $jobId): void
{
$batch = $this->getBatch($batchId);
if (!$batch) {
if (! $batch) {
return;
}
@@ -96,7 +97,7 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
$this->logger->warning('Job batch job failed', LogContext::withData([
'batch_id' => $batchId,
'job_id' => $jobId,
'failed_jobs' => $updatedBatch->failedJobs
'failed_jobs' => $updatedBatch->failedJobs,
]));
}
@@ -106,12 +107,12 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
$affected = $this->connection->execute(SqlQuery::create($sql, [
JobBatchStatus::CANCELLED->value,
date('Y-m-d H:i:s'),
$batchId
$batchId,
]));
if ($affected > 0) {
$this->logger->info('Job batch cancelled', LogContext::withData([
'batch_id' => $batchId
'batch_id' => $batchId,
]));
}
@@ -150,7 +151,7 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
'count' => (int) $row['count'],
'avg_processed' => (float) $row['avg_processed'],
'total_jobs' => (int) $row['total_jobs'],
'total_failed' => (int) $row['total_failed']
'total_failed' => (int) $row['total_failed'],
];
}
@@ -169,13 +170,13 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
$cutoffDate,
JobBatchStatus::COMPLETED->value,
JobBatchStatus::FAILED->value,
JobBatchStatus::CANCELLED->value
JobBatchStatus::CANCELLED->value,
]));
if ($deleted > 0) {
$this->logger->info('Cleaned up old job batches', LogContext::withData([
'deleted_count' => $deleted,
'older_than_days' => $olderThanDays
'older_than_days' => $olderThanDays,
]));
}
@@ -185,7 +186,7 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
public function getBatchProgress(string $batchId): array
{
$batch = $this->getBatch($batchId);
if (!$batch) {
if (! $batch) {
return [];
}
@@ -198,7 +199,7 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
'processed_jobs' => $batch->processedJobs,
'failed_jobs' => $batch->failedJobs,
'remaining_jobs' => $batch->getRemainingJobs(),
'is_finished' => $batch->isFinished()
'is_finished' => $batch->isFinished(),
];
}
@@ -220,7 +221,7 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
$batch->startedAt?->toSqlString(),
$batch->completedAt?->toSqlString(),
$batch->failedAt?->toSqlString(),
$batch->batchId
$batch->batchId,
]));
}
@@ -246,4 +247,4 @@ final readonly class DatabaseJobBatchManager implements JobBatchManagerInterface
{
return 'batch_' . uniqid() . '_' . time();
}
}
}

View File

@@ -4,18 +4,21 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\Contracts\JobChainManagerInterface;
use App\Framework\Queue\ValueObjects\JobChain;
use App\Framework\Queue\Entities\JobChainEntry;
use App\Framework\Database\EntityManagerInterface;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Contracts\JobChainManagerInterface;
use App\Framework\Queue\Entities\JobChainEntry;
use App\Framework\Queue\Exceptions\ChainNotFoundException;
use App\Framework\Queue\Exceptions\InvalidChainStateException;
use App\Framework\Queue\ValueObjects\JobChain;
final readonly class DatabaseJobChainManager implements JobChainManagerInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private Logger $logger
) {}
) {
}
public function createChain(JobChain $jobChain): void
{
@@ -25,7 +28,7 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
'chain_id' => $jobChain->chainId,
'name' => $jobChain->name,
'job_count' => count($jobChain->jobIds),
'execution_mode' => $jobChain->executionMode->value
'execution_mode' => $jobChain->executionMode->value,
]);
$this->entityManager->save($entry);
@@ -37,7 +40,7 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
$query = "SELECT * FROM job_chains WHERE chain_id = ?";
$result = $this->entityManager->getConnection()->fetchOne($query, [$chainId]);
if (!$result) {
if (! $result) {
return null;
}
@@ -48,8 +51,8 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
{
$existing = $this->getChain($jobChain->chainId);
if (!$existing) {
throw new \RuntimeException("Chain with ID '{$jobChain->chainId}' not found");
if (! $existing) {
throw ChainNotFoundException::byId($jobChain->chainId);
}
$updated = new JobChainEntry(
@@ -69,7 +72,7 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
$this->logger->info('Updating job chain', [
'chain_id' => $jobChain->chainId,
'name' => $jobChain->name
'name' => $jobChain->name,
]);
$this->entityManager->save($updated);
@@ -81,7 +84,7 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
$query = "DELETE FROM job_chains WHERE chain_id = ?";
$this->logger->info('Deleting job chain', [
'chain_id' => $chainId
'chain_id' => $chainId,
]);
$this->entityManager->getConnection()->execute($query, [$chainId]);
@@ -91,19 +94,19 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
{
$chain = $this->getChain($chainId);
if (!$chain) {
throw new \RuntimeException("Chain with ID '{$chainId}' not found");
if (! $chain) {
throw ChainNotFoundException::byId($chainId);
}
if (!$chain->isPending()) {
throw new \RuntimeException("Chain '{$chainId}' is not in pending status");
if (! $chain->isPending()) {
throw InvalidChainStateException::notPending($chainId, $chain->status);
}
$started = $chain->markAsStarted();
$this->logger->info('Starting job chain', [
'chain_id' => $chainId,
'job_count' => count($started->getJobIdsArray())
'job_count' => count($started->getJobIdsArray()),
]);
$this->entityManager->save($started);
@@ -115,7 +118,7 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
$query = "SELECT * FROM job_chains WHERE JSON_CONTAINS(job_ids, ?)";
$results = $this->entityManager->getConnection()->fetchAll($query, [json_encode($jobId)]);
return array_map(function(array $row) {
return array_map(function (array $row) {
return $this->mapRowToEntity($row);
}, $results);
}
@@ -125,7 +128,7 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
$query = "SELECT * FROM job_chains WHERE status = 'running' ORDER BY started_at";
$results = $this->entityManager->getConnection()->fetchAll($query);
return array_map(function(array $row) {
return array_map(function (array $row) {
return $this->mapRowToEntity($row);
}, $results);
}
@@ -135,7 +138,7 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
$query = "SELECT * FROM job_chains WHERE status = 'pending' ORDER BY created_at";
$results = $this->entityManager->getConnection()->fetchAll($query);
return array_map(function(array $row) {
return array_map(function (array $row) {
return $this->mapRowToEntity($row);
}, $results);
}
@@ -144,14 +147,14 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
{
$chain = $this->getChain($chainId);
if (!$chain) {
throw new \RuntimeException("Chain with ID '{$chainId}' not found");
if (! $chain) {
throw ChainNotFoundException::byId($chainId);
}
$completed = $chain->markAsCompleted();
$this->logger->info('Marking chain as completed', [
'chain_id' => $chainId
'chain_id' => $chainId,
]);
$this->entityManager->save($completed);
@@ -162,14 +165,14 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
{
$chain = $this->getChain($chainId);
if (!$chain) {
throw new \RuntimeException("Chain with ID '{$chainId}' not found");
if (! $chain) {
throw ChainNotFoundException::byId($chainId);
}
$failed = $chain->markAsFailed();
$this->logger->info('Marking chain as failed', [
'chain_id' => $chainId
'chain_id' => $chainId,
]);
$this->entityManager->save($failed);
@@ -180,7 +183,7 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
{
$chain = $this->getChain($chainId);
if (!$chain) {
if (! $chain) {
return null;
}
@@ -198,7 +201,7 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
{
$chain = $this->getChain($chainId);
if (!$chain) {
if (! $chain) {
return false;
}
@@ -210,19 +213,20 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
$chains = $this->getChainsForJob($jobId);
foreach ($chains as $chainEntry) {
if (!$chainEntry->isRunning()) {
if (! $chainEntry->isRunning()) {
continue;
}
$jobChain = $chainEntry->getJobChain();
// If chain requires stopping on failure and job failed
if (!$successful && $jobChain->stopOnFailure) {
if (! $successful && $jobChain->stopOnFailure) {
$this->markChainAsFailed($chainEntry->chainId);
$this->logger->warning('Chain failed due to job failure', [
'chain_id' => $chainEntry->chainId,
'failed_job_id' => $jobId
'failed_job_id' => $jobId,
]);
continue;
}
@@ -234,13 +238,13 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
$this->markChainAsCompleted($chainEntry->chainId);
$this->logger->info('Chain completed', [
'chain_id' => $chainEntry->chainId,
'last_job_id' => $jobId
'last_job_id' => $jobId,
]);
} else {
$this->logger->info('Job completed in chain, next job available', [
'chain_id' => $chainEntry->chainId,
'completed_job_id' => $jobId,
'next_job_id' => $nextJobId
'next_job_id' => $nextJobId,
]);
}
}
@@ -250,8 +254,8 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
{
$chain = $this->getChain($chainId);
if (!$chain) {
throw new \RuntimeException("Chain with ID '{$chainId}' not found");
if (! $chain) {
throw ChainNotFoundException::byId($chainId);
}
$jobChain = $chain->getJobChain();
@@ -264,25 +268,25 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
'completed_jobs' => 0,
'total_jobs' => $totalJobs,
'percentage' => 0,
'status' => 'pending'
'status' => 'pending',
],
'running' => [
'completed_jobs' => 0, // Would need actual job status check
'total_jobs' => $totalJobs,
'percentage' => 0, // Would calculate based on completed jobs
'status' => 'running'
'status' => 'running',
],
'completed' => [
'completed_jobs' => $totalJobs,
'total_jobs' => $totalJobs,
'percentage' => 100,
'status' => 'completed'
'status' => 'completed',
],
'failed' => [
'completed_jobs' => 0, // Would need actual job status check
'total_jobs' => $totalJobs,
'percentage' => 0, // Partial progress before failure
'status' => 'failed'
'status' => 'failed',
]
};
@@ -292,7 +296,7 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
'execution_mode' => $chain->executionMode,
'stop_on_failure' => $chain->stopOnFailure,
'started_at' => $chain->startedAt,
'completed_at' => $chain->completedAt
'completed_at' => $chain->completedAt,
]);
}
@@ -305,13 +309,13 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
$this->logger->info('Cleaning up old chains', [
'cutoff_date' => $cutoffDate,
'older_than_days' => $olderThanDays
'older_than_days' => $olderThanDays,
]);
$affectedRows = $this->entityManager->getConnection()->execute($query, [$cutoffDate]);
$this->logger->info('Old chains cleaned up', [
'deleted_count' => $affectedRows
'deleted_count' => $affectedRows,
]);
return $affectedRows;
@@ -334,4 +338,4 @@ final readonly class DatabaseJobChainManager implements JobChainManagerInterface
completedAt: $row['completed_at']
);
}
}
}

View File

@@ -4,19 +4,20 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Queue\ValueObjects\JobDependency;
use App\Framework\Queue\Entities\JobDependencyEntry;
use App\Framework\Database\EntityManagerInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Queue\Entities\JobDependencyEntry;
use App\Framework\Queue\ValueObjects\JobDependency;
final readonly class DatabaseJobDependencyManager implements JobDependencyManagerInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private Logger $logger
) {}
) {
}
public function addDependency(JobDependency $dependency): void
{
@@ -25,7 +26,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
$this->logger->info('Adding job dependency', [
'dependent_job_id' => $dependency->dependentJobId,
'depends_on_job_id' => $dependency->dependsOnJobId,
'type' => $dependency->type->value
'type' => $dependency->type->value,
]);
$this->entityManager->save($entry);
@@ -39,7 +40,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
$this->logger->info('Removing job dependency', [
'dependent_job_id' => $dependentJobId,
'depends_on_job_id' => $dependsOnJobId
'depends_on_job_id' => $dependsOnJobId,
]);
$this->entityManager->getConnection()->execute(
@@ -54,7 +55,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
SqlQuery::create($query, [$jobId])
)->fetchAll();
return array_map(function(array $row) {
return array_map(function (array $row) {
return $this->mapRowToEntity($row);
}, $results);
}
@@ -66,7 +67,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
SqlQuery::create($query, [$jobId])
)->fetchAll();
return array_map(function(array $row) {
return array_map(function (array $row) {
return $this->mapRowToEntity($row);
}, $results);
}
@@ -79,6 +80,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
$result = $this->entityManager->getConnection()->queryOne(
SqlQuery::create($query, [$jobId])
);
return $result['count'] > 0;
}
@@ -92,7 +94,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
SqlQuery::create($query, [$jobId])
)->fetchAll();
return array_map(function(array $row) {
return array_map(function (array $row) {
return $this->mapRowToEntity($row);
}, $results);
}
@@ -107,7 +109,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
$this->logger->info('Marking dependency as satisfied', [
'dependent_job_id' => $dependentJobId,
'depends_on_job_id' => $dependsOnJobId
'depends_on_job_id' => $dependsOnJobId,
]);
$this->entityManager->getConnection()->execute(
@@ -117,7 +119,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
public function canJobBeExecuted(string $jobId): bool
{
return !$this->hasUnsatisfiedDependencies($jobId);
return ! $this->hasUnsatisfiedDependencies($jobId);
}
public function getReadyJobs(): array
@@ -142,7 +144,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
{
$this->logger->info('Resolving job completion for dependencies', [
'job_id' => $jobId,
'successful' => $successful
'successful' => $successful,
]);
// Get all jobs that depend on this completed job
@@ -159,7 +161,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
\App\Framework\Queue\ValueObjects\DependencyType::CONDITIONAL => $this->evaluateCondition($dependency->condition, $successful)
};
if ($shouldSatisfy && !$dependencyEntry->isSatisfied) {
if ($shouldSatisfy && ! $dependencyEntry->isSatisfied) {
$this->markDependencyAsSatisfied($dependency->dependentJobId, $jobId);
// Check if the dependent job is now ready to execute
@@ -172,7 +174,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
$this->logger->info('Job completion resolved', [
'job_id' => $jobId,
'resolved_jobs_count' => count($resolvedJobs),
'resolved_jobs' => $resolvedJobs
'resolved_jobs' => $resolvedJobs,
]);
return $resolvedJobs;
@@ -186,7 +188,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
$this->buildDependencyChain($jobId, $visited, $chain);
// Remove the starting job from the chain if it's there
return array_filter($chain, fn($id) => $id !== $jobId);
return array_filter($chain, fn ($id) => $id !== $jobId);
}
public function hasCircularDependencies(string $jobId): bool
@@ -206,7 +208,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
$this->logger->info('Cleaning up old dependencies', [
'cutoff_date' => $cutoffDate,
'older_than_days' => $olderThanDays
'older_than_days' => $olderThanDays,
]);
$affectedRows = $this->entityManager->getConnection()->execute(
@@ -214,7 +216,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
);
$this->logger->info('Old dependencies cleaned up', [
'deleted_count' => $affectedRows
'deleted_count' => $affectedRows,
]);
return $affectedRows;
@@ -247,7 +249,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
$dependency = $dependencyEntry->getJobDependency();
$dependsOnJobId = $dependency->dependsOnJobId;
if (!isset($visited[$dependsOnJobId])) {
if (! isset($visited[$dependsOnJobId])) {
if ($this->detectCircularDependency($dependsOnJobId, $visited, $recursionStack)) {
return true;
}
@@ -257,6 +259,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
}
unset($recursionStack[$jobId]);
return false;
}
@@ -269,7 +272,7 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
// Simple condition evaluation - can be extended for more complex conditions
return match($condition) {
'on_success' => $jobSuccessful,
'on_failure' => !$jobSuccessful,
'on_failure' => ! $jobSuccessful,
'always' => true,
default => true
};
@@ -289,4 +292,4 @@ final readonly class DatabaseJobDependencyManager implements JobDependencyManage
satisfiedAt: $row['satisfied_at']
);
}
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\EntityManager;
use App\Framework\Database\ValueObjects\SqlQuery;
@@ -11,7 +12,6 @@ use App\Framework\Queue\Contracts\JobProgressTrackerInterface;
use App\Framework\Queue\Entities\JobProgressEntry;
use App\Framework\Queue\ValueObjects\JobProgress;
use App\Framework\Queue\ValueObjects\ProgressStep;
use App\Framework\Core\ValueObjects\Percentage;
/**
* Database-backed job progress tracker implementation
@@ -21,7 +21,8 @@ final readonly class DatabaseJobProgressTracker implements JobProgressTrackerInt
public function __construct(
private ConnectionInterface $connection,
private EntityManager $entityManager
) {}
) {
}
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void
{
@@ -47,6 +48,7 @@ final readonly class DatabaseJobProgressTracker implements JobProgressTrackerInt
public function getCurrentProgress(string $jobId): ?JobProgress
{
$latestEntry = $this->getLatestProgressEntry($jobId);
return $latestEntry?->getJobProgress();
}
@@ -131,6 +133,7 @@ final readonly class DatabaseJobProgressTracker implements JobProgressTrackerInt
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$olderThanDays} days"));
$sql = "DELETE FROM job_progress WHERE updated_at < ?";
return $this->connection->execute(SqlQuery::create($sql, [$cutoffDate]));
}
@@ -192,4 +195,4 @@ final readonly class DatabaseJobProgressTracker implements JobProgressTrackerInt
isFailed: (bool) $row['is_failed']
);
}
}
}

View File

@@ -7,6 +7,7 @@ namespace App\Framework\Queue\Services;
use App\Framework\Queue\Contracts\DeadLetterQueueInterface;
use App\Framework\Queue\Entities\DeadLetterJob;
use App\Framework\Queue\Entities\JobIndexEntry;
use App\Framework\Queue\ProductionJobPersistenceLayer;
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
use App\Framework\Queue\ValueObjects\FailureReason;
use App\Framework\Queue\ValueObjects\QueueName;
@@ -19,7 +20,8 @@ final readonly class DeadLetterManager
public function __construct(
private DeadLetterQueueInterface $deadLetterQueue,
private ProductionJobPersistenceLayer $persistenceLayer
) {}
) {
}
/**
* Move a failed job to the dead letter queue
@@ -57,7 +59,7 @@ final readonly class DeadLetterManager
int $maxAttempts = 3
): bool {
$jobEntry = $this->persistenceLayer->getJobById($jobId);
if (!$jobEntry) {
if (! $jobEntry) {
return false;
}
@@ -156,4 +158,4 @@ final readonly class DeadLetterManager
{
return $this->deadLetterQueue->getAvailableQueues();
}
}
}

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Contracts\JobChainManagerInterface;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Queue\Contracts\QueueInterface;
use App\Framework\Queue\ValueObjects\Job;
use App\Framework\Logging\Logger;
final readonly class DependencyResolutionEngine
{
@@ -17,7 +17,8 @@ final readonly class DependencyResolutionEngine
private JobChainManagerInterface $chainManager,
private QueueInterface $queue,
private Logger $logger
) {}
) {
}
/**
* Process job completion and resolve any dependent jobs that can now be executed
@@ -26,7 +27,7 @@ final readonly class DependencyResolutionEngine
{
$this->logger->info('Starting dependency resolution for completed job', [
'job_id' => $jobId,
'successful' => $successful
'successful' => $successful,
]);
$resolvedJobs = [];
@@ -38,7 +39,7 @@ final readonly class DependencyResolutionEngine
$resolvedJobs[] = [
'job_id' => $dependentJobId,
'type' => 'dependency_resolved',
'trigger_job' => $jobId
'trigger_job' => $jobId,
];
}
@@ -59,13 +60,13 @@ final readonly class DependencyResolutionEngine
'job_id' => $nextJobId,
'type' => 'chain_progression',
'chain_id' => $chainEntry->chainId,
'trigger_job' => $jobId
'trigger_job' => $jobId,
];
} else {
$this->logger->info('Next job in chain has unsatisfied dependencies', [
'chain_id' => $chainEntry->chainId,
'next_job_id' => $nextJobId,
'unsatisfied_deps' => count($this->dependencyManager->getUnsatisfiedDependencies($nextJobId))
'unsatisfied_deps' => count($this->dependencyManager->getUnsatisfiedDependencies($nextJobId)),
]);
}
}
@@ -75,7 +76,7 @@ final readonly class DependencyResolutionEngine
$this->logger->info('Dependency resolution completed', [
'trigger_job_id' => $jobId,
'resolved_jobs_count' => count($resolvedJobs),
'resolved_jobs' => array_column($resolvedJobs, 'job_id')
'resolved_jobs' => array_column($resolvedJobs, 'job_id'),
]);
return $resolvedJobs;
@@ -90,7 +91,7 @@ final readonly class DependencyResolutionEngine
$this->logger->debug('Found jobs ready for execution', [
'ready_jobs_count' => count($readyJobs),
'ready_jobs' => $readyJobs
'ready_jobs' => $readyJobs,
]);
return $readyJobs;
@@ -113,17 +114,17 @@ final readonly class DependencyResolutionEngine
'job_id' => $jobId,
'valid' => false,
'error' => 'circular_dependency',
'dependency_chain' => $dependencyChain
'dependency_chain' => $dependencyChain,
];
$this->logger->warning('Circular dependency detected', [
'job_id' => $jobId,
'dependency_chain' => $dependencyChain
'dependency_chain' => $dependencyChain,
]);
} else {
$validationResults[] = [
'job_id' => $jobId,
'valid' => true
'valid' => true,
];
}
}
@@ -145,21 +146,21 @@ final readonly class DependencyResolutionEngine
return [
'job_id' => $jobId,
'can_execute' => $canExecute,
'direct_dependencies' => array_map(fn($dep) => [
'direct_dependencies' => array_map(fn ($dep) => [
'depends_on_job_id' => $dep->dependsOnJobId,
'dependency_type' => $dep->dependencyType,
'is_satisfied' => $dep->isSatisfied,
'condition' => $dep->conditionExpression
'condition' => $dep->conditionExpression,
], $dependencies),
'dependent_jobs' => array_map(fn($dep) => [
'dependent_jobs' => array_map(fn ($dep) => [
'dependent_job_id' => $dep->dependentJobId,
'dependency_type' => $dep->dependencyType,
'is_satisfied' => $dep->isSatisfied
'is_satisfied' => $dep->isSatisfied,
], $dependents),
'unsatisfied_dependencies' => array_map(fn($dep) => [
'unsatisfied_dependencies' => array_map(fn ($dep) => [
'depends_on_job_id' => $dep->dependsOnJobId,
'dependency_type' => $dep->dependencyType,
'condition' => $dep->conditionExpression
'condition' => $dep->conditionExpression,
], $unsatisfiedDeps),
'full_dependency_chain' => $dependencyChain,
'statistics' => [
@@ -167,8 +168,8 @@ final readonly class DependencyResolutionEngine
'satisfied_dependencies' => count($dependencies) - count($unsatisfiedDeps),
'unsatisfied_dependencies' => count($unsatisfiedDeps),
'total_dependents' => count($dependents),
'chain_depth' => count($dependencyChain)
]
'chain_depth' => count($dependencyChain),
],
];
}
@@ -193,14 +194,14 @@ final readonly class DependencyResolutionEngine
'progress' => $progress,
'next_job_after_current' => $nextJob,
'job_position' => $this->getJobPositionInChain($jobId, $chainEntry),
'total_jobs' => count($chainEntry->getJobIdsArray())
'total_jobs' => count($chainEntry->getJobIdsArray()),
];
}
return [
'job_id' => $jobId,
'chains' => $chainAnalysis,
'total_chains' => count($chains)
'total_chains' => count($chains),
];
}
@@ -228,7 +229,7 @@ final readonly class DependencyResolutionEngine
$issues[] = [
'type' => 'stalled_chain',
'chain_id' => $chain->chainId,
'hours_running' => round($hoursRunning, 2)
'hours_running' => round($hoursRunning, 2),
];
}
}
@@ -241,7 +242,7 @@ final readonly class DependencyResolutionEngine
$issues[] = [
'type' => 'many_unsatisfied_dependencies',
'job_id' => $jobId,
'unsatisfied_count' => $analysis['statistics']['unsatisfied_dependencies']
'unsatisfied_count' => $analysis['statistics']['unsatisfied_dependencies'],
];
}
}
@@ -253,14 +254,14 @@ final readonly class DependencyResolutionEngine
'active_chains' => count($activeChains),
'pending_chains' => count($pendingChains),
'ready_jobs' => count($readyJobs),
'detected_issues' => count($issues)
'detected_issues' => count($issues),
],
'issues' => $issues
'issues' => $issues,
];
$this->logger->info('Dependency system health check completed', [
'status' => $healthReport['status'],
'issues_count' => count($issues)
'issues_count' => count($issues),
]);
return $healthReport;
@@ -269,6 +270,7 @@ final readonly class DependencyResolutionEngine
private function getJobPositionInChain(string $jobId, object $chainEntry): ?int
{
$jobIds = $chainEntry->getJobIdsArray();
return array_search($jobId, $jobIds, true) ?: null;
}
}
}

View File

@@ -4,17 +4,17 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;
/**
* Failover und Recovery Service für robustes Distributed Job Processing
@@ -32,7 +32,8 @@ final readonly class FailoverRecoveryService
private DistributedLockInterface $distributedLock,
private ConnectionInterface $connection,
private Logger $logger
) {}
) {
}
/**
* Vollständige Failover und Recovery Prozedur durchführen
@@ -48,7 +49,7 @@ final readonly class FailoverRecoveryService
'jobs_reassigned' => 0,
'workers_cleaned' => 0,
'locks_released' => 0,
'errors' => []
'errors' => [],
];
try {
@@ -56,10 +57,10 @@ final readonly class FailoverRecoveryService
$failedWorkers = $this->detectFailedWorkers();
$results['failed_workers_detected'] = count($failedWorkers);
if (!empty($failedWorkers)) {
if (! empty($failedWorkers)) {
$this->logger->warning('Failed workers detected', LogContext::withData([
'failed_worker_count' => count($failedWorkers),
'worker_ids' => array_map(fn($w) => $w->id->toString(), $failedWorkers)
'worker_ids' => array_map(fn ($w) => $w->id->toString(), $failedWorkers),
]));
// 2. Jobs von fehlgeschlagenen Workern wiederherstellen
@@ -69,7 +70,7 @@ final readonly class FailoverRecoveryService
$results['jobs_reassigned'] += $recovered['jobs_reassigned'];
$results['locks_released'] += $recovered['locks_released'];
if (!empty($recovered['errors'])) {
if (! empty($recovered['errors'])) {
$results['errors'] = array_merge($results['errors'], $recovered['errors']);
}
}
@@ -95,7 +96,7 @@ final readonly class FailoverRecoveryService
$results['errors'][] = 'Recovery process failed: ' . $e->getMessage();
$this->logger->error('Failover and recovery failed', LogContext::withData([
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
'trace' => $e->getTraceAsString(),
]));
}
@@ -125,7 +126,7 @@ final readonly class FailoverRecoveryService
private function isWorkerFailed(Worker $worker): bool
{
// Heartbeat-basierte Überprüfung
if (!$worker->lastHeartbeat) {
if (! $worker->lastHeartbeat) {
return true;
}
@@ -150,14 +151,14 @@ final readonly class FailoverRecoveryService
{
$this->logger->info('Recovering jobs from failed worker', LogContext::withData([
'worker_id' => $worker->id->toString(),
'hostname' => $worker->hostname
'hostname' => $worker->hostname,
]));
$results = [
'jobs_recovered' => 0,
'jobs_reassigned' => 0,
'locks_released' => 0,
'errors' => []
'errors' => [],
];
try {
@@ -165,12 +166,13 @@ final readonly class FailoverRecoveryService
$recoveryLock = LockKey::forWorker($worker->id)->withPrefix('recovery');
$recoveryWorkerId = WorkerId::generate();
if (!$this->distributedLock->acquire(
if (! $this->distributedLock->acquire(
$recoveryLock,
$recoveryWorkerId,
Duration::fromSeconds(self::RECOVERY_LOCK_TTL_SECONDS)
)) {
$results['errors'][] = 'Failed to acquire recovery lock';
return $results;
}
@@ -205,7 +207,7 @@ final readonly class FailoverRecoveryService
$results['errors'][] = 'Worker recovery failed: ' . $e->getMessage();
$this->logger->error('Failed to recover worker jobs', LogContext::withData([
'worker_id' => $worker->id->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
}
@@ -223,13 +225,15 @@ final readonly class FailoverRecoveryService
WHERE worker_id = :worker_id";
$result = $this->connection->query(SqlQuery::create($sql, ['worker_id' => $workerId->toString()]));
return $result->fetchAll();
} catch (\Exception $e) {
$this->logger->error('Failed to find worker jobs', LogContext::withData([
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
return [];
}
}
@@ -247,7 +251,7 @@ final readonly class FailoverRecoveryService
$this->logger->info('Job successfully reassigned', LogContext::withData([
'job_id' => $jobId->toString(),
'failed_worker_id' => $failedWorkerId->toString(),
'new_worker_id' => $newWorkerId->toString()
'new_worker_id' => $newWorkerId->toString(),
]));
// Failover Event loggen
@@ -262,8 +266,9 @@ final readonly class FailoverRecoveryService
$this->logger->error('Failed to reassign job', LogContext::withData([
'job_id' => $jobId->toString(),
'failed_worker_id' => $failedWorkerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
return false;
}
}
@@ -282,7 +287,7 @@ final readonly class FailoverRecoveryService
$this->logger->info('Failed worker cleaned up', LogContext::withData([
'worker_id' => $worker->id->toString(),
'hostname' => $worker->hostname
'hostname' => $worker->hostname,
]));
$cleanedCount++;
@@ -290,7 +295,7 @@ final readonly class FailoverRecoveryService
} catch (\Exception $e) {
$this->logger->error('Failed to cleanup worker', LogContext::withData([
'worker_id' => $worker->id->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
}
}
@@ -317,7 +322,7 @@ final readonly class FailoverRecoveryService
$result = $this->connection->query(SqlQuery::create($sql, [
'threshold' => self::WORKER_FAILURE_THRESHOLD_SECONDS,
'job_timeout' => self::JOB_TIMEOUT_THRESHOLD_SECONDS
'job_timeout' => self::JOB_TIMEOUT_THRESHOLD_SECONDS,
]));
$orphanedJobs = $result->fetchAll();
@@ -340,22 +345,23 @@ final readonly class FailoverRecoveryService
$this->logger->info('Orphaned job recovered', LogContext::withData([
'job_id' => $jobId->toString(),
'old_worker_id' => $oldWorkerId->toString(),
'new_worker_id' => $newWorkerId->toString()
'new_worker_id' => $newWorkerId->toString(),
]));
}
}
$this->logger->info('Orphaned jobs recovery completed', LogContext::withData([
'total_orphaned' => count($orphanedJobs),
'recovered' => $recoveredCount
'recovered' => $recoveredCount,
]));
return $recoveredCount;
} catch (\Exception $e) {
$this->logger->error('Failed to recover orphaned jobs', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
return 0;
}
}
@@ -375,13 +381,13 @@ final readonly class FailoverRecoveryService
$this->connection->execute(SqlQuery::create($sql, [
'job_id' => $jobId->toString(),
'failed_worker_id' => $failedWorkerId->toString(),
'new_worker_id' => $newWorkerId->toString()
'new_worker_id' => $newWorkerId->toString(),
]));
} catch (\Exception $e) {
$this->logger->warning('Failed to log failover event', LogContext::withData([
'job_id' => $jobId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
}
}
@@ -424,15 +430,16 @@ final readonly class FailoverRecoveryService
'failed_workers' => (int) $stats['failed_workers'],
'recovery_workers' => (int) $stats['recovery_workers'],
'affected_jobs' => (int) $stats['affected_jobs'],
'avg_time_since_failover' => round((float) $stats['avg_time_since_failover'], 2)
'avg_time_since_failover' => round((float) $stats['avg_time_since_failover'], 2),
],
'daily_trends' => $trends
'daily_trends' => $trends,
];
} catch (\Exception $e) {
$this->logger->error('Failed to get failover statistics', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -495,22 +502,22 @@ final readonly class FailoverRecoveryService
'worker_availability' => round($activeRatio * 100, 1),
'recent_failovers_24h' => $recentFailovers,
'active_locks' => $lockStats['active_locks'],
'expired_locks' => $lockStats['expired_locks']
'expired_locks' => $lockStats['expired_locks'],
],
'calculated_at' => date('Y-m-d H:i:s')
'calculated_at' => date('Y-m-d H:i:s'),
];
} catch (\Exception $e) {
$this->logger->error('Failed to calculate resilience score', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
return [
'score' => 0,
'status' => 'unknown',
'factors' => ['Calculation failed'],
'error' => $e->getMessage()
'error' => $e->getMessage(),
];
}
}
}
}

View File

@@ -4,17 +4,20 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Core\Events\EventDispatcherInterface;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Contracts\JobChainManagerInterface;
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
use App\Framework\Queue\Contracts\QueueInterface;
use App\Framework\Queue\ValueObjects\Job;
use App\Framework\Queue\ValueObjects\JobChain;
use App\Framework\Queue\ValueObjects\ChainExecutionMode;
use App\Framework\Core\Events\EventDispatcherInterface;
use App\Framework\Queue\Events\JobChainStartedEvent;
use App\Framework\Queue\Events\JobChainCompletedEvent;
use App\Framework\Queue\Events\JobChainFailedEvent;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Events\JobChainStartedEvent;
use App\Framework\Queue\Exceptions\ChainNotFoundException;
use App\Framework\Queue\Exceptions\CircularDependencyException;
use App\Framework\Queue\Exceptions\InvalidChainStateException;
use App\Framework\Queue\ValueObjects\ChainExecutionMode;
use App\Framework\Queue\ValueObjects\Job;
use App\Framework\Queue\ValueObjects\JobChain;
final readonly class JobChainExecutionCoordinator
{
@@ -25,7 +28,8 @@ final readonly class JobChainExecutionCoordinator
private EventDispatcherInterface $eventDispatcher,
private DependencyResolutionEngine $resolutionEngine,
private Logger $logger
) {}
) {
}
/**
* Start execution of a job chain
@@ -34,12 +38,12 @@ final readonly class JobChainExecutionCoordinator
{
$chainEntry = $this->chainManager->getChain($chainId);
if (!$chainEntry) {
throw new \RuntimeException("Chain with ID '{$chainId}' not found");
if (! $chainEntry) {
throw ChainNotFoundException::byId($chainId);
}
if (!$chainEntry->isPending()) {
throw new \RuntimeException("Chain '{$chainId}' is not in pending status");
if (! $chainEntry->isPending()) {
throw InvalidChainStateException::notPending($chainId, $chainEntry->status);
}
$jobChain = $chainEntry->getJobChain();
@@ -48,19 +52,20 @@ final readonly class JobChainExecutionCoordinator
'chain_id' => $chainId,
'execution_mode' => $jobChain->executionMode->value,
'job_count' => count($jobChain->jobIds),
'stop_on_failure' => $jobChain->stopOnFailure
'stop_on_failure' => $jobChain->stopOnFailure,
]);
// Validate dependency graph before starting
$validationResults = $this->resolutionEngine->validateDependencyGraph($jobChain->jobIds);
$hasCircularDeps = array_filter($validationResults, fn($result) => !$result['valid']);
$hasCircularDeps = array_filter($validationResults, fn ($result) => ! $result['valid']);
if (!empty($hasCircularDeps)) {
if (! empty($hasCircularDeps)) {
$this->logger->error('Cannot start chain - circular dependencies detected', [
'chain_id' => $chainId,
'circular_dependencies' => $hasCircularDeps
'circular_dependencies' => $hasCircularDeps,
]);
throw new \RuntimeException("Chain '{$chainId}' has circular dependencies");
throw CircularDependencyException::inChain($chainId, $hasCircularDeps);
}
// Mark chain as started
@@ -89,14 +94,14 @@ final readonly class JobChainExecutionCoordinator
{
$this->logger->info('Handling job completion in chain context', [
'job_id' => $jobId,
'successful' => $successful
'successful' => $successful,
]);
// Get all chains this job belongs to
$chains = $this->chainManager->getChainsForJob($jobId);
foreach ($chains as $chainEntry) {
if (!$chainEntry->isRunning()) {
if (! $chainEntry->isRunning()) {
continue;
}
@@ -105,12 +110,13 @@ final readonly class JobChainExecutionCoordinator
$this->logger->info('Processing job completion for chain', [
'chain_id' => $chainEntry->chainId,
'job_id' => $jobId,
'execution_mode' => $jobChain->executionMode->value
'execution_mode' => $jobChain->executionMode->value,
]);
// Handle failure scenarios
if (!$successful && $jobChain->stopOnFailure) {
if (! $successful && $jobChain->stopOnFailure) {
$this->handleChainFailure($chainEntry->chainId, $jobId);
continue;
}
@@ -130,8 +136,8 @@ final readonly class JobChainExecutionCoordinator
{
$chainEntry = $this->chainManager->getChain($chainId);
if (!$chainEntry) {
throw new \RuntimeException("Chain with ID '{$chainId}' not found");
if (! $chainEntry) {
throw ChainNotFoundException::byId($chainId);
}
$progress = $this->chainManager->getChainProgress($chainId);
@@ -148,7 +154,7 @@ final readonly class JobChainExecutionCoordinator
'can_execute' => $dependencyAnalysis['can_execute'],
'dependencies_satisfied' => $dependencyAnalysis['statistics']['satisfied_dependencies'],
'dependencies_total' => $dependencyAnalysis['statistics']['total_dependencies'],
'status' => $this->getJobStatus($jobId) // Would need integration with job status tracking
'status' => $this->getJobStatus($jobId), // Would need integration with job status tracking
];
}
@@ -161,7 +167,7 @@ final readonly class JobChainExecutionCoordinator
'job_statuses' => $jobStatuses,
'started_at' => $chainEntry->startedAt,
'completed_at' => $chainEntry->completedAt,
'metadata' => $chainEntry->getMetadataArray()
'metadata' => $chainEntry->getMetadataArray(),
];
}
@@ -195,13 +201,14 @@ final readonly class JobChainExecutionCoordinator
private function startSequentialExecution(JobChain $jobChain): void
{
$this->logger->info('Starting sequential chain execution', [
'chain_id' => $jobChain->chainId
'chain_id' => $jobChain->chainId,
]);
// Queue the first job that has no unsatisfied dependencies
foreach ($jobChain->jobIds as $jobId) {
if ($this->dependencyManager->canJobBeExecuted($jobId)) {
$this->queueJob($jobId);
break; // Only start first eligible job in sequential mode
}
}
@@ -210,7 +217,7 @@ final readonly class JobChainExecutionCoordinator
private function startParallelExecution(JobChain $jobChain): void
{
$this->logger->info('Starting parallel chain execution', [
'chain_id' => $jobChain->chainId
'chain_id' => $jobChain->chainId,
]);
// Queue all jobs that have no unsatisfied dependencies
@@ -224,7 +231,7 @@ final readonly class JobChainExecutionCoordinator
private function startConditionalExecution(JobChain $jobChain): void
{
$this->logger->info('Starting conditional chain execution', [
'chain_id' => $jobChain->chainId
'chain_id' => $jobChain->chainId,
]);
// Similar to parallel, but with conditional dependency evaluation
@@ -254,14 +261,14 @@ final readonly class JobChainExecutionCoordinator
// Check if any more jobs can be executed
$hasExecutableJobs = false;
foreach ($jobChain->jobIds as $jobId) {
if ($this->dependencyManager->canJobBeExecuted($jobId) && !$this->isJobCompleted($jobId)) {
if ($this->dependencyManager->canJobBeExecuted($jobId) && ! $this->isJobCompleted($jobId)) {
$this->queueJob($jobId);
$hasExecutableJobs = true;
}
}
// Check if chain is complete
if (!$hasExecutableJobs && $this->areAllJobsCompleted($jobChain->jobIds)) {
if (! $hasExecutableJobs && $this->areAllJobsCompleted($jobChain->jobIds)) {
$this->handleChainCompletion($chainEntry->chainId);
}
}
@@ -273,7 +280,7 @@ final readonly class JobChainExecutionCoordinator
// Re-evaluate all dependencies in case conditions have changed
foreach ($jobChain->jobIds as $jobId) {
if ($this->dependencyManager->canJobBeExecuted($jobId) && !$this->isJobCompleted($jobId)) {
if ($this->dependencyManager->canJobBeExecuted($jobId) && ! $this->isJobCompleted($jobId)) {
$this->queueJob($jobId);
}
}
@@ -302,7 +309,7 @@ final readonly class JobChainExecutionCoordinator
{
$this->logger->warning('Chain execution failed', [
'chain_id' => $chainId,
'failed_job_id' => $failedJobId
'failed_job_id' => $failedJobId,
]);
$this->chainManager->markChainAsFailed($chainId);
@@ -344,10 +351,11 @@ final readonly class JobChainExecutionCoordinator
private function areAllJobsCompleted(array $jobIds): bool
{
foreach ($jobIds as $jobId) {
if (!$this->isJobCompleted($jobId)) {
if (! $this->isJobCompleted($jobId)) {
return false;
}
}
return true;
}
}
}

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Logging\Logger;
@@ -31,7 +30,8 @@ final readonly class JobCleanupService
private Logger $logger,
private MemoryMonitor $memoryMonitor,
private JobMemoryManager $memoryManager
) {}
) {
}
/**
* Clean up old completed jobs
@@ -44,7 +44,7 @@ final readonly class JobCleanupService
$this->logger->info('Starting completed jobs cleanup', LogContext::withData([
'older_than_days' => $olderThan->toHours() / 24,
'cutoff_date' => $cutoffDate
'cutoff_date' => $cutoffDate,
]));
try {
@@ -61,14 +61,15 @@ final readonly class JobCleanupService
if ($this->memoryManager->shouldAbortJob('cleanup_completed_jobs')) {
$this->logger->warning('Cleanup aborted due to memory constraints', LogContext::withData([
'deleted_so_far' => $totalDeleted,
'batch_count' => $batchCount
'batch_count' => $batchCount,
]));
break;
}
$deleted = $this->connection->execute(SqlQuery::create($sql, [
'cutoff' => $cutoffDate,
'limit' => self::CLEANUP_BATCH_SIZE
'limit' => self::CLEANUP_BATCH_SIZE,
]));
$totalDeleted += $deleted;
$batchCount++;
@@ -77,7 +78,7 @@ final readonly class JobCleanupService
$this->logger->debug('Deleted batch of completed jobs', LogContext::withData([
'batch_size' => $deleted,
'total_deleted' => $totalDeleted,
'batch_number' => $batchCount
'batch_number' => $batchCount,
]));
}
@@ -92,15 +93,16 @@ final readonly class JobCleanupService
$this->logger->info('Completed jobs cleanup finished', LogContext::withData([
'total_deleted' => $totalDeleted,
'batches_processed' => $batchCount
'batches_processed' => $batchCount,
]));
} catch (\Exception $e) {
$this->connection->rollback();
$this->logger->error('Failed to cleanup completed jobs', LogContext::withData([
'error' => $e->getMessage(),
'deleted_before_error' => $totalDeleted
'deleted_before_error' => $totalDeleted,
]));
throw $e;
}
@@ -117,7 +119,7 @@ final readonly class JobCleanupService
$this->logger->info('Starting failed jobs cleanup', LogContext::withData([
'older_than_days' => $olderThan->toHours() / 24,
'cutoff_date' => $cutoffDate
'cutoff_date' => $cutoffDate,
]));
try {
@@ -135,7 +137,7 @@ final readonly class JobCleanupService
$deleted = $this->connection->execute(SqlQuery::create($sql, [
'cutoff' => $cutoffDate,
'limit' => self::CLEANUP_BATCH_SIZE
'limit' => self::CLEANUP_BATCH_SIZE,
]));
$totalDeleted += $deleted;
@@ -148,14 +150,15 @@ final readonly class JobCleanupService
$this->connection->commit();
$this->logger->info('Failed jobs cleanup finished', LogContext::withData([
'total_deleted' => $totalDeleted
'total_deleted' => $totalDeleted,
]));
} catch (\Exception $e) {
$this->connection->rollback();
$this->logger->error('Failed to cleanup failed jobs', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
@@ -172,7 +175,7 @@ final readonly class JobCleanupService
$this->logger->info('Starting job metrics cleanup', LogContext::withData([
'older_than_days' => $olderThan->toHours() / 24,
'cutoff_date' => $cutoffDate
'cutoff_date' => $cutoffDate,
]));
try {
@@ -189,7 +192,7 @@ final readonly class JobCleanupService
$deleted = $this->connection->execute(SqlQuery::create($sql, [
'cutoff' => $cutoffDate,
'limit' => self::CLEANUP_BATCH_SIZE
'limit' => self::CLEANUP_BATCH_SIZE,
]));
$totalDeleted += $deleted;
@@ -202,14 +205,15 @@ final readonly class JobCleanupService
$this->connection->commit();
$this->logger->info('Job metrics cleanup finished', LogContext::withData([
'total_deleted' => $totalDeleted
'total_deleted' => $totalDeleted,
]));
} catch (\Exception $e) {
$this->connection->rollback();
$this->logger->error('Failed to cleanup job metrics', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
@@ -226,7 +230,7 @@ final readonly class JobCleanupService
$this->logger->info('Starting dead letter jobs cleanup', LogContext::withData([
'older_than_days' => $olderThan->toHours() / 24,
'cutoff_date' => $cutoffDate
'cutoff_date' => $cutoffDate,
]));
try {
@@ -243,7 +247,7 @@ final readonly class JobCleanupService
$deleted = $this->connection->execute(SqlQuery::create($sql, [
'cutoff' => $cutoffDate,
'limit' => self::CLEANUP_BATCH_SIZE
'limit' => self::CLEANUP_BATCH_SIZE,
]));
$totalDeleted += $deleted;
@@ -256,14 +260,15 @@ final readonly class JobCleanupService
$this->connection->commit();
$this->logger->info('Dead letter jobs cleanup finished', LogContext::withData([
'total_deleted' => $totalDeleted
'total_deleted' => $totalDeleted,
]));
} catch (\Exception $e) {
$this->connection->rollback();
$this->logger->error('Failed to cleanup dead letter jobs', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
@@ -287,7 +292,7 @@ final readonly class JobCleanupService
'job_metrics' => 0,
'dead_letter_jobs' => 0,
'total_deleted' => 0,
'errors' => []
'errors' => [],
];
// Clean completed jobs
@@ -360,7 +365,7 @@ final readonly class JobCleanupService
$result = $this->connection->queryOne(SqlQuery::create($sql, [
'cutoff' => $this->calculateCutoffDate(
Duration::fromDays(self::DEFAULT_COMPLETED_RETENTION_DAYS)
)
),
]));
$stats['eligible_completed_jobs'] = $result['count'] ?? 0;
@@ -372,7 +377,7 @@ final readonly class JobCleanupService
$result = $this->connection->queryOne(SqlQuery::create($sql, [
'cutoff' => $this->calculateCutoffDate(
Duration::fromDays(self::DEFAULT_FAILED_RETENTION_DAYS)
)
),
]));
$stats['eligible_failed_jobs'] = $result['count'] ?? 0;
@@ -383,7 +388,7 @@ final readonly class JobCleanupService
$result = $this->connection->queryOne(SqlQuery::create($sql, [
'cutoff' => $this->calculateCutoffDate(
Duration::fromDays(self::DEFAULT_METRICS_RETENTION_DAYS)
)
),
]));
$stats['eligible_metrics'] = $result['count'] ?? 0;
@@ -394,7 +399,7 @@ final readonly class JobCleanupService
$result = $this->connection->queryOne(SqlQuery::create($sql, [
'cutoff' => $this->calculateCutoffDate(
Duration::fromDays(self::DEFAULT_DEAD_LETTER_RETENTION_DAYS)
)
),
]));
$stats['eligible_dead_letter_jobs'] = $result['count'] ?? 0;
@@ -411,13 +416,14 @@ final readonly class JobCleanupService
'completed_jobs' => self::DEFAULT_COMPLETED_RETENTION_DAYS,
'failed_jobs' => self::DEFAULT_FAILED_RETENTION_DAYS,
'metrics' => self::DEFAULT_METRICS_RETENTION_DAYS,
'dead_letter_jobs' => self::DEFAULT_DEAD_LETTER_RETENTION_DAYS
'dead_letter_jobs' => self::DEFAULT_DEAD_LETTER_RETENTION_DAYS,
];
} catch (\Exception $e) {
$this->logger->error('Failed to get cleanup statistics', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
@@ -431,6 +437,7 @@ final readonly class JobCleanupService
{
$seconds = $olderThan->toSeconds();
$cutoffTimestamp = time() - (int) $seconds;
return date('Y-m-d H:i:s', $cutoffTimestamp);
}
}
}

View File

@@ -4,17 +4,17 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\Interfaces\DistributedLockInterface;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;
/**
* Service für intelligente Job-Verteilung an Worker
@@ -29,7 +29,8 @@ final readonly class JobDistributionService
private DistributedLockInterface $distributedLock,
private ConnectionInterface $connection,
private Logger $logger
) {}
) {
}
/**
* Job an besten verfügbaren Worker verteilen
@@ -38,14 +39,14 @@ final readonly class JobDistributionService
{
$this->logger->info('Starting job distribution', LogContext::withData([
'job_id' => $jobId->toString(),
'queue_name' => $queueName->toString()
'queue_name' => $queueName->toString(),
]));
// Lock für Distribution-Prozess erwerben
$distributionLock = LockKey::forQueue($queueName)->withSuffix('distribution');
$tempWorkerId = WorkerId::generate(); // Temporäre Worker ID für Distribution Lock
if (!$this->distributedLock->acquireWithTimeout(
if (! $this->distributedLock->acquireWithTimeout(
$distributionLock,
$tempWorkerId,
Duration::fromSeconds(self::DISTRIBUTION_LOCK_TTL_SECONDS),
@@ -53,47 +54,51 @@ final readonly class JobDistributionService
)) {
$this->logger->warning('Failed to acquire distribution lock', LogContext::withData([
'job_id' => $jobId->toString(),
'queue_name' => $queueName->toString()
'queue_name' => $queueName->toString(),
]));
return null;
}
try {
// Job Lock erwerben
$jobLock = LockKey::forJob($jobId);
if (!$this->distributedLock->acquire(
if (! $this->distributedLock->acquire(
$jobLock,
$tempWorkerId,
Duration::fromSeconds(self::JOB_LOCK_TTL_SECONDS)
)) {
$this->logger->warning('Job already locked by another process', LogContext::withData([
'job_id' => $jobId->toString()
'job_id' => $jobId->toString(),
]));
return null;
}
// Besten Worker finden
$worker = $this->findBestWorkerForJob($queueName, $jobData);
if (!$worker) {
if (! $worker) {
$this->logger->warning('No suitable worker found', LogContext::withData([
'job_id' => $jobId->toString(),
'queue_name' => $queueName->toString()
'queue_name' => $queueName->toString(),
]));
// Job Lock freigeben
$this->distributedLock->release($jobLock, $tempWorkerId);
return null;
}
// Job Lock an Worker übertragen
if (!$this->transferJobLock($jobLock, $tempWorkerId, $worker->id)) {
if (! $this->transferJobLock($jobLock, $tempWorkerId, $worker->id)) {
$this->logger->error('Failed to transfer job lock to worker', LogContext::withData([
'job_id' => $jobId->toString(),
'worker_id' => $worker->id->toString()
'worker_id' => $worker->id->toString(),
]));
// Job Lock freigeben
$this->distributedLock->release($jobLock, $tempWorkerId);
return null;
}
@@ -104,7 +109,7 @@ final readonly class JobDistributionService
'job_id' => $jobId->toString(),
'worker_id' => $worker->id->toString(),
'queue_name' => $queueName->toString(),
'worker_load' => $worker->getLoadPercentage()->getValue()
'worker_load' => $worker->getLoadPercentage()->getValue(),
]));
return $worker->id;
@@ -131,14 +136,14 @@ final readonly class JobDistributionService
$scoredWorkers = [];
foreach ($workers as $worker) {
if (!$worker->isAvailableForJobs()) {
if (! $worker->isAvailableForJobs()) {
continue;
}
$score = $this->calculateWorkerScore($worker, $jobData);
$scoredWorkers[] = [
'worker' => $worker,
'score' => $score
'score' => $score,
];
}
@@ -147,7 +152,7 @@ final readonly class JobDistributionService
}
// Nach Score sortieren (höchster Score = bester Worker)
usort($scoredWorkers, fn($a, $b) => $b['score'] <=> $a['score']);
usort($scoredWorkers, fn ($a, $b) => $b['score'] <=> $a['score']);
return $scoredWorkers[0]['worker'];
}
@@ -197,7 +202,7 @@ final readonly class JobDistributionService
private function transferJobLock(LockKey $jobLock, WorkerId $fromWorker, WorkerId $toWorker): bool
{
// Neuen Lock für Ziel-Worker erstellen
if (!$this->distributedLock->acquire(
if (! $this->distributedLock->acquire(
$jobLock,
$toWorker,
Duration::fromSeconds(self::JOB_LOCK_TTL_SECONDS)
@@ -228,14 +233,14 @@ final readonly class JobDistributionService
$this->connection->execute(SqlQuery::create($sql, [
'job_id' => $jobId->toString(),
'worker_id' => $workerId->toString(),
'queue_name' => $queueName->toString()
'queue_name' => $queueName->toString(),
]));
} catch (\Exception $e) {
$this->logger->error('Failed to record job assignment', LogContext::withData([
'job_id' => $jobId->toString(),
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
// Nicht critical - Job kann trotzdem verarbeitet werden
}
@@ -248,7 +253,7 @@ final readonly class JobDistributionService
{
$this->logger->info('Releasing job from worker', LogContext::withData([
'job_id' => $jobId->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
// Job Lock freigeben
@@ -263,20 +268,20 @@ final readonly class JobDistributionService
$this->connection->execute(SqlQuery::create($sql, [
'job_id' => $jobId->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
} catch (\Exception $e) {
$this->logger->warning('Failed to remove job assignment record', LogContext::withData([
'job_id' => $jobId->toString(),
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
}
$this->logger->info('Job released successfully', LogContext::withData([
'job_id' => $jobId->toString(),
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
}
@@ -291,6 +296,7 @@ final readonly class JobDistributionService
$extension = $extension ?? Duration::fromSeconds(self::JOB_LOCK_TTL_SECONDS);
$jobLock = LockKey::forJob($jobId);
return $this->distributedLock->extend($jobLock, $workerId, $extension);
}
@@ -300,7 +306,7 @@ final readonly class JobDistributionService
public function releaseAllWorkerJobs(WorkerId $workerId): int
{
$this->logger->info('Releasing all jobs for worker', LogContext::withData([
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
// Alle Locks des Workers freigeben
@@ -310,19 +316,19 @@ final readonly class JobDistributionService
try {
$sql = "DELETE FROM job_assignments WHERE worker_id = :worker_id";
$releasedAssignments = $this->connection->execute(SqlQuery::create($sql, [
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
$this->logger->info('Released all worker jobs', LogContext::withData([
'worker_id' => $workerId->toString(),
'released_locks' => $releasedLocks,
'released_assignments' => $releasedAssignments
'released_assignments' => $releasedAssignments,
]));
} catch (\Exception $e) {
$this->logger->error('Failed to release worker job assignments', LogContext::withData([
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
}
@@ -356,7 +362,7 @@ final readonly class JobDistributionService
'current_jobs' => $worker->currentJobs,
'max_jobs' => $worker->maxJobs,
'load_percentage' => $worker->getLoadPercentage()->getValue(),
'queues' => array_map(fn($q) => $q->toString(), $worker->queues)
'queues' => array_map(fn ($q) => $q->toString(), $worker->queues),
];
}
@@ -368,17 +374,18 @@ final readonly class JobDistributionService
'total' => (int) $assignmentStats['total_assignments'],
'active_workers' => (int) $assignmentStats['active_workers'],
'active_queues' => (int) $assignmentStats['active_queues'],
'avg_assignment_age_seconds' => round((float) $assignmentStats['avg_assignment_age_seconds'], 2)
'avg_assignment_age_seconds' => round((float) $assignmentStats['avg_assignment_age_seconds'], 2),
],
'workers' => $workerStats,
'locks' => $lockStats,
'distribution_health' => $this->calculateDistributionHealth($workerStats)
'distribution_health' => $this->calculateDistributionHealth($workerStats),
];
} catch (\Exception $e) {
$this->logger->error('Failed to get distribution statistics', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -392,7 +399,7 @@ final readonly class JobDistributionService
return [
'score' => 0,
'status' => 'critical',
'issues' => ['No active workers']
'issues' => ['No active workers'],
];
}
@@ -421,7 +428,7 @@ final readonly class JobDistributionService
}
// Worker Availability prüfen
$availableWorkers = array_filter($workerStats, fn($w) => $w['load_percentage'] < 80);
$availableWorkers = array_filter($workerStats, fn ($w) => $w['load_percentage'] < 80);
$availabilityRatio = count($availableWorkers) / count($workerStats);
if ($availabilityRatio < 0.3) {
@@ -443,7 +450,7 @@ final readonly class JobDistributionService
'max_load' => $maxLoad,
'load_variance' => $loadVariance,
'availability_ratio' => round($availabilityRatio * 100, 2),
'issues' => $issues
'issues' => $issues,
];
}
}
}

View File

@@ -6,11 +6,11 @@ namespace App\Framework\Queue\Services;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Performance\ValueObjects\MemorySummary;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
/**
* Manages memory usage and optimization for queue jobs using framework's performance monitoring
@@ -24,7 +24,8 @@ final readonly class JobMemoryManager
public function __construct(
private MemoryMonitor $memoryMonitor,
private Logger $logger
) {}
) {
}
/**
* Get current memory usage for job context
@@ -47,7 +48,7 @@ final readonly class JobMemoryManager
'usage_percentage' => $summary->getUsagePercentageFormatted(),
'is_critical' => $summary->isMemoryCritical(self::CRITICAL_MEMORY_THRESHOLD),
'is_warning' => $summary->isMemoryLow(self::WARNING_MEMORY_THRESHOLD),
'status' => $this->getMemoryStatus($summary)
'status' => $this->getMemoryStatus($summary),
];
}
@@ -65,14 +66,14 @@ final readonly class JobMemoryManager
'phase' => $phase,
'memory' => $summary->getCurrentHumanReadable(),
'usage' => $summary->getUsagePercentageFormatted(),
'available' => $summary->getAvailableMemory()->toHumanReadable()
'available' => $summary->getAvailableMemory()->toHumanReadable(),
]));
} elseif ($summary->isMemoryLow(self::WARNING_MEMORY_THRESHOLD)) {
$this->logger->warning('High memory usage during job execution', LogContext::withData([
'job_id' => $jobId,
'phase' => $phase,
'memory' => $summary->getCurrentHumanReadable(),
'usage' => $summary->getUsagePercentageFormatted()
'usage' => $summary->getUsagePercentageFormatted(),
]));
}
@@ -91,7 +92,7 @@ final readonly class JobMemoryManager
'job_id' => $jobId,
'memory' => $summary->getCurrentHumanReadable(),
'usage' => $summary->getUsagePercentageFormatted(),
'limit' => $summary->getLimitHumanReadable()
'limit' => $summary->getLimitHumanReadable(),
]));
return true;
@@ -123,14 +124,14 @@ final readonly class JobMemoryManager
'freed' => $freedMemory->toHumanReadable(),
'freed_bytes' => $freedMemory->toBytes(),
'usage_before' => $before->getUsagePercentageFormatted(),
'usage_after' => $after->getUsagePercentageFormatted()
'usage_after' => $after->getUsagePercentageFormatted(),
];
$this->logger->debug('Memory optimized for job', LogContext::withData([
'job_id' => $jobId,
'freed' => $freedMemory->toHumanReadable(),
'usage_before' => $before->getUsagePercentageFormatted(),
'usage_after' => $after->getUsagePercentageFormatted()
'usage_after' => $after->getUsagePercentageFormatted(),
]));
return $optimization;
@@ -148,7 +149,7 @@ final readonly class JobMemoryManager
'peak_memory' => Byte::fromBytes(0),
'total_memory' => Byte::fromBytes(0),
'efficiency_score' => Percentage::from(100),
'efficiency_rating' => 'excellent'
'efficiency_rating' => 'excellent',
];
}
@@ -187,7 +188,7 @@ final readonly class JobMemoryManager
'total_memory_human' => $totalMemory->toHumanReadable(),
'efficiency_score' => $efficiencyScore,
'efficiency_score_formatted' => $efficiencyScore->format(2),
'efficiency_rating' => $this->getEfficiencyRating($efficiencyScore)
'efficiency_rating' => $this->getEfficiencyRating($efficiencyScore),
];
}
@@ -209,8 +210,8 @@ final readonly class JobMemoryManager
'Increase memory limit or reduce job complexity',
'Force garbage collection before new jobs',
'Split large jobs into smaller chunks',
'Consider job queue throttling'
]
'Consider job queue throttling',
],
];
} elseif ($summary->isMemoryLow(self::WARNING_MEMORY_THRESHOLD)) {
$priority = 'warning';
@@ -221,8 +222,8 @@ final readonly class JobMemoryManager
'Schedule periodic garbage collection',
'Review job memory patterns',
'Consider optimizing job processing',
'Monitor for memory leaks'
]
'Monitor for memory leaks',
],
];
} else {
$recommendations[] = [
@@ -230,8 +231,8 @@ final readonly class JobMemoryManager
'message' => 'Memory usage is within normal range.',
'actions' => [
'Continue monitoring',
'Maintain current optimization strategies'
]
'Maintain current optimization strategies',
],
];
}
@@ -240,7 +241,7 @@ final readonly class JobMemoryManager
'current_usage' => $summary->getUsagePercentageFormatted(),
'available' => $summary->getAvailableMemory()->toHumanReadable(),
'recommendations' => $recommendations,
'timestamp' => date('Y-m-d H:i:s')
'timestamp' => date('Y-m-d H:i:s'),
];
}
@@ -289,4 +290,4 @@ final readonly class JobMemoryManager
default => 'critical'
};
}
}
}

View File

@@ -4,19 +4,19 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Queue\ValueObjects\QueueMetrics;
use App\Framework\Queue\Entities\JobMetricsEntry;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Logging\Logger;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Queue\Entities\JobMetricsEntry;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Queue\ValueObjects\QueueMetrics;
final readonly class JobMetricsManager
{
public function __construct(
private ConnectionInterface $connection,
private Logger $logger
) {}
) {
}
public function recordJobMetrics(JobMetrics $metrics): void
{
@@ -37,13 +37,14 @@ final readonly class JobMetricsManager
'queue_name' => $metrics->queueName,
'status' => $metrics->status,
'execution_time_ms' => $metrics->executionTimeMs,
'memory_usage_mb' => $metrics->getMemoryUsageMB()
'memory_usage_mb' => $metrics->getMemoryUsageMB(),
]);
}
public function getJobMetrics(string $jobId): ?JobMetrics
{
$entry = $this->findJobMetricsEntry($jobId);
return $entry ? $entry->getJobMetrics() : null;
}
@@ -57,7 +58,7 @@ final readonly class JobMetricsManager
$results = $this->connection->fetchAll($query, [$queueName, $windowStart]);
$jobMetrics = array_map(function(array $row) {
$jobMetrics = array_map(function (array $row) {
return $this->mapRowToJobMetrics($row);
}, $results);
@@ -86,7 +87,7 @@ final readonly class JobMetricsManager
$results = $this->connection->fetchAll($query, [$jobId]);
return array_map(function(array $row) {
return array_map(function (array $row) {
return $this->mapRowToJobMetrics($row);
}, $results);
}
@@ -104,7 +105,7 @@ final readonly class JobMetricsManager
$params[] = $limit;
$results = $this->connection->fetchAll($query, $params);
return array_map(function(array $row) {
return array_map(function (array $row) {
return $this->mapRowToJobMetrics($row);
}, $results);
}
@@ -122,7 +123,7 @@ final readonly class JobMetricsManager
$params[] = $limit;
$results = $this->connection->fetchAll($query, $params);
return array_map(function(array $row) {
return array_map(function (array $row) {
return $this->mapRowToJobMetrics($row);
}, $results);
}
@@ -145,7 +146,7 @@ final readonly class JobMetricsManager
$results = $this->connection->fetchAll($query, $params);
return array_map(function(array $row) {
return array_map(function (array $row) {
return $this->mapRowToJobMetrics($row);
}, $results);
}
@@ -199,7 +200,7 @@ final readonly class JobMetricsManager
'min_memory_usage_bytes' => (int) $result['min_memory_usage_bytes'],
'max_memory_usage_bytes' => (int) $result['max_memory_usage_bytes'],
'average_memory_usage_mb' => (int) $result['avg_memory_usage_bytes'] / (1024 * 1024),
'average_attempts' => (float) $result['avg_attempts']
'average_attempts' => (float) $result['avg_attempts'],
];
}
@@ -235,7 +236,7 @@ final readonly class JobMetricsManager
'queue_name' => $queueName,
'total_completed' => $totalCompleted,
'average_throughput_per_hour' => $avgThroughput,
'hourly_breakdown' => $hourlyStats
'hourly_breakdown' => $hourlyStats,
];
}
@@ -247,13 +248,13 @@ final readonly class JobMetricsManager
$this->logger->info('Cleaning up old job metrics', [
'cutoff_date' => $cutoffDate,
'older_than_days' => $olderThanDays
'older_than_days' => $olderThanDays,
]);
$affectedRows = $this->connection->execute($query, [$cutoffDate]);
$this->logger->info('Old job metrics cleaned up', [
'deleted_count' => $affectedRows
'deleted_count' => $affectedRows,
]);
return $affectedRows;
@@ -301,7 +302,7 @@ final readonly class JobMetricsManager
'overall_success_rate' => $totalJobs > 0 ? ($totalCompleted / $totalJobs) * 100 : 100,
'average_execution_time_ms' => $queueCount > 0 ? $avgExecutionTime / $queueCount : 0,
'average_memory_usage_mb' => $queueCount > 0 ? $avgMemoryUsage / $queueCount : 0,
'queue_metrics' => $allMetrics
'queue_metrics' => $allMetrics,
];
}
@@ -310,7 +311,7 @@ final readonly class JobMetricsManager
$query = "SELECT * FROM job_metrics WHERE job_id = ? ORDER BY updated_at DESC LIMIT 1";
$result = $this->connection->fetchOne($query, [$jobId]);
if (!$result) {
if (! $result) {
return null;
}
@@ -356,4 +357,4 @@ final readonly class JobMetricsManager
metadata: $row['metadata'] ? json_decode($row['metadata'], true) : []
);
}
}
}

View File

@@ -28,4 +28,4 @@ interface JobMetricsManagerInterface
public function getThroughputStats(?string $queueName, string $timeWindow): array;
public function cleanupOldMetrics(int $olderThanDays): int;
}
}

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Logging\Logger;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Queue\ValueObjects\QueueMetrics;
use App\Framework\Logging\Logger;
use App\Framework\Core\ValueObjects\Percentage;
/**
* Mock implementation of JobMetricsManager for demonstration purposes
@@ -16,7 +16,8 @@ final readonly class MockJobMetricsManager implements JobMetricsManagerInterface
{
public function __construct(
private Logger $logger
) {}
) {
}
public function recordJobMetrics(JobMetrics $metrics): void
{
@@ -45,7 +46,7 @@ final readonly class MockJobMetricsManager implements JobMetricsManagerInterface
'emails' => $this->createMockQueueMetrics('emails', 850, 95.2),
'notifications' => $this->createMockQueueMetrics('notifications', 300, 98.0),
'reports' => $this->createMockQueueMetrics('reports', 100, 85.0),
]
],
];
}
@@ -97,6 +98,7 @@ final readonly class MockJobMetricsManager implements JobMetricsManagerInterface
metadata: []
);
}
return $jobs;
}
@@ -120,6 +122,7 @@ final readonly class MockJobMetricsManager implements JobMetricsManagerInterface
metadata: []
);
}
return $jobs;
}
@@ -143,6 +146,7 @@ final readonly class MockJobMetricsManager implements JobMetricsManagerInterface
metadata: []
);
}
return $jobs;
}
@@ -170,14 +174,14 @@ final readonly class MockJobMetricsManager implements JobMetricsManagerInterface
for ($hour = 0; $hour < 24; $hour++) {
$hourlyData[] = [
'hour' => sprintf('2024-01-15 %02d:00:00', $hour),
'jobs_completed' => rand(10, 50)
'jobs_completed' => rand(10, 50),
];
}
return [
'total_completed' => 800,
'average_throughput_per_hour' => 33.3,
'hourly_breakdown' => $hourlyData
'hourly_breakdown' => $hourlyData,
];
}
@@ -207,4 +211,4 @@ final readonly class MockJobMetricsManager implements JobMetricsManagerInterface
measuredAt: date('Y-m-d H:i:s')
);
}
}
}

View File

@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Queue\ValueObjects\QueueMetrics;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Logging\Logger;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Queue\ValueObjects\QueueMetrics;
/**
* Production implementation of JobMetricsManager with full database integration
@@ -20,7 +20,8 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
public function __construct(
private ConnectionInterface $connection,
private Logger $logger
) {}
) {
}
public function recordJobMetrics(JobMetrics $metrics): void
{
@@ -49,7 +50,7 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
$this->logger->error('Failed to record job metrics', [
'job_id' => $metrics->jobId,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
throw $e;
@@ -63,7 +64,7 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
SqlQuery::create($query, [$jobId])
);
if (!$result) {
if (! $result) {
return null;
}
@@ -82,13 +83,51 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
SqlQuery::create($query, [$queueName, $windowStart])
)->fetchAll();
$jobMetrics = array_map(function(array $row) {
$jobMetrics = array_map(function (array $row) {
return $this->mapRowToJobMetrics($row);
}, $results);
return $this->calculateQueueMetrics($queueName, $jobMetrics, $timeWindow);
}
/**
* Get metrics for all queues in the time window
* Optimized to avoid N+1 query problem
*
* @return array<QueueMetrics>
*/
public function getAllQueueMetrics(string $timeWindow = '1 hour'): array
{
$windowStart = date('Y-m-d H:i:s', strtotime("-{$timeWindow}"));
// Get all metrics in a single query and aggregate in PHP
$query = "SELECT * FROM job_metrics
WHERE created_at >= ?
ORDER BY queue_name, created_at DESC";
$results = $this->connection->query(
SqlQuery::create($query, [$windowStart])
)->fetchAll();
// Group by queue name
$metricsByQueue = [];
foreach ($results as $row) {
$queueName = $row['queue_name'];
if (!isset($metricsByQueue[$queueName])) {
$metricsByQueue[$queueName] = [];
}
$metricsByQueue[$queueName][] = $this->mapRowToJobMetrics($row);
}
// Calculate metrics for each queue
$allMetrics = [];
foreach ($metricsByQueue as $queueName => $jobMetrics) {
$allMetrics[] = $this->calculateQueueMetrics($queueName, $jobMetrics, $timeWindow);
}
return $allMetrics;
}
public function getSystemOverview(): array
{
$windowStart = date('Y-m-d H:i:s', strtotime('-24 hours'));
@@ -149,7 +188,7 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
'overall_success_rate' => round($successRate, 1),
'average_execution_time_ms' => round((float) $overallStats['avg_execution_time'], 1),
'average_memory_usage_mb' => round((float) $overallStats['avg_memory_mb'], 1),
'queue_metrics' => $queueMetrics
'queue_metrics' => $queueMetrics,
];
}
@@ -310,7 +349,7 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
return [
'total_completed' => $totalCompleted,
'average_throughput_per_hour' => $averageThroughput,
'hourly_breakdown' => $hourlyStats
'hourly_breakdown' => $hourlyStats,
];
}
@@ -325,7 +364,7 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
$this->logger->info('Job metrics cleanup completed', [
'cutoff_date' => $cutoffDate,
'deleted_rows' => $affectedRows
'deleted_rows' => $affectedRows,
]);
return $affectedRows;
@@ -354,7 +393,7 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
$metrics->completedAt,
$metrics->failedAt,
$metrics->errorMessage,
$metrics->metadata ? json_encode($metrics->metadata) : null
$metrics->metadata ? json_encode($metrics->metadata) : null,
])
);
}
@@ -378,7 +417,7 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
$metrics->failedAt,
$metrics->errorMessage,
$metrics->metadata ? json_encode($metrics->metadata) : null,
$metrics->jobId
$metrics->jobId,
])
);
}
@@ -440,18 +479,23 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
switch ($metrics->status) {
case 'completed':
$completed++;
break;
case 'failed':
$failed++;
break;
case 'pending':
$pending++;
break;
case 'running':
$running++;
break;
case 'dead_letter':
$deadLetter++;
break;
}
@@ -501,10 +545,11 @@ final readonly class ProductionJobMetricsManager implements JobMetricsManagerInt
try {
// Use framework's Duration class instead of manual parsing
$duration = Duration::parse($timeWindow);
return $duration->toHours();
} catch (\Exception) {
// Fallback to 1 hour if parsing fails
return 1.0;
}
}
}
}

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Queue\Contracts\JobProgressTrackerInterface;
use App\Framework\Queue\ValueObjects\JobProgress;
use App\Framework\Queue\ValueObjects\ProgressStep;
use App\Framework\Core\ValueObjects\Percentage;
/**
* High-level progress management service for coordinating job progress tracking
@@ -16,7 +16,8 @@ final readonly class ProgressManager
{
public function __construct(
private JobProgressTrackerInterface $progressTracker
) {}
) {
}
public function startJob(string $jobId, string $message = 'Job started'): void
{
@@ -96,4 +97,4 @@ final readonly class ProgressManager
{
return new StepProgressTracker($jobId, $steps, $this);
}
}
}

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\ValueObjects\ProgressStep;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Queue\Exceptions\AllStepsCompletedException;
/**
* Helper class for tracking progress through multiple steps
@@ -13,6 +13,7 @@ use App\Framework\Core\ValueObjects\Percentage;
final class StepProgressTracker
{
private int $currentStepIndex = 0;
private array $completedSteps = [];
public function __construct(
@@ -26,7 +27,7 @@ final class StepProgressTracker
// Validate step structure
foreach ($steps as $index => $step) {
if (!is_array($step) || !isset($step['name'], $step['description'])) {
if (! is_array($step) || ! isset($step['name'], $step['description'])) {
throw new \InvalidArgumentException("Step at index {$index} must have 'name' and 'description' keys");
}
}
@@ -40,7 +41,7 @@ final class StepProgressTracker
public function completeCurrentStep(?array $metadata = null): void
{
if ($this->currentStepIndex >= count($this->steps)) {
throw new \RuntimeException('All steps have already been completed');
throw AllStepsCompletedException::forJob($this->jobId, count($this->steps));
}
$step = $this->steps[$this->currentStepIndex];
@@ -71,7 +72,7 @@ final class StepProgressTracker
[
'current_step' => $this->currentStepIndex,
'total_steps' => count($this->steps),
'completed_steps' => $this->completedSteps
'completed_steps' => $this->completedSteps,
]
);
}
@@ -82,7 +83,7 @@ final class StepProgressTracker
?array $metadata = null
): void {
if ($this->currentStepIndex >= count($this->steps)) {
throw new \RuntimeException('All steps have already been completed');
throw AllStepsCompletedException::forJob($this->jobId, count($this->steps));
}
// Calculate overall percentage: completed steps + current step progress
@@ -104,7 +105,7 @@ final class StepProgressTracker
'total_steps' => count($this->steps),
'step_name' => $step['name'],
'step_percentage' => $stepPercentage,
'completed_steps' => $this->completedSteps
'completed_steps' => $this->completedSteps,
]);
$this->progressManager->updateJobProgress(
@@ -167,4 +168,4 @@ final class StepProgressTracker
{
return ($this->currentStepIndex / count($this->steps)) * 100;
}
}
}

View File

@@ -4,15 +4,14 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\WorkerId;
/**
* Worker Health Check Service für Monitoring und Überwachung
@@ -30,7 +29,8 @@ final readonly class WorkerHealthCheckService
private WorkerRegistry $workerRegistry,
private ConnectionInterface $connection,
private Logger $logger
) {}
) {
}
/**
* Health Check für alle aktiven Worker durchführen
@@ -62,16 +62,16 @@ final readonly class WorkerHealthCheckService
$this->logger->info('Worker health check completed', LogContext::withData([
'total_workers' => count($workers),
'healthy_workers' => count(array_filter($healthResults, fn($h) => $h['status'] === 'healthy')),
'warning_workers' => count(array_filter($healthResults, fn($h) => $h['status'] === 'warning')),
'critical_workers' => count(array_filter($healthResults, fn($h) => $h['status'] === 'critical')),
'overall_status' => $overallHealth['status']
'healthy_workers' => count(array_filter($healthResults, fn ($h) => $h['status'] === 'healthy')),
'warning_workers' => count(array_filter($healthResults, fn ($h) => $h['status'] === 'warning')),
'critical_workers' => count(array_filter($healthResults, fn ($h) => $h['status'] === 'critical')),
'overall_status' => $overallHealth['status'],
]));
return [
'workers' => $healthResults,
'overall' => $overallHealth,
'checked_at' => date('Y-m-d H:i:s')
'checked_at' => date('Y-m-d H:i:s'),
];
}
@@ -128,15 +128,15 @@ final readonly class WorkerHealthCheckService
}
// Aktiv-Status prüfen
if (!$worker->isActive) {
if (! $worker->isActive) {
$issues[] = "Worker marked as inactive";
$score -= 100;
}
// Status basierend auf Score bestimmen
$status = match (true) {
$score <= 30 || !empty($issues) => 'critical',
$score <= 60 || !empty($warnings) => 'warning',
$score <= 30 || ! empty($issues) => 'critical',
$score <= 60 || ! empty($warnings) => 'warning',
default => 'healthy'
};
@@ -153,11 +153,11 @@ final readonly class WorkerHealthCheckService
'job_load_percent' => $loadPercentage,
'current_jobs' => $worker->currentJobs,
'max_jobs' => $worker->maxJobs,
'is_active' => $worker->isActive
'is_active' => $worker->isActive,
],
'issues' => $issues,
'warnings' => $warnings,
'checked_at' => date('Y-m-d H:i:s')
'checked_at' => date('Y-m-d H:i:s'),
];
}
@@ -179,13 +179,13 @@ final readonly class WorkerHealthCheckService
'score' => $healthData['score'],
'metrics' => json_encode($healthData['metrics']),
'issues' => json_encode($healthData['issues']),
'warnings' => json_encode($healthData['warnings'])
'warnings' => json_encode($healthData['warnings']),
]));
} catch (\Exception $e) {
$this->logger->error('Failed to record worker health status', LogContext::withData([
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
}
}
@@ -199,7 +199,7 @@ final readonly class WorkerHealthCheckService
'worker_id' => $worker->id->toString(),
'hostname' => $worker->hostname,
'issues' => $healthData['issues'],
'score' => $healthData['score']
'score' => $healthData['score'],
]));
// Worker als inaktiv markieren wenn zu viele kritische Issues
@@ -211,13 +211,13 @@ final readonly class WorkerHealthCheckService
$this->logger->warning('Worker deregistered due to critical issues', LogContext::withData([
'worker_id' => $worker->id->toString(),
'hostname' => $worker->hostname,
'critical_issues' => $criticalIssueCount
'critical_issues' => $criticalIssueCount,
]));
} catch (\Exception $e) {
$this->logger->error('Failed to deregister critical worker', LogContext::withData([
'worker_id' => $worker->id->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
}
}
@@ -233,14 +233,14 @@ final readonly class WorkerHealthCheckService
'status' => 'critical',
'score' => 0,
'worker_count' => 0,
'issues' => ['No workers available']
'issues' => ['No workers available'],
];
}
$totalWorkers = count($healthResults);
$healthyWorkers = count(array_filter($healthResults, fn($h) => $h['status'] === 'healthy'));
$warningWorkers = count(array_filter($healthResults, fn($h) => $h['status'] === 'warning'));
$criticalWorkers = count(array_filter($healthResults, fn($h) => $h['status'] === 'critical'));
$healthyWorkers = count(array_filter($healthResults, fn ($h) => $h['status'] === 'healthy'));
$warningWorkers = count(array_filter($healthResults, fn ($h) => $h['status'] === 'warning'));
$criticalWorkers = count(array_filter($healthResults, fn ($h) => $h['status'] === 'critical'));
// Durchschnittlicher Score
$avgScore = array_sum(array_column($healthResults, 'score')) / $totalWorkers;
@@ -274,7 +274,7 @@ final readonly class WorkerHealthCheckService
'warning_workers' => $warningWorkers,
'critical_workers' => $criticalWorkers,
'healthy_percentage' => round($healthyPercentage, 1),
'issues' => $overallIssues
'issues' => $overallIssues,
];
}
@@ -294,7 +294,7 @@ final readonly class WorkerHealthCheckService
$result = $this->connection->query(SqlQuery::create($sql, [
'worker_id' => $workerId->toString(),
'hours' => $period->toHours()
'hours' => $period->toHours(),
]));
$history = [];
@@ -305,7 +305,7 @@ final readonly class WorkerHealthCheckService
'metrics' => json_decode($row['metrics'], true),
'issues' => json_decode($row['issues'], true),
'warnings' => json_decode($row['warnings'], true),
'checked_at' => $row['checked_at']
'checked_at' => $row['checked_at'],
];
}
@@ -314,8 +314,9 @@ final readonly class WorkerHealthCheckService
} catch (\Exception $e) {
$this->logger->error('Failed to get worker health history', LogContext::withData([
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -365,13 +366,14 @@ final readonly class WorkerHealthCheckService
'current_health' => $currentHealth,
'trends_24h' => $trends,
'top_issues_24h' => $topIssues,
'generated_at' => date('Y-m-d H:i:s')
'generated_at' => date('Y-m-d H:i:s'),
];
} catch (\Exception $e) {
$this->logger->error('Failed to generate system health report', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -388,21 +390,22 @@ final readonly class WorkerHealthCheckService
WHERE checked_at < DATE_SUB(NOW(), INTERVAL :hours HOUR)";
$deletedCount = $this->connection->execute(SqlQuery::create($sql, [
'hours' => $retentionPeriod->toHours()
'hours' => $retentionPeriod->toHours(),
]));
$this->logger->info('Health check cleanup completed', LogContext::withData([
'deleted_records' => $deletedCount,
'retention_days' => $retentionPeriod->toHours() / 24
'retention_days' => $retentionPeriod->toHours() / 24,
]));
return $deletedCount;
} catch (\Exception $e) {
$this->logger->error('Failed to cleanup health checks', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
}
}

View File

@@ -4,15 +4,15 @@ declare(strict_types=1);
namespace App\Framework\Queue\Services;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;
/**
* Worker Registry für Distributed Job Processing
@@ -24,7 +24,8 @@ final readonly class WorkerRegistry
public function __construct(
private ConnectionInterface $connection,
private Logger $logger
) {}
) {
}
/**
* Worker registrieren
@@ -36,7 +37,7 @@ final readonly class WorkerRegistry
'hostname' => $worker->hostname,
'process_id' => $worker->processId,
'queues' => $worker->queues,
'max_jobs' => $worker->maxJobs
'max_jobs' => $worker->maxJobs,
]));
try {
@@ -48,29 +49,31 @@ final readonly class WorkerRegistry
:id, :hostname, :process_id, :queues, :max_jobs, :current_jobs,
:is_active, :cpu_usage, :memory_usage_bytes, :registered_at,
:last_heartbeat, :capabilities, :version
) ON DUPLICATE KEY UPDATE
is_active = VALUES(is_active),
last_heartbeat = VALUES(last_heartbeat),
cpu_usage = VALUES(cpu_usage),
memory_usage_bytes = VALUES(memory_usage_bytes),
current_jobs = VALUES(current_jobs),
capabilities = VALUES(capabilities),
version = VALUES(version)";
) ON CONFLICT (id) DO UPDATE SET
is_active = EXCLUDED.is_active,
last_heartbeat = EXCLUDED.last_heartbeat,
cpu_usage = EXCLUDED.cpu_usage,
memory_usage_bytes = EXCLUDED.memory_usage_bytes,
current_jobs = EXCLUDED.current_jobs,
capabilities = EXCLUDED.capabilities,
version = EXCLUDED.version";
$data = $worker->toArray();
$data['is_active'] = $data['is_active'] ? 1 : 0;
// PostgreSQL expects boolean values directly
$data['is_active'] = $data['is_active'] ? true : false;
$this->connection->execute(SqlQuery::create($sql, $data));
$this->logger->debug('Worker registered successfully', LogContext::withData([
'worker_id' => $worker->id->toString()
'worker_id' => $worker->id->toString(),
]));
} catch (\Exception $e) {
$this->logger->error('Failed to register worker', LogContext::withData([
'worker_id' => $worker->id->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -81,25 +84,26 @@ final readonly class WorkerRegistry
public function deregister(WorkerId $workerId): void
{
$this->logger->info('Deregistering worker', LogContext::withData([
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
try {
$sql = "UPDATE " . self::TABLE_NAME . "
SET is_active = 0, last_heartbeat = NOW()
SET is_active = false, last_heartbeat = NOW()
WHERE id = :id";
$this->connection->execute(SqlQuery::create($sql, ['id' => $workerId->toString()]));
$this->logger->debug('Worker deregistered successfully', LogContext::withData([
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
} catch (\Exception $e) {
$this->logger->error('Failed to deregister worker', LogContext::withData([
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -119,27 +123,28 @@ final readonly class WorkerRegistry
cpu_usage = :cpu_usage,
memory_usage_bytes = :memory_usage_bytes,
current_jobs = :current_jobs,
is_active = 1
is_active = true
WHERE id = :id";
$affectedRows = $this->connection->execute(SqlQuery::create($sql, [
'id' => $workerId->toString(),
'cpu_usage' => $cpuUsage->getValue(),
'memory_usage_bytes' => $memoryUsage->toBytes(),
'current_jobs' => $currentJobs
'current_jobs' => $currentJobs,
]));
if ($affectedRows === 0) {
$this->logger->warning('Heartbeat update failed - worker not found', LogContext::withData([
'worker_id' => $workerId->toString()
'worker_id' => $workerId->toString(),
]));
}
} catch (\Exception $e) {
$this->logger->error('Failed to update worker heartbeat', LogContext::withData([
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -152,7 +157,7 @@ final readonly class WorkerRegistry
try {
$sql = "SELECT * FROM " . self::TABLE_NAME . " WHERE id = :id";
$data = $this->connection->queryOne(SqlQuery::create($sql, ['id' => $workerId->toString()]));
if (!$data) {
if (! $data) {
return null;
}
@@ -161,8 +166,9 @@ final readonly class WorkerRegistry
} catch (\Exception $e) {
$this->logger->error('Failed to find worker by ID', LogContext::withData([
'worker_id' => $workerId->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -174,8 +180,8 @@ final readonly class WorkerRegistry
{
try {
$sql = "SELECT * FROM " . self::TABLE_NAME . "
WHERE is_active = 1
AND last_heartbeat > DATE_SUB(NOW(), INTERVAL 2 MINUTE)
WHERE is_active = true
AND last_heartbeat > NOW() - INTERVAL '2 minutes'
ORDER BY hostname, process_id";
$result = $this->connection->query(SqlQuery::create($sql));
@@ -188,8 +194,9 @@ final readonly class WorkerRegistry
} catch (\Exception $e) {
$this->logger->error('Failed to find active workers', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -201,9 +208,9 @@ final readonly class WorkerRegistry
{
try {
$sql = "SELECT * FROM " . self::TABLE_NAME . "
WHERE is_active = 1
AND last_heartbeat > DATE_SUB(NOW(), INTERVAL 2 MINUTE)
AND JSON_CONTAINS(queues, JSON_QUOTE(:queue_name))
WHERE is_active = true
AND last_heartbeat > NOW() - INTERVAL '2 minutes'
AND queues::jsonb @> to_jsonb(:queue_name::text)
ORDER BY current_jobs ASC, cpu_usage ASC";
$result = $this->connection->query(SqlQuery::create($sql, ['queue_name' => $queueName->toString()]));
@@ -220,8 +227,9 @@ final readonly class WorkerRegistry
} catch (\Exception $e) {
$this->logger->error('Failed to find workers for queue', LogContext::withData([
'queue_name' => $queueName->toString(),
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -251,28 +259,29 @@ final readonly class WorkerRegistry
public function cleanupInactiveWorkers(int $inactiveMinutes = 5): int
{
$this->logger->info('Starting cleanup of inactive workers', LogContext::withData([
'inactive_minutes' => $inactiveMinutes
'inactive_minutes' => $inactiveMinutes,
]));
try {
$sql = "UPDATE " . self::TABLE_NAME . "
SET is_active = 0
WHERE is_active = 1
AND (last_heartbeat IS NULL OR last_heartbeat < DATE_SUB(NOW(), INTERVAL :minutes MINUTE))";
SET is_active = false
WHERE is_active = true
AND (last_heartbeat IS NULL OR last_heartbeat < NOW() - INTERVAL '1 minute' * :minutes)";
$count = $this->connection->execute(SqlQuery::create($sql, ['minutes' => $inactiveMinutes]));
$this->logger->info('Inactive workers cleanup completed', LogContext::withData([
'deactivated_count' => $count,
'inactive_minutes' => $inactiveMinutes
'inactive_minutes' => $inactiveMinutes,
]));
return $count;
} catch (\Exception $e) {
$this->logger->error('Failed to cleanup inactive workers', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -285,8 +294,8 @@ final readonly class WorkerRegistry
try {
$sql = "SELECT
COUNT(*) as total_workers,
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_workers,
SUM(CASE WHEN is_active = 1 AND last_heartbeat > DATE_SUB(NOW(), INTERVAL 2 MINUTE) THEN 1 ELSE 0 END) as healthy_workers,
SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) as active_workers,
SUM(CASE WHEN is_active = true AND last_heartbeat > NOW() - INTERVAL '2 minutes' THEN 1 ELSE 0 END) as healthy_workers,
SUM(max_jobs) as total_capacity,
SUM(current_jobs) as current_load,
AVG(cpu_usage) as avg_cpu_usage,
@@ -311,13 +320,14 @@ final readonly class WorkerRegistry
: 0,
'avg_cpu_usage' => round((float) $stats['avg_cpu_usage'], 2),
'avg_memory_usage_mb' => round(((float) $stats['avg_memory_usage']) / 1024 / 1024, 2),
'queue_distribution' => $queueStats
'queue_distribution' => $queueStats,
];
} catch (\Exception $e) {
$this->logger->error('Failed to get worker statistics', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
@@ -328,7 +338,7 @@ final readonly class WorkerRegistry
private function getQueueStatistics(): array
{
try {
$sql = "SELECT queues FROM " . self::TABLE_NAME . " WHERE is_active = 1";
$sql = "SELECT queues FROM " . self::TABLE_NAME . " WHERE is_active = true";
$result = $this->connection->query(SqlQuery::create($sql));
$queueCounts = [];
@@ -340,12 +350,14 @@ final readonly class WorkerRegistry
}
arsort($queueCounts);
return $queueCounts;
} catch (\Exception $e) {
$this->logger->warning('Failed to get queue statistics', LogContext::withData([
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
return [];
}
}
@@ -357,9 +369,9 @@ final readonly class WorkerRegistry
{
try {
$sql = "SELECT * FROM " . self::TABLE_NAME . "
WHERE is_active = 1
AND last_heartbeat > DATE_SUB(NOW(), INTERVAL 2 MINUTE)
AND JSON_CONTAINS(capabilities, JSON_QUOTE(:capability))
WHERE is_active = true
AND last_heartbeat > NOW() - INTERVAL '2 minutes'
AND capabilities::jsonb @> to_jsonb(:capability::text)
ORDER BY current_jobs ASC";
$result = $this->connection->query(SqlQuery::create($sql, ['capability' => $capability]));
@@ -376,9 +388,10 @@ final readonly class WorkerRegistry
} catch (\Exception $e) {
$this->logger->error('Failed to find workers with capability', LogContext::withData([
'capability' => $capability,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]));
throw $e;
}
}
}
}