boazsegev / iodine

iodine - HTTP / WebSockets Server for Ruby with Pub/Sub support
MIT License
916 stars 51 forks source link

Feature Request: GraphQL Subscriptions Implementation #66

Open ianks opened 5 years ago

ianks commented 5 years ago

GraphQL has become a staple in the Ruby community for building APIs. GraphQL subscriptions work on top of GraphQL to add real time data streaming, and has powerful client side implementations such as Apollo.

Currently, the only FOSS implementation for graphql subscriptions in ruby is the ActionCable implementation. Obviously, there is a lot to be desired performance wise from this implementation.

I think Iodine could attract a lot of users who currently have to use the AC version. What are your thoughts on including something like this in Iodine?

boazsegev commented 5 years ago

Hi @ianks ,

Thank you for bringing this up.

@ohler55 already started working on GraphQL for the Agoo server and synchronizing the API and approach was previously discussed.

Personally, I'm all for it, I just didn't have the time to dig in to GraphQL and I wanted to learn more before I could work out (or contribute to) a sustainable design that will be both performant and liberating.

I also can't help but wonder: is it impossible to implement GraphQL subscriptions and queries either as Middleware or an App using the the iodine server? If possible, would it better to place GraphQL support into a separate gem?

Anyway, I'm hoping a discussion could provide a better understanding of this requested feature, making the GraphQL support and API as easy to use and as effective as possible.

Kindly, Bo.

ohler55 commented 5 years ago

I've got it working for Agoo. I need to update Agoo-C so you have an example. Maybe we can meet up and figure out how to us it in April when I am in Boston for the marathon.

On March 10, 2019 9:32:06 AM EDT, Bo notifications@github.com wrote:

Hi @ianks ,

Thank you for bringing this up.

@ohler55 already started working on GraphQL for the Agoo server and synchronizing the API and approach was previously discussed.

Personally, I'm all for it, I just didn't have the time to dig in to GraphQL and I wanted to learn more before I could work out (or contribute to) a sustainable design that will be both performant and liberating.

I also can't help but wonder: is it impossible to implement GraphQL subscriptions and queries either as Middleware or an App using the the iodine server? If possible, would it better to place GraphQL support into a separate gem?

Anyway, I'm hoping a discussion could provide a better understanding of this requested feature, making the GraphQL support and API as easy to use and as effective as possible.

Kindly, Bo.

-- You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub: https://github.com/boazsegev/iodine/issues/66#issuecomment-471306444

boazsegev commented 5 years ago

@ohler55 - I'm traveling in the UK and the EU until mid May or June, so I think the discussion would remain "virtual" for now 😔

I'm happy you've got it working 🎉☺️👍🏻 Maybe @ianks 's input will help improve the design (or maybe it will re-affirm some of your design choices). I curious to know what Ian might think about it.

B.

ianks commented 5 years ago

The main issues we would have with Agoo's imementation are:

  1. It does not support GraphQL Ruby, I think this is vital. Because of this, GraphQL support at the http server layer is not necessary, we just use a controller to deal with parsing input and executing query.
  2. The main benefit we would get from direct support would specifically be GraphQL subscriptions, since the real time aspect is something Ruby tradionally struggles with.

It is totally possible to build it with Iodine server, but building it right one time would benefit a lot of people IMO. Hope that helps, please feel free to ask me any questions. We have a fairly advanced setup and would love to be the an rats for this experiment 😀

ohler55 commented 5 years ago

Let me jump in and see if I can get some clarification.

  1. Is there anything special that needs to be done to support the graphql gem?
  2. Subscriptions are in the works for Agoo which will eventually mean they will show up in Iodine if we get the Agoo graphql code into Iodine.

I would be very happy to have someone help get the GraphQL functionality more friendly. What is it about the graphql gem that you like over standard GraphQL defined with SDL?

ianks commented 5 years ago
  1. For subscriptions, there is an adapter API needed to support GraphQL Ruby
  2. It has a lot of funtionality out of the box that would otherwise be a lot of work to implement. A couple of things we use often are connections, lookahead selection, Json schema output, etc.
ianks commented 5 years ago

Also, I'm interested in the relationship of Agoo <-> Iodine. Do they work together in certain ways?

ohler55 commented 5 years ago

It sounds like supporting the adapter API might be the quick win and then leave the other when performance is needed.

Bo and I collaborated on a Rack spec proposal to add support for websockets. We are both interested in providing high performance web servers first and less concerned with who's server is better. So competitive but in a friendly, cooperative way.

Did I get that about right, Bo?

boazsegev commented 5 years ago

For subscriptions, there is an adapter API needed to support GraphQL Ruby

I think I missed a step - which adaptor API? This one? ...doesn't look friendly... but maybe I need to better understand your needs.

I'm interested in the relationship of Agoo <-> Iodine. Do they work together in certain ways?

I think @ohler55 put it nicely: "We are both interested in providing high performance web servers first and less concerned with who's server is better". Neither of us seems concerned about competition. Personally, I just want to make iodine (and the facil.io framework) the best it could be.

Although, I have to admit, that I often feel that we're both offloading different framework features onto the servers to make things work faster. This makes the difference in approaches more pronounced. For example, Agoo supports a request router (which I want to design differently) and iodine offers a Mustache template engine and Redis connectivity (they come with the facil.io framework).

ianks commented 5 years ago

so that adapter interface is the low-level interface which Iodine would implement, the actual user-facing portion is documented here: https://graphql-ruby.org/guides#subscriptions-guides

ohler55 commented 5 years ago

I looked at the APIs a bit. Am I correct in understanding that the graphql gem uses ActionCable for subscriptions? If that is the case then both Iodine and Agoo might already be compatible as both support the Rack hijack option. Am I missing something? With the quick look I wasn't able to figure out how the gem actually gets called by the server. Does it expect to be the server itself?

Would you mind giving use cases for connections, lookahead selection, and JSON schema output? It seemed like the first two are necessary only due to the nature of the gem's API but, again, I don't have any experience using the gem so may be missing the point.

ianks commented 5 years ago

ActionCable is one possible adapter, if you use Rails. Not everyone uses Rails (we don’t 😀). Also, wouldn’t the performance of the Action able adapter be worse than using the supported Iodine / Agoo / Rack-Proposal websocket interfaces?

The other supported adapter is for https://pusher.com/ but it is not FOSS.

For lookahead, we use it to determine whether or not we need to perform potentially expensive joins on a DB table. Say you had a posts table with many comments, it would not make sense to join in the query:

{
  posts {
    id
    title
    // would want to join if we had selected comments
  }
}

For connections, it offers a conventional mechanism for dealing with pagination (https://graphql.org/learn/pagination/). It handles the implementation details of dealing with paginating relations in a uniform way.

JSON schema output is crucial for integrating with Apollo, which reflects on the metadata to handle caching/code generation for typescript, etc.

ohler55 commented 5 years ago

Yes, ActionCable is much slower. When subscriptions are supported I expect to use the Iodine/Agoo web socket approach.

look-ahead: I understand the concept of look ahead. What I didn't get was why it was needed if the Ruby code can see the query. Wouldn't it have all the information it needed to optimize the database queries?

connections: So connections are just the graphql gem's approach to pagination. Can I assume any pagination approach would work for you as long as the API was reasonable?

JSON Schema: Generating JSON schema should be an easy addition but I'm not sure what the schema would be for. Do you provide a graphql query and then ask what the JSON schema would look like?

ianks commented 5 years ago

look-ahead: I understand the concept of look ahead. What I didn't get was why it was needed if the Ruby code can see the query. Wouldn't it have all the information it needed to optimize the database queries?

WRT to connections, not just any API, the GraphQL community has pretty much rallied around the Connection interface and there is a lot of tooling and code written for it. So not using Connections would cause incompatibilities. Take a look at some of public GraphQL APIs such as Github, they almost all specifically use the Connection interface.

Yes but that would require diving into the AST which is not the most maintainable approach

Sorry for being unclear about the JSON schema, it's mainly used for developer tools such as apollo-codegen.

ohler55 commented 5 years ago

connection: got it, thanks. I'll do some digging.

JSON schema: The schema is for the JSON response to some query, right? Or is it something else? I'm just looking at how I could add that so knowing what the schema is for would help.

look ahead: I'm still not 100% on this so I'll have to do some more research.

Thanks for your patience.

boazsegev commented 5 years ago

I might be quiet, but it's because I'm listening and learning.

Let me see if I understood correctly:

wouldn’t the performance of the ActionCable adapter be worse than using the supported Iodine / Agoo / Rack-Proposal websocket interfaces?

Yes, it probably would be, both in terms of memory consumption and in terms of speed... however, where speed is concerned, much of the performance hit is related to the ActiveCable pub/sub design rather then the extra IO reactor (which consumes more memory but should have a smaller performance penalty).

For lookahead, we use it to determine whether or not we need to perform potentially expensive joins on a DB table...

Is this something that the server should handle? I was under the impression that the GraphQL could be an almost "pass-through" to the database, except for some filtering and/or validations performed by the application.

ohler55 commented 5 years ago

Let me answer to start with and then @ianks can correct me where I missed the mark.

• GraphQL queries and mutation are generally received over HTTP or HTTPS. The GraphQL spec in regard to subscriptions is more open so WebSockets, SSE, or just an open connection would in theory be fine. I think for Iodine and Agoo WebSockets and SSE are a natural fit.

• GraphQL is used to define an API. In order to bypass Ruby and go directly to the database would require either a database that has a GraphQL endpoint (I've got one :-) ) or a web server that does the conversion. The thing is GraphQL provides a lot of help building responses but it provides no structure for queries other than names and args. There is no SQL like component to identify data. That has to be implemented by the application.

• Going by the GraphQL specs the endpoint can be anything although /graphql is the suggested endpoint. It does not address multiple endpoints. While that would be possible it would be unusual and kind of goes against the GraphQL approach of being able to get all the data you need from one query which would of course be on one endpoint.

• The short answer is yes. Each client could subscribe to the same field and expect back different structures in the response. My plans for Agoo are to support server handling of the subscriptions based on the field/method but also allow the Ruby side to initiate the publish. Both would be configurable to both could be used or one or the other. I've kind of got the details worked out except for how to unsubscribe. The GraphQL spec is vague on how to do that from an API perspective. I have to do a little more searching to see what is common. At the very least the application (Ruby) would be able to cancel the subscription but in most cases I'd expect the client to initiate the unsubscribe.

Is this something that the server should handle? I was under the impression that the GraphQL could be an almost "pass-through" to the database, except for some filtering and/or validations performed by the application.

It is a common misconception to think of GraphQL as a database front end. It is a way of describing an application API. If the application chooses to map the graph nodes or objects to a database that is the choice of the application. I suspect @ianks can describe more how heavy or light that layer is between the server and the database.

I still haven't figured out what it is the server needs to provide for look-ahead. Probably because I don't know the internals of how the graphql gem interacts with the rest of the Ruby application.

ianks commented 5 years ago

I might be quiet, but it's because I'm listening and learning.

Let me see if I understood correctly:

  • Would GraphQL API calls use HTTP or would they be limited to WebSocket clients? I assume HTTP support would be important for older clients, although it would preclude subscriptions for these clients.

Most graphql calls will be over HTTP. Only subscriptions would use websocket/SSE.

  • Is it possible that, as ORMs and databases become friendlier, the GraphQL API call will be forwarded to the database "as is" (barring authentication / permission validation)... as in this approach? If this is the case, do I understand correctly that having the server parse the GraphQL API and react to it's events seems like a duplication of effort? Also, wouldn't it preclude the idea that the GraphQL API should map to Ruby objects (the current Agoo approach)?

That's one way to do it, but doing so removes many of the benefits of using GraphQL. GraphQL can be used to aggregate all types of data, some could be queries from the DB, others could be calls to an API, etc. Coupling directly to the DB takes away that ability.

  • Normally an application requires a single route per GraphQL queries... but, should there be the option to set more than a single GraphQL endpoint?

Yes there definitely should. We do this. Say for example you are building your application "monolith-first", meaning you deploy one server which has multiple logical "apps". In our example, we have one endpoint for each app which corresponds to:

You can use one endpoint and implement authorization, etc. But it gets messy quick.

  • Subscriptions follow a single resource mutation and publish a single query... however, is it possible that each client will follow the same resource with a different query?

Can you post an example of what you mean?

For lookahead, we use it to determine whether or not we need to perform potentially expensive joins on a DB table...

Is this something that the server should handle? I was under the impression that the GraphQL could be an almost "pass-through" to the database, except for some filtering and/or validations performed by the application.

No, application logic can handle this as long as there is a decent API for this. graphql-ruby handles this already.

ohler55 commented 5 years ago

@ianks I'm putting together a list of features to add to the GraphQL feature. I have some holes in my understanding of what you are looking for though. So far the list is:

  1. Subscriptions - was next on the list anyway. Pretty clear although still figuring out the unsubscribe API.

  2. Connections - The plans for pagination match up fairly well although my terminology needs some adjustment.

  3. JSON Schema - I am not clear on this at all. I understand it is used for developer tools but don't know how or where. Is a JSON schema generated? Are you referring to https://json-schema.org or are you referring to representing a GraphQL as a JSON document? Can you help clarify this?

  4. Look-Ahead - In the dark here as well but I haven't spent much time figuring it out either so only provide more details if you feel up to it.

Interested in helping move Iodine and Agoo forward with your expertise?

boazsegev commented 5 years ago

@ianks ,

Again, thank you very much for taking the time to describe your present (and possibly future) requirements.

I'm sorry for my long inquiries and messages, I'm just trying to understand better.

Form what I understand so far:

I think I summed up the discussion so far, right...?

boazsegev commented 5 years ago

@ohler55 and @ianks , from what I can tell, there are three possible solutions (please correct me if I'm wrong about this):

  1. Add iodine / agoo subscription support to the graphql-ruby gem.

    Pro:

    1. Leverage existing code.
    2. The iodine & agoo pub/sub API is probably close enough for a single code base(?),

    Con:

    1. Might require more attention from application developers where mutations are concerned.
    2. Might miss out on possible server-side optimizations already existing in the C layer.
  2. Add iodine / agoo GraphQL support only for subscriptions.

    Pro:

    1. Application development for GraphQL pub/sub will be easier to maintain.
    2. Server-side optimizations might be leveraged (in some instances, the Ruby layer might be circumvented).

    Con:

    1. Duplication of effort. Both the graphql-ruby gem and the servers will need to parse and process GraphQL API calls (in addition to parsing the GraphQL schema).
    2. A lot of work.
  3. Add GraphQL support to iodine / agoo in a way that will provide an alternative to the existing graphql-ruby solution.

    Pro:

    1. An opportunity to learn from existing solutions and improve on their API / approach.
    2. Application development for GraphQL could be easier to maintain.
    3. Server-side optimizations might be leveraged (in some instances, the Ruby layer might be circumvented).

    Con:

    1. Doesn't take advantage of existing code (the graphql-ruby gem).
    2. Introduces a learning curve for new developers (that may already be comfortable with the graphql-ruby API).
    3. A lot of work.

    I'm ignoring possible breaking changes to Agoo's API, since it's either a pro (Agoo API / code remains as is) or a con (it breaks).

@ianks - I believe that in your case, considering all the hours already spent on developing using graphql-ruby, I believe the first approach (adding the feature to graphql-ruby) would be best.

@ohler55 - However, for future greenfield projects, it's possible that integrating the lessons learned here into a common GraphQL API for iodine and Agoo (the third approach) might be interesting and helpful.

ohler55 commented 5 years ago

@boazsegev I think option 1 does not preclude option 3 which is good.

rmosolgo commented 5 years ago

👋 Just thought I'd link https://github.com/Envek/graphql-anycable which might provide some reference for how GraphQL subscriptions can be delivered on another transport (I admit, I haven't looked into it a ton!).

ianks commented 5 years ago

@rmosolgo thank you! https://github.com/Envek/graphql-anycable/blob/master/lib/graphql/subscriptions/anycable_subscriptions.rb demonstrates the interface I was proposing to use perfectly 😀

palkan commented 4 years ago

Hey everyone

Robert already mentioned graphql-anycable gem (which could be adopted to support Iodine as well, I think).

Also, take a look at this comment: https://github.com/anycable/anycable/issues/160

It sheds the light on how subscriptions are implemented in Action Cable.

boazsegev commented 4 years ago

Hi @palkan ,

Welcome to the discussion and thank you for the link and your input.

It's a slow discussion on my part, as I don't use GraphQL in any of my projects, so I don't have enough of an understanding to implement iodine support for it just yet.

As far as the comment goes, I don't think I understand how a callback part could be used together with publish. I am pretty sure I didn't allow for such a contingency in the pub/sub layer since I didn't see how a callback publish could scale across machine / process boundaries.

On the other hand, iodine supports server-side subscriptions (subscribe) using blocks or methods (the docs for global subscriptions and client bound subscriptions detail the way to use server-side processing). I'm not sure if that's what the callback was trying to achieve.