stash light

This commit is contained in:
team 1
2026-02-12 10:03:52 +01:00
parent 5b650a8f28
commit 0bb0c0b42f
51 changed files with 6864 additions and 72 deletions

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Service;
use App\Entity\Document;
use App\Entity\DocumentVersion;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
class DocumentService
{
public function __construct(
private EntityManagerInterface $em
) {}
/**
* Erstellt ein neues Dokument inkl. Version 1
*/
public function createDocument(
string $title,
string $filePath,
User $user
): Document {
$document = new Document();
$document->setTitle($title);
$document->setCreatedBy($user);
$version = new DocumentVersion();
$version->setVersionNumber(1);
$version->setFilePath($filePath);
$version->setChecksum($this->calculateChecksum($filePath));
$version->setCreatedBy($user);
$version->setActive(true);
$document->addVersion($version);
$document->setCurrentVersion($version);
$this->em->persist($document);
$this->em->persist($version);
$this->em->flush();
return $document;
}
/**
* Fügt neue Version hinzu (immutable)
*/
public function addVersion(
Document $document,
string $filePath,
User $user
): DocumentVersion {
$nextVersionNumber = $this->getNextVersionNumber($document);
$version = new DocumentVersion();
$version->setVersionNumber($nextVersionNumber);
$version->setFilePath($filePath);
$version->setChecksum($this->calculateChecksum($filePath));
$version->setCreatedBy($user);
$version->setActive(false);
$document->addVersion($version);
$this->em->persist($version);
$this->em->flush();
return $version;
}
/**
* Aktiviert eine Version (setzt andere inaktiv)
*/
public function activateVersion(DocumentVersion $version): void
{
$document = $version->getDocument();
foreach ($document->getVersions() as $existingVersion) {
$existingVersion->setActive(false);
}
$version->setActive(true);
$document->setCurrentVersion($version);
$this->em->flush();
}
/**
* Archiviert Dokument
*/
public function archive(Document $document): void
{
$document->archive();
$this->em->flush();
}
/**
* Berechnet SHA256 Checksum
*/
private function calculateChecksum(string $filePath): string
{
if (!file_exists($filePath)) {
throw new \RuntimeException('File not found for checksum.');
}
return hash_file('sha256', $filePath);
}
/**
* Ermittelt nächste Versionsnummer
*/
private function getNextVersionNumber(Document $document): int
{
$max = 0;
foreach ($document->getVersions() as $version) {
$max = max($max, $version->getVersionNumber());
}
return $max + 1;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Service;
use App\Entity\IngestJob;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Uid\Uuid;
final class IngestJobService
{
public function __construct(private EntityManagerInterface $em)
{
}
public function startJob(
string $type,
?User $user = null,
?Uuid $documentId = null,
?Uuid $documentVersionId = null,
?string $logPath = null
): IngestJob
{
$job = new IngestJob($type);
$job->setStartedBy($user);
$job->setDocumentId($documentId);
$job->setDocumentVersionId($documentVersionId);
$job->setLogPath($logPath);
$this->em->persist($job);
$this->em->flush();
return $job;
}
public function markCompleted(IngestJob $job): void
{
$job->markCompleted();
$this->em->flush();
}
public function markFailed(IngestJob $job, string $message): void
{
$job->markFailed($message);
$this->em->flush();
}
public function markAborted(IngestJob $job): void
{
$job->markAborted();
$this->em->flush();
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Service;
use App\Entity\DocumentVersion;
use App\Entity\IngestJob;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
class IngestOrchestrator
{
public function __construct(
private LockService $lockService,
private IngestJobService $jobService,
private EntityManagerInterface $em,
) {}
/**
* Startet Ingest für eine bestimmte DocumentVersion
*/
public function runForVersion(
DocumentVersion $version,
User $user,
bool $dryRun = false
): IngestJob {
if (!$this->lockService->acquire()) {
throw new \RuntimeException('Another ingest job is already running.');
}
$job = null;
try {
// --------------------------------------
// Job anlegen
// --------------------------------------
$job = $this->jobService->startJob(
IngestJob::TYPE_DOCUMENT,
$user,
$version->getDocument()->getId(),
$version->getId(),
);
// --------------------------------------
// Version Status RUNNING
// --------------------------------------
$version->setIngestStatus(DocumentVersion::INGEST_RUNNING);
$this->em->flush();
// --------------------------------------
// Simulierter Ablauf (noch kein echter Ingest)
// --------------------------------------
if ($dryRun) {
usleep(200000);
} else {
// Später:
// - KnowledgeIngestService
// - ChunkWriter
// - VectorIngestCommand
}
// --------------------------------------
// Erfolg
// --------------------------------------
$version->setIngestStatus(DocumentVersion::INGEST_INDEXED);
$this->jobService->markCompleted($job);
$this->em->flush();
} catch (\Throwable $e) {
if ($job) {
$this->jobService->markFailed($job, $e->getMessage());
}
$version->setIngestStatus(DocumentVersion::INGEST_FAILED);
$this->em->flush();
throw $e;
} finally {
$this->lockService->release();
}
return $job;
}
/**
* Globaler Reindex
*/
public function runGlobal(User $user, bool $dryRun = false): IngestJob
{
if (!$this->lockService->acquire()) {
throw new \RuntimeException('Another ingest job is already running.');
}
$job = null;
try {
$job = $this->jobService->startJob(
IngestJob::TYPE_GLOBAL_REINDEX,
$user
);
if ($dryRun) {
usleep(200000);
} else {
// Später:
// - Alle aktiven Dokumente neu ingestieren
// - Global vector rebuild
}
$this->jobService->markCompleted($job);
} catch (\Throwable $e) {
if ($job) {
$this->jobService->markFailed($job, $e->getMessage());
}
throw $e;
} finally {
$this->lockService->release();
}
return $job;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Service;
final class LockService
{
private $handle = null;
private string $lockFile;
public function __construct(string $projectDir)
{
$dir = $projectDir . '/var/locks';
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$this->lockFile = $dir . '/ingest.lock';
}
/**
* Gibt true zurück, wenn Lock erfolgreich gesetzt wurde.
* Wenn false: ein anderer Prozess hält den Lock.
*/
public function acquire(): bool
{
$handle = fopen($this->lockFile, 'c+');
if (!$handle) {
throw new \RuntimeException('Could not open lock file.');
}
// Nicht-blockierend: sofort true/false
if (!flock($handle, LOCK_EX | LOCK_NB)) {
fclose($handle);
return false;
}
// Lock halten: Handle offen lassen
$this->handle = $handle;
// Optional: Metainfo reinschreiben
ftruncate($this->handle, 0);
fwrite($this->handle, (string) time());
return true;
}
public function release(): void
{
if ($this->handle) {
flock($this->handle, LOCK_UN);
fclose($this->handle);
$this->handle = null;
}
}
public function isLocked(): bool
{
$handle = fopen($this->lockFile, 'c+');
if (!$handle) {
return false;
}
$locked = !flock($handle, LOCK_EX | LOCK_NB);
if (!$locked) {
flock($handle, LOCK_UN);
}
fclose($handle);
return $locked;
}
}