kuzzleio / kuzzle

Open-source Back-end, self-hostable & ready to use - Real-time, storage, advanced search - Web, Apps, Mobile, IoT -
https://kuzzle.io
Apache License 2.0
1.43k stars 123 forks source link

Losing subscription strangely #2516

Open ScreamZ opened 7 months ago

ScreamZ commented 7 months ago

Hi, I'm using Kuzzle SDK JS for a React Native mobile application and ESP32 devices (using WebSocket custom calls in C/C++ for them).

Both are using API Keys for authentication. So for C++, I'm just setting the jwt key in the pushed JSON.

I'm observing the same behavior on both devices. Or at least something very close.

Context (Environment)

Observations

Everything works well for some time. Then at some point, I'm losing real-time subscriptions.

Possible cause

I've seen using an observer on the server side that strange things occur

1. Subscribe on the Server side to presence (click to unfold code) ```ts kuzzle.realtime.subscribe( index, "presence", {}, (notification) => { if (notification.type !== "user" || !notification.volatile.device) return; console.log( `Presence change for device ${notification.volatile.device} to ${notification.user}`, ); }, { users: UserOption.all, scope: ScopeOption.none }, ); } ```
  1. Boot the device and subscribe.
  2. Disconnect abruptly (like killing the app, or pressing the restart button on an ESP32)
  3. and therefore boot and subscribe again.

-> Everything works.

Until the server receives the timeout event of the first socket being closed abruptly.

Presence change for device ESP_1 to in
Presence change for device ESP_1 to in
Presence change for device ESP_1 to out
Presence change for device ESP_1 to out

-> It seems it clears all new subscriptions. ?

Possible cause

If confirmed, I'm pretty sure the issue is about hotelClerk thinking it's the same connection and, therefore, the same subscription and clear all ? But it seems the ws instance is generating a uuid v4 for connectionID, so I don't know how its possible.

Might it be related to API Keys usage ?

Issue happens on SDK JS too, so I'm not sure this is related to my implementation

Workaround

etrousset commented 7 months ago

This could possibly be due to the websocket layer sending a PING message and the device no responding with a PONG message? Can you check this?

https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#pings_and_pongs_the_heartbeat_of_websockets

ScreamZ commented 7 months ago

@etrousset Thanks for helping me with that :)

While this could justify the issue with the disconnection, it seems it has been handled already by ArduinoWebsocket library:

https://github.com/Links2004/arduinoWebSockets/blob/93707d455fff3c667aedd92b485276101f5e6eba/src/WebSockets.h#L269

https://github.com/Links2004/arduinoWebSockets/blob/93707d455fff3c667aedd92b485276101f5e6eba/src/WebSockets.cpp#L490-L500

But what about losing the subscription in case of reconnecting with the JavaScript SDK?

ScreamZ commented 7 months ago

After building my own minimal client with reconnection I can confirm that Kuzzle sdk JS seems not be the issue.

After losing a connection and reconnect I still receive notifications for a small duration then at some point I get nothing.

Now I need to check if it's related to react native or Kuzzle server itself.

ScreamZ commented 7 months ago

I made a custom websocket proxy and i can confirm that now I don't lose my subscriptions anymore.

I can confirm that kuzzle works strangely under the scenario of a react native mobile application going out of range of a local area network and coming back. The websocket reconnect (connection 2) and connection 1 drop after the server timeout resulting in connection 2 losing subscriptions.

Here is the proxy server code.

You can just use bun to power a uWebSocket server.

import type { ServerWebSocket } from "bun";

const kuzzleSocket = new WebSocket("ws://192.168.1.140:7512");
let reactSocketSet = new Set<ServerWebSocket<unknown>>();
let heartBeatRef: NodeJS.Timer | null = null;

kuzzleSocket.addEventListener("open", () => {
  console.log("✅ Connected to Kuzzle");
  heartBeatRef = setInterval(() => kuzzleSocket.send(JSON.stringify({ p: 1 })), 5000);
});

kuzzleSocket.addEventListener("message", (event) => {
  if (event.data !== '{"p":2}') console.log(`🦝 Kuzzle Received -> forwarding to React`);
  reactSocketSet.forEach((reactSocket) => reactSocket.send(event.data));
});

kuzzleSocket.addEventListener("close", () => {
  heartBeatRef && clearInterval(heartBeatRef);
  console.log("❌ Kuzzle closed socket");
});

const server = Bun.serve({
  fetch(req, server) {
    const success = server.upgrade(req);
    if (success) {
      // Bun automatically returns a 101 Switching Protocols
      // if the upgrade succeeds
      return undefined;
    }

    return new Response("Not supported!");
  },
  websocket: {
    open(ws) {
      reactSocketSet.add(ws);
      console.log("✅ React opened socket. Socket set size :", reactSocketSet.size);
    },
    async message(ws, message) {
      if (message !== '{"p":1}') {
        console.log(`⚛️ React Received -> forwarding to Kuzzle`);
      }
      kuzzleSocket.send(message);
    },
    close(ws) {
      reactSocketSet.delete(ws);
      console.log("❌ React closed socket. Socket set size :", reactSocketSet.size);
    },
  },
});