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

Add Sync Worker #1255

Merged
merged 18 commits into from
Dec 30, 2020
Merged

Add Sync Worker #1255

merged 18 commits into from
Dec 30, 2020

Conversation

theck13
Copy link
Contributor

@theck13 theck13 commented Dec 29, 2020

Fix

Add a worker to run syncing tasks in the background to avoid data loss as described in #992 among other issues. The background tasks are starting and stopping the note, tag, and preference buckets, which effectively triggers syncing. Currently, the buckets are stopped after the app has been closed for ten seconds. The worker added in these changes starts the buckets after that delay. It also has a backoff criteria of one minute and a constraint which requires an active network connection. The worker starts the buckets and stops them after ten seconds. The flow is as follows:

  • After the app is closed for ten seconds, start the worker
  • If there is an active network connection...
    • Start the buckets
    • Stop the buckets after ten seconds
  • When the app is resumed, stop the worker

Effectively, the buckets are started for ten seconds every fifteen minutes when there is a network connection and the app is in the background.

Adding a sync worker was originally attempted in #1137. That pull request was closed without merging due to concerns about the implementation. These changes build on that pull request with three major differences. First, the sync worker was updated from Worker to ListenableWorker. Worker is a class that performs work synchronously on a background thread provided by WorkManager. ListenableWorker is a class that can perform work asynchronously in WorkManager. Since starting and stopping the buckets after ten seconds is asynchronous, ListenableWorker was used. Second, the original implementation started and stopped the buckets every minute. This implementation uses the PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS constant, which is fifteen minutes, for the repeat interval. Third, creating and enqueuing the sync worker was moved from ApplicationLifecycleMonitor.onActivityPaused to ApplicationLifecycleMonitor.onTrimMemory to avoid the race condition when the buckets are stopped in ApplicationLifecycleMonitor.onTrimMemory after they are started in SyncWoker.startWork. That would nullify the sync worker and disable syncing until the repeat interval has elapsed fifteen minutes later.

Test

There aren't any user-facing changes to this pull request. The best way to see what's happening is to watch the logcat with the logs filtered to show "Debug" only and the "(Started)|(Stopped)" regular expression is used.

To test the flow described above, open the app then tap the recents/overview system navigation button to trigger starting the sync worker after a ten second delay. If you watch the logs for another fifteen minutes without opening the Simplenote app, you'll see the buckets started and stopped again. See the logs below for example.

2020-12-29 09:06:23.758 com.automattic.simplenote.debug D/Simplenote.onActivityResumed: Stopped worker
2020-12-29 09:07:11.601 com.automattic.simplenote.debug D/Simplenote.onTrimMemory: Started worker
2020-12-29 09:07:11.624 com.automattic.simplenote.debug D/SyncWorker.startWork: Started buckets
2020-12-29 09:07:21.631 com.automattic.simplenote.debug D/SyncWorker.startWork: Stopped buckets
2020-12-29 09:22:21.657 com.automattic.simplenote.debug D/SyncWorker.startWork: Started buckets
2020-12-29 09:22:31.700 com.automattic.simplenote.debug D/SyncWorker.startWork: Stopped buckets

To test the network connection constraint, enable airplane mode before tapping the recents/overview system navigation button. The sync worker will be started, but the buckets will not be started and stopped until airplane mode is disabled. See the logs below for example.

2020-12-29 09:23:24.598 com.automattic.simplenote.debug D/Simplenote.onActivityResumed: Stopped worker
2020-12-29 09:23:37.457 com.automattic.simplenote.debug D/Simplenote.onTrimMemory: Started worker
2020-12-29 09:24:43.636 com.automattic.simplenote.debug D/SyncWorker.startWork: Started buckets
2020-12-29 09:24:53.652 com.automattic.simplenote.debug D/SyncWorker.startWork: Stopped buckets

Review

Only one developer is required to review these changes, but anyone can perform the review. @malinajirka, I requested you as a reviewer since you reviewed the initial pull request for adding a sync worker. Don't feel obligated to review this pull request too, but you can if you want. I just wanted to notify you of the second attempt in case you wanted to verify your concerns have been addressed.

Release

RELEASE-NOTES.txt was updated in 2f92028 with:

Added syncing in the background to avoid data loss when exiting the app

@theck13 theck13 added [status] needs code review task This issue is some sort of task or a spike of time not directly related to a feature. labels Dec 29, 2020
@theck13 theck13 added this to the 2.15 milestone Dec 29, 2020
@peril-automattic
Copy link

peril-automattic bot commented Dec 29, 2020

You can test the changes on this Pull Request by downloading the APK here.

@dmsnell
Copy link
Member

dmsnell commented Dec 29, 2020

This should definitely help keep things more up to date while people have the app closed or in the background, but I know you have been concerned about battery usage and it makes me wonder why we are starting the worker when we have no changes queued up.

Wouldn't we want to only run this until all local changes have been synced? That is, once the app closes we keep a background worker alive until the local queue has flushed out and then we can safely shut everything down. Any changes that come in while the app is asleep should apply cleanly since we wouldn't have had the opportunity to create new local changes.

Is the goal to make sure that we never get behind on a bucket's cv so that the initial init operation always has all the recent changes?

@theck13
Copy link
Contributor Author

theck13 commented Dec 29, 2020

Our theory is the high battery usage reported years ago was because the buckets remained started and tried syncing even if there wasn't a network connection. Both the active network connection constraint and the WorkManager used in these changes are meant to avoid that. The sync worker will not be triggered if there isn't an active network connection. WorkManager is designed to queue work and dispatch it in a battery-efficient manner by respecting things like battery saver and doze modes set by Android.

Wouldn't we want to only run this until all local changes have been synced?

Yeah, it would be great if there was a callback from Simperium that told us all local changes have been synced. We could use that mechanism as a constraint for the sync worker such that we only trigger the background sync worker if there are local changes. That may also help us display the last synced time per note.

That is, once the app closes we keep a background worker alive until the local queue has flushed out and then we can safely shut everything down.

That is a little tricky. If there is no active network connection, we may be keeping the background worker alive indefinitely causing the high battery usage issue reported before.

Is the goal to make sure that we never get behind on a bucket's cv so that the initial init operation always has all the recent changes?

That's would be one benefit. The major reason is the user experience. Users expect that any edit made on Android will be synced across all their devices as soon as their Android device has a network connection whether the Simplenote app is open or not. Currently, syncing only occurs when there is a network connection and the Simplenote app is open. Adding background sync will help meet user expectations when they make an edit on Android and close the app before syncing completes due to the network connection or something else.

@dmsnell
Copy link
Member

dmsnell commented Dec 29, 2020

That is a little tricky. If there is no active network connection, we may be keeping the background worker alive indefinitely causing the high battery usage issue reported before.

Seems like a non-issue since I would assume we'd follow the same steps this PR follows to avoid that scenario. The only difference would be killing the background worker once local changes are sync'd.

Users expect that any edit made on Android will be synced across all their devices as soon as their Android device has a network connection whether the Simplenote app is open or not. Currently, syncing only occurs when there is a network connection and the Simplenote app is open. Adding background sync will help meet user expectations when they make an edit on Android and close the app before syncing completes due to the network connection or something else.

This again points me to question the need to keep a worker alive forever beyond the point where those changes are sync'd.

Yeah, it would be great if there was a callback from Simperium that told us all local changes have been synced.

Should we not make one then? I'd be happy to try and help. It seems like the benefit would be big. I guess there's limited harm merging this without it but I was remember how much you highlighted the other concerns running a worker forever.

@theck13
Copy link
Contributor Author

theck13 commented Dec 29, 2020

Seems like a non-issue since I would assume we'd follow the same steps this PR follows to avoid that scenario. The only difference would be killing the background worker once local changes are sync'd.

Ah, I thought you meant doing something different than these changes and just keeping the worker alive regardless of constraints.

Should we not make one then? I'd be happy to try and help. It seems like the benefit would be big.

Yeah, let's do it. It's on my list to do after this.

I guess there's limited harm merging this without it but I was remember how much you highlighted the other concerns running a worker forever.

I agree. The previous concerns were more about the frequency of starting/stopping the buckets, which was once per minute. Now, they are once per fifteen minutes, which I think it fine since it's the minimum interval for WorkManager. Updating this feature with the sync callback would be a great enhancement and mitigate my concerns even further.

@hichamboushaba hichamboushaba self-assigned this Dec 30, 2020
Copy link
Member

@hichamboushaba hichamboushaba left a comment

Choose a reason for hiding this comment

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

While I think having a periodic worker running each 15 minutes is an overkill for the sync issue, I understand the current limitation of Simperium which requires it, please just check my comment regarding the execution of the code scheduling the worker.

And as stated in our previous discussion, I don't think using WorkManager workers will solve the issue 100%, as they are not guaranteed to run at the specified constraints, but may be delayed, or not run at all when the device is in Doze mode (or in some OEM ROMs, speaking from a previous experience 🙂). I still think that firing a foreground service when the app exits and let it run for a short period, or use the new WorkManager API, is the only guaranteed way to make sure it will run. And when Simperium gets updated with a callback about sync status, it'll make using a foreground service a lot better, as it will only run if there is some sync to be done, and will be finished when the sync has been completed.

But for a short term solution, I think this PR will solve the issue for most users.

Comment on lines 213 to 227
PeriodicWorkRequest syncWorkRequest = new PeriodicWorkRequest.Builder(
SyncWorker.class,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS
)
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.setBackoffCriteria(BackoffPolicy.LINEAR, ONE_MINUTE_MILLIS, TimeUnit.MILLISECONDS)
.addTag(TAG_SYNC)
.build();
WorkManager.getInstance(getApplicationContext()).enqueueUniquePeriodicWork(
TAG_SYNC,
ExistingPeriodicWorkPolicy.REPLACE,
syncWorkRequest
);
Log.d("Simplenote.onTrimMemory", "Started worker");
Copy link
Member

Choose a reason for hiding this comment

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

This code is not guaranteed to run, as the system may kill the app before the 10s delay finishes. You can reproduce it by swiping away the app from recents menu.
I suggest you move this block outside the delayed runnable, and use setInitialDelay on the request to have a minimum delay of 10 seconds.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved the enqueue outside of the post delayed runnable and added a twenty second initial delay in 22d02da and d0c9c30. I chose to use twenty seconds as the initial delay to ensure a race condition doesn't occur when the buckets are stopped in ApplicationLifecycleMonitor.onTrimMemory after they are started in SyncWoker.startWork.

Copy link
Contributor Author

@theck13 theck13 left a comment

Choose a reason for hiding this comment

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

If a device doesn't follow AOSP practices or constraints, then we don't support that device. If the sync worker is delayed, that's fine. Since background syncing does not require an active user and doesn't need to run at an exact time, a deferred task using WorkManager is the recommended implementation according to the documentation for background processing. If battery saver or doze mode are enabled and our sync worker isn't triggered, that's fine. Actually, that behavior is preferable since it respects the operating system constraints. We don't want to violate things like that.

I think using WorkManager is better than a foreground service. A foreground service is meant for performing an operation noticeable to the user. Showing an indicator on every sync is against the minimalistic design of Simplenote. Also, a foreground service requires a status bar notification. Showing a notification periodically even when the app hasn't been used can be confusing. I don't think we should use WorkManager 2.3.0-alpha02 since it is the same as a foreground service and using an alpha library in production can be buggy.

Comment on lines 213 to 227
PeriodicWorkRequest syncWorkRequest = new PeriodicWorkRequest.Builder(
SyncWorker.class,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS
)
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.setBackoffCriteria(BackoffPolicy.LINEAR, ONE_MINUTE_MILLIS, TimeUnit.MILLISECONDS)
.addTag(TAG_SYNC)
.build();
WorkManager.getInstance(getApplicationContext()).enqueueUniquePeriodicWork(
TAG_SYNC,
ExistingPeriodicWorkPolicy.REPLACE,
syncWorkRequest
);
Log.d("Simplenote.onTrimMemory", "Started worker");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved the enqueue outside of the post delayed runnable and added a twenty second initial delay in 22d02da and d0c9c30. I chose to use twenty seconds as the initial delay to ensure a race condition doesn't occur when the buckets are stopped in ApplicationLifecycleMonitor.onTrimMemory after they are started in SyncWoker.startWork.

@theck13 theck13 assigned hichamboushaba and unassigned theck13 Dec 30, 2020
@hichamboushaba
Copy link
Member

hichamboushaba commented Dec 30, 2020

If a device doesn't follow AOSP practices or constraints, then we don't support that device

I don't think we can actually do that, as it will impact a lot of users.

If the sync worker is delayed, that's fine.

It depends on the user's usage, if a user is using two devices at the same time, the delay won't be OK 🙂.

I still think that a foreground service will be more battery friendly than the current solution, and it won't bother users to see the notification for some seconds until the sync is done (which is not needed in most cases, as the sync would be complete before the user exits the app), FI the same behavior is used by a lot of apps, including Youtube when checking for "downloads recommendations".

But anyway, let's agree to disagree on this point 🙂

@theck13
Copy link
Contributor Author

theck13 commented Dec 30, 2020

I don't think we can actually do that, as it will impact a lot of users, and it was your remark too in our previous discussion 🙂:

I wish we only supported AOSP ROM’s. That would be an Android developer’s dream. No more customized flavors of Android created by OEM’s.

Officially, we only support AOSP ROM's. Unofficially, we help users with devices that are AOSP-based, but not necessarily pure AOSP ROM's.

If the sync worker is delayed, that's fine.
It depends on the user's usage, if a user is using two devices at the same time, the delay won't be OK 🙂.

Simplenote is not marketed as a real-time collaboration app in which two devices are used simultaneously. If the devices and networks are fast enough to do that, that's fine, but it's not our targeted behavior. Also, if the two devices are being used at the same time, there won't be a delay due to a background sync worker if the app is in the foreground.

I still think that a foreground service will be more battery friendly than the current solution

That's not entirely true. There are definitely situations in which a foreground service would use more battery than WorkManager.

it won't bother users to see the notification for some seconds until the sync is done

That's not always true. It's quite possible any notification for any duration will bother users.

the same behavior is used by a lot of apps, including Youtube when checking for "downloads recommendations".

Simplenote is not trying to copy the behavior of apps like YouTube.

If you would like to discuss this further, send me a direct message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
task This issue is some sort of task or a spike of time not directly related to a feature.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants