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

Android AsyncStorage: "Error restoring state" for large datasets #284

Closed
jamesisaac opened this issue Feb 17, 2017 · 15 comments
Closed

Android AsyncStorage: "Error restoring state" for large datasets #284

jamesisaac opened this issue Feb 17, 2017 · 15 comments

Comments

@jamesisaac
Copy link

This library has been working perfectly for me with smaller size state trees, but trying to use it on bigger ones I'm running into these errors when relaunching the app:

10:39:16 redux-persist/getStoredState: Error restoring data for key: entities {}

10:39:18 Possible Unhandled Promise Rejection (id: 0):
Couldn't read row 0, col 0 from CursorWindow.  Make sure the Cursor is initialized correctly before accessing data from it.

Specifically, I'm trying to store a lot of data from a server locally. To give an idea of the size, running JSON.stringify(payload).length gives 2368916, so it looks like it should be within the 6MB limit.

I'm not having any of the performance issues described in #185 either - the app runs fairly smoothly (perhaps because writes are used sparingly). It's just a case of closing the app and reopening it leading to this error when it tries to rehydrate.

@rt2zz
Copy link
Owner

rt2zz commented Feb 22, 2017

There is some additional discussion around android limits here: #199 (comment)

I do not have any solves off the top of my head, but I am open to ways we can be more resilient to storage limits.

@jamesisaac
Copy link
Author

jamesisaac commented Feb 22, 2017

Hmm, I thought it might not be due to the size limit, as from my (limited) understanding, if JSON.stringify(payload).length gives 2368916, then the size being stored is about 2.4MB (1 byte per char?), so under Android's limit of 6MB?

But if this sounds exactly like the storage limit problem, no worries, I will look into some of the solutions specific to that limitation.

@jamesisaac
Copy link
Author

Ahhh, I've just realised that it might be to do with the development platform I'm using (Exponent) running multiple apps from the same host app. So I guess that 6MB storage limit might be shared among all of their saved data. Will investigate further.

@rt2zz
Copy link
Owner

rt2zz commented Feb 22, 2017

@jamesisaac interesting, that could definitely be related. Do you have control over expanding the limit? Or possibly using other native modules like https://github.com/sriraman/react-native-shared-preferences

@jorge627
Copy link

Hi
I have exactly the same problem, I'm trying to store a lot of data from a server locally in device.

redux-persist/getStoredState: Error restoring data for key: pos Error: Couldn't read row 0, col 0 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.

Couldn't read row 0, col 0 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.
Error: Couldn't read row 0, col 0 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.

I've tried things like this in MainApplication.java - onCreate method:
long size = 50L * 1024L * 1024L; // 50 MB
com.facebook.react.modules.storage.ReactDatabaseSupplier.getInstance(getApplicationContext()).setMaximumSize(size)

but it doesn't work for me

@jamesisaac could you solve it ??

thanks in advance

@kennethpdev
Copy link

I have the same problem like @jorge627 anyone?

@johnwayner
Copy link

I'm having a similar issue. I suspect we are hitting the max sqlite cursor window size on Android which is 2MB: http://stackoverflow.com/questions/21432556/android-java-lang-illegalstateexception-couldnt-read-row-0-col-0-from-cursorw

@kennethpdev
Copy link

@johnwayner so no way around this rather than reducing the size/data of a key or the reducer?

@johnwayner
Copy link

@kenma9123 That's the assumption I'm working under. I have a data dump from a user that clearly demonstrates that the limit ( at least on their device) is 2MB.

I'll be adding code to delete old data. If that doesn't keep key data under the limit, then I'll be splitting up my larger keys. It will result in more cross talk between reducers, which is unfortunate.

@robwalkerco
Copy link
Contributor

robwalkerco commented Apr 5, 2017

@kenma9123 I've worked around this issue by creating my own storage for Android that uses the filesystem rather that AsyncStorage.

Hopefully that will help others with this same issue.
Perhaps there would be a way to integrate the storage implementation back into redux-persist so as to provide a more robust React Native Android storage solution?

Expand to see the code
/**
* @flow
*/

import RNFetchBlob from 'react-native-fetch-blob'

const DocumentDir = RNFetchBlob.fs.dirs.DocumentDir
const storagePath = `${DocumentDir}/persistStore`
const encoding = 'utf8'

const toFileName = (name: string) => name.split(':').join('-')
const fromFileName = (name: string) => name.split('-').join(':')

const pathForKey = (key: string) => `${storagePath}/${toFileName(key)}`

const AndroidFileStorage = {
  setItem: (
    key: string,
    value: string,
    callback?: ?(error: ?Error) => void,
  ) =>
    new Promise((resolve, reject) =>
      RNFetchBlob.fs.writeFile(pathForKey(key), value, encoding)
        .then(() => {
          if (callback) {
            callback()
          }
          resolve()
        })
        .catch(error => {
          if (callback) {
            callback(error && error)
          }
          reject(error)
        })
  ),
  getItem: (
    key: string,
    callback?: ?(error: ?Error, result: ?string) => void
  ) =>
    new Promise((resolve, reject) =>
      RNFetchBlob.fs.readFile(pathForKey(toFileName(key)), encoding)
        .then(data => {
          if (callback) {
            callback(null, data)
          }
          resolve(data)
        })
        .catch(error => {
          if (callback) {
            callback(error)
          }
          reject(error)
        })
  ),
  removeItem: (
    key: string,
    callback?: ?(error: ?Error) => void,
  ) =>
    new Promise((resolve, reject) =>
      RNFetchBlob.fs.unlink(pathForKey(toFileName(key)))
        .then(() => {
          if (callback) {
            callback()
          }
          resolve()
        })
        .catch(error => {
          if (callback) {
            callback(error)
          }
          reject(error)
        })
  ),
  getAllKeys: (
    callback?: ?(error: ?Error, keys: ?Array<string>) => void,
  ) =>
    new Promise((resolve, reject) =>
      RNFetchBlob.fs.exists(storagePath)
      .then(exists =>
        exists ? Promise.resolve() : RNFetchBlob.fs.mkdir(storagePath)
      )
      .then(() =>
        RNFetchBlob.fs.ls(storagePath)
          .then(files => files.map(file => fromFileName(file)))
          .then(files => {
            if (callback) {
              callback(null, files)
            }
            resolve(files)
          })
      )
      .catch(error => {
        if (callback) {
          callback(error)
        }
        reject(error)
      })
  ),
}

export default AndroidFileStorage

@kennethpdev
Copy link

@robwalkerco wow great idea. I tried it and it works fine. You should create a repo for this and let it grow.

@robwalkerco
Copy link
Contributor

@kenma9123 I've created a project on npm with a cleaned up version of the above code. Check out https://www.npmjs.com/package/redux-persist-filesystem-storage

@kelset
Copy link

kelset commented Sep 11, 2017

I just had the same issue and @robwalkerco 's lib fixed it - thanks a lot man!

(btw I think this issue can be closed..?)

@rt2zz rt2zz closed this as completed Sep 21, 2017
maxkomarychev added a commit to maxkomarychev/redux-persist-filesystem-storage that referenced this issue Jan 26, 2018
maxkomarychev added a commit to maxkomarychev/redux-persist-filesystem-storage that referenced this issue Jan 26, 2018
maxkomarychev added a commit to maxkomarychev/redux-persist-filesystem-storage that referenced this issue Jan 26, 2018
Provide sample snippet which lets app migrate data previously stored in
`AsyncStorage` to `redux-persist-filesystem-storage`

Original issues:
rt2zz/redux-persist#199
rt2zz/redux-persist#284

Converastions related to provided snippet:
rt2zz/redux-persist#679
robwalkerco#7
maxkomarychev added a commit to maxkomarychev/redux-persist-filesystem-storage that referenced this issue Jan 26, 2018
Provide sample snippet which lets app migrate data previously stored in
`AsyncStorage` to `redux-persist-filesystem-storage`

Original issues:
rt2zz/redux-persist#199
rt2zz/redux-persist#284

Converastions related to provided snippet:
rt2zz/redux-persist#679
robwalkerco#7
gnprice pushed a commit to zulip/zulip-mobile that referenced this issue Jul 27, 2018
The motivation that makes this important to do now is the storage
limits imposed by RN's AsyncStorage on Android.

In particular, on Android the open-source RN uses a backend called
AsyncSQLiteDBStorage (it prefers one called AsyncRocksDBStorage if
present, but the implementation of that one seems to be internal to
Facebook).  In that backend:

 * A limit of 6 MiB is imposed on the size of the entire database.
   This is a choice made inside RN; see `mMaximumDatabaseSize` in
   upstream's ReactDatabaseSupplier.java .

 * The system's SQLite library retrieves data in windows of, by
   default, 2 MiB, and rows larger than that will cause an error.
   The backend stores each key-value pair as a row, and redux-persist
   gives it each top-level subtree of the state into a key-value pair;
   so when one top-level subtree goes over 2 MB or so, it fails,
   discarding the whole persisted store:
     rt2zz/redux-persist#284

   The `users` state subtree for chat.zulip.org now comes to a little
   over 1.8M characters, so we must be approaching this limit.  (This
   also means it's getting encoded as bytes reasonably efficiently,
   presumably as UTF-8.)

   Looking at Android docs, it looks like a sufficiently determined
   Android app could use `SQLiteCursor#setWindow` to change this size;
   but the code that'd have to do that is deep inside RN, so even
   doing that as a quick hack would take some work.

On iOS, by contrast, AsyncStorage uses a simple custom key-value store
with large values stored out of line (see RCTAsyncLocalStorage.m .)
That one doesn't seem to have any such limitations.

A library "redux-persist-transform-compress" is intended to solve this
problem; but it implements the compression algorithm in JS, which
makes it slow and adds serious lag to the app.  So we don't use that.

Instead, implement a native module to do the compression in Java, and
provide a wrapper for RN's AsyncStorage to use it.  Note that we
can't do this with redux-persist's "transformer" concept, because
transformers don't support async operations.  Loading and compressing
state can take enough time to cause frame drops, so we need it to
happen async.

[greg: expanded the explanation / research about storage limits;
 also deactivated the actual invocation of CompressedAsyncStorage,
 pending the needed fallback logic.]
@rahulbhankar786
Copy link

@robwalkerco Thanks you saved my day.

@shehzadosama
Copy link

For me, this solution worked.
Add this piece of code at the end of onCreate() in MainApplication.java

try {
  Field field = CursorWindow.class.getDeclaredField("sCursorWindowSize");
  field.setAccessible(true);
  field.set(null, 100 * 1024 * 1024); //100MB
  } catch (Exception e) {
 if (BuildConfig.DEBUG) {
  e.printStackTrace();
  }
  }

Also import these on top of the MainApplication.java

import android.database.CursorWindow;
import java.lang.reflect.Field;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants