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

Fix xhr response handlers getting called 3 times. #255

Closed
wants to merge 3 commits into from

Conversation

eskimor
Copy link
Contributor

@eskimor eskimor commented Nov 19, 2018

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:

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
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.

@ryantrinkle
Copy link
Member

Can't we just capture the readyState before returning from the original handler?

@eskimor
Copy link
Contributor Author

eskimor commented Nov 19, 2018

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 GHCJS.DOM on then :-(

@eskimor
Copy link
Contributor Author

eskimor commented Nov 20, 2018

Actually there is an even easier solution, just use on load instead of on readystatechanged: https://developer.mozilla.org/en-US/docs/Web/Events/load

I will fix this PR accordingly.

@eskimor eskimor changed the title Fix xhr response handlers getting called 3 times. WIP: Fix xhr response handlers getting called 3 times. Nov 20, 2018
@eskimor
Copy link
Contributor Author

eskimor commented Nov 20, 2018

Actually, I am not sure this is a good idea: It adds the risk of new browser incompatibility issues.

@eskimor eskimor changed the title WIP: Fix xhr response handlers getting called 3 times. Fix xhr response handlers getting called 3 times. Nov 20, 2018
@luigy
Copy link
Member

luigy commented Jan 9, 2019

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

@eskimor
Copy link
Contributor Author

eskimor commented Jan 10, 2019

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.

@luigy
Copy link
Member

luigy commented Jan 10, 2019

Ah right, I got that backwards, but also I just noticed that there might have also been a mixup between ghcjs-dom vs jsaddle wrt which package had an effect #234 (comment)

@hSloan hSloan added bug fix Fixes an existing bug follow up Follow up with Merge Requester to take some action labels Jan 23, 2019
@ali-abrar
Copy link
Member

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.
@eskimor
Copy link
Contributor Author

eskimor commented Mar 8, 2019

@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.

@ryantrinkle
Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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 :-)

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.
@ryantrinkle
Copy link
Member

Is there some reason we would not just make the handler synchronous? That seems like it would be the right way to solve this.

@eskimor
Copy link
Contributor Author

eskimor commented May 6, 2020

So just using onSync here instead of on .. Or evaluate why readyStateChange is defined as an async event in the first place. Either way, I guess that should work as well.

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.

@3noch
Copy link
Member

3noch commented May 6, 2020

@eskimor Thanks!

We should try @ryantrinkle's suggestion. Do you happen to remember how to reproduce this?

@eskimor
Copy link
Contributor Author

eskimor commented May 6, 2020

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.

@eskimor
Copy link
Contributor Author

eskimor commented May 6, 2020

If I remember correctly, it was not a Heisenbug and could be reproduced reliably.

tomsmalley added a commit that referenced this pull request Jul 7, 2020
This change prevents multiple firings of the response event.

Fixes GitHub issue #467
Supersedes GitHub PR #255
@tomsmalley
Copy link
Member

Superseded by #399

@tomsmalley tomsmalley closed this Jul 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug fix Fixes an existing bug follow up Follow up with Merge Requester to take some action non-breaking change
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants