Repository: zulip/zulip

Issue:
Create stream in an atomic transaction
`create_stream_if_needed` does:
https://github.com/zulip/zulip/blob/862061fa53ad085e1a528a26cc89e636485fb0b9/zerver/lib/streams.py#L105-L125

This has a window when the newly-created stream has no `Recipient` object, which can lead to odd exceptions.  We should create the interconnected Stream and Recipient objects in a single transaction, so that other reads can't find a stream with inconsistent state.


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

<!-- areaLabelAddition -->

@mateuszmandera @abhijeetbodas2001 FYI.
@zulipbot claim
Hello @aadityasinha-dotcom, you have been unassigned from this issue because you have not updated this issue or any referenced pull requests for over 14 days.

You can reclaim this issue or claim any other issue by commenting `@zulipbot claim` on that issue.

Thanks for your contributions, and hope to see you again soon!


Resolving patch:
diff --git a/zerver/lib/streams.py b/zerver/lib/streams.py
--- a/zerver/lib/streams.py
+++ b/zerver/lib/streams.py
@@ -1,5 +1,6 @@
 from typing import Collection, List, Optional, Set, Tuple, Union
 
+from django.db import transaction
 from django.db.models.query import QuerySet
 from django.utils.timezone import now as timezone_now
 from django.utils.translation import gettext as _
@@ -102,42 +103,44 @@ def create_stream_if_needed(
         realm, invite_only, history_public_to_subscribers
     )
 
-    (stream, created) = Stream.objects.get_or_create(
-        realm=realm,
-        name__iexact=stream_name,
-        defaults=dict(
-            name=stream_name,
-            description=stream_description,
-            invite_only=invite_only,
-            is_web_public=is_web_public,
-            stream_post_policy=stream_post_policy,
-            history_public_to_subscribers=history_public_to_subscribers,
-            is_in_zephyr_realm=realm.is_zephyr_mirror_realm,
-            message_retention_days=message_retention_days,
-        ),
-    )
+    with transaction.atomic():
+        (stream, created) = Stream.objects.get_or_create(
+            realm=realm,
+            name__iexact=stream_name,
+            defaults=dict(
+                name=stream_name,
+                description=stream_description,
+                invite_only=invite_only,
+                is_web_public=is_web_public,
+                stream_post_policy=stream_post_policy,
+                history_public_to_subscribers=history_public_to_subscribers,
+                is_in_zephyr_realm=realm.is_zephyr_mirror_realm,
+                message_retention_days=message_retention_days,
+            ),
+        )
 
+        if created:
+            recipient = Recipient.objects.create(type_id=stream.id, type=Recipient.STREAM)
+
+            stream.recipient = recipient
+            stream.rendered_description = render_stream_description(stream_description)
+            stream.save(update_fields=["recipient", "rendered_description"])
+
+            event_time = timezone_now()
+            RealmAuditLog.objects.create(
+                realm=realm,
+                acting_user=acting_user,
+                modified_stream=stream,
+                event_type=RealmAuditLog.STREAM_CREATED,
+                event_time=event_time,
+            )
     if created:
-        recipient = Recipient.objects.create(type_id=stream.id, type=Recipient.STREAM)
-
-        stream.recipient = recipient
-        stream.rendered_description = render_stream_description(stream_description)
-        stream.save(update_fields=["recipient", "rendered_description"])
-
         if stream.is_public():
             send_stream_creation_event(stream, active_non_guest_user_ids(stream.realm_id))
         else:
             realm_admin_ids = [user.id for user in stream.realm.get_admin_users_and_bots()]
             send_stream_creation_event(stream, realm_admin_ids)
 
-        event_time = timezone_now()
-        RealmAuditLog.objects.create(
-            realm=realm,
-            acting_user=acting_user,
-            modified_stream=stream,
-            event_type=RealmAuditLog.STREAM_CREATED,
-            event_time=event_time,
-        )
     return stream, created
 
 


Test patch: