ohler55 / agoo

A High Performance HTTP Server for Ruby
MIT License
912 stars 39 forks source link

GraphQL in rails #93

Open robotex82 opened 4 years ago

robotex82 commented 4 years ago

I have been thinking about using the graphQL implementation in a Rails app. To do this I'll have to mount a graphQL schema inside the rails routes as a rack app:

Rails.application.routes.draw do
  mount Agoo::GraphQL.schema(Schema.new), at: '/graphql'
end

Could this be possible?

My goal would be to write an extensible schema for an existing ruby on rails cms.

ohler55 commented 4 years ago

Using the Rails.application.routes will not perform as well as using Agoo routes. Using Agoo for routing allows static assets to be loaded directly. It also means the GraphQL endpoint is accessed directly instead of going through rails. Have you considered this? If so, is there a reason you decided against that approach?

robotex82 commented 4 years ago

Thank you for your advice! This is a thing that has to be taken into consideration when it comes to performance.

I'd like to stay in the bounds of what i feel is "the rails way" to be able to have a drop in graphql api that just works as any other rails engine. If performance becomes an issue one could provide an optimization guide where the app is mounted i.e. via the config.ru file of the rails application. But for the simplicity of usage (coming from a rails perspective) this should be optional.

I hope i was able to clarify the situation.

ohler55 commented 4 years ago

I understand. I'll have to do some research on how the Rails routes work. If you have a good understanding of what is needed that would help. Do you know methods the mount must respond to?

robotex82 commented 4 years ago

Rails can mount vanilla rack apps via routes:

Rails.application.routes.draw do
  mount SomeRackApp, at: '/foo'
end

https://guides.rubyonrails.org/routing.html#routing-to-rack-applications

ohler55 commented 4 years ago

Great, thanks. That should be possible. A bit of overhead going back and forth with strings but if performance isn't a concern it should be possible. Being a performance geek it does make me cringe though. 😄

robotex82 commented 4 years ago

I feel you concerning the performance. Nowadays people tend to prefer to pay a bit more for servers than to pay developers for optimizations...

ohler55 commented 4 years ago

I started working on this today. The approach basically take the request and reforms that request and make another http request. I was wondering if Rails supported a redirect. If so then instead of a mount it would just redirect the request to /graphql. Is that possible?

robotex82 commented 4 years ago

I'm not sure if I understand. I'll make a small example app where we can expore ideas.

robotex82 commented 4 years ago

I created a test app: https://github.com/robotex82/rails-agoo-graphql-test

The app is available here: http://rails-agoo-graphql-test.herokuapp.com/

I added two rails engines (i called them frontend and backend) with separate graphql endpoints. Both have graphiql UIs to fiddle with them.

The frontend graphql endpoint is at /graphql. The corresponding graphiql interface is at /graphiql.

image

The backend graphql endpoint is at /backend/graphql. The corresponding graphiql interface is at /backend/graphiql.

image

At the moment graphql is implemented as dummy rack apps:

Frontend: https://github.com/robotex82/rails-agoo-graphql-test/blob/master/engines/frontend/app/rack_apps/frontend/graphql_app.rb

Backend: https://github.com/robotex82/rails-agoo-graphql-test/blob/master/engines/backend/app/rack_apps/backend/graphql_app.rb

The use case would be to replace those dummy apps by some piece of agoo rack app where the schema can be built on to of it.

If this is made with a redirect, where would it redirect to?

ohler55 commented 4 years ago

Let me try to explain a bit better than I did the first time.

So any web server will accept requests and then form responses based on the URL path. Something like this:

                    /-asset/logo.jpg-->
request --> server ---graphql-->
                    \-user/123-->

With Agoo doing the routing and using Rails the diagram becomes:

                  /-asset/logo.jpg-->
request --> agoo ---graphql-->
                  \-*-->rails-->user/123-->

This is the most direct and gives the best performance. If Rails is to do the routing then this is the diagram:

                           /-asset/logo.jpg-->
request --> agoo -*-->rails---graphql-->???
                           \-user/123-->

So what I think you want is to have the ??? be the Agoo GraphQL handler. Conceptually this is what it would look like:

                   /-private/graphql-->
                  /         /-asset/logo.jpg-->
request --> agoo -*-->rails---graphql-->{http://localhost/private/graphql}
                           \-user/123-->

The question is how to implement the part in {}. One approach is to make a Agoo::GraphQL implement the #call() method. This ends up being pretty convoluted. The second approach I was asking you about would be for the path in {} to be an HTTP redirect that would call out over HTTP to get the response.

You also suggested something else with your mention of two servers. The redirect could go to a different server entirely.

robotex82 commented 4 years ago

Decoupling the graphql part of agoo from the usage of agoo as server opens up the implementation to many more use cases (like the simple usage in rails or any other rack compilant application).

So to keep it in the bounds of the rails application as you have pointed out the correct solution is to implement the "???" part in your diagram. This would be a rack app that you can feed a GraphQL schema. This could be either by inheritance or when calling it:

I.e.:

# app/graphql/my_graphql.rb
class MyGraphql < Agoo::Graphql::RackApp
  class Schema
  end

  def schema
    Schema.new
  end

  def sdl
    "type Query { hello: String }"
  end
end

# config/routes.rb
mount MyGraphql, at: '/graphql'

With such a construct one could for example implement a code first sdl generator and many other things.

ohler55 commented 4 years ago

"Correct" is probably a matter of priorities but I can see your desire. That approach will take a fair amount of reworking of the Agoo code base so it's not going to happen right away. Lets keep this as an open issue though and at some point I'll separate the graphQL component from the server.

robotex82 commented 4 years ago

Sorry, "correct" was meant from the rails perspective. I apologize for this clumsy choice of words.

I've seen in this file https://github.com/ohler55/agoo/blob/develop/example/graphql/song.rb that graphql seems to be enabled by this line:

Agoo::GraphQL.schema($schema) {Agoo::GraphQL.load_file('song.graphql')}

I assume that there would be the need to some kind of interface to separate the GraphQL stuff from the server. As agoo is already a rack server, could something like this be desirable?

          Actual            +     With Rack Interface
                            |
   +-------------------+    |    +-------------------+
   |                   |    |    |                   |
   |                   |    |    |                   |
   |    Agoo Server    |    |    |    Agoo Server    |
   |                   |    |    |                   |
   |                   |    |    |                   |
   +-------------------+    |    +-------------------+
            ^               |              ^
            |   ?           |              |   Rack-Compatible Interface
            |               |              |
   +-------------------+    |    +-------------------+
   |                   |    |    |                   |
   |                   |    |    |                   |
   |    Agoo GraphQL   |    |    |    Agoo GraphQL   |
   |                   |    |    |                   |
   |                   |    |    |                   |
   +-------------------+    |    +-------------------+
                            |
                            |
                            +

If this i a viable possibility can you point me in a direction to what needs to be done? Where is the interface between the http server and the graphql in the actual implementation (the question mark in the ascii picture).

ohler55 commented 4 years ago

The diagram isn't quite right. Requests hit the server first. Next the server looks at the URL path and picks a handler. The GraphQL component is one of those handlers. Handlers are chosen before converting a request to Ruby. Thats on of the things that makes Agoo fast. Routing is all done in C. If the URL path is for a Rack handler then a Ruby request object is created and processed.

With that in mind the Rack call would then have to invoke the GraphQL handler but that expects lower level data such as a string to parse or a URL path to pull apart. The effort to separate splitting the code to allow entry from a different point. That is compounded by the API being multi-threaded. So, yes, it is possible but will take some time.

robotex82 commented 4 years ago

I see, so it's more like that:

 1.) Agoo as Server with builtin GraphQl and Rails as rack app (This is already possible)

     +---------------+                  +---------------+
     |               |  Rack Interface  |               |
     |  Agoo Server  |  (config.ru)     |               |
     |               +----------------->+     Rails     |
     |  Agoo GraphQL |                  |               |
     |               |                  |               |
     +---------------+                  +---------------+

+----------------------------------------------------------------------------------------------+

 2.) Rails with agoo GraphQL mounted as RackApp in Rails routes (This would be possible)

     +---------------+                  +---------------+                    +---------------+
     |               |  Rack Interface  |               |  Rack Interface    |               |
     |  Agoo Server  |  (config.ru)     |               |  (rails routes)    |               |
     |               +----------------->+     Rails     +------------------->+  Agoo GraphQL |
     |  Agoo GraphQL |                  |               |                    |               |
     |               |                  |               |                    |               |
     +---------------+                  +---------------+                    +---------------+

 +---------------------------------------------------------------------------------------------+

 3.) Rails and Agoo GraphQL side by side as rack apps configured in config.ru (This would be
     possible, but I don't see many use cases)

     +---------------+        Rack Interface         +---------------+
     |               |        (config.ru)            |               |
     |               +------------------------------>+               |
     |      Puma     |                               |     Rails     |
     |               +-----------+                   |               |
     |               |           |                   |               |
     +---------------+           |                   +---------------+
                                 |
                                 |
                                 |
                                 |                   +---------------+
                                 |                   |               |
                                 |                   |               |
                                 +------------------>+  Agoo GraphQL |
                                                     |               |
                                                     |               |
                                                     +---------------+

+----------------------------------------------------------------------------------------------+
ohler55 commented 4 years ago

Yes, more like 1 although maybe this is more accurate since Agoo GraphQL is a separate handler in the code.

 +--------------+                +--------------+
 |              |                |              |
 |              +--------------->+    Rails.    |
 |  Agoo Server |                |              |
 |              +----+           +--------------+
 |              |    |
 +--------------+    |           +--------------+
                     |           |              |
                     +---------->+ Agoo GraphQL |
                                 |              |
                                 +--------------+

I agree that 2 and 3 would be possible if Agoo GraphQL supported the #call() method. 3 might be a littler trickier though.

robotex82 commented 4 years ago

Cases two and three should both work with the same interface. Because its all rack.

Is there anythink in ruby that can be done or is it all C? I can assist with the ruby stuff, but my C knowlege is, lets say, rudimentary at best.

ohler55 commented 4 years ago

It's really all C.

As for case 3, the issue that may come into play is the package initialization that right now includes the server so to handle case 3 all GraphQL code would have to be completely separate with no dependencies on the server. Since the GraphQL part is multithreaded it might mean that both a single threaded API as well as the multi-threaded API might have to be supported. Once for performance and one for Rack compatibility.

robotex82 commented 4 years ago

Its sad that my C skills are virtually non-existent.

I totally forgot the initial use case i tried to explain here at first. Coming from rails this would be the "default stack" integration. This would be the most important from my very rails centric perspective:

4.) Rails and Agoo GraphQL side by side as rack apps configured in config.ru (This would be
    possible, but I don't see many use cases)

    +---------------+                      +---------------+                  +---------------+
    |               |    Rack Interface    |               |  Rack Interface  |               |
    |               |    (config.ru)       |               |  (Rails routes)  |               |
    |      Puma     +--------------------->+     Rails     +----------------->+  Agoo GraphQL |
    |               |                      |               |                  |               |
    |               |                      |               |                  |               |
    +---------------+                      +---------------+                  +---------------+
ohler55 commented 4 years ago

Since the API is #call() in all cases other than the Current one it would in theory work. Like I said though. It is a lot of work and not going to happen over night.

robotex82 commented 4 years ago

Ok, thank you for looking into this! I'm very much looking forward into using the agoo graphQL in Rails!

ohler55 commented 4 years ago

It can already be uses with Rails just not using the Rails router. The request is on the list though.