I’ve got a race condition between WorkManager workers and my main app that I need to figure out how to prevent in the proper way.
I’m using WorkManager background workers which have access to RoomDB - we’ll call it UploadWorker.
I also have background IO threads in my main application which query the network and get the result - we’ll call that operation a Refresh.
The UploadWorker grabs the value from the db and sends it over the network to an API.
The Refresh operation also calls the API and gets most recent values for the same table for which the UploadWorker is saving changes. If the Refresh happens during the UploadWorker, it can possibly overwrite the values that were saved that UploadWorker is trying to update in the API. A classic race condition that sometimes works and sometimes fails, just depending on the timing of things.
So where should I put a lock or some synchronized code that will prevent this? Once the user hits Upload, I don’t want the value in the DB to change until the UploadWorker completes successfully. So that means I need to lock out the Refresh operation from happening.
I’m pretty sure but don’t completely understand whether a Background Worker is actually using the same threads that the main app does. So is there no other place to create a lock than using SharedPreferences or RoomDB? Is there an Android approved or recommended way of implementing the synchronization I need here?
If there isn’t already a way that’s recommended, my thought is to create a @Transaction around a RoomDB method function so that I can call a Query and another Insert or Update all at once on a separate locking table which will keep track of which operation has the lock. But this feels like I am reinventing the wheel doing something that should have already been provided by Android somehow.
Depending on your setup, you likely have an issue even without the race condition. If the online data changed and the local data changed, which should take priority, do you have some way to merge the two changes?
Everything in your app runs in the same process unless you’ve specified otherwise in the manifest so you can use regular locking techniques. You can even specify the executor in the WorkManager’s Configuration.
If your conflict resolution is something trivial like “upload wins over refresh” then I suggest you just disable refresh if you’ve made changes and have an upload pending. If a refresh is in progress while the changes happen then you probably want to cancel it. You may need some locking there if multiple threads are involved.
Frankly, I just avoid locks entirely for the most part and ensure any data is only ever edited by a single thread. With coroutines, it’s easy to wrap methods with a withContext call to specify a single thread dispatcher. Room, WorkManager, and Retrofit (guessing on your REST library) all work seamlessly with Kotlin coroutines and you potentially don’t need the background threads at all (the libraries use background threads internally, but not you). I encourage you to take a look at the guide and WorkManager meets Kotlin and Room query return types
What I ended up doing was a few things specific to my app. I cancelled network “Refresh” coroutines that were in progress when my Save function was called, and set a flag on the records I was saving to lock them from being overwritten until the save was finished. Any other new Refresh calls would check if there were any locks and not do the network request, but just return local data. Another way I could have done it would be to extend my local cache (which would prevent a network refresh) until the save is done, but I wanted to make sure certain Save-in-progress records weren’t overwritten, so I used locks and cancellation of coroutines instead.