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

AnyCable vs. GraphQL-Ruby, or why doesn't AnyCable support Custom stream callbacks? #40

Open
gastonmorixe opened this Issue Apr 11, 2018 · 14 comments

Comments

Projects
None yet
5 participants
@gastonmorixe
Copy link

gastonmorixe commented Apr 11, 2018

Hi, awesome work here.

I'd like to understand why there is no support for "Custom stream callbacks".

I am trying to use it along side with "graphql" gem for subscriptions and it uses a stream_from block.

If you guide me here I contribute to add this feature.

Thank you

@palkan

This comment has been minimized.

Copy link
Member

palkan commented Apr 11, 2018

Hi!

First of all, thanks for the feedback!

The reason is that a stream_from callback must called within a Rails application (in most cases, like in graphql gem).

When using AnyCable all the broadcasting is done within AnyCable server which doesn't know anything about your application.

In general, running custom code for every transmission (especially such heavy code as GQL run, if I understand correctly) doesn't scale well. Just imagine that you have a thousand of clients subscribing to the same GQL subscription and you're triggering the update.

In theory, we can add stream_from callbacks support in the following way:

  • when stream_from is called with block, we mark this stream as callable within AnyCable
  • when AnyCable wants to send the message to this stream, it makes another RPC call (one for each subscribed client) to Ruby server to evaluate the block and send the resulted message if any.

Looks feasible though.

@gastonmorixe

This comment has been minimized.

Copy link

gastonmorixe commented Apr 11, 2018

Hey @palkan thank you for the quick and detailed response.

I will be using actioncable directly because I don't see myself with a lot of time to dig on this. Although I will keep subscribed and ready to switch when this gets done.

I believe the utility of this for graphql subscriptions would be huge.

Thank you

@fernandes

This comment has been minimized.

Copy link

fernandes commented May 20, 2018

hey guys..

I've been playing with anycable and graphql, @gastonmorixe you mean rmosolgo/graphql-ruby right?

stream_from 2nd argument is the callback, not the block

stream_from(broadcasting, callback = nil, coder: nil, &block)

and as I've seen on graphql subscription implementation it only pass the broadcasting, coder and block, not the callback... am I missing something here?

@palkan what great job on anycable 😉

@fernandes

This comment has been minimized.

Copy link

fernandes commented May 20, 2018

digging into anycable code, I just found:

raise ArgumentError('Unsupported') if callback.present? || coder.present? || block_given?

so yeah... graphql-ruby is unsupported heheh

@semenovDL

This comment has been minimized.

Copy link

semenovDL commented Aug 9, 2018

Hey, @palkan.

I need this functionality for our project and would like to implement it. Maybe you can share your vision of how this task should be implemented, what to look at first? For example, at cultofmartians.com.

@fernandes

This comment has been minimized.

Copy link

fernandes commented Aug 9, 2018

@semenovDL custom callbacks are used to run a code inside the actioncable server, useful if you have rails loaded and using a ruby server, in the case of anycable, as the process runs separately, what code will run with custom callbacks? This is what I understood reading the code.

@palkan may correct me if I got it wrong

@semenovDL

This comment has been minimized.

Copy link

semenovDL commented Aug 9, 2018

@fernandes Yea, that's the point why we need callbacks.
At the moment I'm interested in how best to approach this task. Maybe there are some reference implementations.

@Envek

This comment has been minimized.

Copy link
Member

Envek commented Aug 9, 2018

We've discussed it with @palkan and there is one caveat: where to store callbacks itself? The code that will be executed. That brings us to second feature not yet supported by AnyCable: channel instance variables.

This is a problem because in heavily loaded setups there can be several RPC servers (Rails apps handling client connection, subscription and disconnection) and queries from AnyCable servers are distributed between them in a round-robin fashion.

This is not a problem in ActionCable as AC server is the process that both hold a connection to the particular client and execute Ruby code – in AnyCable these two responsibilities are decoupled.

One possible solution is to enhance API between AnyCable and gRPC server to include special callback request from the AnyCable to RPC, asking to execute callback and including all required data about a client (and serialized channel instance variables).

So the process of broadcasting of the message that requires custom callback will look like this:

mermaid-diagram-20180809225950 svg

There will be some limitations:

  • callback can be only some already existing method (I don't think that saving Ruby code, passing it back and forth and reevaluating it is a good idea).
  • all instance variables should be small-sized and serializable (to be passed from cable to RPC).

Another option is to discard from round-robin between RPCs but it will complicate deploys, scaling, and setup.

@Envek

This comment has been minimized.

Copy link
Member

Envek commented Aug 9, 2018

ActionCable subscriptions available in open-source in graphql gem by @rmosolgo works quite interesting:

On every GraphQL subscription, two ActionCable subscriptions are created.

When application triggers GraphQL subscription:
AppSchema.subscriptions.trigger(:sub_name, args, object, scope: current_user_id)

Then into first of these cable subscriptions quite a little info broadcasted: serialized object (its globalid).

Next, ActionCable receives this message from Redis and runs a custom callback in which:

  1. Takes GraphQL subscription from channel instance variables (not supported by AnyCable) in which contained query text, context, and variables.
  2. deserializes object passed into trigger (loads it from the database)
  3. reevaluates GraphQL query
  4. Broadcasts (again) results into ActionCable (yeah, into Redis again), nothing goes to clients
  5. This second message received ActionCable and just transmitted to clients (without any magic).

It looks something like this:

mermaid-diagram-20180809234923 svg

If you have 1000 subscribed clients you will get 1000 of GraphQL query reevaluations in your ActionCable processes.

One possible solution for GraphQL is to write custom subscription implementation with ideas from GraphQL Pro's Pusher implementation: http://graphql-ruby.org/subscriptions/pusher_implementation.html

Idea is to store info about subscriptions in Redis or somewhere and to re-execute GraphQL queries in the process that triggers subscriptions and broadcast only results (and AnyCable will work fine here, but thousands of reevaluations still will be there).

@palkan

This comment has been minimized.

Copy link
Member

palkan commented Aug 9, 2018

@Envek Thanks for this write-up! (awesome charts, btw 👍)

Just want to add that even if we add support for custom callbacks and instance variables, we would still have this problem:

If you have 1000 subscribed clients you will get 1000 of GraphQL query reevaluations in your ActionCable processes.

But it would transform into a little bit more scary one: If you have 1000 subscribed clients you will get 1000 AnyCable RPC calls and 1000 of GraphQL query reevaluations.

Idea is to store info about subscriptions in Redis or somewhere and to re-execute GraphQL queries in the process that triggers subscriptions and broadcast only results (and AnyCable will work fine here, but thousands of reevaluations still will be there).

Probably, we could combine similar subscriptions to decrease the number of evaluations.

We have some ideas on how to do that with some restrictions: for example, if you do not use contexts in your subscriptions (context-free queries), than it should be easy to create a stream per unique subscriptions (schema + params) and re-use it for multiple clients

@palkan palkan changed the title Why doesn't AnyCable support Custom stream callbacks ? AnyCable vs. GraphQL-Ruby, or why doesn't AnyCable support Custom stream callbacks? Aug 9, 2018

@palkan

This comment has been minimized.

Copy link
Member

palkan commented Aug 9, 2018

@gastonmorixe I've updated the title to better reflect what we're talking here about; hope you don't mind)

@palkan palkan added the discussion label Aug 9, 2018

@gastonmorixe

This comment has been minimized.

Copy link

gastonmorixe commented Aug 9, 2018

@palkan

This comment has been minimized.

Copy link
Member

palkan commented Aug 15, 2018

Probably, we could combine similar subscriptions to decrease the number of evaluations.

Persisted queries feature looks like something relevant http://graphql-ruby.org/operation_store/overview.html.

So, GraphQL Pro already has the functionality to de-duplicate queries. Thus we can use subscription's operationId as a stream name and avoid callbacks at all (again, if and only if we use context-free subscriptions).

@Envek

This comment has been minimized.

Copy link
Member

Envek commented Aug 25, 2018

Played with the idea to store subscription information in Redis (as GraphQL Pro's Pusher implementation does).

Released proof of concept as a graphql-anycable gem: https://github.com/Envek/graphql-anycable

It works with AnyCable locally 😃

Everyone interested, please try it, play with it and say your feedback (it is not production-ready yet).

@Envek Envek referenced this issue Jan 12, 2019

Merged

Static compatibility checks with Rubocop #55

5 of 5 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment