stash light
This commit is contained in:
123
src/Service/DocumentService.php
Normal file
123
src/Service/DocumentService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
54
src/Service/IngestJobService.php
Normal file
54
src/Service/IngestJobService.php
Normal 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();
|
||||
}
|
||||
}
|
||||
130
src/Service/IngestOrchestrator.php
Normal file
130
src/Service/IngestOrchestrator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
71
src/Service/LockService.php
Normal file
71
src/Service/LockService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user