Bernard

Bernard

Subscription catchup / state delta

So this is a problem that’s come up in both large projects I’ve used Absinthe/GraphQL with, and it’s a little bit surprising to me that I can’t find more people who strike it. But perhaps I’m just bad at googling or I’m missing some obvious angle.

The issue comes down to using GraphQL subscriptions for state updates. Take a simple example: user presence on simple IM server. When a client connects, they want:

a) The initial state (all of their friends and whether they’re online or offline), and
b) Notification of all changes to that state (when a friend comes online or goes offline).

Obviously a) can be achieved with a simple query.

The oblivious way (indeed, the only real-time GraphQL way) to do b) is with a subscription. Simple enough - fire an event when a user’s state changes.

So what’s the problem? It’s the intersection of the two. If the client issues the query first, then the subscription, they might miss an update during that gap and their view of the state will be wrong. Conversely, if you do things the other way around, they might get an update that was sent before their query ran, but with the multiple processes involved in Absinthe’s subscriptions there’s no way to guarantee that that will arrive before the query result (which might allow the client to trivially discard it), even if the authoritative data all comes from a single process. And that’s assuming the query is even sent over the same websocket as the query.

One way to solve this would be to include some kind of ordinal field (e.g. a timestamp) to the query and subscription data so that the client could discard subscription updates with an older ordinal than is attached to the query, and apply newer ones. And indeed that works fine and is what I’ve done to this point. But I really don’t like it - it shifts the burden of data consistency to the client when the server has all the data it needs to do it itself (self-evidently, since it’s the one generating the ordinal values).

The problem is, I’ve struggled for a long time to come up with a better system within the constraints of Absinthe and I haven’t been able to. Some of the approaches I’ve looked at:

  • Add a catchup function to subscriptions (see over in https://forum.elixirforum.com/t/adding-a-subscription-catchup-function-to-absinthe/16363). In spite of being implemented, that never really went anywhere and in retrospect that’s probably a good thing because it doesn’t actually solve the problem above - it only removes the need for an extra query without avoiding the race condition.

  • Add some callback configuration to the subscription definitions which generate and compare ordinals from within the subscription execution. That ends up not working too because the subscription resolution is batched and as a result there’s no good place to shove the last ordinal value for each connection to that subscription which is accessible when it needs to be.

  • Do something similar with middleware and/or plugins - that ends up having the same issue as above: middleware and plugins aren’t executed for each connection.

As such I’ve kind of come to the realisation that the current Absinthe architecture probably can’t support what I want and at a bare minimum I’m going to have to start digging into absinthe_phoenix’s pubsub stuff if I want any chance of making this work.

So I guess my questions are: Am I missing something? Is this whole thing a fool’s errand and I should just let the client deal with it and get on with my life? Does anyone have any really clever insights I’ve missed?

Thanks!

Marked As Solved

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

This sort of thing is ideally implemented within Absinthe itself so that it isn’t directly dependent on the channels impl, but it’s a bit non trivial to sort out when to trigger the update. The challenge is that when you run a subscription, what is returned is a topic. The client then needs to subscribe to the topic, and this act of subscribing is just a call into the pubsub behavior, it isn’t something that pings any Absinthe code. That is to say, Absinthe has no way of knowing today when the end client actually subscribes.

Any proposed solution here would basically require then three things:

  1. An Absinthe.Subscription.prime/n function that takes a client topic (as distinct from the general shared topic returned from config) that you want to prime.

  2. Some way of figuring out what that root_value is. This could probably be a function returned from config on the subscription called maybe prime:. Eg: prime: fn -> Repo.get(Shipment, args.shipment_id) end).

  3. Code in Absinthe.Phoenix to call prime.

A PR containing these two ideas would be welcome.

Also Liked

Bernard

Bernard

Brilliant, thanks Ben. I came to a roughly similar conclusion myself, I think, and I’m in the process of putting together some code for it. I’ll definitely look to this as guidance though.

Where Next?

Popular in Questions Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

Other popular topics Top

Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53690 245
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement