Repository: zulip/zulip

Issue:
Unable to delete a topic
Hi! I'm attemptimg to delete a large (over 50k messages) topic that contains alerts and logs quotations sent by elastalert. When deleting it from the web gui, the delete request gets stuck for about a minute and then i get 502 with no any entry in zulip logs. Messages from other topics and entire topics still can be deleted, only this one remains untouched.

Is there any way to delete such large topics? Can i delete messages from the database directly?


Discussion:
Hello @zulip/server-message-view members, this issue was labeled with the "area: message-editing" label, so you may want to check it out!

<!-- areaLabelAddition -->

Yikes, 50k messages is a topic is a lot of log spam. You can likely delete it using `do_delete_messages` via a management command shell (https://zulip.readthedocs.io/en/latest/production/management-commands.html), which would avoid the time limit for requests that we have configured. 

The other option is to modify `/etc/zulip/uwsgi.conf` to dramatically increase the timeout (the setting is called `harikiri` in that file; and then you'll need use `scripts/restart-server` to restart the server) and see if that's good enough; might be easier.
@timabbott Thanks for you response! I'm a bit confused, manage.py doesn't show such command:

```
zulip@f866a8398bf6:~/deployments/current$ ./manage.py help do_delete_messages
Unknown command: 'do_delete_messages'. Did you mean restore_messages?
```

And yes, i'm running Zulip in docker, if it's relevant.
@timabbott increased hahakiri to about 3 hours as you suggested but it still fails after the time is up, looks like it takes much more... Maybe you could point how can that `do_delete_messages` be used? Or if it is possible to delete messages from the database directly?
To make it easier to address follow-up questions, I [started a thread in the development community](https://chat.zulip.org/#narrow/stream/31-production-help/topic/Unable.20to.20delete.20a.20topic.20.2322821/near/1432762).
I said "management command shell" - I meant `manage.py shell`, which is documented on that page. `do_delete_messages` is a function you need to call there; it's a Python shell. If you need help scripting that, I recommend getting a support contract.

In terms of a bug fix for Zulip, we'll likely just want to paginate the queries to do blocks of 1k messages in the topic as individual transactions. @mateuszmandera perhaps you can look at whether this is easy to do. I think it'd be a significant improvement even if it still times out, if it just manages to make clear progress before doing so, since that at least would offer a UI level workaround of doing it repeatedly. Eventually we'll need to move to the `deferred_work` queue processor.

Resolving patch:
diff --git a/version.py b/version.py
--- a/version.py
+++ b/version.py
@@ -33,7 +33,7 @@
 # Changes should be accompanied by documentation explaining what the
 # new level means in templates/zerver/api/changelog.md, as well as
 # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`.
-API_FEATURE_LEVEL = 146
+API_FEATURE_LEVEL = 147
 
 # Bump the minor PROVISION_VERSION to indicate that folks should provision
 # only when going from an old version of the code to a newer version. Bump
diff --git a/zerver/views/streams.py b/zerver/views/streams.py
--- a/zerver/views/streams.py
+++ b/zerver/views/streams.py
@@ -55,6 +55,7 @@
 from zerver.lib.mention import MentionBackend, silent_mention_syntax_for_user
 from zerver.lib.request import REQ, has_request_variables
 from zerver.lib.response import json_success
+from zerver.lib.retention import STREAM_MESSAGE_BATCH_SIZE as RETENTION_STREAM_MESSAGE_BATCH_SIZE
 from zerver.lib.retention import parse_message_retention_days
 from zerver.lib.streams import (
     StreamDict,
@@ -848,7 +849,6 @@ def get_topics_backend(
     return json_success(request, data=dict(topics=result))
 
 
-@transaction.atomic
 @require_realm_admin
 @has_request_variables
 def delete_in_topic(
@@ -857,7 +857,7 @@ def delete_in_topic(
     stream_id: int = REQ(converter=to_non_negative_int, path_only=True),
     topic_name: str = REQ("topic_name"),
 ) -> HttpResponse:
-    (stream, sub) = access_stream_by_id(user_profile, stream_id)
+    stream, ignored_sub = access_stream_by_id(user_profile, stream_id)
 
     messages = messages_for_topic(assert_is_not_none(stream.recipient_id), topic_name)
     if not stream.is_history_public_to_subscribers():
@@ -867,9 +867,20 @@ def delete_in_topic(
         ).values_list("message_id", flat=True)
         messages = messages.filter(id__in=deletable_message_ids)
 
-    messages = messages.select_for_update(of=("self",))
-
-    do_delete_messages(user_profile.realm, messages)
+    # Topics can be large enough that this request will inevitably time out.
+    # In such a case, it's good for some progress to be accomplished, so that
+    # full deletion can be achieved by repeating the request. For that purpose,
+    # we delete messages in atomic batches, committing after each batch.
+    # TODO: Ideally this should be moved to the deferred_work queue.
+    batch_size = RETENTION_STREAM_MESSAGE_BATCH_SIZE
+    while True:
+        with transaction.atomic(durable=True):
+            messages_to_delete = messages.order_by("-id")[0:batch_size].select_for_update(
+                of=("self",)
+            )
+            if not messages_to_delete:
+                break
+            do_delete_messages(user_profile.realm, messages_to_delete)
 
     return json_success(request)
 


Test patch: