Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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? #160

Closed
gastonmorixe opened this issue Apr 11, 2018 · 27 comments

Comments

@gastonmorixe
Copy link

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

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

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

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

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

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

@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
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
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: https://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
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
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)

@gastonmorixe
Copy link
Author

gastonmorixe commented Aug 9, 2018 via email

@palkan
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 https://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
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).

@palkan
Copy link
Member

palkan commented Jan 21, 2020

Closing since we've been using https://github.com/Envek/graphql-anycable in production for more than a year. Thanks @Envek!

@butsjoh
Copy link

butsjoh commented Jul 1, 2020

@palkan Maybe slightly of-topic but did not know where to ask it elsewhere. Since you have been using it for a while in production, I would like to ask if you are running it together with litecable or not. Cause i am a bit confused when i look at the litecable github page claiming that it's primary use would be for test and development but then later it says it is compatible with anycable. So question is more is litecable production ready or should i just use it in dev/test only and use full actioncable in prod? We are looking at providing graphql subscription support for our graph (and we don't use rails) and found: https://github.com/anycable/graphql-anycable which say it works with litecable for none rails. So hope you could help me with my confusion :)

@palkan
Copy link
Member

palkan commented Jul 1, 2020

Hey @butsjoh!
LiteCable is production-ready when used with AnyCable. Its built-in Ruby WebSocket server is meant for dev/test only.

It should work with graphql-anycable, probably, with some additional setup. @Envek any thoughts?

@butsjoh
Copy link

butsjoh commented Jul 1, 2020

Hi, thnx for the answer. After a second look at the litecable readme i noticed now that indeed that the part about dev/test is optional. Sorry for my misunderstanding but at least i got the confirmation :) thnx

@Envek
Copy link
Member

Envek commented Jul 1, 2020

@butsjoh I've not tried LiteCable with graphql-anycable yet, but it should work.
In case of any troubles or questions, please create a new issue at https://github.com/anycable/graphql-anycable and we will help

@phlegx
Copy link

phlegx commented Mar 18, 2022

Hi! Are stream callbacks planned in one of the next versions? I also need this functionality. Thx

@palkan
Copy link
Member

palkan commented Mar 18, 2022

Hey @phlegx!

Not really. Mostly because we don't see a demand for this feature. Could tell a bit more about your use case? (It would be better to start a new discussion)

@phlegx
Copy link

phlegx commented Mar 18, 2022

Here my use case @palkan:

An authenticated user owns several devices. The list view shows all this owned devices. An ActiveJob handles a device state change and broadcasts the state and the id of the device on the general device channel. The method stream_from (or stream_for) used with a block should check if the connected user owns this device. If yes, then the message should be transmitted. If not, the message should be retained.

@Envek
Copy link
Member

Envek commented Mar 18, 2022

broadcasts the state and the id of the device on the general device channel

Usually, subscription scopes are used for that:

YourAppSchema.subscriptions.trigger :event_name, {}, { data: "here" }, scope: device.user_id

Internally, user identifier is got included into Redis channel name so all broadcasts become scoped to one user only.

@phlegx
Copy link

phlegx commented Mar 18, 2022

@Envek thank you for the reply, but I dont use GraphQL-Ruby and I'm not familiar with that.

@palkan
Copy link
Member

palkan commented Mar 19, 2022

@phlegx You can implement scoping yourself. Instead of broadcasting update to a device, consider broadcasting updates to a user specific channel. I guess, you have something like stream_from "devices" do ... end, and you need stream_from user. With filtering, you broadcast all updates to all connected users (though most of them are filtered out)—a lot of unnecessary work for a server.

@phlegx
Copy link

phlegx commented Mar 20, 2022

@palkan but stream_from with ah block do |msg| ... end don't work with anycable, right?

@palkan
Copy link
Member

palkan commented Mar 21, 2022

stream_from with ah block do |msg| ... end don't work with anycable, right?

Correct

@palkan palkan transferred this issue from anycable/anycable-rails Aug 15, 2022
@anycable anycable locked and limited conversation to collaborators Aug 15, 2022
@palkan palkan converted this issue into discussion #161 Aug 15, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests

7 participants