Skip to content

🪲 BUG-#33: Auto-clear UID cache after iteration and warn on large caches#53

Merged
FernandoCelmer merged 2 commits into
developfrom
feature/33
Apr 16, 2026
Merged

🪲 BUG-#33: Auto-clear UID cache after iteration and warn on large caches#53
FernandoCelmer merged 2 commits into
developfrom
feature/33

Conversation

@FernandoCelmer
Copy link
Copy Markdown
Member

Summary

  • Wraps the messages() generator body in try/finally so clear_cache() is called when iteration completes or the generator is closed/GC'd, preventing unbounded UID cache growth.
  • Adds a warning log in _uids() when the cached UID list exceeds 50,000 entries, advising the caller to consider pagination.

Test plan

  • All 149 existing tests pass (pytest tests/ -x -q)
  • Verify warning appears when a mailbox returns >50k UIDs
  • Confirm _cached_uids is None after fully iterating messages()

Closes #33

Copy link
Copy Markdown
Member Author

@FernandoCelmer FernandoCelmer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Code Review

Code issues found: 1

See inline comment below.

for fetched in fetcher.chunks(uids):
done = min(done + size, total)
logger.info(
"Fetched %d/%d messages from %s (mode=%s)",
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocking]

Problem — The clear_cache() call inside the finally block of messages() fires whenever the generator is closed, including partial consumption. This means calling first(), which iterates only one message and then abandons the generator, will trigger clear_cache() and set _cached_uids = None.

Failure scenario — Consider this common usage pattern:

where = app.inbox.where(Q.unseen())
count = where.count()          # caches UIDs via _uids()
msg = where.first()            # calls messages(chunk_size=1), returns 1st msg
                                # generator abandoned -> finally fires -> cache cleared
count2 = where.count()         # _cached_uids is None -> re-queries IMAP server!

Every call to first(), last(), or partial for loop iteration will destroy the cached UIDs, forcing a full IMAP SEARCH on the next access. This is a significant performance regression for interactive or polling use cases.

Fix — Instead of unconditionally clearing in finally, only clear the cache when the generator is fully exhausted (not abandoned):

def messages(self, ...):
    uids = self._uids()
    ...
    exhausted = False
    try:
        for fetched in fetcher.chunks(uids):
            ...
        exhausted = True
    finally:
        if exhausted:
            self.clear_cache()

Alternatively, consider a TTL-based expiration or a max-size eviction policy instead of aggressive clearing.

@FernandoCelmer FernandoCelmer merged commit a05ab21 into develop Apr 16, 2026
@FernandoCelmer FernandoCelmer deleted the feature/33 branch April 16, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant