fix(mail) : synchro multi-dossiers fiable contre OVH
Trois causes racines révélées par une vraie synchro complète (139 dossiers) : - contrainte UNIQUE globale sur message_id : fausse pour IMAP (un même Message-ID existe dans plusieurs dossiers) → violation → fermeture de l'EntityManager → cascade qui tuait tous les dossiers suivants. Migration : index simple à la place. - 139 connexions IMAP (une par dossier) → throttling OVH (failed to authenticate) : réutilisation d'une seule connexion (closeConnection() ajouté à l'interface). - état de connexion corrompu après un dossier en erreur (must be in SELECTED state) : reconnexion ciblée après chaque dossier en échec. - garde anti-cascade : reset du ManagerRegistry + arrêt propre si l'EM se ferme. Résultat : 456 messages sur 57 dossiers (avant : 188/30 puis crash). Les rares dossiers à encodage spécial sont skippés proprement et réessayés au cycle suivant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ use App\Repository\MailFolderRepository;
|
||||
use App\Repository\MailMessageRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
use Throwable;
|
||||
@@ -32,6 +33,7 @@ final class MailSyncService
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LockFactory $lockFactory,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly ManagerRegistry $managerRegistry,
|
||||
) {}
|
||||
|
||||
public function syncAll(): MailSyncReport
|
||||
@@ -57,6 +59,7 @@ final class MailSyncService
|
||||
try {
|
||||
return $this->doSyncAll($startedAt);
|
||||
} finally {
|
||||
$this->provider->closeConnection();
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
@@ -274,9 +277,28 @@ final class MailSyncService
|
||||
$totalDeleted += $report->deletedCount;
|
||||
++$totalFolders;
|
||||
$allErrors = array_merge($allErrors, $report->errors);
|
||||
// A folder error can leave the reused IMAP connection in a bad
|
||||
// selection state ("must be in SELECTED state", "empty response").
|
||||
// Drop it so the next folder reconnects on a clean session.
|
||||
if ([] !== $report->errors) {
|
||||
$this->provider->closeConnection();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error(sprintf('doSyncAll: syncFolder[%s] threw: %s', $folder->getPath(), $e->getMessage()));
|
||||
$allErrors[] = $e->getMessage();
|
||||
$this->provider->closeConnection();
|
||||
}
|
||||
|
||||
// A failed flush closes the Doctrine EntityManager; without a reset
|
||||
// every subsequent folder would fail with "EntityManager is closed".
|
||||
// Reset it via the registry and stop the run cleanly — the next cron
|
||||
// cycle resumes incrementally from where we left off.
|
||||
if (!$this->entityManager->isOpen()) {
|
||||
$this->logger->error('doSyncAll: EntityManager was closed mid-sync, resetting and aborting this run');
|
||||
$this->managerRegistry->resetManager();
|
||||
$allErrors[] = 'EntityManager closed mid-sync — run aborted, will resume next cycle';
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user