-
Notifications
You must be signed in to change notification settings - Fork 145
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
Fix xhr response handlers getting called 3 times. #255
Conversation
Can't we just capture the readyState before returning from the original handler? |
You mean the JS handler that gets registered that forwards to the ghcjs runtime? That would work too, but we could not use the nice |
Actually there is an even easier solution, just use on I will fix this PR accordingly. |
Actually, I am not sure this is a good idea: It adds the risk of new browser incompatibility issues. |
This looks related to #234 ? Wondering if you were using an older version as mentioned in the other PR and if you can repro this with a newer version |
The other PR essentially downgrades ghcjs-dom, this suggests this might be a regression in ghcjs-dom. I will check that out, when I have some time. |
Ah right, I got that backwards, but also I just noticed that there might have also been a mixup between |
What's the status of this PR? The comments seem inconclusive. |
`onreadystatechange` gets fired four times: ``` 1 (OPENED): the request starts 2 (HEADERS_RECEIVED): the HTTP headers have been received 3 (LOADING): the response begins to download 4 (DONE): the response has been downloaded ``` The problem: between 2, 3 and 4 barely any time passes. Due to the asynchronous nature of ghcjs and even worse with jsaddle the following seems to happen: `onreadystatechange` triggers, because of a transition from 1 to 2, so the reflex-dom handler gets called (eventually). Unfortunately, when the handler finally gets executed, `readystate` might already be in state 4, so the check in Reflex.Dom.Xhr: ```haskell when (readyState == 4) $ do ... ``` passes and the user callback will be called. The problem, afterwards the handler will be called again for the transitions from 2 to 3 and from 3 t 4, resulting in the user callback getting called three times instead of once. At first, I simply disconnected the event handler in the handler, which worked. But depending on the actual implementation and ghcjs/jsaddle internals this solution might fail itself for some race condition, so I decided to simply encode my intent: Check and set atomically whether or not the caller has already been called or not, and don't do anything if that was already the case.
73c872a
to
d140fda
Compare
@ali-abrar This fixes the issue for reflex-dom, but it most likely is just a manifestation of a regression in ghcjs-dom/jsaddle. I created an issue now and referenced it in a comment in this fix. I will try to find some time soon to track this issue down properly, but in the meantime this PR would provide a fix for reflex-dom based applications. |
I think we need to understand why this handler is marked async at all: ghcjs/jsaddle-dom#14 Hopefully @hamishmack can fill us in! |
when (readyState == 4) $ do | ||
handled <- liftIO $ atomicModifyIORef' alreadyHandled $ \handled -> | ||
if readyState == 4 then (True, handled) else (handled, handled) | ||
when (readyState == 4 && not handled) $ do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like it would make more sense to move the handled
logic into when
since both are checking for readyState == 4
and it would avoid the atomicModifyIORef'
when it's not relevant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I just meant to make it thread safe just in case. Otherwise in a multi-threaded context, the handler could still be called twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moving the whole logic into the when
would be thread safe as well but possibly be faster (fewer IORef operations) and not duplicate the readyState == 4
logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha right, now I get what you mean. Yep, that would be smarter :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok fixed (untested), can't really remember why I thought in 2018 I would need that atomicModifyIORef'
in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I know why I thought the atomicModifyIORef'
is needed, because it actually is ... stupid me My younger me, actually thought this through. Will fix in a second.
Also get rid of atomicModify.
Is there some reason we would not just make the handler synchronous? That seems like it would be the right way to solve this. |
So just using Given that the async/sync handling is pretty hidden, I guess it just did not think of this possibility. I am pretty sure I have not tried it. |
@eskimor Thanks! We should try @ryantrinkle's suggestion. Do you happen to remember how to reproduce this? |
Reproduction is simple: Do an xhr request and log some output in the response handler, you will notice that you get the logged output three times for each request. Back then, the issue became apparent in pact-web (chainweaver these days). I fixed it in this commit. But it is definitely simpler to reproduce this with a fresh obelisk template ... Also it's been almost two years, the issue might have been fixed in some other place already. |
If I remember correctly, it was not a Heisenbug and could be reproduced reliably. |
Superseded by #399 |
onreadystatechange
gets fired four times:The problem: between 2, 3 and 4 barely any time passes. Due to the
asynchronous nature of ghcjs and even worse with jsaddle the following
seems to happen:
onreadystatechange
triggers, because of a transition from 1 to 2, sothe reflex-dom handler gets called (eventually). Unfortunately, when the
handler finally gets executed,
readystate
might already be in state 4, sothe check in Reflex.Dom.Xhr:
passes and the user callback will be called. The problem, afterwards the
handler will be called again for the transitions from 2 to 3 and from 3
to 4, resulting in the user callback getting called three times instead
of once.
At first, I simply disconnected the event handler in the handler, which
worked. But depending on the actual implementation and ghcjs/jsaddle
internals this solution might fail itself for some race condition, so I
decided to simply encode my intent: Check and set atomically whether or
not the caller has already been called or not, and don't do anything if
that was already the case.