Reputation: 1753
For my website I store notifications per user in a UserDocument
.
Each UserDocument has a collection named notifications
.
In this collection there are multiple NotificationDocuments
.
users/<uid>/notifications/<notification_id>
NotificationDocument:
message: str
is_read: bool
generated_id: str
A notifcation could be: "You have 2 replies on your comment X"
When another reply is posted on the users' comment, the SAME NotificationDocument is updated to become:
"You have 3 replies on your comment X".
Now, the NotifcationDocument has a is_read status. By default is_read=False
.
However, when a user reads a notification, this NotifcationDocument is set to `is_read=True.
Then there is the race condition.
What if a user wants to mark it's notification as read, but in the meanwhile another notification enters, updating the content to "You have 4 replies on your comment X".
If the NotificationDocument has changed within the time that the user wants to mark it as is_read=True
I want to skip the update.
So, I thought, let's use Firebase transactions: https://cloud.google.com/firestore/docs/manage-data/transactions
This is the code I have for marking a NotificationDocument as read:
def mark_notification_as_read(
self, notification_id: str, expected_generated_id: str, uid: str
) -> None:
"""
Mark a notification with given `notification_id` as read, but only if it hasn't been updated in the meanwhile.
This is done by checking if `generated_id` is still the same.
"""
path = f"users/{uid}/notifications"
transaction = self.client.transaction()
notification_ref = self.client.collection(path).document(notification_id)
@firestore.transactional
def update_in_transaction(transaction, notification_ref):
snapshot = notification_ref.get(transaction=transaction)
time.sleep(10)
found_generated_id = snapshot.get('generated_id')
if found_generated_id == expected_generated_id:
transaction.update(notification_ref, {
'is_read': True,
})
else:
# Log for now, to be able to monitor if this is handled well
logger.info(msg="Can't mark notification as read as it has been updated!")
update_in_transaction(transaction, notification_ref)
Note that within the time.sleep(10), I update the generated_id
to a new value in the firebase console. I expect that this transaction should then fail.
However, after those 10 seconds are passed, I see that the notification is marked as is_read=True anyways.
What am I doing wrong?
Upvotes: 0
Views: 55
Reputation: 1753
After some searching, I solved my specific problem by not using transactions, but by using a write_option
.
Basically, I receive the snapshot's update_time
(a field given by Firebase), and when I do the update, I pass this on as a write_option. Stating: if on update the last_update is not the same, then fail the update
action.
from google.api_core.exceptions import FailedPrecondition
[...]
path = f"users/{uid}/notifications"
notification_ref = self.client.collection(path).document(id)
snapshot = notification_ref.get()
if snapshot.exists:
if snapshot.get("generated_id") == generated_id:
write_option = self.client.write_option(
last_update_time=snapshot.update_time
)
try:
notification_ref.update({"is_read": True}, option=write_option)
except FailedPrecondition:
pass
[...]
Upvotes: 1