amazon-web-servicesaws-appsyncaws-amplifyaws-datastore

AWS-Amplify DataStoreStateError: Tried to execute DataStore.query() while DataStore was "Stopping"


JavaScript Framework React Native

Amplify APIs DataStore

Amplify Categories api

Environment information Details Describe the bug DataStoreStateError: Tried to execute DataStore.query() while DataStore was "Stopping". This can only be done while DataStore is "Started" or "Stopped". To remedy: Ensure all calls to stop() and clear() have completed first. If this is not poss...

Hi, I have an amplify project to host backend for react-native mobile app. Suddenly it stopped to work. Any call to the data store will receive the message i mentioned above.

Our work plan is to provide amplify backend as an NPM package. This package is used from app developer to make calls to AWS-Amplify. Since 6 months everything works fine until 4 days go. We didn't had breaking changes or something. Just added a few lambda function which i don't think it could cause this issue.

We are starting datastore when app is open. DataStore.start() and clear it on SignIng event and SignOut event. We didn't make any change in this flow since a long time (4 months).

We tried to reproduce the issue locally but it is not possible for somehow. Since we are using typescript to write our NPM package. We test functionality locally with node or tsx where everything works, then we publish it and downloaded in another project react-native where everything works good. Once it is deployed, no action from DataStore could be executed. The app crashed and Sentry show us the message above. And again that start to happen 4 days ago until now.

Expected behavior To be able to execute DataStore manipulation functionality (query, save, delete... etc) / Get connection with data store.

Reproduction steps Whenever we call datastore functions in the react-native app. It will show this message:

DataStoreStateError: Tried to execute DataStore.query() while DataStore was "Stopping". This can only be done while DataStore is "Started" or "Stopped". To remedy: Ensure all calls to stop() and clear() have completed first. If this is not poss...

Code Snippet

// Put your code below this line.
    // where i clear data store and start it, just using auth events 
    // Clear the local datastore when signing out.
    // As advised in: https://docs.amplify.aws/lib/datastore/sync/q/platform/js/#clear-local-data
    EventHandler.OnSignOut = async () => {
        if (Application.hasBooted()) {
            await Application.clear();
        }
    };

    // Clear the local datastore when signing in.
    // As advised in: https://docs.amplify.aws/lib/datastore/sync/q/platform/js/#clear-local-data
    EventHandler.OnSignIn = async () => {
        if (Application.hasBooted()) {
            await Application.clear();
        }
    };

    // Boot the application as soon as the DataSync is completed
    // Check if it has not been booted already first
    EventHandler.OnDataSynced = async () => {
        if (!Application.hasBooted()) {
            await Application.boot();
        }
    };
    
    // Where i do datastore call
    /**
 * @name SaveAccount
 * @description: Save or update an account.
 *              If the account already exists in the datastore based
 *              on its id, it is updates. Otherwise a
 * @type {Function}
 * @param {AccountDetails} accountDetails
 * @param {AccountAttributes} params
 * @returns {Promise<AccountDetails>}
 */
const SaveAccount = async (
    accountDetails: AccountDetails,
    params?: AccountAttributes,
): Promise<AccountDetails> => {
    const current = await GetAccountDetails(accountDetails.id);
    if (current instanceof AccountDetails) {
        return await DataStore.save(
            AccountDetails.copyOf(
                current,
                updated => {
                    if (params) {
                        for (const key in params) {
                            updated[key] = params[key];
                        }
                    }
                },
            ),
        );
    }

    accountDetails = accountDetails instanceof AccountDetails
        ? accountDetails
        : new AccountDetails(accountDetails);

    return await DataStore.save(accountDetails);
};

aws-exports.js
/* eslint-disable */
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
"aws_project_region": "",
"aws_cognito_identity_pool_id": "",
"aws_cognito_region": "",
"aws_user_pools_id": "",
"aws_user_pools_web_client_id": "",
"oauth": {},
"aws_cognito_username_attributes": [
"EMAIL"
],
"aws_cognito_social_providers": [],
"aws_cognito_signup_attributes": [],
"aws_cognito_mfa_configuration": "OFF",
"aws_cognito_mfa_types": [
"SMS"
],
"aws_cognito_password_protection_settings": {
"passwordPolicyMinLength": 8,
"passwordPolicyCharacters": [
"REQUIRES_LOWERCASE",
"REQUIRES_NUMBERS",
"REQUIRES_UPPERCASE"
]
},
"aws_cognito_verification_mechanisms": [
"EMAIL"
],
"aws_appsync_graphqlEndpoint": "*****",
"aws_appsync_region": "",
"aws_appsync_authenticationType": "API_KEY",
"aws_appsync_apiKey": "",
"aws_user_files_s3_bucket": "",
"aws_user_files_s3_bucket_region": "*"
};

package.json
{
  "name": "@financiallease/react-native-amplify",
  "version": "7.1.0",
  "description": "",
  "author": "itsupport@financiallease.nl",
  "main": "dist/financiallease.js",
  "module": "dist/financiallease.js",
  "browser": "dist/financiallease.js",
  "typings": "dist/financiallease.d.ts",
  "types": "dist/financiallease.d.ts",
  "license": "LGPL",
  "scripts": {
    "__BUNDLING": null,
    "build:clean": "rimraf dist/",
    "build": "npm run build:clean && rollup -c",
    "bundle-local": "npm run build && npm pack && mv -v financiallease-react-native-amplify-*.tgz /usr/local/npm/@financiallease/react-native-amplify.tgz",
    "__LINTING": null,
    "autoformat": "npm run lint-typescript -- --fix && npm run lint-nodejs -- --fix",
    "lint-nodejs": "eslint --config amplify/.eslintrc.js 'amplify/backend/function/**/index.js'",
    "lint-typescript": "eslint --config .eslintrc.js '{src,test}/**/*.ts'",
    "lint": "npm run lint-typescript && npm run lint-nodejs",
    "coverage": "jest -c jest.config.ts --collectCoverage --coverageDirectory=\"./coverage\" --ci --reporters=default --reporters=jest-junit --watchAll=false",
    "test": "jest -c jest.config.ts",
    "__DOC GENERATION": null,
    "docs:generate": "npm run build:clean && sed '/[[_TOC_]]/d' README.md > README.sanitized.md && typedoc --readme README.sanitized.md --entryPoints src --entryPointStrategy expand --out docs --theme hierarchy --name \"React Native Amplify - docs\" --includeVersion",
    "docs:serve": "node -r esm --inspect docker/server.js",
    "__AMPLIFY BACKEND": null,
    "amplify-modelgen": "node amplify/scripts/amplify-modelgen.js",
    "amplify-push": "node amplify/scripts/amplify-push.js",
    "scan": "npm run build && npm run lint && npm run test && npm run docs:generate",
    "upgrade-amplify-deps": "npx npm-check-updates -i '/(@?aws-amplify|@react-native-community/netinfo)/' && npm update"
  },
  "publishConfig": {
    "@financiallease:registry": "https://gitlab.com/api/v4/projects/35071033/packages/npm/"
  },
  "dependencies": {
    "@algolia/client-search": "^4.14.2",
    "@algolia/transporter": "^4.14.2",
    "@aws-amplify/core": "^4.7.2",
    "@aws-amplify/datastore": "^3.12.8",
    "@react-native-async-storage/async-storage": "^1.17.4",
    "@react-native-community/netinfo": "^9.3.0",
    "@types/amplify": "^1.1.25",
    "aws-amplify": "^4.3.33",
    "aws-amplify-react-native": "^6.0.5",
    "aws-sdk": "^2.1142.0",
    "deep-equal": "^2.0.5"
  },
  "devDependencies": {
    "@aws-amplify/cli-extensibility-helper": "^2.3.33",
    "@babel/core": "^7.17.9",
    "@babel/preset-env": "^7.16.11",
    "@babel/preset-typescript": "^7.16.7",
    "@rollup/plugin-alias": "^3.1.9",
    "@rollup/plugin-babel": "^5.3.1",
    "@rollup/plugin-commonjs": "^21.0.3",
    "@rollup/plugin-json": "^4.1.0",
    "@rollup/plugin-multi-entry": "^4.1.0",
    "@rollup/plugin-node-resolve": "^13.1.3",
    "@rollup/plugin-typescript": "^8.3.1",
    "@types/jest": "^27.4.1",
    "@types/jest-when": "^3.5.2",
    "@types/node": "^17.0.30",
    "@typescript-eslint/eslint-plugin": "^5.35.1",
    "@typescript-eslint/parser": "^5.18.0",
    "aws-sdk-mock": "^5.7.0",
    "babel-jest": "^28.0.3",
    "babel-plugin-module-resolver": "^4.1.0",
    "base64-js": "^1.5.1",
    "eslint": "^8.12.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-import-resolver-alias": "^1.1.2",
    "eslint-plugin-jsdoc": "^39.2.9",
    "eslint-plugin-react": "^7.29.4",
    "eslint-plugin-sort-exports": "^0.6.0",
    "esm": "^3.2.25",
    "fetch-mock": "^9.11.0",
    "isomorphic-unfetch": "^3.1.0",
    "jest": "^27.5.1",
    "jest-junit": "^13.2.0",
    "jest-when": "^3.5.1",
    "jsdoc": "^3.6.10",
    "mustache": "^4.2.0",
    "nodemon": "^2.0.16",
    "prettier": "^2.7.1",
    "rimraf": "^3.0.2",
    "rollup": "^2.70.1",
    "rollup-plugin-copy": "^3.4.0",
    "rollup-plugin-dts": "^4.2.2",
    "rollup-plugin-flat-dts": "^1.7.0",
    "rollup-plugin-sourcemaps": "^0.6.3",
    "rollup-plugin-terser": "^7.0.2",
    "rollup-plugin-ts": "^3.0.2",
    "ts-node": "^10.7.0",
    "tsconfig-paths": "^3.14.1",
    "tslib": "^2.3.1",
    "typedoc": "^0.22.15",
    "typedoc-theme-hierarchy": "^1.1.1",
    "typescript": "^4.6.3",
    "uuid": "^8.3.2"
  }
}

Logs from Sentry enter image description here

enter image description here


Solution

  • This change was introduced in this PR to explicitly handle internal race conditions between overlapping clears/stops and starts/queries/mutations. Previously, DataStore consumers would see random, erratic transaction/locking/corruption errors that were uninstructive, difficult to recover from, or silent. This change raises the conflicts so consuming code can be aware of and handle those conflicts.

    With that said, you just need to ensure DataStore.clear() resolves first, after making sure the query you're trying to run should be on the post-clear() side of that boundary! This could be as simple as a retry loop or a flag — or even using the clear() promise as your "flag".

    Initialize your flag:

    let onReady = Promise.resolve();
    let isReady = true;
    

    When you need to clear:

    let onReady = DataStore.clear();
    isReady = false;
    onReady.then(() => isReady = true);
    

    Then, for operations that should operate once the clear is complete:

    async getResults() {
      await onReady;
      return DataStore.query(...);
    }
    

    And for those that should be canceled (no longer make sense) if they interrupt a clear, something like:

    async getResults() {
      if (isReady) {
        return DataStore.query(...);  
      } else {
        throw new Error(
          "Sorry. We're still clearing data. Try again shortly."
        );
      }
    }
    

    I've provided a little more detail in the cross-post of this question on GitHub.