Repository: WeblateOrg/weblate

Issue:
Alerts cleanup takes too long
### Describe the issue

Presently, each alert is tried to be removed individually. This can lead to excessively many database queries on simple component save.

Example can be seen here (non-public): https://sentry.weblate.org/organizations/weblate/performance/weblate:b86596c42d154893bc2b4e1713c8fd79/

### I already tried

- [X] I've read and searched [the documentation](https://docs.weblate.org/).
- [X] I've searched for similar issues in this repository.

### Steps to reproduce the behavior

1. Perform update of component with many linked components.
2. See the high number of alert related queries.

### Expected behavior

There should be way less queries needed as in most cases there will be no changes - it should fetch all alerts once and check the changes against this list.

### Screenshots

_No response_

### Exception traceback

_No response_

### How do you run Weblate?

weblate.org service

### Weblate versions

4.17

### Weblate deploy checks

_No response_

### Additional context

_No response_


Discussion:

Resolving patch:
diff --git a/weblate/trans/models/component.py b/weblate/trans/models/component.py
--- a/weblate/trans/models/component.py
+++ b/weblate/trans/models/component.py
@@ -2043,6 +2043,10 @@ def all_active_alerts(self):
         list(result)
         return result
 
+    @cached_property
+    def all_alerts(self):
+        return {alert.name: alert for alert in self.alert_set.all()}
+
     @property
     def lock_alerts(self):
         if not self.auto_lock_error:
@@ -2058,27 +2062,33 @@ def trigger_alert(self, name: str, **kwargs):
             self.alerts_trigger[name] = [kwargs]
 
     def delete_alert(self, alert: str):
-        deleted = self.alert_set.filter(name=alert).delete()[0]
-        if (
-            deleted
-            and self.locked
-            and self.auto_lock_error
-            and alert in LOCKING_ALERTS
-            and not self.alert_set.filter(name__in=LOCKING_ALERTS).exists()
-            and self.change_set.filter(action=Change.ACTION_LOCK)
-            .order_by("-id")[0]
-            .auto_status
-        ):
-            self.do_lock(user=None, lock=False, auto=True)
+        if alert in self.all_alerts:
+            self.all_alerts[alert].delete()
+            del self.all_alerts[alert]
+            if (
+                self.locked
+                and self.auto_lock_error
+                and alert in LOCKING_ALERTS
+                and not self.alert_set.filter(name__in=LOCKING_ALERTS).exists()
+                and self.change_set.filter(action=Change.ACTION_LOCK)
+                .order_by("-id")[0]
+                .auto_status
+            ):
+                self.do_lock(user=None, lock=False, auto=True)
 
         if ALERTS[alert].link_wide:
             for component in self.linked_childs:
                 component.delete_alert(alert)
 
     def add_alert(self, alert: str, noupdate: bool = False, **details):
-        obj, created = self.alert_set.get_or_create(
-            name=alert, defaults={"details": details}
-        )
+        if alert in self.all_alerts:
+            obj = self.all_alerts[alert]
+            created = False
+        else:
+            obj, created = self.alert_set.get_or_create(
+                name=alert, defaults={"details": details}
+            )
+            self.all_alerts[alert] = obj
 
         # Automatically lock on error
         if created and self.auto_lock_error and alert in LOCKING_ALERTS:
@@ -2953,6 +2963,8 @@ def update_link_alerts(self, noupdate: bool = False):
             self.delete_alert("DuplicateFilemask")
 
     def update_alerts(self):  # noqa: C901
+        # Flush alerts case, mostly needed for tests
+        self.__dict__.pop("all_alerts", None)
         if (
             self.project.access_control == self.project.ACCESS_PUBLIC
             and settings.LICENSE_REQUIRED


Test patch: