Repository: cisagov/manage.get.gov

Issue:
Increase Cache efficiency by not pulling hosts/contacts unless needed
### Issue Description

The current _fetch_cache function has an if-block for contacts and hosts respectively. Don't pull the host or contact information unless it is explicitly the property that is asked for in order to limit epp calls, but ensure that whatever was stored in the cache for these two items isn't removed.

Implementation notes:
- fetch_hosts and fetch_contacts is commented out in each if. Uncomment these but then make sure to store what was in the cache for these items in a temporary variable and then add it to the new cache object that is made in this function.


### Additional Context (optional)

_No response_

### Issue Links

_No response_


Discussion:

Resolving patch:
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -960,75 +960,38 @@ def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
             # statuses can just be a list no need to keep the epp object
             if "statuses" in cleaned.keys():
                 cleaned["statuses"] = [status.state for status in cleaned["statuses"]]
+
+            # Capture and store old hosts and contacts from cache if they exist
+            old_cache_hosts = self._cache.get("hosts")
+            old_cache_contacts = self._cache.get("contacts")
+
             # get contact info, if there are any
             if (
-                # fetch_contacts and
-                "_contacts" in cleaned
+                fetch_contacts
+                and "_contacts" in cleaned
                 and isinstance(cleaned["_contacts"], list)
                 and len(cleaned["_contacts"])
             ):
-                cleaned["contacts"] = []
-                for domainContact in cleaned["_contacts"]:
-                    # we do not use _get_or_create_* because we expect the object we
-                    # just asked the registry for still exists --
-                    # if not, that's a problem
-
-                    # TODO- discuss-should we check if contact is in public contacts
-                    # and add it if not- this is really to keep in mine the transisiton
-                    req = commands.InfoContact(id=domainContact.contact)
-                    data = registry.send(req, cleaned=True).res_data[0]
-
-                    # extract properties from response
-                    # (Ellipsis is used to mean "null")
-                    # convert this to use PublicContactInstead
-                    contact = {
-                        "id": domainContact.contact,
-                        "type": domainContact.type,
-                        "auth_info": getattr(data, "auth_info", ...),
-                        "cr_date": getattr(data, "cr_date", ...),
-                        "disclose": getattr(data, "disclose", ...),
-                        "email": getattr(data, "email", ...),
-                        "fax": getattr(data, "fax", ...),
-                        "postal_info": getattr(data, "postal_info", ...),
-                        "statuses": getattr(data, "statuses", ...),
-                        "tr_date": getattr(data, "tr_date", ...),
-                        "up_date": getattr(data, "up_date", ...),
-                        "voice": getattr(data, "voice", ...),
-                    }
-
-                    cleaned["contacts"].append(
-                        {k: v for k, v in contact.items() if v is not ...}
-                    )
+                cleaned["contacts"] = self._fetch_contacts(cleaned["_contacts"])
+                # We're only getting contacts, so retain the old
+                # hosts that existed in cache (if they existed)
+                # and pass them along.
+                if old_cache_hosts is not None:
+                    cleaned["hosts"] = old_cache_hosts
 
             # get nameserver info, if there are any
             if (
-                # fetch_hosts and
-                "_hosts" in cleaned
+                fetch_hosts
+                and "_hosts" in cleaned
                 and isinstance(cleaned["_hosts"], list)
                 and len(cleaned["_hosts"])
             ):
-                # TODO- add elif in cache set it to be the old cache value
-                # no point in removing
-                cleaned["hosts"] = []
-                for name in cleaned["_hosts"]:
-                    # we do not use _get_or_create_* because we expect the object we
-                    # just asked the registry for still exists --
-                    # if not, that's a problem
-                    req = commands.InfoHost(name=name)
-                    data = registry.send(req, cleaned=True).res_data[0]
-                    # extract properties from response
-                    # (Ellipsis is used to mean "null")
-                    host = {
-                        "name": name,
-                        "addrs": getattr(data, "addrs", ...),
-                        "cr_date": getattr(data, "cr_date", ...),
-                        "statuses": getattr(data, "statuses", ...),
-                        "tr_date": getattr(data, "tr_date", ...),
-                        "up_date": getattr(data, "up_date", ...),
-                    }
-                    cleaned["hosts"].append(
-                        {k: v for k, v in host.items() if v is not ...}
-                    )
+                cleaned["hosts"] = self._fetch_hosts(cleaned["_hosts"])
+                # We're only getting hosts, so retain the old
+                # contacts that existed in cache (if they existed)
+                # and pass them along.
+                if old_cache_contacts is not None:
+                    cleaned["contacts"] = old_cache_contacts
 
             # replace the prior cache with new data
             self._cache = cleaned
@@ -1036,6 +999,46 @@ def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
         except RegistryError as e:
             logger.error(e)
 
+    def _fetch_contacts(self, contact_data):
+        """Fetch contact info."""
+        contacts = []
+        for domainContact in contact_data:
+            req = commands.InfoContact(id=domainContact.contact)
+            data = registry.send(req, cleaned=True).res_data[0]
+            contact = {
+                "id": domainContact.contact,
+                "type": domainContact.type,
+                "auth_info": getattr(data, "auth_info", ...),
+                "cr_date": getattr(data, "cr_date", ...),
+                "disclose": getattr(data, "disclose", ...),
+                "email": getattr(data, "email", ...),
+                "fax": getattr(data, "fax", ...),
+                "postal_info": getattr(data, "postal_info", ...),
+                "statuses": getattr(data, "statuses", ...),
+                "tr_date": getattr(data, "tr_date", ...),
+                "up_date": getattr(data, "up_date", ...),
+                "voice": getattr(data, "voice", ...),
+            }
+            contacts.append({k: v for k, v in contact.items() if v is not ...})
+        return contacts
+
+    def _fetch_hosts(self, host_data):
+        """Fetch host info."""
+        hosts = []
+        for name in host_data:
+            req = commands.InfoHost(name=name)
+            data = registry.send(req, cleaned=True).res_data[0]
+            host = {
+                "name": name,
+                "addrs": getattr(data, "addrs", ...),
+                "cr_date": getattr(data, "cr_date", ...),
+                "statuses": getattr(data, "statuses", ...),
+                "tr_date": getattr(data, "tr_date", ...),
+                "up_date": getattr(data, "up_date", ...),
+            }
+            hosts.append({k: v for k, v in host.items() if v is not ...})
+        return hosts
+
     def _invalidate_cache(self):
         """Remove cache data when updates are made."""
         self._cache = {}


Test patch:
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -49,8 +49,6 @@ def test_cache_sets_resets(self):
                     commands.InfoDomain(name="igorville.gov", auth_info=None),
                     cleaned=True,
                 ),
-                call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
-                call(commands.InfoHost(name="fake.host.com"), cleaned=True),
             ],
             any_order=False,  # Ensure calls are in the specified order
         )
@@ -72,8 +70,6 @@ def test_cache_used_when_avail(self):
             call(
                 commands.InfoDomain(name="igorville.gov", auth_info=None), cleaned=True
             ),
-            call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
-            call(commands.InfoHost(name="fake.host.com"), cleaned=True),
         ]
 
         self.mockedSendFunction.assert_has_calls(expectedCalls)
@@ -108,6 +104,19 @@ def test_cache_nested_elements(self):
         # get and check hosts is set correctly
         domain._get_property("hosts")
         self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
+        self.assertEqual(domain._cache["contacts"], [expectedContactsDict])
+
+        # invalidate cache
+        domain._cache = {}
+
+        # get host
+        domain._get_property("hosts")
+        self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
+
+        # get contacts
+        domain._get_property("contacts")
+        self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
+        self.assertEqual(domain._cache["contacts"], [expectedContactsDict])
 
     def tearDown(self) -> None:
         Domain.objects.all().delete()
@@ -168,8 +177,6 @@ def test_accessing_domain_properties_creates_domain_in_registry(self):
                     commands.InfoDomain(name="beef-tongue.gov", auth_info=None),
                     cleaned=True,
                 ),
-                call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
-                call(commands.InfoHost(name="fake.host.com"), cleaned=True),
             ],
             any_order=False,  # Ensure calls are in the specified order
         )
@@ -219,8 +226,6 @@ def test_get_status(self):
                     commands.InfoDomain(name="chicken-liver.gov", auth_info=None),
                     cleaned=True,
                 ),
-                call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
-                call(commands.InfoHost(name="fake.host.com"), cleaned=True),
             ],
             any_order=False,  # Ensure calls are in the specified order
         )