Open ianks opened 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.
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
@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.
The main issues we would have with Agoo's imementation are:
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 😀
Let me jump in and see if I can get some clarification.
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?
Also, I'm interested in the relationship of Agoo <-> Iodine. Do they work together in certain ways?
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?
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).
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
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.
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.
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?
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.
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.
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:
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.
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.
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.
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 queriesYou 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.
@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:
Subscriptions - was next on the list anyway. Pretty clear although still figuring out the unsubscribe API.
Connections - The plans for pagination match up fairly well although my terminology needs some adjustment.
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?
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?
@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...?
@ohler55 and @ianks , from what I can tell, there are three possible solutions (please correct me if I'm wrong about this):
Add iodine / agoo subscription support to the graphql-ruby
gem.
Pro:
Con:
Add iodine / agoo GraphQL support only for subscriptions.
Pro:
Con:
graphql-ruby
gem and the servers will need to parse and process GraphQL API calls (in addition to parsing the GraphQL schema).Add GraphQL support to iodine / agoo in a way that will provide an alternative to the existing graphql-ruby
solution.
Pro:
Con:
graphql-ruby
gem).graphql-ruby
API).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.
@boazsegev I think option 1 does not preclude option 3 which is good.
👋 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!).
@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 😀
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.
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.
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?