Skip to content

Epoch verification runs on non-IMAP providers every full sync, drowning the IOS-EPOCH-001 signal #75

Description

@tabmail-kmyi

SyncEngine.verifyAndBootstrapPrePopulatedFolderEpoch has two call sites, and they are gated differently:

  • SyncEngineFullSync.swift, the deletion-reconcile loop — guarded by if provider.staleWindowMode == .uid, let imapProvider = provider as? IMAPProvider. Correct.
  • SyncEngineFullSync.swift, at the head of runSyncMessagesno provider guard at all. runSyncMessages takes provider: any EmailProvider, so this runs for Gmail and Microsoft Graph accounts too.

For a non-IMAP provider the outcome is benign but wasteful. The function performs a dbPool.read (fetch the Folder, then MessageHeader.fetchCount for the folder, then sample UIDs), and then either:

  • finds no row carrying an rfc822MessageId, logs populated but no row carries an rfc822 Message-ID — cannot verify, leaving the epoch unknown, and returns .unobservable; or
  • reaches step 2, calls sampleHeadersForEpochVerification, whose EmailProvider default for HTTP-based providers returns ([], nil), and then step 3's anti-brick guard returns .unobservable.

Either way there is no network call, no quarantine and no mutation — the return value is discarded (@discardableResult). This is not a correctness problem.

What it costs

  1. Two extra DB reads per folder per full sync, on accounts where a UIDVALIDITY epoch is not a meaningful concept.
  2. Log noise that actively degrades a signal that matters. A 1.7.12 device session emitted 45 [EpochVerify] … cannot verify, leaving the epoch unknown lines, and all 45 were for the two Gmail accounts and the one Graph account — zero for the two genuine IMAP accounts. That message names the accepted IOS-EPOCH-001 fail-closed window, so anyone grepping [EpochVerify] to find a real IMAP folder stuck in that window has to filter out a majority of entries that can never be in it.

Suggested direction

Either apply the same provider.staleWindowMode == .uid guard used at the other call site, or add an early-out inside verifyAndBootstrapPrePopulatedFolderEpoch before the dbPool.read.

Note the existing comment at the guarded call site, which should be preserved in whatever shape is chosen:

provider (not imapProvider) is passed on purpose: the seam is a protocol member, and routing it through a downcast would silently send every non-IMAPProvider conformer that models a bound epoch down the do-nothing leg.

So the early-out should key on the capability (staleWindowMode, or the provider's own answer about whether it models an epoch) rather than on is IMAPProvider.

References

  • TabMail/Services/Sync/SyncEngineEpochVerify.swiftverifyAndBootstrapPrePopulatedFolderEpoch, steps 1–3
  • TabMail/Services/Sync/SyncEngineFullSync.swift — the guarded reconcile-loop call site and the unguarded runSyncMessages call site
  • TabMail/Providers/EmailProvider.swift — the sampleHeadersForEpochVerification default returning ([], nil), and staleWindowMode
  • KNOWN_ISSUES.md IOS-EPOCH-001 — the accepted window this log line reports

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    code cleanupNon-behavioral refactoring for code quality enhancementpriority: P5Code cleanup, chores, coverage, tooling and docs hygiene

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions