aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.44k stars 2.13k forks source link

AWS Amplify DataStore does not update properly #12916

Open koreahn opened 10 months ago

koreahn commented 10 months ago

Before opening, please confirm:

JavaScript Framework

React Native

Amplify APIs

DataStore

Amplify Version

v5

Amplify Categories

Not applicable

Backend

Amplify CLI

Environment information

``` # Put output below this line System: OS: macOS 14.2.1 CPU: (8) arm64 Apple M1 Memory: 68.44 MB / 16.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 18.15.0 - /usr/local/bin/node npm: 9.8.1 - /usr/local/bin/npm Watchman: 2023.12.04.00 - /opt/homebrew/bin/watchman Browsers: Chrome: 121.0.6167.85 Safari: 17.2.1 npmPackages: @aws-amplify/rtn-push-notification: ^1.2.3 => 1.2.9 (1.1.9) @aws-amplify/ui-react-native: ^1.2.23 => 1.2.29 @azure/core-asynciterator-polyfill: ^1.0.2 => 1.0.2 @babel/core: ^7.20.0 => 7.23.6 @babel/preset-env: ^7.20.0 => 7.23.6 @babel/runtime: ^7.20.0 => 7.23.6 @lunalee/react-native-raw-bottom-sheet: ^2.2.4 => 2.2.4 @react-native-async-storage/async-storage: ^1.19.1 => 1.21.0 @react-native-community/netinfo: ^9.4.1 => 9.5.0 @react-native-firebase/analytics: ^18.7.3 => 18.7.3 @react-native-firebase/app: ^18.7.3 => 18.7.3 @react-native-firebase/crashlytics: ^18.7.3 => 18.7.3 @react-native/eslint-config: ^0.72.2 => 0.72.2 @react-native/metro-config: ^0.72.9 => 0.72.11 @react-navigation/material-bottom-tabs: ^6.2.16 => 6.2.19 @react-navigation/native: ^6.1.7 => 6.1.9 @react-navigation/native-stack: ^6.9.13 => 6.9.17 @tsconfig/react-native: ^3.0.0 => 3.0.2 @types/react: ^18.0.24 => 18.2.45 @types/react-test-renderer: ^18.0.0 => 18.0.7 HelloWorld: 0.0.1 amazon-cognito-identity-js: ^6.3.1 => 6.3.8 amazon-cognito-identity-js/internals: undefined () aws-amplify: ^5.3.6 => 5.3.13 aws-sdk: ^2.1450.0 => 2.1525.0 babel-jest: ^29.2.1 => 29.7.0 eslint: ^8.19.0 => 8.56.0 exampleapp: 0.0.1 jest: ^29.2.1 => 29.7.0 metro-react-native-babel-preset: 0.76.7 => 0.76.7 (0.76.8) prettier: ^2.4.1 => 2.8.8 react: 18.2.0 => 18.2.0 react-hook-form: ^7.45.4 => 7.49.2 react-native: 0.72.3 => 0.72.3 react-native-chart-kit: ^6.12.0 => 6.12.0 react-native-code-push: ^8.1.0 => 8.1.1 react-native-config: ^1.5.1 => 1.5.1 react-native-date-picker: ^4.3.3 => 4.3.5 react-native-geocoding: ^0.5.0 => 0.5.0 react-native-geolocation-service: ^5.3.1 => 5.3.1 react-native-get-random-values: ^1.9.0 => 1.10.0 react-native-image-picker: ^7.1.0 => 7.1.0 react-native-maps: ^1.7.1 => 1.8.4 react-native-maps-directions: ^1.9.0 => 1.9.0 react-native-modal: ^13.0.1 => 13.0.1 react-native-safe-area-context: ^4.7.1 => 4.8.2 react-native-screens: ^3.24.0 => 3.29.0 react-native-sectioned-multi-select: ^0.10.0 => 0.10.0 react-native-select-dropdown: ^3.4.0 => 3.4.0 react-native-splash-screen: ^3.3.0 => 3.3.0 react-native-svg: ^13.13.0 => 13.14.0 react-native-upi-payment: ^1.0.5 => 1.0.5 react-native-url-polyfill: ^2.0.0 => 2.0.0 (1.3.0) react-native-vector-icons: ^10.0.0 => 10.0.3 react-test-renderer: 18.2.0 => 18.2.0 stream: ^0.0.2 => 0.0.2 typescript: 4.8.4 => 4.8.4 npmGlobalPackages: @aws-amplify/cli: 12.8.2 appcenter-cli: 2.14.0 corepack: 0.15.3 create-expo-app: 2.0.3 create-react-native-app: 3.8.0 eas-cli: 3.12.1 expo-cli: 6.3.10 npm: 9.8.1 ```

Describe the bug

I want to save the user token to DB(DynamoDB), so I checked and saved in App.js. During testing, I noticed that the token often doesn't update. So I tested the token alternately to two different simulators.

Belows are my code.

  useEffect(() => {
    const updateDeviceToken = async () => {
      console.log('Entering updateDeviceToken');
      if (deviceToken) {
        console.log('deviceToken is present');
        if (dbUser && deviceToken) {
          if (dbUser && dbUser.token !== deviceToken) {
            console.log('auth', deviceToken);
            console.log('dbUser.token', dbUser.token);
            console.log('dbUser.id', dbUser.id);

            try {
              const newUser = await DataStore.query(User, user =>
                user.id.eq(dbUser.id),
              );
              console.log('user1', newUser[0]);

              const dbuser = await DataStore.save(
                User.copyOf(newUser[0], updated => {
                  updated.token = deviceToken;
                }),
              );
              console.log('user2', dbuser);
              setDbUser(dbuser);
            } catch (err) {
              console.err('update token error', err);
            }
          }
        } else {
          console.log('dbUser.token is the same as deviceToken');
        }
      } else {
        console.log('deviceToken is not present');
      }
    }

  updateDeviceToken();
}

Belows are logs.

LOG Entering updateDeviceToken LOG deviceToken is present LOG auth tokenaaaaa <<<<<<<<<< phisical device token. LOG dbUser.token tokenbbbbb <<<<<<<<<< saved token in DynamoDB earlier LOG dbUser.id 73e114d0-6c73-40d0-9b18-c015ae9edd8d LOG user1 {....."token": "tokenbbbbb",.....} LOG user2 {....."token": "tokenaaaaa",.....}

On the log, the update appears to be normal. But when I check the actual data on the amplify console, it doesn't update and still shows 'tokenbbbbb'.

And sometimes there is also a problem with the 'User' table query. The 'User' table has not updated token to tokenbbbbb, but when I run the program again and query the 'User' table, it is inquired to tokenbbbbbbbb. (Actual DB shows tokenaaaa in Amplify console).

Several tables have been created and are being used, but this problem only occurs in the 'User' table.

Expected behavior

The token of the physical device shall be updated to DynamoDB

Reproduction steps

  1. Install the react-native app on two physical devices
  2. Log in to the first device and update the token in the User Table
  3. Log in as the second device and update the token in the User table again

Code Snippet

// Put your code below this line.
  useEffect(() => {
    const updateDeviceToken = async () => {
      console.log('Entering updateDeviceToken');
      if (deviceToken) {
        console.log('deviceToken is present');
        if (dbUser && deviceToken) {
          if (dbUser && dbUser.token !== deviceToken) {
            console.log('auth', deviceToken);
            console.log('dbUser.token', dbUser.token);
            console.log('dbUser.id', dbUser.id);

            try {
              const newUser = await DataStore.query(User, user =>
                user.id.eq(dbUser.id),
              );
              console.log('user1', newUser[0]);

              const dbuser = await DataStore.save(
                User.copyOf(newUser[0], updated => {
                  updated.token = deviceToken;
                }),
              );
              console.log('user2', dbuser);
              setDbUser(dbuser);
            } catch (err) {
              console.err('update token error', err);
            }
          }
        } else {
          console.log('dbUser.token is the same as deviceToken');
        }
      } else {
        console.log('deviceToken is not present');
      }
    }

  updateDeviceToken();
}

Log output

``` // Put your logs below this line LOG Entering updateDeviceToken LOG deviceToken is present LOG auth tokenaaaaa <<<<<<<<<< phisical device token. LOG dbUser.token tokenbbbbb <<<<<<<<<< saved token in DynamoDB earlier LOG dbUser.id 73e114d0-6c73-40d0-9b18-c015ae9edd8d LOG user1 {....."token": "tokenbbbbb",.....} LOG user2 {....."token": "tokenaaaaa",.....} ```

aws-exports.js

No response

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

cwomack commented 9 months ago

Hello, @koreahn and sorry to hear you're experiencing this. Have a couple of questions and requests to see if we can help unblock you.

It looks like you're using v5.X of Amplify, so are you able to add logging to your app via the Console Logger? This will allow us to turn the logging level to debug and get a little more information about what's happening with the network requests when attempting to make that update. Looking to see if there's an error from the AppSync side that would give more details about why it's having trouble resolving on the backend. Using the Hub events emitted for DataStore may help as well to isolate this.

Outside of this, can you clarify if you are using auto-merge or OCC for conflict resolution? Thanks!

koreahn commented 9 months ago

Hello, @koreahn and sorry to hear you're experiencing this. Have a couple of questions and requests to see if we can help unblock you.

It looks like you're using v5.X of Amplify, so are you able to add logging to your app via the Console Logger? This will allow us to turn the logging level to debug and get a little more information about what's happening with the network requests when attempting to make that update. Looking to see if there's an error from the AppSync side that would give more details about why it's having trouble resolving on the backend. Using the Hub events emitted for DataStore may help as well to isolate this.

Outside of this, can you clarify if you are using auto-merge or OCC for conflict resolution? Thanks!

Hello. Thank you for your reply. I added logging like below.

import {Logger} from 'aws-amplify';
import {Hub} from 'aws-amplify';
...
const logger = new Logger('AuthContext');
...
...
const listener = Hub.listen('datastore', async hubData => {
    const {event, data} = hubData.payload;
    if (event === 'networkStatus') {
      console.log(`User has a network connection: ${data.active}`);
    }
  });
...
...
const updateDeviceToken = async () => {
      console.log('Entering updateDeviceToken');
      logger.debug('Entering updateDeviceToken');
      if (deviceToken) {
        console.log('deviceToken is present');
        logger.debug('deviceToken is present');
        if (dbUser && deviceToken) {
          if (dbUser && dbUser.token !== deviceToken) {
            console.log('auth', deviceToken);
            logger.debug('auth', deviceToken);
            console.log('dbUser.token', dbUser.token);
            logger.debug('dbUser.token', dbUser.token);
            console.log('dbUser.id', dbUser.id);
            logger.debug('dbUser.id', dbUser.id);

            try {
              const newUser = await DataStore.query(User, user =>
                user.id.eq(dbUser.id),
              );
              console.log('user1', newUser[0]);
              logger.debug('user1', newUser[0]);

              const dbuser = await DataStore.save(
                User.copyOf(newUser[0], updated => {
                  updated.token = deviceToken;
                }),
              );
              console.log('user2', dbuser);
              logger.debug('user2', dbuser);
              setDbUser(dbuser);
            } catch (err) {
              console.err('update token error', err);
              logger.error('update token error', err);
            }
          }
        } else {
          console.log('dbUser.token is the same as deviceToken');
          logger.debug('dbUser.token is the same as deviceToken');
        }
      } else {
        console.log('deviceToken is not present');
        logger.debug('deviceToken is not present');
      }
    };

Belows are the logs. It's same as before. And the log of the catch clause was not printed.

LOG  User has a network connection: true
LOG  User has a network connection: true
LOG  Entering updateDeviceToken
LOG  deviceToken is present
LOG auth tokenaaaaa <<<<<<<<<< phisical device token.
LOG dbUser.token tokenbbbbb <<<<<<<<<< saved token in DynamoDB earlier
LOG dbUser.id 73e114d0-6c73-40d0-9b18-c015ae9edd8d
LOG user1 {....."token": "tokenbbbbb",.....}
LOG user2 {....."token": "tokenaaaaa",.....}

I am very new to using Amplify. Did I apply the logs properly? And I don't know what auto-merge and OCC are. Can you tell me how to check them?

Let me know if you need anything else. Thank you.

chrisbonifacio commented 9 months ago

Hi @koreahn, Auto Merge and OCC (Optimistic Concurrency) are conflict resolution strategies you can use with DataStore enabled.

For more info on how they work and differ in behavior please refer to the AppSync documentation:

https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html#conflict-detection-and-resolution

I think what @cwomack was referring to is adding this to your app where you are configuring Amplify.

Amplify.Logger.LOG_LEVEL = 'DEBUG'

This will log all of the Amplify events for all categories

koreahn commented 9 months ago

Thank you @chrisbonifacio.

@cwomack I am not using auto-merge or OCC. Is this the cause of my problem?

koreahn commented 9 months ago

Hi @koreahn, Auto Merge and OCC (Optimistic Concurrency) are conflict resolution strategies you can use with DataStore enabled.

For more info on how they work and differ in behavior please refer to the AppSync documentation:

https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html#conflict-detection-and-resolution

I think what @cwomack was referring to is adding this to your app where you are configuring Amplify.

Amplify.Logger.LOG_LEVEL = 'DEBUG'

This will log all of the Amplify events for all categories

Can you help me? What should I do?

chrisbonifacio commented 9 months ago

@koreahn what conflict resolution strategy are you using? Custom Lambda?

nadetastic commented 9 months ago

Hi @koreahn following up here - have you had a chance to see the comment from @chrisbonifacio above?

koreahn commented 9 months ago

Hello @chrisbonifacio, @nadetastic.

I am very new to Amplify, so I don't know what the conflict resolution strategy is. I am solely using the DataStore object, nothing else.

How can I check it?

cwomack commented 8 months ago

Thanks for the replies on this, @koreahn. It sounds like you could be using AutoMerge as the default conflict resolution strategy if you haven't made any changes yourself.

Could you share your schema and possibly the code that's running queries of the object for the second device? Also, are you using the SQLite adapter? Any further reproduction steps or clarity on differences for each device (i.e. code they are running, if they are being opened at same time, etc.) would be helpful as well! Thanks.

koreahn commented 8 months ago

Thank you very much @cwomack.

I have not made any changes. below is my scheme of User table.

type EagerUser = {
  readonly [__modelMeta__]: {
    identifier: ManagedIdentifier<User, 'id'>;
    readOnlyFields: 'createdAt' | 'updatedAt';
  };
  readonly id: string;
  readonly sub?: string | null;
  readonly phoneNumber: string;
  readonly userName?: string | null;
  readonly email?: string | null;
  readonly role?: string | null;
  readonly token?: string | null;
  readonly subRole?: SubRole | keyof typeof SubRole | null;
  readonly createdId?: string | null;
  readonly createdName?: string | null;
  readonly updatedId?: string | null;
  readonly updatedName?: string | null;
  readonly createdAt?: string | null;
  readonly updatedAt?: string | null;
}

type LazyUser = {
  readonly [__modelMeta__]: {
    identifier: ManagedIdentifier<User, 'id'>;
    readOnlyFields: 'createdAt' | 'updatedAt';
  };
  readonly id: string;
  readonly sub?: string | null;
  readonly phoneNumber: string;
  readonly userName?: string | null;
  readonly email?: string | null;
  readonly role?: string | null;
  readonly token?: string | null;
  readonly subRole?: SubRole | keyof typeof SubRole | null;
  readonly createdId?: string | null;
  readonly createdName?: string | null;
  readonly updatedId?: string | null;
  readonly updatedName?: string | null;
  readonly createdAt?: string | null;
  readonly updatedAt?: string | null;
}

export declare type User = LazyLoading extends LazyLoadingDisabled ? EagerUser : LazyUser

export declare const User: (new (init: ModelInit<User>) => User) & {
  copyOf(source: User, mutator: (draft: MutableModel<User>) => MutableModel<User> | void): User;
}

And below is query of the object.

await DataStore.query(User, user =>
        user.sub.eq(authUser?.attributes?.sub),
      )
        .then(users => {
          setDbUser(users[0]);
        })
        .catch(e => {
          console.error(e);
          setDbUser(null);
        });

I dont use SQLite adapter.

I have conducted tests using multiple versions of iOS and Android simulators, as well as physical Android devices. However, I have encountered inconsistency in updates, experiencing cases where updates occur on the same device while other times they do not. I haven't been able to identify any specific rules governing this behavior.

I am not sure if I am answering correctly or not. If these are not correct answer, could you provide more specific details about what tasks I can handle please?