Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adapter life-cycle management and long-running process functionality #3662

Open
1 of 2 tasks
bytenik opened this issue Oct 5, 2020 · 10 comments
Open
1 of 2 tasks

Adapter life-cycle management and long-running process functionality #3662

bytenik opened this issue Oct 5, 2020 · 10 comments
Labels
enhancement New feature or request

Comments

@bytenik
Copy link
Contributor

bytenik commented Oct 5, 2020

You want to:

  • report a bug
  • request a feature

Current behaviour

  • socket.io instantiates an adapter, but does not wait for a callback that it is ready. This works fine for many scenarios, but not one where the adapter needs to spool up long-running processes where the adapter is not ready to do work until they are completed. Connections from clients are handled immediately, and if you use a simple pattern of emit-on-connect from client, an early client can easily connect prior to adapter readiness.
  • socket.io calls functions on the adapter, and then immediately invokes its own internal callback function with an argument of null.
  • socket.io does not notify its adapter that it is shutting down (closing). As a result, there is no opportunity to stop long-running processes within the socket or disconnect from whatever scale-out solution is being used.

Expected behaviour

  • socket.io should pass a callback to the adapter constructor and wait for it to be called prior to continuing to stand up.
  • socket.io should pass a callback function to adapter functions and then use that to trigger callback to the rest of the socket.io stack.
  • socket.io should call a method on the adapter to indicate shutdown. It need not wait for a callback, because if shutdown fails, what are you going to do -- not shut down? The in-memory adapter can simply make this function a no-op. While I haven't reviewed the Redis adapter in enough depth to see if there is utility to this function in that adapter, I would imagine that it would be good to detach from Redis to return system resources.

Other information (e.g. stacktraces, related issues, suggestions how to fix)

This is particularly critical for the adapter that I have built that works with AWS SQS/SNS:
https://github.com/thinkalpha/socket.io-sqs (socket.io-sqs on npm)

For example, actions like addAll can't be properly handled in time; e.g. if you addAll a socket to room xxx and then immediately .to(xxx).emit(...), your message doesn't necessarily end up back at that socket.

@darrachequesne
Copy link
Member

darrachequesne commented Oct 6, 2020

The adapter actually had a callback in the previous versions (included in Socket.IO v2), see here.

We've removed it since it was not needed anymore by the Redis adapter, but it seems other adapters may benefit from this. We can add it back in the form of a Promise, would it suit your use case?

Edit: could you please merge the 3 other issues into this one please, it will be easier to follow 👼

@darrachequesne darrachequesne added the enhancement New feature or request label Oct 6, 2020
@bytenik
Copy link
Contributor Author

bytenik commented Oct 6, 2020

Yes, making it promise-based would be ideal!

@bytenik bytenik changed the title adapters do not have an opportunity to do long-running activities such as add to room Adapter life-cycle management and long-running process functionality Oct 6, 2020
@bytenik
Copy link
Contributor Author

bytenik commented Oct 6, 2020

Incorporating #3661, #3660, and #3659 into this issue.

@bytenik
Copy link
Contributor Author

bytenik commented Oct 6, 2020

I've worked around some of these issues in a really hacky way by making a factory for the adapter class and then firing callbacks on arguments passed into the factory, but its definitely not ideal.

darrachequesne added a commit to socketio/socket.io-adapter that referenced this issue Oct 20, 2020
These extension points may be used by another adapter, in order to open
or close a connection to a database for example.

In Socket.IO v2, the join() method did accept a callback:

```js
socket.join("room1", () => {
  io.to("room1").emit("hello");
});
```

Depending on the adapter, it may now return a promise:

```js
await socket.join("room1");
io.to("room1").emit("hello");
```

Related: socketio/socket.io#3662
darrachequesne added a commit that referenced this issue Oct 21, 2020
Depending on the adapter, Socket#join() may return:

- nothing (in-memory and Redis adapters)
- a promise (custom adapters)

Breaking change: Socket#join() and Socket#leave() do not accept a
callback argument anymore.

Before:

```js
socket.join("room1", () => {
 io.to("room1").emit("hello");
});
```

After:

```
socket.join("room1");
io.to("room1").emit("hello");
// or await socket.join("room1"); for custom adapters
```

Note: the need for an asynchronous method came from the Redis adapter,
which did override the Adapter#add() method in earlier versions, but
this is not the case anymore.

Reference:

- https://github.com/socketio/socket.io/blob/2.3.0/lib/socket.js#L236-L258
- https://github.com/socketio/socket.io-adapter/blob/1.1.2/index.js#L56-L65
- socketio/socket.io-redis-adapter@05f926e

Related: #3662
@darrachequesne
Copy link
Member

@bytenik I've updated the adapter class (socketio/socket.io-adapter@2e023bf) and the socket#join() method (129c641).

I'm not sure about the public API for the Adapter#init() and Adapter#close() methods though. Should those methods be called by the end users directly?

const server = require("http").createServer();
const io = require("socket.io")(server);

await this.of("/").adapter.init();

server.listen(3000);

// and then...
server.close();
await this.of("/").adapter.close();

@bytenik
Copy link
Contributor Author

bytenik commented Oct 22, 2020

@darrachequesne Awesome, that looks great.

Right now the Adapter constructor is passed in, not a constructed adapter. socket.io constructs the adapter. So, there's no good way for the end user to call init or close. The lifespan is controlled by the io instance.

@darrachequesne
Copy link
Member

Yes you are right. Currently, there is one Adapter per namespace, and they are constructed when:

  • a namespace is created, with io.of("/my-namespace")
  • the adapter constructor is updated (for all existing namespaces) with io.adapter(CustomAdapter)

But we could change that behavior, though I'm not really sure about the implications.

@bytenik
Copy link
Contributor Author

bytenik commented Oct 24, 2020

@darrachequesne I don't think that there's anything inherently wrong with socket.io controlling the adapter lifecycle. I just think that in that case, the adapter needs an opportunity to start long running processes (init) and shutdown long running processes (close). So, both could return promises and socket.io would call those functions and then block until they resolve.

@bytenik
Copy link
Contributor Author

bytenik commented Apr 5, 2021

Hey, I just got back to trying to implement these changes on my end. Is init ever being called? I can't find a call to it, and my debug messages indicate it isn't being hit.

@darrachequesne
Copy link
Member

You are right, the Adapter#init method is not currently called.

We could call it here:

_initAdapter(): void {
this.adapter = new (this.server.adapter()!)(this);
}

But we cannot await a constructor (the initAdapter method is called under the hood):

const io = new Server({
  adapter: myCustomAdapter
});

Is there any problem with manually calling init()?

const io = new Server({
  adapter: myCustomAdapter
});
await io.of("/").adapter.init();

I'm open to suggestions on this.

dzad pushed a commit to dzad/socket.io that referenced this issue May 29, 2023
Depending on the adapter, Socket#join() may return:

- nothing (in-memory and Redis adapters)
- a promise (custom adapters)

Breaking change: Socket#join() and Socket#leave() do not accept a
callback argument anymore.

Before:

```js
socket.join("room1", () => {
 io.to("room1").emit("hello");
});
```

After:

```
socket.join("room1");
io.to("room1").emit("hello");
// or await socket.join("room1"); for custom adapters
```

Note: the need for an asynchronous method came from the Redis adapter,
which did override the Adapter#add() method in earlier versions, but
this is not the case anymore.

Reference:

- https://github.com/socketio/socket.io/blob/2.3.0/lib/socket.js#L236-L258
- https://github.com/socketio/socket.io-adapter/blob/1.1.2/index.js#L56-L65
- socketio/socket.io-redis-adapter@05f926e

Related: socketio#3662
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants