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

Shared server sockets #2300

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

Conversation

RaasAhsan
Copy link

@RaasAhsan RaasAhsan commented Feb 25, 2021

Intends to fix #2289. Relevant Gitter conversation located here: https://gitter.im/functional-streams-for-scala/fs2?at=6035dec142f30f75c7c35746

So, the idea here is that the lifecycles of server sockets and client sockets have independent lifecycles, so it should be possible for us to finalize them separately. This was possible to do in 2.5.x because the SocketGroup server signature returned a Stream[F, Resource[F, Socket[F]]], in conjunction with the forking combinator that was developed in Ember.

Unfortunately, that method is leaky (what happens if you don't call use on the inner resource?), so the signature changed to Stream[F, Socket[F]], where the sockets are actually bound to the server stream. parJoin is necessary here in order to lease the scope and prevent the socket from immediately finalizing, but this sneakily also leases the server socket, preventing that from finalizing until all client sockets have finalized!

One solution to maintaining independent resource lifecycles while preserving safety is to implement a data structure called Shared that can facilitate the transfer of ownership of resources, which is captured in this PR. A client socket can be allocated on the server stream into a shared resource and handed off to the client stream which will acquire the resource. After that, the server socket can release the resource before moving onto accepting the next socket (this won't actually happen when using parJoin because of leasing).

I think the forking function is going to have change a bit; it'll have to operate on a Stream[F, Stream[F, Shared[F, O]]] because we need to ensure that the shared socket is acquired before control is returned to the accept stream. If it doesn't, the accepted socket is going to be immediately finalized after return. See: https://github.com/http4s/http4s/blob/main/ember-server/src/main/scala/org/http4s/ember/server/internal/StreamForking.scala#L71 . In comparison to parJoin, the new definition of forking replaces the notion of implicit scope leasing with the notion of explicit resource transfer.

Notes:

  1. The current server signatures are completely safe when used with parJoin, but I think we can even just revert them to their previous versions, and add a separate signature that exposes Shared.
  2. If this goes through, I'm wondering if we should try to include a (safer) variant of forking in fs2 that people can use to exercise the graceful shutdown behavior
  3. Need to look at fs2-netty to see how this plays out there
  4. Is it possible to formalize safe, implicit/explicit resource transfer into Stream somehow? Keyword there is safe

@mpilquist
Copy link
Member

@RaasAhsan What's that status of this one?

@RaasAhsan RaasAhsan marked this pull request as ready for review March 9, 2021 09:26
@RaasAhsan
Copy link
Author

Sorry for dropping the ball on this (I've been a bit busy), but I think it's ready for a deeper review. It does deserve some tests which I'll try to get to ASAP, but I'm curious to hear thoughts on general approach. I wonder if it's worth exploring a more Stream-native mechanism for resource ownership transfer in the future.

@TimWSpence
Copy link
Member

@mpilquist @RaasAhsan I was actually looking at the StreamForking code yesterday and it seems like it would be a big win. Would you be interested in me trying to get this over the line and if so, is there agreement on what is required? Thanks! :)

@armanbilge
Copy link
Member

@TimWSpence I need to take a deeper look at this PR, but you may be interested in the change I just made in 9e6a59b.

In fact I have some crazy idea that it fixed this :P but yeah, let me actually read about the issue first 😂

@TimWSpence
Copy link
Member

@TimWSpence I need to take a deeper look at this PR, but you may be interested in the change I just made in 9e6a59b.

In fact I have some crazy idea that it fixed this :P but yeah, let me actually read about the issue first 😂

Oh interesting! Is there a parent issue or PR for some conext on that change?

@armanbilge
Copy link
Member

Yes, it's part of #3063.

@TimWSpence
Copy link
Member

@armanbilge thinking about it a bit more, a reference-counted resource wrapper like this might be quite useful generally in cats effect. What do you think?

@TimWSpence
Copy link
Member

@armanbilge I don't think your change does fix this - we still have https://github.com/typelevel/fs2/pull/3063/files#diff-f11c9120aaf385f6214d8d1a82560c137b058a244c84ad7d8b7126e78b2081aeL128 which means that the lifecycle of the connection is tied to that of the outer server stream

@armanbilge
Copy link
Member

armanbilge commented Nov 28, 2022

@TimWSpence perhaps I misunderstood the original problem. I'm going on #2289:

The larger issue here is that the lifecycles of the server socket and all accepted sockets are now bound to the same stream. However, in practice, it's completely sound for them to have disjoint lifecycles e.g. you can close a server socket while leaving sockets that were accepted open.

I'm pretty sure the changes I made make those lifecycles disjoint: the server now accepts sockets into a Channel, which is consumed as a stream. Furthermore, the connection lifecycles are tied to the consuming stream, not the stream producing accepted sockets into the Channel. So it should be fully possible to close the server socket and thus close the Channel, without terminating the consuming stream.

@TimWSpence
Copy link
Member

@TimWSpence perhaps I misunderstood the original problem. I'm going on #2289:

The larger issue here is that the lifecycles of the server socket and all accepted sockets are now bound to the same stream. However, in practice, it's completely sound for them to have disjoint lifecycles e.g. you can close a server socket while leaving sockets that were accepted open.

I'm pretty sure the changes I made make those lifecycles disjoint: the server now accepts sockets into a Channel, which is consumed as a stream. Furthermore, the connection lifecycles are tied to the consuming stream, not the stream producing accepted sockets into the Channel. So it should be fully possible to close the server socket and thus close the Channel, without terminating the consuming stream.

Oh sorry, yeah I missed that those resources are tied to the stream coming from the channel. From some brief experimentation it didn't look like it was working for me though. I'll do some more digging

@armanbilge
Copy link
Member

Well, maybe that's not really the problem 😂 see subsequent comment in #2289 (comment). I'm not sure if FS2 actually needs any changes. I suspect we can do this http4s, maybe with some strategic use of allocatedCase to manage the outer resource.

@TimWSpence
Copy link
Member

Well, maybe that's not really the problem 😂 see subsequent comment in #2289 (comment). I'm not sure if FS2 actually needs any changes. I suspect we can do this http4s, maybe with some strategic use of allocatedCase to manage the outer resource.

🤦 how did I miss that. Nice, I'll poke at http4s a bit and see what I can come up with

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Semantically breaking change in SocketGroup.serverResource
5 participants