Repository: pypi/warehouse

Issue:
Unnecessary purges on `User` model change
Currently right now any time the `User` model changes, we purge all the cache keys for that user's project.

This includes attribute changes that don't actually affect the project pages, like `last_login`, `password` etc.

We should filter out "purge-able" attribute changes and only issue purges when necessary. Said attributes include:
* `username`
* `name`
* `emails`


Discussion:
~~it also issues an `all-projects` which purges `/simple`~~
\* was looking at wrong log :)
Hmm, shouldn't be...
my bad, sneaky edited my original comment after checking logs more closely.
Ernest mentioned we need this before launching beta, because otherwise it's so costly to serve users who have many projects each.

Resolving patch:
diff --git a/warehouse/cache/origin/__init__.py b/warehouse/cache/origin/__init__.py
--- a/warehouse/cache/origin/__init__.py
+++ b/warehouse/cache/origin/__init__.py
@@ -15,6 +15,8 @@
 import operator
 from itertools import chain
 
+from sqlalchemy.orm.session import Session
+
 from warehouse import db
 from warehouse.cache.origin.interfaces import IOriginCache
 
@@ -132,6 +134,19 @@ def register_origin_cache_keys(config, klass, cache_keys=None,
     )
 
 
+def receive_set(attribute, config, target):
+    cache_keys = config.registry["cache_keys"]
+    session = Session.object_session(target)
+    if session:
+        purges = session.info.setdefault(
+            "warehouse.cache.origin.purges",
+            set()
+        )
+        key_maker = cache_keys[attribute]
+        keys = key_maker(target).purge
+        purges.update(list(keys))
+
+
 def includeme(config):
     if "origin_cache.backend" in config.registry.settings:
         cache_class = config.maybe_dotted(
diff --git a/warehouse/packaging/__init__.py b/warehouse/packaging/__init__.py
--- a/warehouse/packaging/__init__.py
+++ b/warehouse/packaging/__init__.py
@@ -11,14 +11,25 @@
 # limitations under the License.
 
 from celery.schedules import crontab
+from warehouse import db
 
-from warehouse.accounts.models import User
-from warehouse.cache.origin import key_factory
+from warehouse.accounts.models import User, Email
+from warehouse.cache.origin import key_factory, receive_set
 from warehouse.packaging.interfaces import IFileStorage
 from warehouse.packaging.models import Project, Release
 from warehouse.packaging.tasks import compute_trending
 
 
+@db.listens_for(User.name, 'set')
+def user_name_receive_set(config, target, value, oldvalue, initiator):
+    receive_set(User.name, config, target)
+
+
+@db.listens_for(Email.primary, 'set')
+def email_primary_receive_set(config, target, value, oldvalue, initiator):
+    receive_set(Email.primary, config, target)
+
+
 def includeme(config):
     # Register whatever file storage backend has been configured for storing
     # our package files.
@@ -49,11 +60,24 @@ def includeme(config):
     config.register_origin_cache_keys(
         User,
         cache_keys=["user/{obj.username}"],
+    )
+    config.register_origin_cache_keys(
+        User.name,
         purge_keys=[
             key_factory("user/{obj.username}"),
             key_factory("project/{itr.normalized_name}", iterate_on='projects')
         ],
     )
+    config.register_origin_cache_keys(
+        Email.primary,
+        purge_keys=[
+            key_factory("user/{obj.user.username}"),
+            key_factory(
+                "project/{itr.normalized_name}",
+                iterate_on='user.projects',
+            )
+        ],
+    )
 
     # Add a periodic task to compute trending once a day, assuming we have
     # been configured to be able to access BigQuery.


Test patch:
diff --git a/tests/common/db/accounts.py b/tests/common/db/accounts.py
--- a/tests/common/db/accounts.py
+++ b/tests/common/db/accounts.py
@@ -37,6 +37,7 @@ class Meta:
     last_login = factory.fuzzy.FuzzyNaiveDateTime(
         datetime.datetime(2011, 1, 1),
     )
+    projects = []
 
 
 class EmailFactory(WarehouseFactory):
diff --git a/tests/unit/packaging/test_init.py b/tests/unit/packaging/test_init.py
--- a/tests/unit/packaging/test_init.py
+++ b/tests/unit/packaging/test_init.py
@@ -16,8 +16,9 @@
 from celery.schedules import crontab
 
 from warehouse import packaging
+from warehouse.accounts.models import Email, User
 from warehouse.packaging.interfaces import IFileStorage
-from warehouse.packaging.models import Project, Release, User
+from warehouse.packaging.models import Project, Release
 from warehouse.packaging.tasks import compute_trending
 
 
@@ -73,6 +74,9 @@ def key_factory(keystring, iterate_on=None):
         pretend.call(
             User,
             cache_keys=["user/{obj.username}"],
+        ),
+        pretend.call(
+            User.name,
             purge_keys=[
                 key_factory("user/{obj.username}"),
                 key_factory(
@@ -81,6 +85,16 @@ def key_factory(keystring, iterate_on=None):
                 ),
             ],
         ),
+        pretend.call(
+            Email.primary,
+            purge_keys=[
+                key_factory("user/{obj.user.username}"),
+                key_factory(
+                    "project/{itr.normalized_name}",
+                    iterate_on='user.projects',
+                ),
+            ],
+        ),
     ]
 
     if with_trending: