jruby / jruby

JRuby, an implementation of Ruby on the JVM
https://www.jruby.org
Other
3.79k stars 923 forks source link

ServerSocket still relevant #4748

Open ioquatix opened 7 years ago

ioquatix commented 7 years ago

I ran into this issue

https://travis-ci.org/socketry/async-http/jobs/267440658#L617-L618

looked at https://github.com/jruby/jruby/wiki/ServerSocket

I thought JRuby uses native UNIX functions where possible?

Just wondering what is considered best practice here.

headius commented 7 years ago

The implementation of Socket in JRuby is a bit troublesome because the JDK separates socket types by class. So if you want to create a server, you need to construct a server socket object. In Ruby, however, you can create a socket object and later decide whether it's a server or a client.

JRuby sacrifices that ability of Ruby by only supporting client connections in the bare Socket class and mentioning the ServerSocket class as an alternative. Unfortunately since this class doesn't exist in regular Ruby, nobody knows about it and nobody uses it.

The async-io library could be modified to use a specific server socket type (like TCPServer), perhaps?

I did have an experiment on a branch where I delayed the creation of the Java socket, but it's really pretty ugly; I have to simulate a bunch of intermediate steps like "bind" and all the errors that go with them, since I can't actually initiate a socket until later.

There is of course the possibility that we could improve our Socket impl to use native functions, but native socket functions are notoriously troublesome to bind through FFI, since they have various struct shapes across platforms and libraries.

ioquatix commented 6 years ago

I played around with this a bit more. It's troublesome.

The async-io library could be modified to use a specific server socket type (like TCPServer), perhaps?

This isn't really a good option. I'd really have to implement the ServerSocket / ClientSocket split into the core of the io abstraction in order to make it work.

It's probably the only solution that's going to work well, but the problem is core Ruby doesn't make this split, while JRuby does. It's a bit of a mess to be honest.

The only solution I can really see working, is to actually re-implement the socket API from the ground up.

I did have an experiment on a branch where I delayed the creation of the Java socket, but it's really pretty ugly; I have to simulate a bunch of intermediate steps like "bind" and all the errors that go with them, since I can't actually initiate a socket until later.

I tried this too. It was hard work, but it did work for the most part.

There is of course the possibility that we could improve our Socket impl to use native functions, but native socket functions are notoriously troublesome to bind through FFI, since they have various struct shapes across platforms and libraries.

Honestly, I think this is the best route if you want to make things just work without additional work for the user. Or, the above, where you basically cache all the method calls until the actual socket is allocated.

The problem is, it's not clear whether you have a server socket or client socket. That's because a user might call bind (server) or bind & connect (bind to local address for connection). So, it's hard to detect the exact sequence.

The real issue is Java has made a decision about how sockets should work without actually providing the real basic API level functions.

Are there any external libraries you could depend on to provide a proper socket implementation?

ioquatix commented 6 years ago

Here is the hack which for the most part worked, but failed with UDP sockets.

https://github.com/socketry/async-io/blob/8214304d84111a48560f002c07155129850768d9/lib/async/io/socket.rb#L24-L107

ioquatix commented 6 years ago

I think that the cross-interpreter costs w.r.t. how the APIs work is stupidly complex for typical developers to manage which is unfortunate.

I could modify async-io to only expose a very limited set of APIs, but my goal was to provide drop-in replacements for core Ruby IO classes.

headius commented 6 years ago

The long term answer is that we need to start moving toward all-native Socket. There's a solid start on this in the rubysl-socket library, but it has a number of dependencies on Rubinius or on having an initial build step per-platform, which we can't provide. The TruffleRuby folks may have a modified version that we could drop in.

I have attempted at various times to make Socket defer the decision whether to use a Java TCPSocket or TCPServer, but it's very hacky and is incompatible with code that attempts to make use of the file descriptor between the time the socket is created and bound.

@ioquatix You might have a look at https://github.com/rubysl/rubysl-socket or at the modified version in TruffleRuby at https://github.com/oracle/truffleruby/tree/master/lib/truffle/socket and see how much work would be needed to get just the basic Socket class working natively.

eregon commented 4 years ago

FWIW, the version in TruffleRuby is more complete and has less bugs than rubysl-socket (BTW that link seems dead).

headius commented 4 years ago

@ioquatix We have never made a move to using native calls for all socket support because none of the available options work without a build step. JRuby runs out of the box on any system with a JDK because we rely on so many built-in JDK classes (e.g. the socket subsystem) and requiring a build step for native sockets is not in our plan right now.

The plan long term would be to support fully-native socket support, pregenerated for as many platforms as possible, with a fallback to our non-native sockets. Unfortunately we don't have many resources to work on that these days.

eregon commented 4 years ago

rubysl-socket doesn't require any build step, isn't it? It's just FFI. But I have not tried whether it works well on Windows and exotic platforms though.

headius commented 4 years ago

@eregon Last time I looked it required a bunch of properties to be set for the shape and sizing of the struct, which are generated at build time on Rubinius (and presumably on TruffleRuby). In any case the structs differ across platforms, so we'd need to have predefined values for any platforms we want to support. I did not see that in rubysl-socket nor in TruffleRuby socket last time I looked.

headius commented 4 years ago

@eregon Example: https://github.com/oracle/truffleruby/blob/master/lib/truffle/socket/socket.rb#L142-L150

We do not have these values set and they would be per-platform, so I assumed TruffleRuby generates them when built for each platform. JRuby has no per-platform build step (outside of jffi and) so we can't provide these values.

eregon commented 4 years ago

I think those values could be stored in jffi then. Here is the script generating them: https://github.com/oracle/truffleruby/blob/master/tool/generate-native-config.rb

headius commented 4 years ago

@eregon Ah there's a bit of magic I did not know about. It would be reasonable for us to generate these configs, and when a platform's configs are not present we fall back on the JDK-based sockets. Just need to do something about all those pesky Truffle/Rubinius primitives.

eregon commented 4 years ago

There are no primitives used in lib/truffle/socket, only FFI.

headius commented 4 years ago

I was referring to things that I believe were primitives in rbx. Perhaps they're not "primitives" in the same sense in TruffleRuby, but they don't exist as standard APIs nor as bound FFI endpoints:

https://github.com/oracle/truffleruby/blob/master/lib/truffle/socket/basic_socket.rb#L76 https://github.com/oracle/truffleruby/blob/master/lib/truffle/socket/basic_socket.rb#L178 https://github.com/oracle/truffleruby/blob/master/lib/truffle/socket/tcp_socket.rb#L48

Granted, most of this code was written just atop FFI originally, and it seems like that's still the case in your improved version. Hopefully we'll have some cycles free to attempt using it in JRuby soon.

eregon commented 4 years ago

Of these 3 only the first one is not defined directly there, but it's still pure Ruby: https://github.com/oracle/truffleruby/blob/9b8b8579e86673b5d3e62e08836ee71ebca21af5/src/main/ruby/truffleruby/core/type.rb#L596-L600

The other two are just defined in lib/truffle/socket files. The Truffle prefix is just what replaced the Rubinius or RubySL prefix, it's just a private namespace for helper methods and classes. It would probably make sense to find another naming convention for internal namespaces.

eregon commented 4 years ago

Longer-term I think it could be cool to make socket a default gem and include this FFI-based implementation in it.

eregon commented 4 years ago

For clarification, TruffleRuby has primitives, they look like TrufflePrimitive.foo(...). None are used in that code.