-
-
Notifications
You must be signed in to change notification settings - Fork 663
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 persistent data in a sound way #4841
Comments
Some in-progress work to mitigate this issue is in #4694 -- that will arrange things so that when several subtrees get updated, we write out the updates in quick succession, in hopes that that reduces how often we end up with only some of the updates written out so that the data is inconsistent. I don't have a definite plan for how to properly fix the issue, so the next steps there are some more investigation and discussion. In general, the standard way to handle this kind of problem is with a database -- this is why they were invented. In the context of a mobile app, the database software I'd almost certainly want to use is SQLite: it's highly reliable, highly portable, and has good efficiency. I don't know what the state is of convenient bindings for using SQLite from RN, or for that matter from Swift. It may also be possible to fix this soundness issue with a smaller change on top of our existing system. One idea mentioned in previous discussion is to keep storing new versions of whole state subtrees, but with some kind of metadata to identify a tuple of those as a complete actual snapshot of the tree. This basically means we'd be writing our own very simple database system. That sounds like famous last words, but it might work for our case: we currently tolerate some performance characteristics that general-purpose databases can't accept and that drive a lot of their complexity. In particular, rewriting the entire |
A couple of related issues are #3408 and #4446. Those are at a different layer -- they're both about properly maintaining the state at the app level, in our reducers. But they have the same property that we currently get to paper over a lot of small bugs there with the fact that we're constantly refreshing the state from scratch, so anything we get wrong in updating the state just gets wiped away before very long. In a future where we're maintaining the state incrementally for longer periods, the results of those bugs will accumulate, so that even when the bugs are all in situations that happen at low frequency, a user is likely to have several glitches scattered around their state. So those issues will also become more important to fix as we implement any of the features mentioned above. |
This isn't itself server data -- it's what we use for acquiring server data -- which is why we didn't cover it when expanding these checks in 916dc4b. But like the server data, it's something we assume is present when rendering the main UI of the app. And because of the same issue zulip#4841, the same lack of consistency in our data-storage model with redux-persist that caused zulip#4587 and made that fix necessary, it's possible for it to be lacking data when the existing checks in this function all pass. So, add a check for this too. Build on the comments added in the previous commit to reason through why this won't get us stuck at the loading screen; and add a comment at the otherwise inessential-looking bit of code that provides one of the assumptions we rely on for that reasoning. Also correct a related point in the comment at the end of the function.
This isn't itself server data -- it's what we use for acquiring server data -- which is why we didn't cover it when expanding these checks in 916dc4b. But like the server data, it's something we assume is present when rendering the main UI of the app. And because of the same issue zulip#4841, the same lack of consistency in our data-storage model with redux-persist that caused zulip#4587 and made that fix necessary, it's possible for it to be lacking data when the existing checks in this function all pass. So, add a check for this too. Build on the comments added in the previous commit to reason through why this won't get us stuck at the loading screen; and add a comment at the otherwise inessential-looking bit of code that provides one of the assumptions we rely on for that reasoning. Also correct a related point in the comment at the end of the function.
This isn't itself server data -- it's what we use for acquiring server data -- which is why we didn't cover it when expanding these checks in 916dc4b. But like the server data, it's something we assume is present when rendering the main UI of the app. And because of the same issue zulip#4841, the same lack of consistency in our data-storage model with redux-persist that caused zulip#4587 and made that fix necessary, it's possible for it to be lacking data when the existing checks in this function all pass. So, add a check for this too. Build on the comments added in the previous commit to reason through why this won't get us stuck at the loading screen; and add a comment at the otherwise inessential-looking bit of code that provides one of the assumptions we rely on for that reasoning. Also correct a related point in the comment at the end of the function.
We talked today in the office about this issue, and I think we have a plan for it.
|
One thing that should help mitigate zulip#4841 is to narrow the window during which we're propagating a Redux state change to persistent storage. This does quite a bit of that, by not having the `setItem` calls separated by (a) the CPU time to do the serialization, or (b) yields, as Greg points out [1]. [1] zulip#4694 (comment)
As in the previous commit, the hope here is to mitigate zulip#4841. It doesn't yet solve zulip#4841, though. We can't count on the sets being done transactionally. Examining `AsyncStorage`'s implementation, we see that the JavaScript connects to native modules the package provides, and calls `RNC_AsyncSQLiteDBStorage` and `RNCAsyncStorage` [1]. These are for Android and iOS, respectively. The native implementations have these comments for `multiSet`: Android (from the `ReactContextBaseJavaModule` with name "RNC_AsyncSQLiteDBStorage" [2]): ``` /** * Inserts multiple (key, value) pairs. If one or more of the pairs cannot be inserted, this will * return AsyncLocalStorageFailure, but all other pairs will have been inserted. * The insertion will replace conflicting (key, value) pairs. */ ``` iOS (from RNCAsyncStorage.h [3]): ``` // Add multiple key value pairs to the cache. ``` [1] https://github.com/react-native-async-storage/async-storage/blob/v1.6.3/lib/AsyncStorage.js#L17-L18 [2] https://github.com/react-native-async-storage/async-storage/blob/v1.6.3/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java#L193-L197 [3] https://github.com/react-native-async-storage/async-storage/blob/v1.6.3/ios/RNCAsyncStorage.h#L40
One thing that should help mitigate zulip#4841 is to narrow the window during which we're propagating a Redux state change to persistent storage. This does quite a bit of that, by not having the `setItem` calls separated by (a) the CPU time to do the serialization, or (b) yields, as Greg points out [1]. [1] zulip#4694 (comment)
As in the previous commit, the hope here is to mitigate zulip#4841. It doesn't yet solve zulip#4841, though. We can't count on the sets being done transactionally. Examining `AsyncStorage`'s implementation, we see that the JavaScript connects to native modules the package provides, and calls `RNC_AsyncSQLiteDBStorage` and `RNCAsyncStorage` [1]. These are for Android and iOS, respectively. The native implementations have these comments for `multiSet`: Android (from the `ReactContextBaseJavaModule` with name "RNC_AsyncSQLiteDBStorage" [2]): ``` /** * Inserts multiple (key, value) pairs. If one or more of the pairs cannot be inserted, this will * return AsyncLocalStorageFailure, but all other pairs will have been inserted. * The insertion will replace conflicting (key, value) pairs. */ ``` iOS (from RNCAsyncStorage.h [3]): ``` // Add multiple key value pairs to the cache. ``` [1] https://github.com/react-native-async-storage/async-storage/blob/v1.6.3/lib/AsyncStorage.js#L17-L18 [2] https://github.com/react-native-async-storage/async-storage/blob/v1.6.3/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java#L193-L197 [3] https://github.com/react-native-async-storage/async-storage/blob/v1.6.3/ios/RNCAsyncStorage.h#L40
OK, and #4694 is merged! There's that one open followup #4694 (comment) , but I think it's orthogonal to the main remaining thing we need to do:
So the next step, replacing the use of |
One test case changes: `setItem` now returns void instead of null. I'm not sure if the upstream AsyncStorage really returns null here, or if it too returns void and just the mock is off. Anyway void is clearly the natural thing here -- it's basically a function that returns no information -- so keep returning that ourselves, and update the test. Real callers should never care. Fixes: zulip#4841
The way we currently store the user's data on the device's storage, between runs of the app, is with the design from redux-persist:
state.accounts
is one,state.users
is another, and so on.state.users
, and so on.As a result, it's possible for the stored data to get into a state that was never the state inside the app, and that's internally inconsistent. This was the cause of #4587, for example.
For the most part, this doesn't cause user-visible problems today -- the crashes in #4587 were an exception to that pattern.
But the reason it doesn't is that we currently don't really rely that heavily on the stored data: each time the app starts up, we go and do an initial fetch of the server data from scratch. That means that for all the data that ultimately comes from the server -- which includes most of our data and most of the complexity in our data -- any inconsistencies that creep into the stored data will typically get wiped away almost immediately after we first even read it from storage. (The way that issue #4587 managed to evade that protection is that it caused a crash before we got far enough to do that initial fetch.) So we're largely protected from consequences of this issue… but the price is that we're not really getting much use out of storing that data.
There are several things we'd like to do that would mean relying more firmly on the stored data, and not constantly refreshing it from scratch:
The status quo means that when you launch the app, the data you're looking at is stale and there's a long lag before you see up-to-date data. This is an especially bad experience when you got there by opening a notification -- there's a message you know you got, it was right there in the notification, but we don't initially know about it. We have the basic design for how to fix this, as Support downgrading to a long-lived event queue #3916; but it means maintaining your data in device storage and relying on that, rather than refreshing from scratch.
We'd like to improve the experience of switching between several accounts (in several orgs): Add quick organization switcher #1321. Doing a really good job of that would require maintaining all the same data about all your accounts as we currently do about the one active account; and fundamentally, doing much at all in that direction will require maintaining at least some data about all your accounts. In principle I guess we could maintain all that data while keeping the reload-from-scratch-on-each-launch strategy; but it'd mean even more data getting reloaded from scratch, so even more lag.
We'd like to offer fuller-featured, higher-quality notifications: with an inline reply action (Allow message responses from within Android notification bar #2512), with end-to-end encryption from the Zulip server (Encrypt push notifications end-to-end from Zulip server #1190), pre-fetching the conversation's messages so it's ready to go if you open the notification (Eagerly fetch relevant conversation on getting notification #3599), among other things.
All of these require more integration between the platform-native code (in Kotlin for Android, and in the future in Swift for iOS) and JS than we currently have: either for the platform-native code to see more of the information that the JS code has, or to send more information to JS, or both. That would be a lot easier if we have the data in a reasonable schema that the platform-native code can consume directly. Even if we don't do that -- even if we do all the integration by having the platform-native code call into JS -- it means continuously using the stored data, beyond the moments after each launch.
If we were to pursue any of those things without giving attention to this issue, it's likely we would encounter a lot more bugs like #4587. That kind of bug can be quite bad for a user, and they're pretty unpleasant to debug too, because fundamentally they involve our data being in a state that doesn't make any sense.
As a result, we need to do some combination of fixing this issue properly; mitigating it to reduce its impact; and carefully working around it in the context of any of the above features that we build before we've fixed it.
The text was updated successfully, but these errors were encountered: