I have a problem with transactions. The data in the transaction is always null and the update handler is called only singe once. The documentation says :
To accomplish this, you pass transaction() an update function which is
used to transform the current value into a new value. If another
client writes to the location before your new value is successfully
written, your update function will be called again with the new
current value, and the write will be retried. This will happen
repeatedly until your write succeeds without conflict or you abort the
transaction by not returning a value from your update function
Now I know that there is no other client accessing the location right now. Secondly if I read the documentation correctly the updateCounters function should be called multiple times should it fail to retrieve and update data.
The other thing - if I take out the condition if (counters === null) the execution will fail as counters is null but on a subsequent attempt the transaction finishes fine - retrieves data and does the update.
simple once - set on this location work just fine but it is not safe.
Please what do I miss?
here is the code
self.myRef.child('counters')
.transaction(function updateCounters(counters){
if (counters === null) {
return;
}
else {
console.log('in transaction counters:', counters);
counters.comments = counters.comments + 1;
return counters;
}
}, function(error, committed, ss){
if (error) {
console.log('transaction aborted');
// TODO error handling
} else if (!committed){
console.log('counters are null - why?');
} else {
console.log('counter increased',ss.val());
}
}, true);
here is the data in the location
counters:{
comments: 1,
alerts: 3,
...
}
By returning undefined in your if( ... === null ) block, you are aborting the transaction. Thus it never sends an attempt to the server, never realizes the locally cached value is not the same as remote, and never retries with the updated value (the actual value from the server).
This is confirmed by the fact that committed is false and the error is null in your success function, which occurs if the transaction is aborted.
Transactions work as follows:
pass the locally cached value into the processing function, if you have never fetched this data from the server, then the locally cached value is null (the most likely remote value for that path)
get the return value from the processing function, if that value is undefined abort the transaction, otherwise, create a hash of the current value (null) and pass that and the new value (returned by processing function) to the server
if the local hash matches the server's current hash, the change is applied and the server returns a success result
if the server transaction is not applied, server returns the new value, client then calls the processing function again with the updated value from the server until successful
when ultimately successful, and unrecoverable error occurs, or the transaction is aborted (by returning undefined from the processing function) then the success method is called with the results.
So to make this work, obviously you can't abort the transaction on the first returned value.
One workaround to accomplish the same result--although it is coupled and not as performant or appropriate as just using the transactions as designed--would be to wrap the transaction in a once('value', ...) callback, which would ensure it's cached locally before running the transaction.
Related
Container.DeleteItemAsync returns an ItemResponse<T> that holds a Resource property. This implies that the call will return the document that is actually deleted.
But the Resource property is always null and the status is set to NoContent, so it seems that this call never returns the document that is actually deleted. Why would it return a typed response, when it never returns an instance of the type. Also this MS blog describes how the response can explicitly be disabled when deleting documents. This also implies that the item should be deleted.
I have checked the database, but the document is actually being deleted. Trying to delete a non-existing document results in a NotFound status and raises an exception.
I have also tried setting EnableContentResponseOnWrite = true, but this doesn't do anything. I tried Microsoft.Azure.Cosmos v3.14 and v3.19. Both had the same result...
The DeleteItemAsync return type is a bit misleading given that ItemResponse<T> suggests a populated T with the deleted item but is actually null.
Perhaps a clearer approach is to use DeleteItemStreamAsync and inspect the response code, like this:
using ResponseMessage response = await container.DeleteItemStreamAsync(itemId, new(partitionKey), options);
if (response.StatusCode == HttpStatusCode.NoContent)
{
// Success
}
else
{
// Failed
}
Now you avoid exception-throwing overhead, and the code doesn't mislead about content being non-null.
From my understanding of Transactions, it can return null for two reasons:
There is actually no value at the node where the transaction is being performed.
The local cache is empty as Firebase Cloud Functions is stateless. Therefore, it may return null the very first time and it will re-run the function.
My question is, how do we distinguish between these two cases? Or does firebase do the distinction by itself?
Myref.transaction(function(currentData) {
if(currentData != null) {
return currentData + 1;
} else {
console.log("Got null")
return;
}
}, function(error, committed, snapshot) {
if(!committed) {
// The transaction returned null.
// But don't know if it's because the node is null or
// because the transaction failed during the first iteration.
}
});
In the above example, the transaction callback will be passed null both when the value at Myref is non-existent and when it attempts to get the data in the very first try when executing the transaction.
If the value of Myref is actually empty, I want the number 1238484 to be filled in there. If it is not, and the null is actually being thrown because of a wrong read by the transaction, how do I make this distinction?
PS: Please don't suggest a listener at the node. Is there any other more effective way of doing this?
On initial run of the transaction and value returned from the update function is undefined, onComplete is invoked with error to be null
For subsequent runs when there is no data, a reason for aborting the transaction is provided.
You can check the value of error to differentiate whether there is no data at the reference or local cache is empty.
error === null && !committed // local cache is empty
error !== null && !committed // there is no data
This is internal implementation detail and you shouldn't rely on it for your queries.
All of my firestore transactions fail when I want to get document.
I've tried getting other files changed rules to be public. I've found out that when i use if checking it seems like get function returned data.
val currentUserDocument = firebaseFirestore.collection("user").document(firebaseAuth.currentUser!!.uid)
val classMemberDocument = firebaseFirestore.collection("class").document(remoteClassID).collection("member").document(firebaseAuth.currentUser!!.uid)
firebaseFirestore.runTransaction { transaction ->
val userSnapshot = transaction.get(currentUserDocument)
val isInClass = userSnapshot.getBoolean("haveRemoteClass")!!
val classID = userSnapshot.getString("remoteClassID")!!
if (isInClass == true && classID == remoteClassID) {
transaction.update(currentUserDocument, "haveRemoteClass", false)
transaction.update(currentUserDocument, "remoteClassID", "")
transaction.delete(classMemberDocument)
} else {
throw FirebaseFirestoreException("You aren't in this class!", FirebaseFirestoreException.Code.ABORTED)
}
null
}
This typically means that the data that you're using in the transaction is seeing a lot of contention.
Each time you run a transaction, Firebase determines the current state of all documents you use in the transaction, and sends that state and the new state of those documents to the server. If the documents that you got were changed between when the transaction started and when the server gets it, it rejects the transaction and the client retries.
For the client to fail like this, it has to retry more often than is reasonable. Consider reducing the scope of your transaction to cover fewer documents, or find another way to reduce contention (such as the approach outlined for distributed counters).
I have a firebase functions which get triggered on onCreate for path /activites/{uid}/{workId}. This function then do some processing with the values and then updates the results (number value) to /users/{uid}/jobs.
// value read from the snapshot function gives
// processing on value
// .
// .
// .
db.ref('/users/{uid}/jobs').transaction(currentVal => {
return (currentVal || 0) + workIdValue;
})
My issue of inconsistent writes appears when let say function is triggered 40 times in span of 1 second per uid i.e. for the same path. This makes data inconsistent since each execution is having different currentValue from path /users/{uid}/jobs
I tired a solution of reading the value of the jobs from db to achieve consistency and then initiate transaction to update value but the result is still the same.
Other solution which I didn't tested is to use a lock mechanism by placing a boolean at the node /users/{uid}/jobs/locked and when trying to do update first check if boolean is false (meaning no other execution is modifying data) then set boolean true update the value and then release the lock setting value false.
Is there any better approach to solve this inconsistency?
We're using Firebase DB together with RxSwift and are running into problems with transactions. I don't think they're related to the combination with RxSwift but that's our context.
Im observing a data in Firebase DB for any value changes:
let child = dbReference.child(uniqueId)
let dbObserverHandle = child.observe(.value, with: { snapshot -> () in
guard snapshot.exists() else {
log.error("empty snapshot - child not found in database")
observer.onError(FirebaseDatabaseConsumerError(type: .notFound))
return
}
//more checks
...
//read the data into our object
...
//finally send the object as Rx event
observer.onNext(parsedObject)
}, withCancel: { _ in
log.error("could not read from database")
observer.onError(FirebaseDatabaseConsumerError(type: .databaseFailure))
})
No problems with this alone. Data is read and observed without any problems. Changes in data are propagated as they should.
Problems occur as soon as another part of the application modifies the data that is observer with a transaction:
dbReference.runTransactionBlock({ (currentData: FIRMutableData) -> FIRTransactionResult in
log.debug("begin transaction to modify the observed data")
guard var ourData = currentData.value as? [String : AnyObject] else {
//seems to be nil data because data is not available yet, retry as stated in the transaction example https://firebase.google.com/docs/database/ios/read-and-write
return TransactionResult.success(withValue: currentData)
}
...
//read and modify data during the transaction
...
log.debug("complete transaction")
return FIRTransactionResult.success(withValue: currentData)
}) { error, committed, _ in
if committed {
log.debug("transaction commited")
observer(.completed)
} else {
let error = error ?? FirebaseDatabaseConsumerError(type: .databaseFailure)
log.error("transaction failed - \(error)")
observer(.error(error))
}
}
The transaction receives nil data at first try (which is something you should be able to handle. We just just call
return TransactionResult.success(withValue: currentData)
in that case.
But this is propagated to the observer described above. The observer runs into the "empty snapshot - child not found in database" case because it receives an empty snapshot.
The transaction is run again, updates the data and commits successfully. And the observer receives another update with the updated data and everything is fine again.
My questions:
Is there any better way to handle the nil-data during the transaction than writing it to the database with FIRTransactionResult.success
This seems to be the only way to complete this transaction run and trigger a re-run with fresh data but maybe I'm missing something-
Why are we receiving the empty currentData at all? The data is obviously there because it's observed.
The transactions seem to be unusable with that behavior if it triggers a 'temporary delete' to all observers of that data.
Update
Gave up and restructured the data to get rid of the necessity to use transactions. With a different datastructure we were able to update the dataset concurrently without risking data corruption.