mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
perf(imap): make the ghost reaper free to run when there is nothing to reap
The reaper runs with the other self-repairs on every mailbox sync, and the answer is "nothing" on every install that has run it once — so the cost that matters is the asking, repeated per mailbox per poll forever, over the largest table here. A partial index over epoch-dated rows makes that a probe of a relation that is normally empty, and a cheap literal existence check in front of the eight-way predicate is what actually hits it. The literal is deliberate: a bound parameter cannot be proven to imply a partial index's condition at plan time, which would have left the index built and unused. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8aeca04f0b
commit
580e6a5bc6
2 files changed
+65
No files matched your search
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Makes "are there any ghost rows left" free to ask.
|
||||
*
|
||||
* GhostMessageReaper runs with the other self-repairs on every mailbox sync,
|
||||
* looking for the epoch-dated corpses a failed IMAP fetch used to leave behind
|
||||
* (see MessageSyncer::isUsableFetch). The answer is no on every install that has
|
||||
* run it once — which, after the first sync, is every install — so the cost that
|
||||
* matters is not the cleanup but the asking, repeated per mailbox per poll
|
||||
* forever.
|
||||
*
|
||||
* Without an index that is a sequential scan of the message table each time, on
|
||||
* a table that is the largest one here by a wide margin. A partial index makes
|
||||
* it an index probe of a relation that is normally empty: nothing real is dated
|
||||
* 1970, so this indexes the rows that should not exist and nothing else. On a
|
||||
* healthy database it occupies essentially no space.
|
||||
*
|
||||
* The predicate is a literal rather than a parameter so the planner can match
|
||||
* it against the reaper's own literal probe — a bound parameter cannot be
|
||||
* proven to imply a partial index's condition at plan time, which would have
|
||||
* left the index built and unused.
|
||||
*/
|
||||
final class Version20260811160000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Partial index over epoch-dated message rows, so the ghost reaper costs nothing when there is nothing to reap';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(
|
||||
"CREATE INDEX idx_message_epoch_ghost ON message (id) WHERE received_at < TIMESTAMP '1971-01-01'",
|
||||
);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX idx_message_epoch_ghost');
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,22 @@ readonly class GhostMessageReaper
|
||||
*/
|
||||
public function reap(): int
|
||||
{
|
||||
// Asked before the real query, and this is the only reason the reaper
|
||||
// can afford to run on every mailbox sync. The full predicate is eight
|
||||
// conditions over the largest table here; this is one probe of a partial
|
||||
// index that is empty on any install that has already been cleaned —
|
||||
// which is every install, after the first run. See
|
||||
// Version20260811160000, and note the literal: a bound parameter cannot
|
||||
// be proven to imply the index's condition, so the index would be built
|
||||
// and then not used.
|
||||
$suspects = $this->em->getConnection()->fetchOne(
|
||||
"SELECT 1 FROM message WHERE received_at < TIMESTAMP '1971-01-01' LIMIT 1",
|
||||
);
|
||||
|
||||
if (false === $suspects) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$ghosts = $this->messages->findEpochGhosts(self::REAP_BATCH);
|
||||
|
||||
if ([] === $ghosts) {
|
||||
|
||||
Reference in new issue
Block a user