Compare commits
1 Commits
8d8389377d
...
feat/monit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2289156f57 |
@@ -116,13 +116,14 @@ task('webpack_encore:build', function () {
|
||||
sh -c '$installCmd'");
|
||||
});
|
||||
|
||||
// Restart Docker containers (entrypoint gère migrations + cache:warmup automatiquement)
|
||||
// Le cache est regénéré par l'entrypoint AVANT que FrankenPHP ne démarre,
|
||||
// ce qui évite la race condition entre FrankenPHP et un docker exec concurrent.
|
||||
// Restart Docker containers (entrypoint gère les migrations automatiquement)
|
||||
// Le cache:clear est fait APRÈS le restart : Docker résout le bind mount au démarrage
|
||||
// du container, pas dynamiquement. Avant restart, docker exec voit encore l'ancienne release.
|
||||
desc('Restart Docker containers');
|
||||
task('docker:restart', function () {
|
||||
run('docker restart mangarr-worker-commands mangarr-worker-events mangarr-worker-scheduler');
|
||||
run('docker restart mangarr');
|
||||
run('docker exec mangarr php bin/console cache:clear --env=prod');
|
||||
});
|
||||
|
||||
// Pas de PHP sur l'hôte : désactiver les tâches Symfony qui en ont besoin
|
||||
|
||||
@@ -53,12 +53,11 @@ if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Vider le cache prod stale et le regénérer AVANT le démarrage de FrankenPHP.
|
||||
# Sans ça, FrankenPHP et le deploy script compilent le container DI en parallèle
|
||||
# → fichiers partiellement écrits → crash au démarrage des workers.
|
||||
# Vider le cache prod stale avant le démarrage des workers FrankenPHP.
|
||||
# Sans ça, les workers chargent l'ancien cache du volume Docker et crashent
|
||||
# en boucle si les classes du cache ne correspondent plus à la version déployée.
|
||||
if [ "$APP_ENV" = "prod" ]; then
|
||||
rm -rf var/cache/prod
|
||||
php bin/console cache:warmup --env=prod
|
||||
fi
|
||||
|
||||
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
|
||||
|
||||
@@ -20,7 +20,7 @@ readonly class CheckMonitoredMangasHandler
|
||||
{
|
||||
$criteria = new MonitoringCriteria(
|
||||
enabled: true,
|
||||
lastCheckBefore: new \DateTimeImmutable('-2 hours')
|
||||
lastCheckBefore: $command->since ?? new \DateTimeImmutable('-1 hour')
|
||||
);
|
||||
|
||||
$monitoredMangas = $this->mangaRepository->findByMonitoringCriteria($criteria);
|
||||
|
||||
@@ -26,26 +26,18 @@ readonly class RefreshMangaChaptersHandler
|
||||
throw new \RuntimeException('Manga not found');
|
||||
}
|
||||
|
||||
// Synchronisation + récupération des numéros de nouveaux chapitres
|
||||
$newChapterNumbers = $this->chapterSynchronizationService->synchronizeChapters($manga);
|
||||
// Synchronisation + récupération des nouveaux IDs
|
||||
$newChapterIds = $this->chapterSynchronizationService->synchronizeChapters($manga);
|
||||
|
||||
// Mise à jour de la date de monitoring
|
||||
$manga->updateLastMonitoringCheck(new \DateTimeImmutable());
|
||||
$this->mangaRepository->save($manga);
|
||||
|
||||
// Événement de scraping pour chaque nouveau chapitre
|
||||
// On retrouve l'ID réel (PK integer) après save() car le chapitre n'a
|
||||
// son identifiant définitif qu'une fois persisté en base.
|
||||
foreach ($newChapterNumbers as $chapterNumber) {
|
||||
$saved = $this->mangaRepository->findChapterByMangaIdAndNumber(
|
||||
$manga->getId()->getValue(),
|
||||
$chapterNumber
|
||||
foreach ($newChapterIds as $chapterId) {
|
||||
$this->eventBus->dispatch(
|
||||
new ChapterReadyForScraping(new ChapterId($chapterId))
|
||||
);
|
||||
if ($saved) {
|
||||
$this->eventBus->dispatch(
|
||||
new ChapterReadyForScraping(new ChapterId($saved->getId()))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ interface ChapterSynchronizationServiceInterface
|
||||
/**
|
||||
* Synchronise les chapitres d'un manga depuis la source externe.
|
||||
*
|
||||
* @return float[] Numéros des nouveaux chapitres ajoutés
|
||||
* @return string[] IDs des nouveaux chapitres ajoutés
|
||||
*/
|
||||
public function synchronizeChapters(Manga $manga): array;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ class MonitoringSchedule implements ScheduleProviderInterface
|
||||
{
|
||||
return (new Schedule())->add(
|
||||
// Toutes les 2 heures, vérifie les mangas qui n'ont pas été vérifiés depuis 2 heures
|
||||
RecurringMessage::every('2 hours', new CheckMonitoredMangas())
|
||||
RecurringMessage::every('2 hours', new CheckMonitoredMangas(
|
||||
new \DateTimeImmutable('-2 hours')
|
||||
))
|
||||
)->stateful($this->cache);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,11 +96,11 @@ readonly class MangadxChapterSynchronizationService implements ChapterSynchroniz
|
||||
|
||||
$newChapterIds = [];
|
||||
|
||||
// Sauvegarde uniquement les nouveaux chapitres et collecte leurs numéros
|
||||
// Sauvegarde uniquement les nouveaux chapitres et collecte leurs IDs
|
||||
foreach ($chaptersByNumber as $chapterNumber => $chapter) {
|
||||
if (!isset($existingChapters[(float) $chapterNumber])) {
|
||||
$manga->addChapter($chapter);
|
||||
$newChapterIds[] = $chapter->getNumber();
|
||||
$newChapterIds[] = $chapter->getId();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,6 @@ use App\Domain\Scraping\Application\Command\ScrapeChapter;
|
||||
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Scraping\Domain\Model\ScrapingJob;
|
||||
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
class AutoScrapingListener
|
||||
@@ -18,7 +15,6 @@ class AutoScrapingListener
|
||||
private readonly ScrapeChapterHandler $scrapeChapterHandler,
|
||||
private readonly ChapterRepositoryInterface $chapterRepository,
|
||||
private readonly MangaRepositoryInterface $mangaRepository,
|
||||
private readonly JobRepositoryInterface $jobRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -29,12 +25,7 @@ class AutoScrapingListener
|
||||
$manga = $this->mangaRepository->getById($chapter->mangaId);
|
||||
|
||||
if ($manga->isMonitored()) {
|
||||
$jobId = Uuid::uuid4()->toString();
|
||||
$job = new ScrapingJob($jobId);
|
||||
$job->context['chapterId'] = $event->chapterId->getValue();
|
||||
$this->jobRepository->save($job);
|
||||
|
||||
$this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue(), $jobId));
|
||||
$this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ class InMemoryChapterSynchronizationService implements ChapterSynchronizationSer
|
||||
'synchronized_at' => new \DateTimeImmutable(),
|
||||
];
|
||||
|
||||
// Retourne les numéros des chapitres synchronisés (simulation)
|
||||
return [1.0, 2.0];
|
||||
// Retourne les IDs des chapitres synchronisés (simulation)
|
||||
return ['chapter-1', 'chapter-2'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Domain\Manga\Application\CommandHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Command\CheckMonitoredMangas;
|
||||
use App\Domain\Manga\Application\Command\RefreshMangaChapters;
|
||||
use App\Domain\Manga\Application\CommandHandler\CheckMonitoredMangasHandler;
|
||||
use App\Domain\Manga\Domain\Model\Manga;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
|
||||
use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository;
|
||||
use App\Tests\Shared\Adapter\InMemoryMessageBus;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CheckMonitoredMangasHandlerTest extends TestCase
|
||||
{
|
||||
private InMemoryMangaRepository $mangaRepository;
|
||||
private InMemoryMessageBus $commandBus;
|
||||
private CheckMonitoredMangasHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->mangaRepository = new InMemoryMangaRepository();
|
||||
$this->commandBus = new InMemoryMessageBus();
|
||||
$this->commandBus->clear();
|
||||
$this->handler = new CheckMonitoredMangasHandler($this->mangaRepository, $this->commandBus);
|
||||
}
|
||||
|
||||
private function createManga(string $id): Manga
|
||||
{
|
||||
return new Manga(
|
||||
new MangaId($id),
|
||||
new MangaTitle('Manga ' . $id),
|
||||
new MangaSlug('manga-' . $id),
|
||||
'Description',
|
||||
'Author',
|
||||
2024,
|
||||
[],
|
||||
'ongoing',
|
||||
new ExternalId('ext-' . $id)
|
||||
);
|
||||
}
|
||||
|
||||
public function testDispatchesRefreshForMonitoredMangaWithOldCheck(): void
|
||||
{
|
||||
$manga = $this->createManga('manga-1');
|
||||
$manga->enableMonitoring();
|
||||
$manga->updateLastMonitoringCheck(new \DateTimeImmutable('-3 hours'));
|
||||
$this->mangaRepository->save($manga);
|
||||
|
||||
$this->handler->handle(new CheckMonitoredMangas());
|
||||
|
||||
$this->assertTrue($this->commandBus->hasMessageOfType(RefreshMangaChapters::class));
|
||||
$dispatched = array_filter(
|
||||
$this->commandBus->getDispatchedMessages(),
|
||||
fn ($m) => $m instanceof RefreshMangaChapters
|
||||
);
|
||||
$this->assertCount(1, $dispatched);
|
||||
$this->assertSame('manga-1', array_values($dispatched)[0]->mangaId->getValue());
|
||||
}
|
||||
|
||||
public function testDoesNotDispatchForNonMonitoredManga(): void
|
||||
{
|
||||
$manga = $this->createManga('manga-2');
|
||||
$this->mangaRepository->save($manga);
|
||||
|
||||
$this->handler->handle(new CheckMonitoredMangas());
|
||||
|
||||
$this->assertFalse($this->commandBus->hasMessageOfType(RefreshMangaChapters::class));
|
||||
}
|
||||
|
||||
public function testDoesNotDispatchForMangaWithRecentCheck(): void
|
||||
{
|
||||
$manga = $this->createManga('manga-3');
|
||||
$manga->enableMonitoring();
|
||||
$manga->updateLastMonitoringCheck(new \DateTimeImmutable('-30 minutes'));
|
||||
$this->mangaRepository->save($manga);
|
||||
|
||||
$this->handler->handle(new CheckMonitoredMangas());
|
||||
|
||||
$this->assertFalse($this->commandBus->hasMessageOfType(RefreshMangaChapters::class));
|
||||
}
|
||||
|
||||
public function testDispatchesOnlyMangasWithOldCheck(): void
|
||||
{
|
||||
$mangaOld = $this->createManga('manga-old');
|
||||
$mangaOld->enableMonitoring();
|
||||
$mangaOld->updateLastMonitoringCheck(new \DateTimeImmutable('-3 hours'));
|
||||
$this->mangaRepository->save($mangaOld);
|
||||
|
||||
$mangaRecent = $this->createManga('manga-recent');
|
||||
$mangaRecent->enableMonitoring();
|
||||
$mangaRecent->updateLastMonitoringCheck(new \DateTimeImmutable('-30 minutes'));
|
||||
$this->mangaRepository->save($mangaRecent);
|
||||
|
||||
$mangaDisabled = $this->createManga('manga-disabled');
|
||||
$this->mangaRepository->save($mangaDisabled);
|
||||
|
||||
$this->handler->handle(new CheckMonitoredMangas());
|
||||
|
||||
$dispatched = array_filter(
|
||||
$this->commandBus->getDispatchedMessages(),
|
||||
fn ($m) => $m instanceof RefreshMangaChapters
|
||||
);
|
||||
$this->assertCount(1, $dispatched);
|
||||
$this->assertSame('manga-old', array_values($dispatched)[0]->mangaId->getValue());
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Domain\Scraping\Infrastructure\EventListener;
|
||||
|
||||
use App\Domain\Manga\Domain\Event\ChapterReadyForScraping;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
|
||||
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;
|
||||
use App\Domain\Scraping\Domain\Model\Chapter;
|
||||
use App\Domain\Scraping\Domain\Model\Manga;
|
||||
use App\Domain\Scraping\Domain\Model\ScrapingJob;
|
||||
use App\Domain\Scraping\Infrastructure\EventListener\AutoScrapingListener;
|
||||
use App\Domain\Shared\Domain\Event\ChapterScraped;
|
||||
use App\Domain\Shared\Domain\Model\JobStatus;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryChapterRepository;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryEventBus;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryImageDownloader;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryImageStorage;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryMangaRepository;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryScraperFactory;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemorySourceRepository;
|
||||
use App\Tests\Domain\Shared\Adapter\InMemoryJobRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AutoScrapingListenerTest extends TestCase
|
||||
{
|
||||
private InMemoryChapterRepository $chapterRepository;
|
||||
private InMemoryMangaRepository $mangaRepository;
|
||||
private InMemoryJobRepository $jobRepository;
|
||||
private InMemoryEventBus $eventBus;
|
||||
private AutoScrapingListener $listener;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->chapterRepository = new InMemoryChapterRepository();
|
||||
$this->mangaRepository = new InMemoryMangaRepository();
|
||||
$this->mangaRepository->clear();
|
||||
$this->jobRepository = new InMemoryJobRepository();
|
||||
$this->eventBus = new InMemoryEventBus();
|
||||
|
||||
$handler = new ScrapeChapterHandler(
|
||||
new InMemoryScraperFactory(),
|
||||
new InMemoryImageDownloader(),
|
||||
new InMemoryImageStorage(),
|
||||
$this->jobRepository,
|
||||
$this->chapterRepository,
|
||||
$this->mangaRepository,
|
||||
new InMemorySourceRepository(),
|
||||
$this->eventBus,
|
||||
);
|
||||
|
||||
$this->listener = new AutoScrapingListener(
|
||||
$handler,
|
||||
$this->chapterRepository,
|
||||
$this->mangaRepository,
|
||||
$this->jobRepository,
|
||||
);
|
||||
}
|
||||
|
||||
public function testCreatesJobAndScrapesWhenMangaIsMonitored(): void
|
||||
{
|
||||
$chapterId = 'chapter-uuid-1';
|
||||
$mangaId = 'manga-monitored';
|
||||
|
||||
$this->chapterRepository->save(new Chapter($chapterId, $mangaId, 1177.0, null));
|
||||
$this->mangaRepository->save(new Manga(
|
||||
$mangaId, 'One Piece', 'one-piece', 'Desc', 'Oda', '1997', true
|
||||
));
|
||||
|
||||
$this->listener->onChapterReadyForScraping(
|
||||
new ChapterReadyForScraping(new ChapterId($chapterId))
|
||||
);
|
||||
|
||||
$jobs = $this->jobRepository->findByType('scraping_job');
|
||||
$this->assertCount(1, $jobs);
|
||||
|
||||
$job = array_values($jobs)[0];
|
||||
$this->assertSame($chapterId, $job->context['chapterId']);
|
||||
$this->assertInstanceOf(ScrapingJob::class, $job);
|
||||
|
||||
$hasChapterScraped = count(array_filter(
|
||||
$this->eventBus->getDispatchedMessages(),
|
||||
fn ($m) => $m instanceof ChapterScraped
|
||||
)) > 0;
|
||||
$this->assertTrue($hasChapterScraped);
|
||||
}
|
||||
|
||||
public function testDoesNothingWhenMangaIsNotMonitored(): void
|
||||
{
|
||||
$chapterId = 'chapter-uuid-2';
|
||||
$mangaId = 'manga-not-monitored';
|
||||
|
||||
$this->chapterRepository->save(new Chapter($chapterId, $mangaId, 1176.0, null));
|
||||
$this->mangaRepository->save(new Manga(
|
||||
$mangaId, 'One Piece', 'one-piece', 'Desc', 'Oda', '1997', false
|
||||
));
|
||||
|
||||
$this->listener->onChapterReadyForScraping(
|
||||
new ChapterReadyForScraping(new ChapterId($chapterId))
|
||||
);
|
||||
|
||||
$this->assertEmpty($this->jobRepository->findByType('scraping_job'));
|
||||
$this->assertEmpty($this->eventBus->getDispatchedMessages());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user