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

"Store reset while query was in flight(not completed in link chain)" when stopQuery is called right after resetStore. Race condition? #3766

Closed
nyanpasu opened this issue Jul 31, 2018 · 18 comments · Fixed by #4409

Comments

@nyanpasu
Copy link

nyanpasu commented Jul 31, 2018

Currently, queries are getting stuck in the QueryManager.fetchQueryPromises Map when stopQuery is called while that query is in flight.

Inside QueryManager.fetchQuery a subscription is added to the QueryInfo (QueryManager.query object) of the Observable representing the current request.

      this.setQuery(queryId, ({ subscriptions }) => ({
        subscriptions: subscriptions.concat([subscription]),
      }));

On the subscription, callbacks are set up to remove the queryId from QueryManager.fetchQueryPromises

        error: (error: ApolloError) => {
          this.removeFetchQueryPromise(requestId);
          // ...
        },
        complete: () => {
          this.removeFetchQueryPromise(requestId);
          // ...
        },

When stopQuery() is called before this query resolves, the promise will stay in that map because the subscription has been removed, therefore the callbacks to remove it will no long trigger.

  public stopQuery(queryId: string) {
    this.stopQueryInStore(queryId);
    this.removeQuery(queryId);
  }

  public removeQuery(queryId: string) {
    const { subscriptions } = this.getQuery(queryId);
    // teardown all links
    subscriptions.forEach(x => x.unsubscribe());
    this.queries.delete(queryId);
  }

Later on, this will cause problems when one tries to call resetStore()

  public resetStore(): Promise<ApolloQueryResult<any>[]> {
    return this.clearStore().then(() => {
      return this.reFetchObservableQueries();
    });
  }

  public clearStore(): Promise<void> {
    this.fetchQueryPromises.forEach(({ reject }) => {
      reject(
        new Error(
          'Store reset while query was in flight(not completed in link chain)',
        ),
      );
    });
    // ...
  }
Error: Network error: Store reset while query was in flight(not completed in link chain)
    at new ApolloError (bundle.umd.js:124)

Intended outcome:
I think queries should be removed from that Map after subscriptions get removed.

So as a solution, I might suggest that as part of the clean up for stopQuery, that the requestId should be removed from QueryManager.fetchQueryPromises. Perhaps there is a cancel method to evoke, or I should reject the promise with a new Error that the query has been cancelled.

Actual outcome:
Queries are staying in that Map.

How to reproduce the issue:
It's an edge case, but this is how I ran into this issue through react-apollo:

  • Render a Query wrapped component.
  • Call resetStore() via user interaction
  • Immediately trigger a state change that results in that Query component being unmounted.

Versions

 apollo-client: 2.3.5
 react-apollo: 2.1.9

In hindsight I should have just written a PR instead.

@hwillson
Copy link
Member

Thanks for the detailed issue report @nyanpasu. If you're interested in submitting a PR to help address this, that would be awesome!

@hwillson
Copy link
Member

hwillson commented Sep 5, 2018

This issue is related to #3555, #3792 and #2919. We'll address them all in one shot.

@zzzzasd
Copy link

zzzzasd commented Oct 3, 2018

Hi, will this issue be resolved soon? Is there a workaround?

vinhnguyen1211 added a commit to vinhnguyen1211/fullstack-apollo-react-express-boilerplate-project that referenced this issue Oct 17, 2018
@abergenw
Copy link
Contributor

I just ran into this same problem but from a slightly different angle. The promise returned by QueryManager.refetchObservableQueries() never resolves if QueryManager.removeQuery() has been called for a query before the result arrives.

@joelgetaction
Copy link

I'm also seeing this problem. Any fix yet? It definitely seems to be a race condition - if I add a timeout between logout and resetStore then this error goes away. It seems like as long as there's one more render between the logout and the resetStore then the error goes away. This is for an @client local query by the way.

@settings settings bot removed reset labels Dec 12, 2018
@mohamedbrahimi
Copy link

I try this and it worked for me :
this.apollo.getClient().resetStore().then(() => {
// your code here
})

@nyanpasu
Copy link
Author

Did you also follow the other steps here to reproduce the issue?

Render a Query wrapped component.
Call resetStore() via user interaction
Immediately trigger a state change that results in that Query component being unmounted.

@cheapsteak
Copy link
Member

I wonder if someone could verify whether this is a good minimal reproduction of this bug?

https://codesandbox.io/s/rj3w4vm4xo

The significant bit:

  componentDidMount() {
    console.log('resetStore 1: start');
    this.props.client.resetStore()
      .then(() => console.log('resetStore 1: done'))
      .catch(e => {
        console.error('resetStore 1: failed', e);
      });

    setTimeout(() => {
      console.log('calling setState');
      // this removes the <Query/> from render and triggers its fetch to be cancelled
      this.setState({ shouldShowQuery: false }, () => {
        console.log('setState callback');
        console.log('resetStore 2: start');
        setTimeout(() => this.props.client
          .resetStore()
          .then(() => console.log('resetStore 2: done'))
          .catch(e => console.error('resetStore 2: failed', e))
          , 1000
        );
      });
    });
  }

Console output:
image

It doesn't seem to matter how long the timeout is, whenever the 2nd resetStore is called, the first resetStore's promise finally throws an exception

@cheapsteak
Copy link
Member

@nyanpasu I couldn't quite reproduce the bug using your flow of

  • Render a Query wrapped component.
  • Call resetStore() via user interaction
  • Immediately trigger a state change that results in that Query component being unmounted.

https://codesandbox.io/s/nnv997ply0

Output:

resetStore 1: start
calling setState
setState callback
resetStore 1: done

Would you have time to take a look and see what I'm missing?

@cheapsteak
Copy link
Member

My bad, wasn't expecting "should fix ###" to close the issue

@cheapsteak cheapsteak reopened this Feb 4, 2019
@jbaxleyiii
Copy link
Contributor

Thanks for reporting this. There hasn't been any activity here in quite some time, so we'll close this issue for now. If this is still a problem (using a modern version of Apollo Client), please let us know. Thanks!

@qeternity
Copy link

Can confirm this is still an issue

@JanSmolko
Copy link

If somebody has still this issue... I resolved it with stop() before clearStore()

...
client.stop()
client.clearStore()
...

@azuxx
Copy link

azuxx commented Apr 27, 2020

If somebody has still this issue... I resolved it with stop() before clearStore()

...
client.stop()
client.clearStore()
...

I used this but it seems only worked in "current session of apollo". Which means that if I am doing the code like below, when I refresh the app I get the previous Apollo client state without the "clearing" thing. So User is still signed in even thought my auth headers have been cleared and signOut mutation completed successfully.

This is my AuthNavigator:

//other code...

//using lazy query for getting currentUser
const [
    getUserMe,
    {loading: loadingUserMe, error: errorUserMe, data: dataUserMe, client}
  ] = useLazyQuery(QUERY_USER_ME);
let _queryCompleted = false;

//other code...

//handler called after calling signOut mutation
signedOut: async () => {
      await AsyncStorage.multiRemove([
        'access-token',
        'client',
        'token-type',
        'expiry',
        'uid'
      ]).then(() => {
        if (client) {
          console.log('can clean Apollo cache');
          client.stop();
          client.clearStore().then((_) => {
            console.log('Apollo cache cleared');
            setAuthState((prev) => ({...prev, isSignout: true, userMe: null}));
            console.log(
              'AuthNavigator got signedOut, so cleared auth headers and cache and we show signIn screen'
            );
          });
        }
      });
    },
///...other code

useEffect(() => {
    const bootstrapAsync = async () => {
      console.log('AuthNavigator called userMe');
      getUserMe();
    };
    bootstrapAsync();
  }, []);

  if (dataUserMe && !_queryCompleted) {
    console.log('AuthNavigator got userMeData', dataUserMe);
    authState.userMe = dataUserMe.me;
    authState.isLoading = false;
    _queryCompleted = true;
  }

///...other code

Is there a way to kind "write" the clearing happened with client.clearStore()? 🙏🏻

@chris-guidry
Copy link

@azuxx to clear the store when signing out, try these two:

this.props.client.cache.reset();
this.props.resetStore();

Also, using await is enough for the async code so the .then is not needed. The proceeding code can just be in line.

@azuxx
Copy link

azuxx commented Apr 27, 2020

@azuxx to clear the store when signing out, try these two:

this.props.client.cache.reset();
this.props.resetStore();

Also, using await is enough for the async code so the .then is not needed. The proceeding code can just be in line.

@chris-guidry Thank you for your suggestion, I am using hooks and useLazyQuery so I tried like this with your suggestion but it does not work. Did the logout flow correctly, then I refreshed the app but there is still the logged user :(

signedOut: async () => {
      await AsyncStorage.multiRemove([
        'access-token',
        'client',
        'token-type',
        'expiry',
        'uid'
      ]);
      if (client) {
        console.log('can clean Apollo cache');
        await client.cache.reset();
        await client.resetStore();
      }
      setAuthState((prev) => ({...prev, isSignout: true, userMe: null}));
      console.log(
        'AuthNavigator got signedOut, so cleared auth headers and cache and we show signIn screen'
      );
    }

@azuxx
Copy link

azuxx commented May 14, 2020

@chris-guidry Fixed! It was also a server issue🤦🏻‍♂️

@benderillo
Copy link

I don't understand how this issue is closed while it is still there and happening.
And in my case neither client.cache.reset(); nor client.stop() helps.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.