ably / ably-js

Javascript, Node, Typescript, React, React Native client library SDK for Ably realtime messaging service
https://ably.com/download
Apache License 2.0
316 stars 55 forks source link

Ably: Protocol.onNack(): serial = 174; count = 1; err = ErrorInfo: Unable to perform operation on channel: *channel name* (not currently attached) #1195

Open lucadalli opened 1 year ago

lucadalli commented 1 year ago

Despite explicitly attaching to the channel using channel.attach(), sometimes I face the error above when I try channel.presence.subscribe or channel.presence.enterClient.

The error is intermittent and I'm not sure if its subscribe or enterClient provoking it.

Edit: I've narrowed it down to enterClient causing this error.

ably-js v1.2.38

Error trace:

16:05:28.015 Ably: Protocol.onNack(): serial = 175; count = 1; err = ErrorInfo: Unable to perform operation on channel: *channel name* (not currently attached)
at new ErrorInfo (/app/node_modules/ably/build/ably-node.js:1064:28)
at Function.ErrorInfo.fromValues (/app/node_modules/ably/build/ably-node.js:1093:36)
at Function.ProtocolMessage.fromDeserialized (/app/node_modules/ably/build/ably-node.js:2075:54)
at Function.ProtocolMessage.deserialize (/app/node_modules/ably/build/ably-node.js:2070:32)
at WebSocketTransport.onWsData (/app/node_modules/ably/build/ably-node.js:9541:62)
at WebSocket.wsConnection.onmessage (/app/node_modules/ably/build/ably-node.js:9502:26)
at WebSocket.onMessage (/app/node_modules/ws/lib/event-target.js:120:16)
at WebSocket.emit (node:events:513:28)
at Receiver.receiverOnMessage (/app/node_modules/ws/lib/websocket.js:720:20)
at Receiver.emit (node:events:513:28) {
code: 90001,
statusCode: 404,
cause: undefined,
nonfatal: false,
href: 'https://help.ably.io/error/90001';
}

┆Issue is synchronized with this Jira Task by Unito

sync-by-unito[bot] commented 1 year ago

➤ Automation for Jira commented:

The link to the corresponding Jira issue is https://ably.atlassian.net/browse/SDK-3544

owenpearson commented 1 year ago

Hey @lucadalli, thanks for reaching out!

This sounds like a race condition where you're calling channel.attach without waiting for the operation to complete and subsequently calling channel.enterClient.

The fix should just be to wait for attachment before calling enterClient, if you're using the library with promises you can do:

// using async/await:
await channel.attach();
await channel.enterClient(...args);

// using promise API:
channel.attach().then(() => {
   channel.enterClient(...args);
});

Or if you're using the library with callbacks you can just do:

channel.attach(() => {
  channel.enterClient(...args);
});

Hope that helps. let us know if you have any further issues

lucadalli commented 1 year ago

@owenpearson I should've mentioned that I am already awaiting the explicit attach as you suggest. But if the channel is not yet attached, doesn't enterClient attach implicitly? I think it does because in an attempt to fix the error I tried to remove the explicit attach and it works most of the time but still face the error above intermittently.

I think the code in your example is incorrect. enterClient is a method of RealTimePresence. It should read channel.presence.enterClient.

My code looks something like this

await channel.attach()
...
Promise.all([
  channel.presence.enterClient(CLIENT_ID).catch((e) => {
      console.error('presence enterClient:', e)
      throw e
  }),
  channel.presence
      .subscribe('update', ({ data }) => {
            ...
      })
      .catch((e) => {
          console.error('presence subscribe:', e)
          throw e
      })
])

Edit: In order to make sure that channel attachments happen predictably I am creating a separate Ably client for this one operation. If I have multiple Ably clients, will the channel attachments be shared amongst them? Or does each client have its own channel attachments?

owenpearson commented 1 year ago

@lucadalli thanks for clarifying, that certainly looks like something is going wrong with the library. You're right that enterClient implicitly attaches and that the enterClient method is on RealtimePresence, not RealtimeChannel.

I've tried running this simple repro script on a loop and haven't been able to reproduce the error (both with and without the implicit attach).

async function main() {
  const client = new Ably.Realtime.Promise({ key });
  const channel = client.channels.get(Math.random().toString());
  await channel.attach();
  await channel.presence.enterClient("my_client_id");
  client.close();
}

A couple of questions to help our investigation:

And nope, clients do not share channel attachments, each will have its own unique connection to Ably.

lucadalli commented 1 year ago

@owenpearson Thanks for investigating the issue. I might have identified the root cause of this error and it does not seem to be on Ably's end.

On error I am now logging the channel and connection state and to my surprise the channel appears to be suspended, despite the explicit attach. This leads me to be believe that the error stems from some sort of connectivity issues and as it turns out, Railway, the platform I'm using to host my Node app doesn't play well with websockets. https://help.railway.app/troubleshooting/hKqw9Dr1moxfDySTy98E6G/websocket-connections-disconnecting/sb6bfjV6UnuMwkRyAvQ3Sb

I have now attached a custom domain to my deployment as advised in the article and will report back soon. If the error persists I will deploy to another provider like Fly.io to rule out the hosting vendor being the root cause.

lucadalli commented 1 year ago

@owenpearson Unfortunately I was able to reproduce the issue on Fly.io too. Logs have revealed that while the when the error occurs the connection is in connected state but the channel is suspended or attaching.

What causes the channel to be suspended and why doesn't the client wait for the channel to attach before attempting the operation?