简体   繁体   English

为什么消费者.py中的事件处理程序function被多次调用,然后web-socket连接被强制终止?

[英]Why an event handler function in consumers.py is called multiple times and then web-socket connection is killed forcefully?

I have created a real time notifier using web-socket, implemented using Django-Channels.我使用 web-socket 创建了一个实时通知程序,使用 Django-Channels 实现。 It sends the "Post Author" a real time notification whenever any user likes the author's post .每当任何用户喜欢作者的帖子时,它都会向“帖子作者”发送实时通知

The problem is, for single like button click, multiple notifications are being saved in database .问题是,对于单次点击按钮,多个通知被保存在数据库中 This is because the function (like_notif()) defined in consumers.py which is saving notification data to database when the 'like button click' event happens is called multiple times (when it should be called only once ) until the web-socket is disconnected.这是因为在consumer.py中定义的 function (like_notif())在发生“like button click”事件时将通知数据保存到数据库被多次调用(当它应该只被调用一次时),直到 web-socket断开连接。

In the following lines I will provide detail of what is actually happening(to the best of my understanding) behind the scene.在接下来的几行中,我将详细介绍幕后实际发生的事情(据我所知)。

  1. Like button is clicked.点赞按钮被点击。
<a class="like-btn" id="like-btn-{{ post.pk }}" data-likes="{{ post.likes.count }}" href="{% url 'like_toggle' post.slug %}">
    Like
</a>
  1. JavaScript prevents the default action and generates an AJAX call to a URL. JavaScript 阻止默认操作并生成对 URL 的 AJAX 调用。
$('.like-btn').click(function (event) {
   event.preventDefault();
   var this_ = $(this);
   var url = this_.attr('href');
   var likesCount = parseInt(this_.attr('data-likes')) || 0;

   $.ajax({
       url: url,
       method: "GET",
       data: {},
       success: function (json) {
          // DOM is manipulated accordingly.
       },
       error: function (json) {
           // Error.
       }
   });
});
  1. The URL is mapped to a function defined in views.py. URL 映射到 views.py 中定义的 function。 (Note: Code for Post Model is at the last if required for reference.) (注:邮政编码Model在最后,如需参考。)
# In urls.py

urlpatterns = [
    path('<slug:slug>/like/', views.post_like_toggle, name="like_toggle"),
]
# In views.py

@login_required
def post_like_toggle(request, slug):
    """
        Function to like/unlike posts using AJAX.
    """
    post = Post.objects.get(slug=slug)
    user = request.user

    if user in post.likes.all():
        # Like removed 
        post.likes.remove(user)
    else:
        # Like Added
        post.likes.add(user)
        user_profile = get_object_or_404(UserProfile, user=user)

        # If Post author is not same as user liking the post
        if str(user_profile.user) != str(post.author):

            # Sending notification to post author is the post is liked.
            channel_layer = get_channel_layer()

            text_dict = {
                # Contains some data in JSON format to be sent.
            }

            async_to_sync(channel_layer.group_send)(
                "like_notif", {
                    "type": "notif_like",
                    "text": json.dumps(text_dict),
                }
            )

    response_data = {
        # Data sent to AJAX success/error function.
    }

    return JsonResponse(response_data)

  1. Consumer created to handle this in consumers.py.在 consumer.py 中创建了 Consumer 来处理这个问题。
# In consumers.py

class LikeNotificationConsumer(AsyncConsumer):

    async def websocket_connect(self, event):
        print("connect", event)
        await self.channel_layer.group_add("like_notif", self.channel_name)
        await self.send({
            "type": "websocket.accept",
        })

    async def websocket_receive(self, event):
        print("receive", event)

    async def websocket_disconnect(self, event):
        print("disconnect", event)
        await self.channel_layer.group_discard("like_notif", self.channel_name)

    async def notif_like(self, event):
        print("like_notif_send", event)

        await self.send({
            "type": "websocket.send",
            "text": event.get('text')
        })

        json_dict = json.loads(event.get('text'))

        recipient_username = json_dict.get('recipient_username')
        recipient = await self.get_user(recipient_username)

        sender_username = json_dict.get('sender_username')
        sender = await self.get_user(sender_username)

        post_pk = json_dict.get('post_pk', None)
        post = await self.get_post(post_pk)

        verb = json_dict.get('verb')
        description = json_dict.get('description', None)
        data_dict = json_dict.get('data', None)
        data = json.dumps(data_dict)

        await self.create_notif(recipient, sender, verb, post, description, data)

    @database_sync_to_async
    def get_user(self, username_):
        return User.objects.get(username=username_)

    @database_sync_to_async
    def get_post(self, pk_):
        return Post.objects.get(pk=pk_)

    @database_sync_to_async
    def create_notif(self, recipient, sender, verb, post=None, description=None, data=None, *args, **kwargs):
        return Notification.objects.create(recipient=recipient, sender=sender,  post=post, verb=verb, description=description,  data=data)

Note: notif_like() function is being called multiple times and thus data is being saved multiple times in the database.注意: notif_like() function 被多次调用,因此数据被多次保存在数据库中。 Why is this function being called multiple times?为什么这个 function 被多次调用?

  1. JavaScript code(written in HTML file) handling websocket connection. JavaScript 代码(写在 HTML 文件中)处理 websocket 连接。 I have used Reconnecting Websockets .我使用了重新连接 Websockets
<script type="text/javascript">

    var loc = window.location
    var wsStart = 'ws://' 
    if(loc.protocol == 'https:') {
        wsStart = 'wss://' 
    }
    var endpoint = wsStart + loc.host + "/like_notification/"
    var likeSocket = new ReconnectingWebSocket(endpoint)

    likeSocket.onmessage = function (e) {
        console.log("message LikeNotificationConsumer", e)
        var data_dict = JSON.parse(e.data)

        /*
            DOM manipulation using data from data_dict.
            NOTE: All the intended data is coming through perfectly fine.
        */

    };

    likeSocket.onopen = function (e) {
        console.log("opened LikeNotificationConsumer", e)
    }

    likeSocket.onerror = function (e) {
        console.log("error LikeNotificationConsumer", e)
    }

    likeSocket.onclose = function (e) {
        console.log("close LikeNotificationConsumer", e)
    }
</script>
  1. Post model for reference贴 model供参考
# In models.py

class Post(models.Model):

    title = models.CharField(max_length=255, blank=False, null=True, verbose_name='Post Title')
    post_content = RichTextUploadingField(blank=False, null=True, verbose_name='Post Content')
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        default=None,
        blank=True,
        null=True
    )
    likes = models.ManyToManyField(User, blank=True, related_name='post_likes')
    published = models.DateTimeField(default=datetime.now, blank=True)
    tags = models.ManyToManyField(Tags, verbose_name='Post Tags', blank=True, default=None)
    slug = models.SlugField(default='', blank=True)

    def save(self, *args, **kwargs):
        self.slug = slugify(self.title + str(self.pk))
        super(Post, self).save()

    def get_like_url(self):
        return reverse("like_toggle", (), {'slug', self.slug})

    def __str__(self):
        return '%s' % self.title
  1. Notification model for reference通知 model供参考
# In models.py

class Notification(models.Model):

    recipient = models.ForeignKey(User, blank=False, on_delete=models.CASCADE, related_name="Recipient",)
    sender = models.ForeignKey(
        User,
        blank=False,
        on_delete=models.CASCADE,
        related_name="Sender"
    )

    # Post (If notification is attached to some post)
    post = models.ForeignKey(
        Post,
        blank=True,
        default=None,
        on_delete=models.CASCADE, 
        related_name="Post",
    )
    verb = models.CharField(max_length=255)
    description = models.TextField(blank=True, null=True)
    data = models.TextField(blank=True, null=True)
    timestamp = models.DateTimeField(default=timezone.now)
    unread = models.BooleanField(default=True, blank=False, db_index=True)

    def __str__(self):
        return self.verb
  1. Channels layer setting.通道层设置。
# In settings.py

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer", 
        "CONFIG": {
            "hosts": [("localhost", 6379)]
        }
    }
}

I expect the notification to be saved only once per click on the like button but it is being saved multiple times as the notif_like() function defined in consumers.py is being called multiple times per click.我希望每次单击like按钮时只保存一次通知,但它会被保存多次,因为在consumers.py中定义的notif_like() function每次点击都会被多次调用。

Moreover, web-socket gets disconnected abruptly after this and following warning is displayed in the terminal -此外,在此之后,web-socket 会突然断开连接,并在终端中显示以下警告 -

Application instance <Task pending coro=<SessionMiddlewareInstance.__call__() running at /home/pirateksh/DjangoProjects/ccwebsite/ccenv/lib/python3.7/site-packages/channels/sessions.py:183> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7fb7603af1c8>()]>> for connection <WebSocketProtocol client=['127.0.0.1', 52114] path=b'/like_notification/'> took too long to shut down and was killed.

I am struggling with this for quite a time now, so it will be really helpful if someone can guide me through this.我已经为此苦苦挣扎了很长一段时间,所以如果有人能指导我完成这件事,那将非常有帮助。

After almost a year I was working on a very similar project and I was able to reproduce this bug and also figured out what was causing this.将近一年后,我从事了一个非常相似的项目,我能够重现此错误,并找出导致此错误的原因。

I am pretty sure that this is somehow being caused by ReconnectingWebSocket .我很确定这是由ReconnectingWebSocket引起的。

The reason for this is that when I switched back to using vanilla WebSocket, everything worked fine.原因是当我切换回使用香草 WebSocket 时,一切正常。

I have also opened an issue regarding this on the ReconnectingWebSocket's Github repository .我还在 ReconnectingWebSocket 的 Github存储库上打开了一个关于此的 问题

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM