fs2 icon indicating copy to clipboard operation
fs2 copied to clipboard

Shared server sockets

Open RaasAhsan opened this issue 4 years ago • 12 comments

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

RaasAhsan avatar Feb 25 '21 08:02 RaasAhsan

@RaasAhsan What's that status of this one?

mpilquist avatar Mar 08 '21 17:03 mpilquist

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.

RaasAhsan avatar Mar 09 '21 09:03 RaasAhsan

@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! :)

TimWSpence avatar Nov 23 '22 11:11 TimWSpence

@TimWSpence I need to take a deeper look at this PR, but you may be interested in the change I just made in https://github.com/typelevel/fs2/pull/3063/commits/9e6a59b8490689a2301d843c9587a5d82e6fbdb5.

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

armanbilge avatar Nov 23 '22 11:11 armanbilge

@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?

TimWSpence avatar Nov 23 '22 11:11 TimWSpence

Yes, it's part of https://github.com/typelevel/fs2/pull/3063.

armanbilge avatar Nov 23 '22 11:11 armanbilge

@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 avatar Nov 23 '22 12:11 TimWSpence

@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

TimWSpence avatar Nov 28 '22 11:11 TimWSpence

@TimWSpence perhaps I misunderstood the original problem. I'm going on https://github.com/typelevel/fs2/issues/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.

armanbilge avatar Nov 28 '22 11:11 armanbilge

@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

TimWSpence avatar Nov 28 '22 11:11 TimWSpence

Well, maybe that's not really the problem 😂 see subsequent comment in https://github.com/typelevel/fs2/issues/2289#issuecomment-783510663. 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.

armanbilge avatar Nov 28 '22 11:11 armanbilge

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

TimWSpence avatar Nov 28 '22 16:11 TimWSpence