Duplicate WebSockets using celery and redis (solved)

Hello there ! I’m a beginner with django and a total newbie to django channel as well, so it might be a simple mistake.
So I am building a chatbot with django using celery and redis. When I connect to my app, there are two websockets connection instead of only one. This duplicate websocket create duplicates in another aspect of the program. For example at the end of the connection I store the discussion with the chatbot in a SQL database. And because of the two websockets, each discussion is stored two times. There are other duplication issues, so I am almost certain it is a problem with the websockets. Another example is the first message of the chatbot that appear 2 two times (but only after sending the first user message).
Do you have any ideas why there are two websocket here ? And how to remove the duplicated one ? Or maybe the two websockets are normal and the duplication issue comes from elsewhere ?

Note: I Already had this issue yesterday but it resolved itself without any change to the code that I can remember. (I tried to solve the issue in the morning but failed. And after coming back from lunch the issue was nowhere to be seen. )

Here is the connection message when I run the server and connect to it:
image

Here is my consumers.py file:

import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
from django.db import connection
import datetime

from .tasks import get_response

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.accept()
        self.session_data = {"data" : []}

    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        get_response.delay(self.channel_name, text_data_json)
        async_to_sync(self.channel_layer.send)(
            self.channel_name,
            {
                "type": "chat_message",
                "text": {"msg": text_data_json["text"], "source": "user"},
            },
        )
    def chat_message(self, event):
        text = event["text"]
        self.session_data["data"].append(text)
        self.send(text_data=json.dumps({"text": text}))

    def disconnect(self, _):
        self.save_session_data()

    def websocket_disconnect(self, message):
        self.save_session_data()
        super().websocket_disconnect(message)
    
    def save_session_data(self):
        if len(self.session_data["data"]) != 0:
            cursor = connection.cursor()
            time = datetime.datetime.now().time()
            date = datetime.date.today()
            val = [date,time,json.dumps(self.session_data)]
            sql = "INSERT INTO chatbot_discussion (date, time, discussion) VALUES (%s, %s, %s)"
            cursor.execute(sql, val)
            cursor.fetchall()
            cursor.close()

Here is my routing.py file :

from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r"ws/chat/$", consumers.ChatConsumer.as_asgi()),
]

Here is my asgi file:

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
from decouple import config

os.environ.setdefault("DJANGO_SETTINGS_MODULE", config("DJANGO_SETTINGS_MODULE"))

# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()

import bot.routing

application = ProtocolTypeRouter(
    {
        "http": django_asgi_app,
        "websocket": AllowedHostsOriginValidator(
            AuthMiddlewareStack(URLRouter(bot.routing.websocket_urlpatterns))
        ),
    }
)

And here is the CHANNEL_LAYERS from the settings.py file:

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

EDIT:

Note 2 : I am also a beginner at javascript, I took the code here (Build a ChatBot Using Python, Django - DEV Community) and changed it a little bit.

Here you have my chat.html file with the client side javascript :

{% extends 'base.html' %} 
{% block body %}
{% load static %}
<div class="p-6 w-full flex flex-col justify-center">
  <h1 class="text-3xl tracking-tight font-light" id="chat-header"></h1>
  <div
    id="chat-log"
    class="mt-4 min-w-25 w-1/3 relative p-6 overflow-y-auto h-[30rem] bg-gray-50 border border-gray-200 rounded-md"
  ></div>
  <div class="mt-4">
    <input
      id="chat-message-input"
      class="min-w-15 w-1/4 py-2 outline-none bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:border-blue-500"
      type="text"
      placeholder="Écrivez votre question..."
    />
    <button
      id="chat-message-submit"
      class="w-32 py-2 px-4 mr-2 border border-transparent text-sm font-medium rounded-md text-white bg-[#ffc107] hover:bg-[#e7ad00]"
      type="submit"
    >
      Envoyer
    </button>
  </div>
</div>

{% block scripts %}
<script>
  
  var submitBtn = document.getElementById("chat-message-submit");
  var wss_protocol = window.location.protocol == "https:" ? "wss://" : "ws://";
  var chatSocket = new WebSocket(
    wss_protocol + window.location.host + "/ws/chat/"
  );
  var messages = [];

  chatSocket.onopen = function (e) {
    document.querySelector("#chat-header").innerHTML =
      "Chatbot v0.1";
      
      var first_message = {
              source: "bot",
              msg: "First message of the bot"
            };

      messages.push(first_message); 
      var str = '<ul class="space-y-2">';
      str += `<li class="flex justify-start">
      ${`<img class="w-12 h-12" src="{% static 'images/bot-icon.png' %}">`}
      <div class="relative flex max-w-xl px-4 py-2 rounded-lg shadow-md
      ${"text-gray-700 bg-white border border-gray-200"}">
        <span className="block sans-serif">${first_message.msg}</span></div></li>`;
    str += "</ul>";
      document.querySelector("#chat-log").innerHTML = str; 
  };

  chatSocket.onmessage = function (e) {
    var data = JSON.parse(e.data);
    var message = data["text"];
    if(message.source=="bot"){submitBtn.removeAttribute('disabled');}
    messages.push(message);
    
    var str = '<ul class="space-y-2">';
    messages.forEach(function (msg) {
      str += `<li class="flex ${
        msg.source == "bot" ? "justify-start" : "justify-end"
      }">
      ${
          msg.source == "bot"
            ? `<img class="w-12 h-12" src="{% static 'images/bot-icon.png' %}">`
            : ""
      }
      <div class="relative flex max-w-xl px-4 py-2 rounded-lg shadow-md
        ${
          msg.source == "bot"
            ? "text-gray-700 bg-white border border-gray-200"
            : "bg-[#dc3545] text-white"
        }">
        <span className="block sans-serif">${msg.msg}</span></div></li>`;
    });
    str += "</ul>";
    document.querySelector("#chat-log").innerHTML = str;
  };

  chatSocket.onclose = function (e) {
    alert("Socket closed unexpectedly, please reload the page.");
  };

  document.querySelector("#chat-message-input").focus();
  document.querySelector("#chat-message-input").onkeyup = function (e) {
    if (e.keyCode === 13) {
      // enter, return
      document.querySelector("#chat-message-submit").click();
    }
  };
  document.querySelector("#chat-message-submit").onclick = function (e) {
    submitBtn.setAttribute('disabled','');
    var messageInputDom = document.querySelector("#chat-message-input");
    var message = messageInputDom.value;
    chatSocket.send(
      JSON.stringify({
        text: message,
      })
    );

    messageInputDom.value = "";
  };
</script>
{% endblock %}
{% endblock %}

And here the base.html he extand from :

{% comment %} theme/templates/base.html {% endcomment %}

{% load static %}
<!DOCTYPE html>
<html lang="fr">
  <head>
    <title>{% block title %}Django Chatbot{% endblock %}</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" type="text/css" href="{% static 'css/output.css' %}">
    <link rel="shortcut icon" type="image/jpg" href="{% static 'images/bot-icon.png' %}"/>

  </head>
  <body>
    {% block body %} {% endblock %}
  </body>
  {% block scripts%}{% endblock %}
</html>

EDIT 2 : Duplications in the database were caused by redundant methods websocket_disconnect and disconnect in the consumer.py and had nothing to do with the duplicated websockets. To solve the issue with websocket duplication look at plopidou response bellow.

what does you client-side code look like (javascript)?

As mentioned by @plopidou above, this is caused by something in your client. The server doesn’t establish websocket connections. All connections are made by the client.

If you’ve got multiple connections, it’s cause by code in the client opening multiple connections.

I added the code of the client side, thx you for the remark.

I just updated my post with client side code. Thx for the info !

edit : DUH.

this:

</body>
{% block scripts%}{% endblock %}

should be this:

{% block scripts%}{% endblock %}
</body>

Your two body and script blocks are nested. Have your tried “un-nesting” them? As in:

{% load static %}
{% block body %}
...
{% endblock %}
{% block scripts %}
...
{% endblock %}

instead of:

{% load static %}
{% block body %}
...
{% block scripts %}
...
{% endblock %}
{% endblock %}

I suspect a “double call” somewhere… you script block get called twice because of nesting, maybe? Best way to check is to type Ctrl+U in your browser and checking the final rendered html code and tell us if you see the contents of the script block duplicated in your page…

1 Like

It solves the double websocket issue, thank you! Unfortunately, it seems that my duplicate issue with the database comes from elsewhere and has nothing to do with websockets. but the issue with the duplicate first message is solved.

EDIT : Database duplicate were because of the two redundant methods websocket_disconnect and disconnect in my consumers.py. So nothing to do with the duplicated websocket indeed.