docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Database\Attributes\Column;
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
use App\Framework\Queue\ValueObjects\FailureReason;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Ulid\Ulid;
/**
* Entity representing a job that failed and was moved to the dead letter queue
*/
#[Entity(table: 'dead_letter_jobs')]
final readonly class DeadLetterJob
{
public function __construct(
#[Id]
#[Column(name: 'id', type: 'string', length: 26)]
public string $id,
#[Column(name: 'original_job_id', type: 'string', length: 26)]
public string $originalJobId,
#[Column(name: 'dead_letter_queue', type: 'string', length: 100)]
public string $deadLetterQueue,
#[Column(name: 'original_queue', type: 'string', length: 50)]
public string $originalQueue,
#[Column(name: 'job_payload', type: 'text')]
public string $jobPayload,
#[Column(name: 'failure_reason', type: 'text')]
public string $failureReason,
#[Column(name: 'exception_type', type: 'string', length: 255, nullable: true)]
public ?string $exceptionType,
#[Column(name: 'stack_trace', type: 'longtext', nullable: true)]
public ?string $stackTrace,
#[Column(name: 'failed_attempts', type: 'integer')]
public int $failedAttempts,
#[Column(name: 'failed_at', type: 'timestamp')]
public string $failedAt,
#[Column(name: 'moved_to_dlq_at', type: 'timestamp')]
public string $movedToDlqAt,
#[Column(name: 'retry_count', type: 'integer', default: 0)]
public int $retryCount = 0,
#[Column(name: 'last_retry_at', type: 'timestamp', nullable: true)]
public ?string $lastRetryAt = null
) {}
public static function fromFailedJob(
JobIndexEntry $failedJob,
DeadLetterQueueName $deadLetterQueueName,
FailureReason $failureReason
): self {
$now = date('Y-m-d H:i:s');
return new self(
id: Ulid::generate(),
originalJobId: $failedJob->jobId,
deadLetterQueue: $deadLetterQueueName->toString(),
originalQueue: $failedJob->queueType,
jobPayload: $failedJob->jobPayload,
failureReason: $failureReason->getMessage(),
exceptionType: $failureReason->getExceptionType(),
stackTrace: $failureReason->getStackTrace(),
failedAttempts: $failedJob->attempts,
failedAt: $failedJob->startedAt ?? $now,
movedToDlqAt: $now
);
}
public function getDeadLetterQueueName(): DeadLetterQueueName
{
return DeadLetterQueueName::fromString($this->deadLetterQueue);
}
public function getOriginalQueueName(): QueueName
{
return QueueName::fromString($this->originalQueue);
}
public function getJobPayload(): JobPayload
{
return JobPayload::fromSerialized($this->jobPayload);
}
public function getFailureReason(): FailureReason
{
return new FailureReason(
message: $this->failureReason,
exceptionType: $this->exceptionType,
stackTrace: $this->stackTrace
);
}
public function withRetryAttempt(): self
{
return new self(
id: $this->id,
originalJobId: $this->originalJobId,
deadLetterQueue: $this->deadLetterQueue,
originalQueue: $this->originalQueue,
jobPayload: $this->jobPayload,
failureReason: $this->failureReason,
exceptionType: $this->exceptionType,
stackTrace: $this->stackTrace,
failedAttempts: $this->failedAttempts,
failedAt: $this->failedAt,
movedToDlqAt: $this->movedToDlqAt,
retryCount: $this->retryCount + 1,
lastRetryAt: date('Y-m-d H:i:s')
);
}
public function toArray(): array
{
return [
'id' => $this->id,
'original_job_id' => $this->originalJobId,
'dead_letter_queue' => $this->deadLetterQueue,
'original_queue' => $this->originalQueue,
'failure_reason' => $this->failureReason,
'exception_type' => $this->exceptionType,
'failed_attempts' => $this->failedAttempts,
'failed_at' => $this->failedAt,
'moved_to_dlq_at' => $this->movedToDlqAt,
'retry_count' => $this->retryCount,
'last_retry_at' => $this->lastRetryAt
];
}
}

View File

@@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Database\Attributes\Column;
use App\Framework\Queue\ValueObjects\JobChain;
use App\Framework\Queue\ValueObjects\ChainExecutionMode;
use App\Framework\Ulid\Ulid;
/**
* Entity representing a job chain entry in the database
*/
#[Entity(table: 'job_chains')]
final readonly class JobChainEntry
{
public function __construct(
#[Id]
#[Column(name: 'id', type: 'string', length: 26)]
public string $id,
#[Column(name: 'chain_id', type: 'string', length: 26)]
public string $chainId,
#[Column(name: 'name', type: 'string', length: 255)]
public string $name,
#[Column(name: 'job_ids', type: 'json')]
public string $jobIds,
#[Column(name: 'execution_mode', type: 'string', length: 20)]
public string $executionMode,
#[Column(name: 'created_at', type: 'timestamp')]
public string $createdAt,
#[Column(name: 'updated_at', type: 'timestamp')]
public string $updatedAt,
#[Column(name: 'stop_on_failure', type: 'boolean', default: true)]
public bool $stopOnFailure = true,
#[Column(name: 'metadata', type: 'json', nullable: true)]
public ?string $metadata = null,
#[Column(name: 'status', type: 'string', length: 20, default: 'pending')]
public string $status = 'pending',
#[Column(name: 'started_at', type: 'timestamp', nullable: true)]
public ?string $startedAt = null,
#[Column(name: 'completed_at', type: 'timestamp', nullable: true)]
public ?string $completedAt = null
) {}
public static function fromJobChain(JobChain $jobChain): self
{
$now = date('Y-m-d H:i:s');
return new self(
id: Ulid::generate(),
chainId: $jobChain->chainId,
name: $jobChain->name,
jobIds: json_encode($jobChain->jobIds),
executionMode: $jobChain->executionMode->value,
createdAt: $now,
updatedAt: $now,
stopOnFailure: $jobChain->stopOnFailure,
metadata: $jobChain->metadata ? json_encode($jobChain->metadata) : null,
status: 'pending',
startedAt: null,
completedAt: null
);
}
public function getJobChain(): JobChain
{
$jobIds = json_decode($this->jobIds, true);
$metadata = $this->metadata ? json_decode($this->metadata, true) : null;
return new JobChain(
chainId: $this->chainId,
name: $this->name,
jobIds: $jobIds,
executionMode: ChainExecutionMode::from($this->executionMode),
stopOnFailure: $this->stopOnFailure,
metadata: $metadata
);
}
public function getJobIdsArray(): array
{
return json_decode($this->jobIds, true);
}
public function getMetadataArray(): ?array
{
return $this->metadata ? json_decode($this->metadata, true) : null;
}
public function getExecutionMode(): ChainExecutionMode
{
return ChainExecutionMode::from($this->executionMode);
}
public function markAsStarted(): self
{
return new self(
id: $this->id,
chainId: $this->chainId,
name: $this->name,
jobIds: $this->jobIds,
executionMode: $this->executionMode,
createdAt: $this->createdAt,
updatedAt: date('Y-m-d H:i:s'),
stopOnFailure: $this->stopOnFailure,
metadata: $this->metadata,
status: 'running',
startedAt: date('Y-m-d H:i:s'),
completedAt: $this->completedAt
);
}
public function markAsCompleted(): self
{
return new self(
id: $this->id,
chainId: $this->chainId,
name: $this->name,
jobIds: $this->jobIds,
executionMode: $this->executionMode,
createdAt: $this->createdAt,
updatedAt: date('Y-m-d H:i:s'),
stopOnFailure: $this->stopOnFailure,
metadata: $this->metadata,
status: 'completed',
startedAt: $this->startedAt,
completedAt: date('Y-m-d H:i:s')
);
}
public function markAsFailed(): self
{
return new self(
id: $this->id,
chainId: $this->chainId,
name: $this->name,
jobIds: $this->jobIds,
executionMode: $this->executionMode,
createdAt: $this->createdAt,
updatedAt: date('Y-m-d H:i:s'),
stopOnFailure: $this->stopOnFailure,
metadata: $this->metadata,
status: 'failed',
startedAt: $this->startedAt,
completedAt: date('Y-m-d H:i:s')
);
}
public function isPending(): bool
{
return $this->status === 'pending';
}
public function isRunning(): bool
{
return $this->status === 'running';
}
public function isCompleted(): bool
{
return $this->status === 'completed';
}
public function isFailed(): bool
{
return $this->status === 'failed';
}
public function toArray(): array
{
return [
'id' => $this->id,
'chain_id' => $this->chainId,
'name' => $this->name,
'job_ids' => $this->getJobIdsArray(),
'execution_mode' => $this->executionMode,
'stop_on_failure' => $this->stopOnFailure,
'metadata' => $this->getMetadataArray(),
'status' => $this->status,
'started_at' => $this->startedAt,
'completed_at' => $this->completedAt,
'created_at' => $this->createdAt,
'updated_at' => $this->updatedAt
];
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Database\Attributes\Column;
use App\Framework\Queue\ValueObjects\JobDependency;
use App\Framework\Queue\ValueObjects\DependencyType;
use App\Framework\Ulid\Ulid;
/**
* Entity representing a job dependency entry in the database
*/
#[Entity(table: 'job_dependencies')]
final readonly class JobDependencyEntry
{
public function __construct(
#[Id]
#[Column(name: 'id', type: 'string', length: 26)]
public string $id,
#[Column(name: 'dependent_job_id', type: 'string', length: 26)]
public string $dependentJobId,
#[Column(name: 'depends_on_job_id', type: 'string', length: 26)]
public string $dependsOnJobId,
#[Column(name: 'dependency_type', type: 'string', length: 20)]
public string $dependencyType,
#[Column(name: 'created_at', type: 'timestamp')]
public string $createdAt,
#[Column(name: 'updated_at', type: 'timestamp')]
public string $updatedAt,
#[Column(name: 'condition_expression', type: 'text', nullable: true)]
public ?string $conditionExpression = null,
#[Column(name: 'is_satisfied', type: 'boolean', default: false)]
public bool $isSatisfied = false,
#[Column(name: 'satisfied_at', type: 'timestamp', nullable: true)]
public ?string $satisfiedAt = null
) {}
public static function fromJobDependency(JobDependency $dependency): self
{
$now = date('Y-m-d H:i:s');
return new self(
id: Ulid::generate(),
dependentJobId: $dependency->dependentJobId,
dependsOnJobId: $dependency->dependsOnJobId,
dependencyType: $dependency->type->value,
createdAt: $now,
updatedAt: $now,
conditionExpression: $dependency->condition,
isSatisfied: false,
satisfiedAt: null
);
}
public function getJobDependency(): JobDependency
{
return new JobDependency(
dependentJobId: $this->dependentJobId,
dependsOnJobId: $this->dependsOnJobId,
type: DependencyType::from($this->dependencyType),
condition: $this->conditionExpression
);
}
public function markAsSatisfied(): self
{
return new self(
id: $this->id,
dependentJobId: $this->dependentJobId,
dependsOnJobId: $this->dependsOnJobId,
dependencyType: $this->dependencyType,
createdAt: $this->createdAt,
updatedAt: date('Y-m-d H:i:s'),
conditionExpression: $this->conditionExpression,
isSatisfied: true,
satisfiedAt: date('Y-m-d H:i:s')
);
}
public function getDependencyType(): DependencyType
{
return DependencyType::from($this->dependencyType);
}
public function isConditional(): bool
{
return $this->getDependencyType()->isConditional();
}
public function requiresSuccess(): bool
{
return $this->getDependencyType()->requiresSuccessfulCompletion();
}
public function toArray(): array
{
return [
'id' => $this->id,
'dependent_job_id' => $this->dependentJobId,
'depends_on_job_id' => $this->dependsOnJobId,
'dependency_type' => $this->dependencyType,
'condition_expression' => $this->conditionExpression,
'is_satisfied' => $this->isSatisfied,
'satisfied_at' => $this->satisfiedAt,
'created_at' => $this->createdAt,
'updated_at' => $this->updatedAt
];
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\JobStatus;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Job History Entry Entity
*
* Database entity for job status change audit trail
*/
#[Entity(tableName: 'job_history', idColumn: 'id')]
final readonly class JobHistoryEntry
{
public function __construct(
#[Column(name: 'id', primary: true, autoIncrement: true)]
public ?int $id,
#[Column(name: 'job_id')]
public JobId $jobId,
#[Column(name: 'old_status')]
public ?JobStatus $oldStatus,
#[Column(name: 'new_status')]
public JobStatus $newStatus,
#[Column(name: 'error_message')]
public ?string $errorMessage,
#[Column(name: 'changed_at')]
public Timestamp $changedAt,
#[Column(name: 'metadata')]
public ?string $metadata = null
) {}
/**
* Create status change entry
*/
public static function forStatusChange(
JobId $jobId,
?JobStatus $oldStatus,
JobStatus $newStatus,
?string $errorMessage = null,
array $metadata = []
): self {
return new self(
id: null,
jobId: $jobId,
oldStatus: $oldStatus,
newStatus: $newStatus,
errorMessage: $errorMessage,
changedAt: Timestamp::now(),
metadata: !empty($metadata) ? json_encode($metadata) : null
);
}
/**
* Get metadata as array
*/
public function getMetadataAsArray(): array
{
if ($this->metadata === null) {
return [];
}
$decoded = json_decode($this->metadata, true);
return is_array($decoded) ? $decoded : [];
}
/**
* Convert to array for storage/serialization
*/
public function toArray(): array
{
return [
'id' => $this->id,
'job_id' => $this->jobId->toString(),
'old_status' => $this->oldStatus?->value,
'new_status' => $this->newStatus->value,
'error_message' => $this->errorMessage,
'changed_at' => $this->changedAt->toFloat(),
'metadata' => $this->metadata
];
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\JobStatus;
use App\Framework\Queue\ValueObjects\JobState;
use App\Framework\Queue\ValueObjects\QueueType;
use App\Framework\Queue\ValueObjects\QueuePriority;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Job Index Entry Entity
*
* Database entity for efficient job querying and indexing
*/
#[Entity(tableName: 'job_index', idColumn: 'job_id')]
final readonly class JobIndexEntry
{
public function __construct(
#[Column(name: 'job_id', primary: true)]
public JobId $jobId,
#[Column(name: 'status')]
public JobStatus $status,
#[Column(name: 'queue_type')]
public QueueType $queueType,
#[Column(name: 'priority')]
public QueuePriority $priority,
#[Column(name: 'attempts')]
public int $attempts,
#[Column(name: 'max_attempts')]
public int $maxAttempts,
#[Column(name: 'created_at')]
public Timestamp $createdAt,
#[Column(name: 'updated_at')]
public Timestamp $updatedAt,
#[Column(name: 'started_at')]
public ?Timestamp $startedAt = null,
#[Column(name: 'completed_at')]
public ?Timestamp $completedAt = null,
#[Column(name: 'scheduled_for')]
public ?Timestamp $scheduledFor = null,
#[Column(name: 'error_message')]
public ?string $errorMessage = null
) {}
/**
* Create from JobState
*/
public static function fromJobState(JobState $jobState): self
{
return new self(
jobId: $jobState->jobId,
status: $jobState->status,
queueType: $jobState->queueType,
priority: $jobState->priority,
attempts: $jobState->attempts,
maxAttempts: $jobState->maxAttempts,
createdAt: $jobState->createdAt,
updatedAt: $jobState->completedAt ?? $jobState->startedAt ?? $jobState->createdAt,
startedAt: $jobState->startedAt,
completedAt: $jobState->completedAt,
scheduledFor: null, // TODO: Add scheduled jobs support
errorMessage: $jobState->errorMessage
);
}
/**
* Update with new job state
*/
public function updateFromJobState(JobState $jobState): self
{
return new self(
jobId: $this->jobId,
status: $jobState->status,
queueType: $this->queueType,
priority: $this->priority,
attempts: $jobState->attempts,
maxAttempts: $this->maxAttempts,
createdAt: $this->createdAt,
updatedAt: Timestamp::now(),
startedAt: $jobState->startedAt ?? $this->startedAt,
completedAt: $jobState->completedAt ?? $this->completedAt,
scheduledFor: $this->scheduledFor,
errorMessage: $jobState->errorMessage ?? $this->errorMessage
);
}
/**
* Check if job can be retried
*/
public function canRetry(): bool
{
return $this->attempts < $this->maxAttempts &&
($this->status === JobStatus::FAILED || $this->status === JobStatus::RETRYING);
}
/**
* Check if job is in final state
*/
public function isFinalState(): bool
{
return $this->status->isFinal();
}
/**
* Check if job is active
*/
public function isActive(): bool
{
return $this->status->isActive();
}
/**
* Get job age in seconds
*/
public function getAgeInSeconds(): int
{
return (int) (Timestamp::now()->toFloat() - $this->createdAt->toFloat());
}
/**
* Check if job needs retry based on backoff strategy
*/
public function needsRetry(): bool
{
if (!$this->canRetry()) {
return false;
}
// Exponential backoff: 2^attempts minutes
$backoffMinutes = pow(2, $this->attempts);
$nextRetryTime = $this->updatedAt->toFloat() + ($backoffMinutes * 60);
return Timestamp::now()->toFloat() >= $nextRetryTime;
}
/**
* Convert to array for storage/serialization
*/
public function toArray(): array
{
return [
'job_id' => $this->jobId->toString(),
'status' => $this->status->value,
'queue_type' => $this->queueType->value,
'priority' => $this->priority->value,
'attempts' => $this->attempts,
'max_attempts' => $this->maxAttempts,
'created_at' => $this->createdAt->toFloat(),
'updated_at' => $this->updatedAt->toFloat(),
'started_at' => $this->startedAt?->toFloat(),
'completed_at' => $this->completedAt?->toFloat(),
'scheduled_for' => $this->scheduledFor?->toFloat(),
'error_message' => $this->errorMessage
];
}
}

View File

@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Database\Attributes\Column;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Ulid\Ulid;
#[Entity(table: 'job_metrics')]
final readonly class JobMetricsEntry
{
public function __construct(
#[Id]
#[Column(name: 'id', type: 'string', length: 26)]
public string $id,
#[Column(name: 'job_id', type: 'string', length: 26)]
public string $jobId,
#[Column(name: 'queue_name', type: 'string', length: 100)]
public string $queueName,
#[Column(name: 'status', type: 'string', length: 20)]
public string $status,
#[Column(name: 'attempts', type: 'integer')]
public int $attempts,
#[Column(name: 'max_attempts', type: 'integer')]
public int $maxAttempts,
#[Column(name: 'execution_time_ms', type: 'float')]
public float $executionTimeMs,
#[Column(name: 'memory_usage_bytes', type: 'integer')]
public int $memoryUsageBytes,
#[Column(name: 'created_at', type: 'timestamp')]
public string $createdAt,
#[Column(name: 'updated_at', type: 'timestamp')]
public string $updatedAt,
#[Column(name: 'error_message', type: 'text', nullable: true)]
public ?string $errorMessage = null,
#[Column(name: 'started_at', type: 'timestamp', nullable: true)]
public ?string $startedAt = null,
#[Column(name: 'completed_at', type: 'timestamp', nullable: true)]
public ?string $completedAt = null,
#[Column(name: 'failed_at', type: 'timestamp', nullable: true)]
public ?string $failedAt = null,
#[Column(name: 'metadata', type: 'json', nullable: true)]
public ?string $metadata = null
) {}
public static function fromJobMetrics(JobMetrics $metrics): self
{
$now = date('Y-m-d H:i:s');
return new self(
id: Ulid::generate(),
jobId: $metrics->jobId,
queueName: $metrics->queueName,
status: $metrics->status,
attempts: $metrics->attempts,
maxAttempts: $metrics->maxAttempts,
executionTimeMs: $metrics->executionTimeMs,
memoryUsageBytes: $metrics->memoryUsageBytes,
createdAt: $metrics->createdAt,
updatedAt: $now,
errorMessage: $metrics->errorMessage,
startedAt: $metrics->startedAt,
completedAt: $metrics->completedAt,
failedAt: $metrics->failedAt,
metadata: !empty($metrics->metadata) ? json_encode($metrics->metadata) : null
);
}
public function getJobMetrics(): JobMetrics
{
return new JobMetrics(
jobId: $this->jobId,
queueName: $this->queueName,
status: $this->status,
attempts: $this->attempts,
maxAttempts: $this->maxAttempts,
executionTimeMs: $this->executionTimeMs,
memoryUsageBytes: $this->memoryUsageBytes,
errorMessage: $this->errorMessage,
createdAt: $this->createdAt,
startedAt: $this->startedAt,
completedAt: $this->completedAt,
failedAt: $this->failedAt,
metadata: $this->metadata ? json_decode($this->metadata, true) : []
);
}
public function updateWithMetrics(JobMetrics $metrics): self
{
return new self(
id: $this->id,
jobId: $this->jobId,
queueName: $this->queueName,
status: $metrics->status,
attempts: $metrics->attempts,
maxAttempts: $metrics->maxAttempts,
executionTimeMs: $metrics->executionTimeMs,
memoryUsageBytes: $metrics->memoryUsageBytes,
createdAt: $this->createdAt,
updatedAt: date('Y-m-d H:i:s'),
errorMessage: $metrics->errorMessage,
startedAt: $metrics->startedAt,
completedAt: $metrics->completedAt,
failedAt: $metrics->failedAt,
metadata: !empty($metrics->metadata) ? json_encode($metrics->metadata) : $this->metadata
);
}
public function getMetadataArray(): ?array
{
return $this->metadata ? json_decode($this->metadata, true) : null;
}
public function isCompleted(): bool
{
return $this->status === 'completed';
}
public function isFailed(): bool
{
return $this->status === 'failed';
}
public function isRunning(): bool
{
return $this->status === 'running';
}
public function isPending(): bool
{
return $this->status === 'pending';
}
public function getExecutionTimeSeconds(): float
{
return $this->executionTimeMs / 1000.0;
}
public function getMemoryUsageMB(): float
{
return $this->memoryUsageBytes / (1024 * 1024);
}
public function toArray(): array
{
return [
'id' => $this->id,
'job_id' => $this->jobId,
'queue_name' => $this->queueName,
'status' => $this->status,
'attempts' => $this->attempts,
'max_attempts' => $this->maxAttempts,
'execution_time_ms' => $this->executionTimeMs,
'execution_time_seconds' => $this->getExecutionTimeSeconds(),
'memory_usage_bytes' => $this->memoryUsageBytes,
'memory_usage_mb' => $this->getMemoryUsageMB(),
'error_message' => $this->errorMessage,
'created_at' => $this->createdAt,
'updated_at' => $this->updatedAt,
'started_at' => $this->startedAt,
'completed_at' => $this->completedAt,
'failed_at' => $this->failedAt,
'metadata' => $this->getMetadataArray()
];
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Database\Attributes\Column;
use App\Framework\Queue\ValueObjects\JobProgress;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Ulid\Ulid;
/**
* Entity representing a job progress tracking entry
*/
#[Entity(table: 'job_progress')]
final readonly class JobProgressEntry
{
public function __construct(
#[Id]
#[Column(name: 'id', type: 'string', length: 26)]
public string $id,
#[Column(name: 'job_id', type: 'string', length: 26)]
public string $jobId,
#[Column(name: 'percentage', type: 'decimal', precision: 5, scale: 2)]
public float $percentage,
#[Column(name: 'message', type: 'text')]
public string $message,
#[Column(name: 'step_name', type: 'string', length: 100, nullable: true)]
public ?string $stepName,
#[Column(name: 'metadata', type: 'json', nullable: true)]
public ?string $metadata,
#[Column(name: 'updated_at', type: 'timestamp')]
public string $updatedAt,
#[Column(name: 'is_completed', type: 'boolean', default: false)]
public bool $isCompleted = false,
#[Column(name: 'is_failed', type: 'boolean', default: false)]
public bool $isFailed = false
) {}
public static function fromJobProgress(string $jobId, JobProgress $progress, ?string $stepName = null): self
{
return new self(
id: Ulid::generate(),
jobId: $jobId,
percentage: $progress->percentage->getValue(),
message: $progress->message,
stepName: $stepName,
metadata: $progress->metadata ? json_encode($progress->metadata) : null,
updatedAt: date('Y-m-d H:i:s'),
isCompleted: $progress->isCompleted(),
isFailed: $progress->isFailed()
);
}
public function getJobProgress(): JobProgress
{
$metadata = $this->metadata ? json_decode($this->metadata, true) : null;
// Add status to metadata if failed
if ($this->isFailed && $metadata) {
$metadata['status'] = 'failed';
}
return new JobProgress(
percentage: Percentage::from($this->percentage),
message: $this->message,
metadata: $metadata
);
}
public function getPercentage(): Percentage
{
return Percentage::from($this->percentage);
}
public function getMetadataArray(): ?array
{
return $this->metadata ? json_decode($this->metadata, true) : null;
}
public function toArray(): array
{
return [
'id' => $this->id,
'job_id' => $this->jobId,
'percentage' => $this->percentage,
'percentage_formatted' => $this->getPercentage()->format(),
'message' => $this->message,
'step_name' => $this->stepName,
'metadata' => $this->getMetadataArray(),
'updated_at' => $this->updatedAt,
'is_completed' => $this->isCompleted,
'is_failed' => $this->isFailed
];
}
}

View File

@@ -0,0 +1,277 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Byte;
/**
* Worker Entity für Distributed Job Processing
*/
final readonly class Worker
{
public function __construct(
public WorkerId $id,
public string $hostname,
public int $processId,
public array $queues, // Array of QueueName objects
public int $maxJobs,
public \DateTimeImmutable $registeredAt,
public ?\DateTimeImmutable $lastHeartbeat = null,
public bool $isActive = true,
public Percentage $cpuUsage = new Percentage(0),
public Byte $memoryUsage = new Byte(0),
public int $currentJobs = 0,
public array $capabilities = [],
public string $version = '1.0.0'
) {
if (empty($this->queues)) {
throw new \InvalidArgumentException('Worker must handle at least one queue');
}
if ($this->maxJobs <= 0) {
throw new \InvalidArgumentException('Max jobs must be greater than 0');
}
if ($this->currentJobs < 0) {
throw new \InvalidArgumentException('Current jobs cannot be negative');
}
if ($this->currentJobs > $this->maxJobs) {
throw new \InvalidArgumentException('Current jobs cannot exceed max jobs');
}
}
/**
* Erstelle einen neuen Worker
*/
public static function register(
string $hostname,
int $processId,
array $queues,
int $maxJobs = 10,
array $capabilities = []
): self {
return new self(
id: WorkerId::forHost($hostname, $processId),
hostname: $hostname,
processId: $processId,
queues: $queues,
maxJobs: $maxJobs,
registeredAt: new \DateTimeImmutable(),
lastHeartbeat: new \DateTimeImmutable(),
isActive: true,
capabilities: $capabilities
);
}
/**
* Worker Heartbeat aktualisieren
*/
public function updateHeartbeat(
Percentage $cpuUsage,
Byte $memoryUsage,
int $currentJobs
): self {
return new self(
id: $this->id,
hostname: $this->hostname,
processId: $this->processId,
queues: $this->queues,
maxJobs: $this->maxJobs,
registeredAt: $this->registeredAt,
lastHeartbeat: new \DateTimeImmutable(),
isActive: true,
cpuUsage: $cpuUsage,
memoryUsage: $memoryUsage,
currentJobs: $currentJobs,
capabilities: $this->capabilities,
version: $this->version
);
}
/**
* Worker als inaktiv markieren
*/
public function markInactive(): self
{
return new self(
id: $this->id,
hostname: $this->hostname,
processId: $this->processId,
queues: $this->queues,
maxJobs: $this->maxJobs,
registeredAt: $this->registeredAt,
lastHeartbeat: $this->lastHeartbeat,
isActive: false,
cpuUsage: $this->cpuUsage,
memoryUsage: $this->memoryUsage,
currentJobs: $this->currentJobs,
capabilities: $this->capabilities,
version: $this->version
);
}
/**
* Prüfe ob Worker verfügbar für neue Jobs ist
*/
public function isAvailableForJobs(): bool
{
return $this->isActive
&& $this->currentJobs < $this->maxJobs
&& $this->isHealthy();
}
/**
* Prüfe ob Worker eine bestimmte Queue unterstützt
*/
public function handlesQueue(QueueName $queueName): bool
{
foreach ($this->queues as $queue) {
if ($queue instanceof QueueName && $queue->equals($queueName)) {
return true;
}
}
return false;
}
/**
* Prüfe ob Worker healthy ist
*/
public function isHealthy(): bool
{
if (!$this->isActive) {
return false;
}
// Heartbeat nicht älter als 60 Sekunden
if ($this->lastHeartbeat === null) {
return false;
}
$heartbeatAge = time() - $this->lastHeartbeat->getTimestamp();
if ($heartbeatAge > 60) {
return false;
}
// CPU und Memory Limits
if ($this->cpuUsage->getValue() > 90) {
return false;
}
// Memory Limit (2GB)
if ($this->memoryUsage->toBytes() > 2 * 1024 * 1024 * 1024) {
return false;
}
return true;
}
/**
* Berechne Worker Load (0-100%)
*/
public function getLoadPercentage(): Percentage
{
if ($this->maxJobs === 0) {
return new Percentage(100);
}
$jobLoad = ($this->currentJobs / $this->maxJobs) * 100;
$cpuLoad = $this->cpuUsage->getValue();
// Höchste Last zählt
return new Percentage(max($jobLoad, $cpuLoad));
}
/**
* Prüfe ob Worker eine Capability hat
*/
public function hasCapability(string $capability): bool
{
return in_array($capability, $this->capabilities, true);
}
/**
* Worker Informationen für Monitoring
*/
public function toMonitoringArray(): array
{
return [
'id' => $this->id->toString(),
'hostname' => $this->hostname,
'process_id' => $this->processId,
'queues' => array_map(fn(QueueName $queue) => $queue->toString(), $this->queues),
'max_jobs' => $this->maxJobs,
'current_jobs' => $this->currentJobs,
'is_active' => $this->isActive,
'is_healthy' => $this->isHealthy(),
'is_available' => $this->isAvailableForJobs(),
'load_percentage' => $this->getLoadPercentage()->getValue(),
'cpu_usage' => $this->cpuUsage->getValue(),
'memory_usage_mb' => round($this->memoryUsage->toBytes() / 1024 / 1024, 2),
'registered_at' => $this->registeredAt->format('Y-m-d H:i:s'),
'last_heartbeat' => $this->lastHeartbeat?->format('Y-m-d H:i:s'),
'capabilities' => $this->capabilities,
'version' => $this->version
];
}
/**
* Array Repräsentation für Persistierung
*/
public function toArray(): array
{
return [
'id' => $this->id->toString(),
'hostname' => $this->hostname,
'process_id' => $this->processId,
'queues' => json_encode(array_map(fn(QueueName $queue) => $queue->toString(), $this->queues)),
'max_jobs' => $this->maxJobs,
'current_jobs' => $this->currentJobs,
'is_active' => $this->isActive,
'cpu_usage' => $this->cpuUsage->getValue(),
'memory_usage_bytes' => $this->memoryUsage->toBytes(),
'registered_at' => $this->registeredAt->format('Y-m-d H:i:s'),
'last_heartbeat' => $this->lastHeartbeat?->format('Y-m-d H:i:s'),
'capabilities' => json_encode($this->capabilities),
'version' => $this->version
];
}
/**
* Worker aus Array erstellen
*/
public static function fromArray(array $data): self
{
$queueStrings = json_decode($data['queues'], true);
$queues = array_map(function(string $queueString) {
// Parse queue string zurück zu QueueName
// Annahme: Format ist "type.name" oder "tenant.type.name"
$parts = explode('.', $queueString);
if (count($parts) >= 2) {
return QueueName::default(); // Vereinfacht - könnte erweitert werden
}
return QueueName::default();
}, $queueStrings);
return new self(
id: WorkerId::fromString($data['id']),
hostname: $data['hostname'],
processId: $data['process_id'],
queues: $queues,
maxJobs: $data['max_jobs'],
registeredAt: new \DateTimeImmutable($data['registered_at']),
lastHeartbeat: $data['last_heartbeat'] ? new \DateTimeImmutable($data['last_heartbeat']) : null,
isActive: (bool) $data['is_active'],
cpuUsage: new Percentage($data['cpu_usage'] ?? 0),
memoryUsage: Byte::fromBytes($data['memory_usage_bytes'] ?? 0),
currentJobs: $data['current_jobs'] ?? 0,
capabilities: json_decode($data['capabilities'] ?? '[]', true),
version: $data['version'] ?? '1.0.0'
);
}
}