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

Feature Request: GraphQL Subscriptions Implementation #66

Open
ianks opened this issue Mar 9, 2019 · 26 comments
Open

Feature Request: GraphQL Subscriptions Implementation #66

ianks opened this issue Mar 9, 2019 · 26 comments

Comments

@ianks
Copy link
Contributor

ianks commented Mar 9, 2019

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?

@ianks ianks changed the title Feature Request: GraphQL Subscriptions implementation Feature Request: GraphQL Subscriptions Implementation Mar 9, 2019
@boazsegev
Copy link
Owner

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
Copy link

ohler55 commented Mar 10, 2019 via email

@boazsegev
Copy link
Owner

@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
Copy link
Contributor Author

ianks commented Mar 12, 2019

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
Copy link

ohler55 commented Mar 12, 2019

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 ianks closed this as completed Mar 12, 2019
@ianks ianks reopened this Mar 12, 2019
@ianks
Copy link
Contributor Author

ianks commented Mar 12, 2019

  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
Copy link
Contributor Author

ianks commented Mar 12, 2019

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

@ohler55
Copy link

ohler55 commented Mar 12, 2019

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
Copy link
Owner

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
Copy link
Contributor Author

ianks commented Mar 13, 2019

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
Copy link

ohler55 commented Mar 13, 2019

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
Copy link
Contributor Author

ianks commented Mar 13, 2019

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
Copy link

ohler55 commented Mar 13, 2019

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
Copy link
Contributor Author

ianks commented Mar 13, 2019

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
Copy link

ohler55 commented Mar 13, 2019

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
Copy link
Owner

boazsegev commented Mar 13, 2019

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.

  • 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)?

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

    I ask because adding a state machine is easier than adding a routing approach... another option (which might allow for HTTP / WebSocket / SSE flexibility), is an API object that's connection agnostic (i.e, Iodine::GraphQL). This object could manage GraphQL actions with a number of possible callbacks, such as:

    class MyGraphQL << Iodine::GraphQL
       def on_query(cmd)
             # add authentication + database code here
       end
       def on_mutation(cmd)
             # add authentication + database code here
       end
       def on_subscription(cmd)
             # subscriptions are handled automatically, this is just for extra actions...
             # ... or maybe ...
             # return `true` to subscribe, `false` to refuse.
       end
       def on_error(cmd)
             # return a rude JSON message for API calls that don't match the schema.
       end
    end
    GQL  = MyGraphQL.new(schema)
    # when handling a GraphQL request, use:
    GQL.process(cmd, client) # where, maybe, client == nil or client == env for HTTP requests?

    The cmd could be an interface that offers cmd.to_s for the original GraphQL query, cmd.env for the HTTP environment (if any) and cmd.client for WebSocket/SSE (if any).

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

    There are two reasons for this question:

    1. I don't thing any server can really follow resource mutation unless it's actively parsing the GraphQL API. This might indicate that the publish method call might be required within the application whenever a GraphQL mutation is performed... which seems error prone.

      It seems safer if there was a GraphQL module in the server that processes the GraphQL API and invoked relevant actions as needed (such as the publish action for subscriptions)...

      ... but if this is the case (which seems to minimize possible errors in the application code and improve application maintenance costs), then the Agoo approach (possibly with some modifications) seems superior.

    2. If each client is allowed to perform a different query (which seems to be the case), there might be a large amount of either database queries (which could probably be avoided) and/or JSON parsing/formatting (which might be harder to avoid)... possibly requiring the pub/sub engine to support internal data that isn't a String.

    This is due to the fact that each subscription will require a different message to be formatted (requiring some sort of per-message filtering).

    On the other hand, if the server was in control of the subscription's query (which does **not** seem to be the case), then it could collate these messages under a single channel and a single message per mutation.
    
     I can accommodate both approaches, but I just want to make sure I understood correctly that subscriptions end up with a unique message per connection.
    

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
Copy link

ohler55 commented Mar 13, 2019

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
Copy link
Contributor Author

ianks commented Mar 13, 2019

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:

  • /graphql: publically facing graphql endpoint
  • /admin/graphql: graphql endpoint specifically for admins, which offers different mutations and queries

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
Copy link

ohler55 commented Mar 13, 2019

@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
Copy link
Owner

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

  • GraphQL support should be (ideally) transport layer agnostic.

    This is because currently GraphQL queries / mutations are performed using HTTP, but they could (in the future) be performed also through the terminal (testing), WebSockets, or even raw TCP/IP (i.e., native mobile applications).

  • Applications require the flexibility to choose how to handle the query.

    Both a "pass-through" approach (where the unparsed query is sent directly to the database) and a complex multi-database / multi-backend approach, which might be required since GraphQL can be used to aggregate all types of data.

  • Applications should be able to provide more than a single GraphQL endpoint (i.e., global vs. admin).

  • Each Subscription follows changes to a specific resource and executes a specific (possibly unique) query once that resource was mutated. These queries are defined by the client(?).

    i.e., subscriptions from clients X and Y might follow any updates to Users. However, client X might only be interested in the User's first name while client Y might be interested in the User's full name (the data to be sent through the subscription...

    ... note that this might result in each subscription requiring a database query to fetch specific data (i.e., a client following forum Posts and requesting also "author": { "name" }).

  • The existing solution, using graphql-ruby, developed by Robert Mosolgo (@rmosolgo , who might help provide some input), lacks subscription support for iodine / agoo and this is the only thing missing(?).

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

@boazsegev
Copy link
Owner

@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
Copy link

ohler55 commented Mar 13, 2019

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

@rmosolgo
Copy link

👋 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
Copy link
Contributor Author

ianks commented May 24, 2019

@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
Copy link

palkan commented Feb 22, 2020

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: anycable/anycable#160

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

@boazsegev
Copy link
Owner

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.

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

No branches or pull requests

5 participants