tolitius / mount

managing Clojure and ClojureScript app state since (reset)
Eclipse Public License 1.0
1.22k stars 88 forks source link

`:start` sometimes gets called twice (ClojureScript) #112

Open jwr opened 4 years ago

jwr commented 4 years ago

After a grueling multi-day trial-and-error debugging session I have finally confirmed that it is possible for the :start method to be called twice by mount in ClojureScript.

I suspected this for a long time, I even had an atom to guard and log cases of dual invocation in one of my files, but I always wrote it off as a temporary browser caching phenomenon, as I only noticed it in development, not in production bundle. However, recently it started happening in the production bundle as well.

Unfortunately, I can't provide a reproducible case. This seems to be an extremely elusive bug: it seems to depend on file sizes or file arrangement. Once the app is built, it will happen predictably every time, and recompiling the same code will reproduce the problem as well, but adding even a single println might stop it from happening. Then removing the println makes it happen again.

In my case, it happens in with a namespace that is used by lots of other namespaces. Race condition somewhere, perhaps?

I verified that mount/start is called only once.

I realize this isn't much to go on, but I think it's something worth knowing. Perhaps others can deduce how this is possible, perhaps safeguards can be put in place so that it doesn't happen. It can result in really hard-to-track problems.

tolitius commented 4 years ago

thanks for reporting this. from the mount side it is difficult to know the intention: i.e. "did you want to call mount/start twice", "did you call (mount/start a) and then (mount/start)", etc..

on a ClojureScript side things are always harder since "there are no threads, but there are threads" due to the way browsers handle the event loop and/or websockets, etc.

by itself calling (mount/start) twice won't hurt an application since mount would not start a state/component in case it is already started. If you observe the double start of a component, that would only mean that either mount's internal state was wiped, or, as you mentioned there could have been a race condition where an application called mount/start at "virtually the same" time and some components might have taken "long" time to load. (this is just a guess).

one thing that might help you is (mount/running-states) that returns a set of states that are running at the moment. in your start function, you can try verifying if your state is started by running this set against its name and ignore starting it if it is already started. This would not be a solution, but more of a "add some clarity / safety" exercise.

jwr commented 4 years ago

To clarify again: I am certain that mount/start is only called once.

In spite of this, at least in some namespaces, the :start method is sometimes called twice (exactly twice, never more).

This does sound like a race condition somewhere, especially because adding or removing a single println could change the behavior.