Hi
Is there any ability to patch part of a document in couchbase lite ?
The scenario I am trying to cater for is the following race condition
I have 2 asynchronous methods that write to the same document, the current process in the thread for updating a document is
- Read Document from db
- Update document
- Write update to db
This means that sometimes this occurs as
Thread 1: Read Document
Thread 1: Update Document
Thread 2: Read Document
Thread 1: Write document
Thread 2: Update document
Thread 2: Write document
This means Thread 2 overwrites Thread 1’s update.
I have though of the following solutions
- Find a way to make these threads sequential, such as locking read, write, update process to this document to only 1 thread at a time
- Assuming these threads write to different parts of the document, just patching that part without affecting the rest of the document would solve this.
Not sure if this will help anyone else facing this problem but I have written a kotlin helper class that will guarantee your read/writes happen sequentially so that you can safely update the same document from different threads
object CouchbaseWriteQueue {
private val writeScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val queue = Channel<Job>(Channel.UNLIMITED)
private val writeLock = Object()
init {
writeScope.launch {
for (job in queue) job.join()
}
}
// These 2 functions are specific to your local setup
private fun getDocument(id: String): MutableDocument {
TODO("read document from your database")
}
private fun saveDocument(document: MutableDocument) {
TODO("Save document to your database")
}
/**
* Adds document update to queue and returns
*/
fun queueUpdate(documentId: String, update: MutableDocument.() -> Unit) {
// performs the update to the document
val updateBlock: CoroutineScope.() -> Unit = {
val document = getDocument(documentId)
update(document)
saveDocument(document)
}
// Creates a job for this update
val job = writeScope.launch(Dispatchers.IO, CoroutineStart.LAZY, updateBlock)
// Adds the job to the queue
writeScope.launch {
synchronized(writeLock) {
queue.trySend(job)
}
}
}
/**
* Adds document update to queue and waits for result
*/
suspend fun queueUpdateAndWait(documentId: String, update: MutableDocument.() -> Unit): Boolean {
val result = CompletableDeferred<Boolean>()
val updateBlock: MutableDocument.() -> Unit = {
try {
update()
result.complete(true)
} catch (e: Exception) {
result.complete(false)
}
}
queueUpdate(documentId, updateBlock)
return result.await()
}
}
Example usage:
These function can safely be called from different threads to update the same document, this guarantees no merge conflicts or race conditions, additionally the order of the updates is maintained.
fun updateDocument(key: String, value: String) {
CouchbaseWriteQueue.queueUpdate("id_1234") {
setString(key, value)
}
}
suspend fun updateDocumentAndWait(key: String, value: String) {
val result = CouchbaseWriteQueue.queueUpdateAndWait("id_1234") {
setString(key, value)
}
Log.i("TAG", "Hey the save was : $result")
}
I can already hear the argument that @meirrosendorff 's solution serializes what should be concurrent operations. The fact is that the Couchbase Lite library already serializes all database access… and even if Couchbase didn’t, writing to the database is just writing to a file: the OS serializes writes. You lose nothing by serializing writes, like this.
Another possible solution, btw, involves using the method:
Collection.save(
@NonNull MutableDocument document,
@NonNull ConflictHandler conflictHandler)
The passed ConflictHander is one of these:
boolean handle(
@NonNull MutableDocument document,
@Nullable Document oldDocument);
… which can merge or update document as you choose
While I hear @blake.meike argument that it serialises what should be concurrent operations, I was unable to find any other way of solving this generically, the trouble with the conflict resolver is that I would need to know exactly what changes each thread is making and then update the document accordingly which could be a pain to maintain so I was hoping to do this more generally.
@meirrosendorff : I think you mistake my argument! I was pointing out that your solution imposes no constraints that are not already imposed by the Couchbase library and the OS.