Skip to content
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

Why present increase when refresh the page? #216

Closed
Frid-Yuandu opened this issue Oct 1, 2024 · 24 comments · Fixed by #217
Closed

Why present increase when refresh the page? #216

Frid-Yuandu opened this issue Oct 1, 2024 · 24 comments · Fixed by #217
Labels

Comments

@Frid-Yuandu
Copy link

I encounter a problem that the "connected clients" count increase each time I refresh the page. What's the reason of this issue and how could I fix it?

phoenix_counter_refresh_increase_bug.mp4
@nelsonic
Copy link
Member

nelsonic commented Oct 1, 2024

@Frid-Yuandu Very interesting. Thanks for reporting this issue. Can you please do us a favour:
open a second web browser to the same page and see if the count increase in the second browser. 💭

@nelsonic nelsonic added help wanted Extra attention is needed user-feedback technical labels Oct 1, 2024
@Frid-Yuandu
Copy link
Author

Sure, both two clients see the count increase.

two_phoenix_counter_clients.mp4

@nelsonic
Copy link
Member

nelsonic commented Oct 1, 2024

Fascinating! Never seen this before.
Cannot replicate this on my localhost or on Fly.io: https://livecount.fly.dev 🤷‍♂️
Note: it briefly increments but then resets to 1 (or however many are connected).

@ndrean
Copy link
Contributor

ndrean commented Oct 1, 2024

@Frid-Yuandu @nelsonic

Could it be a cache problem? Same thing with incognito windows?

Can you try to wrap the subscriptions only once the socket is connect?

def mount(_params, _session, socket) do
    # PubSub.subscribe(Counter.PubSub, @topic)
    Presence.track(self(), @presence_topic, socket.id, %{})

    initial_present =
      Presence.list(@presence_topic)
      |> map_size

    if connected?(socket) do
      PubSub.subscribe(Counter.PubSub, @topic)
      CounterWeb.Endpoint.subscribe(@presence_topic)
    end

    {:ok, assign(socket, val: Count.current(), present: initial_present)}
end

@ndrean ndrean closed this as completed Oct 1, 2024
@ndrean
Copy link
Contributor

ndrean commented Oct 1, 2024

Ooops

@ndrean ndrean reopened this Oct 1, 2024
@Frid-Yuandu
Copy link
Author

Thanks for reply! I try to wrap subscriptions like code you provided, but still occur the problem.
And... interesting, the count in incognito windows always 1 less than the normal window (the upper left one). 🤷‍♂️

image

@ndrean
Copy link
Contributor

ndrean commented Oct 1, 2024

I still believe its a cache problem. Presence works with CRDT so a refresh might briefly increment but should converge. I don't have a smarter answer for this.

@Frid-Yuandu
Copy link
Author

I still believe its a cache problem. Presence works with CRDT so a refresh might briefly increment but should converge. I don't have a smarter answer for this.

I tried to clear the browser cache. After that, the current count goes down to 0, and for a short time the behavior looks normal, but keeps increasing on the next click, like each time reopen this page. Is this the cache issue you are referring to?

clear_browser_cache_retry.mp4

But I still don't know why others haven't encountered this🤔. Cache maybe common for browsers I think.

@ndrean
Copy link
Contributor

ndrean commented Oct 2, 2024

I forked the code and "IT JUST WORKS"(TM)"...
I imagine you tested Firefox as well.

What about the butcher way: a complete refresh by shutting down the computer?

@Frid-Yuandu
Copy link
Author

I forked the code and "IT JUST WORKS"(TM)"...

Interesting, I forked and it works in my pc too :), but my code still not works even if I shutting down the pc.
I compare each files, copy tutorial code to my code, I could not see what difference...

Will it due to the difference of the dependency 🤔? (I post my code here if needed)

# in my mix.exs
  defp deps do
    [
      {:phoenix, "~> 1.7.14"},
      {:phoenix_html, "~> 4.1"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
      {:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
      {:floki, ">= 0.30.0", only: :test},
      {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
      {:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
      {:heroicons,
       github: "tailwindlabs/heroicons",
       tag: "v2.1.1",
       sparse: "optimized",
       app: false,
       compile: false,
       depth: 1},
      {:telemetry_metrics, "~> 1.0"},
      {:telemetry_poller, "~> 1.0"},
      {:jason, "~> 1.2"},
      {:dns_cluster, "~> 0.1.1"},
      {:bandit, "~> 1.5"},

      # Track test coverage: github.com/parroty/excoveralls
      {:excoveralls, "~> 0.16.0", only: [:test, :dev]}
    ]
  end

# in tutorial mix.exs
  defp deps do
    [
      {:phoenix, "~> 1.7.7"},
      {:phoenix_html, "~> 4.0"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_view, "~> 0.20.0"},
      {:floki, ">= 0.30.0", only: :test},
      {:esbuild, "~> 0.7", runtime: Mix.env() == :dev},
      {:tailwind, "~> 0.2.1", runtime: Mix.env() == :dev},
      {:telemetry_metrics, "~> 1.0"},
      {:telemetry_poller, "~> 1.0"},
      {:jason, "~> 1.2"},
      {:plug_cowboy, "~> 2.5"},

      # Track test coverage: github.com/parroty/excoveralls
      {:excoveralls, "~> 0.18.0", only: [:test, :dev]},
    ]
  end

@ndrean
Copy link
Contributor

ndrean commented Oct 3, 2024

ah, so your code is not the repo. OK

I forked it again on another computer.

I changed the deps to yours.

Notice you use {:bandit, "~> 1.5"}, instead of cowboy. So I added adapter: Bandit.PhoenixAdapter,in the "config.exs" file

and ** I can reproduce the bug**. One step closer.

@Frid-Yuandu
Copy link
Author

Thanks for pointing this out. I also tested changing bandit to cowboy and the issue stopped appearing, so I guess that's the problem. But strangely enough, I didn't check the phoenix repo for a description of this behavioural difference :(

Thanks again for the enthusiastic answer that helped me out!

@ndrean
Copy link
Contributor

ndrean commented Oct 3, 2024

By the way, a "live" module should be named CounterWeb.CounterLive (and not CounterWeb.Counter), and the route changed accordingly (in "CounterWeb.Router", live("/", CounterLive)) but this is another subject.

When I wrap the subscriptions with connected?/1 in the mount/3 callback of the "CounterWeb.CounterLive" module as below, it should work (the if macro is a function, so returns the last line). Note also the modification in the handle_info of the "presence_diff" message returned by the Presence process: in order not to "self" count twice, you need to discount the caller. Phoenix kindly uses the socket.id as the key of the metadata it returns, so it is easy to remove it from the map (put dbg in the code if you want to see what happens in the terminal as below).

Please revert. Thks.

def mount(_params, _session, socket) do
    initial_present =
      if connected?(socket) do
        PubSub.subscribe(Counter.PubSub, @topic)
        CounterWeb.Endpoint.subscribe(@presence_topic)
        Presence.track(self(), @presence_topic, socket.id, %{})

        Presence.list(@presence_topic) |> map_size()
      else
        0
      end

    {:ok, assign(socket, val: Count.current(), present: initial_present)}
 end

and add:

def handle_info(
        %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}},
        %{assigns: %{present: present}} = socket
      ) do

   dbg(joins)
   dbg(socket.id)
    {_, joins} = Map.pop(joins, socket.id, %{})
   ^^^
   dbg(joins)

    changes = (map_size(joins) - map_size(leaves))
    new_present = (present + changes) |> dbg()

    {:noreply, assign(socket, :present, new_present)}
  end

The documentation says:

connected?(socket)

Returns true if the socket is connected.
Useful for checking the connectivity status when mounting the view. 
For example, on initial page render, the view is mounted statically, rendered, and the HTML is sent to the client. 
Once the client connects to the server, a LiveView is then spawned and mounted statefully within a process. 
Use [connected?/1](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#connected?/1) to 
conditionally perform stateful work, such as 
subscribing to pubsub topics, sending messages, etc.

@ndrean ndrean reopened this Oct 3, 2024
@ndrean
Copy link
Contributor

ndrean commented Oct 3, 2024

@Frid-Yuandu
I made the correction with bandit. This should work.
This code can then be updated to use the current "way-to-go" HTTP server.

@nelsonic once checked, an update?

@nelsonic
Copy link
Member

nelsonic commented Oct 3, 2024

yeah, we could add a section to the end of the tutorial on using bandit: https://github.com/mtrudel/bandit
But it doesn't ship with Phoenix by default https://hex.pm/packages/phoenix at the time of writing.
So it's a use case we only need to consider for more advanced use.
i.e. this whole issue, while interesting is not super relevant to the beginner tutorial. 💭

@ndrean
Copy link
Contributor

ndrean commented Oct 3, 2024

This shows that these HTTP servers behave - surprisingly - really differently.
Now, only a bunch of motivated people will start an app and change Bandit to Cowboy since the later is shipped by default.
Since I believe that the main usage of this repo is to be able take some of this code and reuse it, I would simply consider rewriting this (it should be minimal), without even mentioning this issue.

@nelsonic
Copy link
Member

nelsonic commented Oct 3, 2024

Agreed. 👌

@Frid-Yuandu
Copy link
Author

I made the correction with bandit. This should work.

Aha, that's helpful

@ndrean
Copy link
Contributor

ndrean commented Oct 3, 2024

@Frid-Yuandu thanks for your help. Can you confirm with these changes? (I mean directly copying the code above in your repo, as mine works).

PR #217

@Frid-Yuandu
Copy link
Author

Pardon me, do you want me to paste these two snippets and test them with bandit?
If so, I did that, and they work as expected.

Please revert. Thks.

def mount(_params, _session, socket) do
    initial_present =
      if connected?(socket) do
        PubSub.subscribe(Counter.PubSub, @topic)
        CounterWeb.Endpoint.subscribe(@presence_topic)
        Presence.track(self(), @presence_topic, socket.id, %{})

        Presence.list(@presence_topic) |> map_size()
      else
        0
      end

    {:ok, assign(socket, val: Count.current(), present: initial_present)}
 end

and add:

def handle_info(
        %{event: "presence_diff", payload: %{joins: joins, leaves: leaves}},
        %{assigns: %{present: present}} = socket
      ) do

   dbg(joins)
   dbg(socket.id)
    {_, joins} = Map.pop(joins, socket.id, %{})
   ^^^
   dbg(joins)

    changes = (map_size(joins) - map_size(leaves))
    new_present = (present + changes) |> dbg()

    {:noreply, assign(socket, :present, new_present)}
  end

@ndrean
Copy link
Contributor

ndrean commented Oct 3, 2024

Yes, perfect. @Frid-Yuandu Many thanks for your input!

closes #185, #216, #214, #213, #211

@ndrean ndrean closed this as completed Oct 3, 2024
@nelsonic
Copy link
Member

nelsonic commented Oct 3, 2024

@Frid-Yuandu while we "have" you, quick question: how did you discover this repo? 💭

@Frid-Yuandu
Copy link
Author

Frid-Yuandu commented Oct 3, 2024

Wow it's been a while, I can't be sure. I think i find phoenix-todo-list-tutorial in Github homepage, and then found this repo.

I've worked with some Phoenix LiveView tutorial before, but they were a bit vague and the complexity gave me a headache. Maybe that's what led me to find this project. lol

@nelsonic
Copy link
Member

nelsonic commented Oct 3, 2024

Cool. Hope you found this tutorial useful in your Elixir/Phoenix/LiveView learning quest! 🤞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants