dillonkearns/elm-graphql

Subscriptions… where be there dragons?

BrianHicks opened this issue · 14 comments

The subscriptions docs for GraphQL warn about big 'ol breaking changes to the mechanisms they use, but don't say what might bite or be awful.

Warning The Subscriptions functionality in this package is in a highly experimental stage and may change rapidly or have issues that make it not ready for production code yet.

Would you mind elaborating on that, and why it wouldn't be ready? Is it an "I think it's OK but want people to test" or "this is more-or-less a huge experiment and really bad in odd ways so probably don't touch it until it stabilizes"?

Hey Brian! Thanks for the question. You’re right it would be good to be more specific, and indeed I know more now and have gone through some design iterations since I wrote that warning a few weeks ago. It’s definitely closer to a “I think it's OK but want people to test” warning than a crazy experiment warning. @ssbb is starting to integrate it into his production application and so far he says it’s looking good! And the happy path appears to be working great!

My biggest concern is that there seem to be some major issues with the Elm websockets package. There are several stale github issues in that package, and any core issues there prevent any Elm code using websockets from being 100% stable. In particular, the issue I have observed is that if there’s an issue with the server connection the websockets package will try to create a new connection every second or so, but as far as the consumer is concerned (in this case Graphqelm), the connection is still healthy. Unfortunately I lost the slack history for the relevant discussion, @ssbb had some thoughts on this and noted some relevant issues on Github (and that https://github.com/fbonetti/elm-phoenix-socket/issues had encountered some similar problems). I slacked him about it in #graphql and I’ll reply here when I get that context back.

I would love to get some help in general in understanding these issues around corner cases, both from people willing to test it and from anyone with knowledge about the Elm WebSockets package. It would be awesome to compile a really solid list of how to reproduce these issues, and which WebSockets issues they stem from (or what possible workarounds Graphqelm could use if possible). I don’t think there’s a ton more I can do to add stability at this point without some fixes to the WebSockets package, but compiling that information and documenting it thoroughly would at the very least allow me to be more specific in warning users, and would allow me to request fixes to the WebSockets package based on these concrete issues.

Besides that major concern, here is the status of Subscriptions in Graphqelm:

Looking Good

  • Happy Path (initialize a connection, listen for incoming messages for the subscription, decode them correctly)
  • Basic API design - several people have slacked me and said they are integrating Graphqelm subscriptions into their apps. I’ve gotten lots of positive feedback, and some suggestions which I’ve already integrated. All in all it appears to be solid and should be reasonably stable.
  • Framework support - I’ve created Graphqelm.Subscription.Protocols for the Rails GraphQL implementation and for Elixir/Absinthe/Phoenix, and they have both been tested and confirmed working for the happy path.

Known Missing

  • Queuing mutation requests If the connection is lost, any mutations you send over the WebSocket will not be queued (you can still send them over Http, though, and this works perfectly). The Elm websocket connection handles retries correctly (I believe), but they are succesful as far as the request’s response code goes, it’s just that the connection to the subscription was lost so it will fail to send them. This should probably be queued up and resent upon reconnection at the Graphqelm layer since Elm can’t be aware of that issue.
  • Other frameworks’ Protocols I’m not sure if the Protocol abstraction will work for other frameworks or not (like the NodeJS one, for example). It may need to evolve in a non-trivial way if there are any major differences in other server’s Subscription protocols. However, I’m not too concerend about this because Elm makes it fairly safe to adapt to API changes, and hopefully most users won’t even notice.
  • Detecting lost Subscription Can I discover based on the server’s response whether the Subscription connection was lost and needs to be re-established? I need to investigate this more in-depth and see what I can do here.
  • Better Subscription event hooks There are some things missing or requiring more polish in terms of registering messages for when an error occurs, when the connection fails, etc.

I’ll work on improving the warning in the docs to be more concrete once I’m back from my trip in a week or so. Thanks again for starting the discussion!

wow! Thank you for the very detailed answer. I'll give it a shot, then. 😄

Dillon, thanks for the detailed writeup. I have been giving some thought lately to whether all this should be built into this project. It seems to me that this project could remain focused on being the source for autogeneration of elm types from a graphql schema. If you think of it that way, your goal for subscriptions would then be to provide a standard set of event / hooks for third-party socket libraries to adapt to. https://github.com/fbonetti/elm-phoenix-socket is a nice one for phoenix and no doubt there are other quality projects. An adapter would be written for each third-party lib to integrate with this project. While I am confident you could write your own support, getting this right is not that easy. The socket needs to reconnect, then the channels, then the subscriptions and it all needs to be resilient to a connection that may be flapping. If you structured things right you could always write your own socket support in parallel, as a sibling project. On the other hand, if socket support remains an integral part of this project, you have fine-grained control but you take on an expanding set of issues that seem adjacent to your core mission.

@akeating thanks for the thoughtful response, I really appreciate your thought process here. @ssbb just reported back to me on his experiments in using Graphqelm Subscriptions in his app, and he told me he ran into some issues with the channel reconnect logic that I mentioned above. He just expressed a similar opinion that he thinks that Graphqelm should let framework-specific packages handle the Subscription transport layer, and Graphqelm should only handle building GraphQL documents and decoding responses.

Here are some points he brought up:

  • Phoenix Presence API - If he used Graphqelm for the end-to-end GraphQL subscriptions, then he’d need to use a different package and a different WebSocket connection for Presence (an Elixir/Phoenix construct for seeing when users log on/off, like seeing who’s online in a chat client for example). That’s something that is clearly outside of the scope of Graphqelm, so that’s a pretty good argument for decoupling them.
  • Other transport protocols - It’s completely possible according to the GraphQL spec to do subscriptions over HTTP polling, for example, or other transport protocols besides WebSockets. This would also definitely be outside of the scope of Graphqelm.

Given all the considerations above I think it makes sense to do as both of you suggest @akeating and @ssbb and to decouple the transport knowledge from Graphqelm. It would be awesome to make this seamless and foolproof so that “if it compiles it works”. We’ll have to play around with the API both on the Graphqelm-end and in the framework specific libraries (I’ll likely make some pull requests and ask for help from people who are experimenting to develop some nice APIs).

It’s super helpful to get concrete code snippets on how this is happening, so keep the feedback coming! Thanks all for this great discussion!

For reference, here is how @ssbb is setting up the GraphQL Subscription using https://github.com/saschatimme/elm-phoenix:

subscriptionPayload =
            JE.object
                [ ( "query", Graphqelm.Document.serializeSubscription subscriptionDocument |> JE.string )
                ]

subscriptionMessage =
      Push.init "__absinthe__:control" "doc"
            |> Push.withPayload subscriptionPayload

And then Phoenix.push taco.websocketUrl subscriptionMessage as cmd.

@ssbb I made the GraphQL response decoders public (see Graphqelm.Document.decoder). Could you try using that for your example using the Elm Phoenix package and let me know how it works for you? @akeating if you'd like to try that as well (or anybody else!), that would be extremely helpful.

Then based on your feedback I will figure out the next steps for a migration path to move Subscriptions to outside packages.

I'm thinking it will look something like:

Documentation

  • Give general info about how to setup Subscriptions
  • Link to known packages that support setting up Graphqelm Subscriptions
  • Add example in examples folder using one of the Phoenix packages

3rd Party Packages

  • Might need to make some pull requests for the Elm/Phoenix packages to make it easier or more robust to setup Graphqelm Subscriptions
  • Does anything exist already that would allow you to do this with Rails or other frameworks? Perhaps for now a message to invite others to contribute these would be a good start.

Remove deprecated

  • Give a warning that the Graphqelm.Subscription is deprecated and going to be removed
  • Remove it (after a month or so maybe)
ssbb commented

Great, thank you! I have same function already (it's just an JSON decoder at all). Since I am decoding raw subscription data I just using Json.Decode.at:

subscriptionDecoder : SelectionSet decodesTo typeLock -> JD.Decoder decodesTo
subscriptionDecoder (SelectionSet fields decoder) =
    JD.at [ "result", "data" ] decoder

I have a deadline right now, but will able to create an example of integrating with elm-phoenix later (need to fix my PR also). Anyway after some work on it I can say it's a good idea to abstract subscriptions and should be easy to create packages for integration.

@dillonkearns How has this matured? We are looking at starting to use subscriptions now, and Graphqelm has been fantastic for us so far!

Hey @anagrius, I'm glad to hear Graphqelm been a good experience for you and your team!

The status is that I'm looking to remove the built-in Subscription functionality as discussed here, but I'll need help from users who are experimenting with Subscriptions, such as @ssbb and perhaps you and your team, to make the transition.

It's definitely a safe bet to go ahead and use Graphqelm to build up your queries in conjunction with elm-phoneix (or another Elm subscription package) to deal with the low-level channel connection logic. This is exactly what @ssbb is doing right now. With this approach, you're not coupled to anything in Graphqelm that will change so there won't be anything for you to change when I remove the subscriptions code. And you could really help me push this forward by reporting back on how that goes! I'd love to hear feedback at any point, either here or on Slack!

I've gone ahead and officially removed the Graphql.Subscription module from this package. You should still be able to get the Subscription GraphQL query and decoder with the Graphql.Document.serializeSubscription and Graphql.Document.decoder functions.

I would love to hear feedback about this! If you are integrating GraphQL subscriptions using dillonkearns/elm-graphql, please extract some code snippets from your actual examples as a basis for conversation. You can either open up a new Github issue, or ping me on the Elm Slack. Thanks everyone for the great discussion here!

Thanks everyone who chimed in on this topic, and thanks Richard for opening the issue!

See the changelog for more details.

Any chance of updating the old example to show how the new package can be used with a manual/externally supplied connection?

Hello @JonRowe, thank you for the suggestion! That's a great idea. I created this issue to track making an Ellie example using the new approach. There are some reasons it might make sense to hold off on this (I captured them in #70). Here are the notes I wrote there for reference:

Now that the transport layer code has moved out of dillonkearns/elm-graphql based on #43, we should have an Ellie example linked to in the README to illustrate how this could be done (as this comment points out).

This may have to wait until the elm/websocket package, and perhaps a package for Elm and Phoenix, are published. This package uses long polling with Phoenix as a workaround, but is listed as not being production ready yet so it might be best to hold off for now.

@dillonkearns I have just started using this package with an Elixir/Absinthe backend. I like it so far and appreciate your work very much!

Do you have any suggestions on how GQL subscriptions can be handled until an elm/websocket package comes along?

Hello @MonadicT, I'm glad you're enjoying the library!

It's actually not that different to use the Elm websockets package compared to going out to a port. In fact, in some ways it's simpler because you can use Phoenix's javascript code for connecting to channels: https://hexdocs.pm/phoenix/js/

All you have to do is wrap some ports that use that library on the JS side, and send a message over to Elm every time new data comes in. You'll need a "Cmd port" (Elm to JS) to send the query over to tell JavaScript to start the subscription. Then you can listen for a Json.Decode.Value through a Sub port (JS to Elm) for every time data comes in.

If you need more details, ask on the #graphql channel in the Elm Slack: https://elmlang.herokuapp.com/. There are lots of friendly people there who can give you a quick answer to your question!

@dillonkearns Thanks for your quick response! Guess I will just use ports. Thanks for the JS code pointer.