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

[Fix] Model store saving to and loading from prefs, and remove duplicates #2099

Merged
merged 3 commits into from
Jun 6, 2024

Conversation

nan-li
Copy link
Contributor

@nan-li nan-li commented May 29, 2024

Description

One Line Summary

Don't overwrite operation model store cache before it as been loaded, and avoid duplicates when loading them.

Details

Bug Fix

  • This is to remedy the Operation Model Store adding duplicate operations when the same ones that were previously added to the store and persisted, are re-read from cache
  • Though the duplicate would not get enqueued to the Operation Repo, the duplicate was still persisted. When the operations are successful, only one of the duplicates will be removed from cache as the Model Store's removal logic utilizes a firstOrNull search for the operation. Then, on the next cold start, the remaining operation will load from cache and become enqueued to the OperationRepo
  • Bug introduced in 5.1.12 with the Operation-loading changes to prevent ANRs
  • See manual testing below for more information

Solution

  • Fix model store to not overwrite cache. Don't persist models before load is called. This is safe because the time gap between any models being added and load being called should be very small.
  • When load is called, the cached models are added to the front. Then the models get persisted altogether again if necessary.
  • Add check for duplicate operations when loading model store

Motivation

Bug fix

Testing

Unit testing

Added test that saves duplicates to Prefs, enqueues a model, then loads from cache.

Manual testing

Android emulator on API 33
Reproduce Bug Before PR

  1. New app install
  2. A login-user and a create-subscription are quickly enqueued and persisted
  3. The Operation Model Store then loads from cache and reads the 2 operations above
  4. They get added to the store
  5. The store now has 4 operations (2 of each)
  6. The operations enqueue to the Operation Repo but it does prevents the duplicates from enqueueing
  7. The Operation Repo flushes the login-user and create-subscription
  8. On success, they are removed from the model store via firstOrNull
  9. The Operation Model Store now has 2 operations left
  10. On the next cold start, the login-user and the create-subscription are read from cache and added to the Operation Repo

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

This change is Reviewable

@nan-li nan-li assigned nan-li and jinliu9508 and unassigned nan-li May 29, 2024
@jinliu9508
Copy link
Contributor

Code looks good and the test works!

Should we consider removing the duplicates since they are most likely unnecessary in the event that a duplicate scenario occurs?

@nan-li
Copy link
Contributor Author

nan-li commented May 29, 2024

Should we consider removing the duplicates since they are most likely unnecessary in the event that a duplicate scenario occurs?

Can you explain what you mean? This PR prevents a duplicate operation from being added. Are you talking about the model store's remove method?

@jinliu9508
Copy link
Contributor

Should we consider removing the duplicates since they are most likely unnecessary in the event that a duplicate scenario occurs?

Can you explain what you mean? This PR prevents a duplicate operation from being added. Are you talking about the model store's remove method?

Sure. If the user already has duplicate operations in the cache from migrating from 5.1.12, the saved operations will not be added to the model store but will remain in the cache. I am thinking we can add an extra step to remove the duplicates if they are detected and ensure a cleaner migration.

@nan-li
Copy link
Contributor Author

nan-li commented May 29, 2024

Sure. If the user already has duplicate operations in the cache from migrating from 5.1.12, the saved operations will not be added to the model store but will remain in the cache. I am thinking we can add an extra step to remove the duplicates if they are detected and ensure a cleaner migration.

I see, I don't think another check or removal from cache is necessary or fixes the issue because of the following reasons:

  • Anytime persist() is called, the cache will be overwritten with the current models. And the current models we now prevent adding duplicates to, so models will not have duplicates anymore.
  • In normal circumstances, you can't tell when an operation is actually a duplicate from the past. From the example above, 2 identical login-user operations were added to the operation model store and cached. The first one should be sent very quickly and removed once the request is successful, leaving one more operation in the store and cache.
  • On the next cold start, that operation will be read from the cache and put in the store. This is actually a duplicate and we would not know.

Copy link
Member

@jkasten2 jkasten2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nan-li Nice catch here! Didn't think about the case where OperationRepo._operationModelStore would still contain the duplicate.

Another question comes to mind now, do we need to keep the duplicate check in internalEnqueue as well? I am thinking yes, since we still need to ensure it doesn't get put into OperationRepo.queue, where it would be processed during the currently runtime.

  • This problem however does point to a duplicate state complexity we have in our system. Something that would be nice to clean up in the future, if possible.

Lastly, could we add a test before merging this in?

@nan-li nan-li force-pushed the fix/duplicate_model_store_cache_loading branch from 938a7cc to b758b34 Compare May 30, 2024 18:11
@jkasten2
Copy link
Member

Thinking about this again the root of the problem is ModelStore.load doesn't do what you would think it does. It should take exactly what is on disk and use that for it state (AKA model list), instead it is appending it which I think is the root of the problem.

@nan-li nan-li force-pushed the fix/duplicate_model_store_cache_loading branch from b758b34 to 691cdd8 Compare May 30, 2024 20:14
@nan-li nan-li changed the title [Fix] Add duplicates check when loading operation models from cache [Fix] When model store loads from cache, do a full replacement, to avoid duplicates May 30, 2024
@nan-li
Copy link
Contributor Author

nan-li commented May 30, 2024

Another question comes to mind now, do we need to keep the duplicate check in internalEnqueue as well?

Yes we still need the check in the Operation Repo. The operations are already enqueued to the Op Repo before load() is called.

Thinking about this again the root of the problem is ModelStore.load doesn't do what you would think it does. It should take exactly what is on disk and use that for it state (AKA model list), instead it is appending it which I think is the root of the problem.

This is a good point, I agree. I checked that the cache should always be updated and correct, so it is safe to do a full replacement. I updated the PR.

@nan-li nan-li requested a review from jkasten2 May 30, 2024 20:24
@nan-li nan-li force-pushed the fix/duplicate_model_store_cache_loading branch from 691cdd8 to 50e7db4 Compare May 30, 2024 20:25
Copy link
Member

@jkasten2 jkasten2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Much simpler logic now!

@nan-li
Copy link
Contributor Author

nan-li commented May 30, 2024

Thinking again about the model store reading from cache, I realized that it will almost always lose old cached operations...

  1. There are some cached operations
  2. If there is a onesignal ID, on next cold start, refresh-user enqueues and persists. This will overwrite the cache.
  3. Then load is called, loading only the refresh-user operation.

@jkasten2
Copy link
Member

I see, so this isn't just a problem with ModelStore.load(). We need to do something with OperationRepo so it doesn't overwrite the local data.

@nan-li nan-li force-pushed the fix/duplicate_model_store_cache_loading branch from 50e7db4 to 5cf77d2 Compare May 31, 2024 17:10
@jkasten2 jkasten2 self-requested a review May 31, 2024 18:15
@nan-li nan-li force-pushed the fix/duplicate_model_store_cache_loading branch 2 times, most recently from fd39e7d to f3c4bdb Compare May 31, 2024 18:34
@nan-li
Copy link
Contributor Author

nan-li commented May 31, 2024

Testing again, I think the refresh-user operation comes after Operation Repo loads operations, so the situations of overwriting cache are rare.

I have only been able to reproduce with the creation of the initial anonymous user, as those operations enqueue really early in initWithContext.
Or, by having a delay to the coroutine that loads operations.

nan-li added a commit that referenced this pull request May 31, 2024
* Migration fix for bug introduced in 5.1.12
* The following check is intended for the operation model store.
* When the call to this method moved out of the operation model store's initializer, duplicate operations could be cached.
* See #2099
@nan-li nan-li force-pushed the fix/duplicate_model_store_cache_loading branch from f3c4bdb to 40d9d7e Compare May 31, 2024 22:27
@nan-li nan-li changed the title [Fix] When model store loads from cache, do a full replacement, to avoid duplicates [Fix] Model store saving to and loading from prefs, and remove duplicate cached operations May 31, 2024
@nan-li nan-li changed the title [Fix] Model store saving to and loading from prefs, and remove duplicate cached operations [Fix] Model store saving to and loading from prefs, and remove duplicates May 31, 2024
@nan-li nan-li force-pushed the fix/duplicate_model_store_cache_loading branch from 23ff508 to 40d9d7e Compare June 1, 2024 01:41
protected fun load() {
if (name == null || _prefs == null) {
return
}

val shouldRePersist = models.isNotEmpty()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in the synchronized(models) {} below to prevent race conditions and concurrent modification expectations.

Background:
* The Operation Model Store was previously adding duplicate operations when the same ones that were previously added to the store and persisted, are re-read from cache.
* In addition, any operations enqueued before load is called overwrote the cache, so old cached operations are lost.

Solution:
* Don't persist models before load is called. This is safe because the time gap between any models being added and load being called should be very small.
* When load is called, the cached models are added to the front. Then the models get persisted altogether again if necessary.
* Migration fix for bug introduced in 5.1.12
* The following check is intended for the operation model store.
* When the call to this method moved out of the operation model store's initializer, duplicate operations could be cached.
* See #2099
@nan-li nan-li force-pushed the fix/duplicate_model_store_cache_loading branch from 40d9d7e to 7e05ed3 Compare June 4, 2024 23:03
@emawby emawby merged commit e48bce4 into main Jun 6, 2024
2 checks passed
@emawby emawby deleted the fix/duplicate_model_store_cache_loading branch June 6, 2024 18:32
@jinliu9508 jinliu9508 mentioned this pull request Jun 7, 2024
rgomezp pushed a commit that referenced this pull request Jun 17, 2024
* Migration fix for bug introduced in 5.1.12
* The following check is intended for the operation model store.
* When the call to this method moved out of the operation model store's initializer, duplicate operations could be cached.
* See #2099
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

Successfully merging this pull request may close these issues.

4 participants