Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase / iOS: runTransactions sometimes doesn't work

I am working on a chat app, where users should get notified about new messages from their contacts. This notification message should also include the number of unread messages. Because both the sender and receiver can update this information runTransaction is preferred. Unfortunately sometimes it doesn't work. It feels "stuck" and then starts working after a while again. The privateChats node (see below) gets always updated with the latest message, but not the openChatMessages node.

Can this happen if many messages are sent in a short period of time, i.e. runTransactions is performed too often for the same ref?

My data structure:

privateChats
    $userId
        $chatId
            $messageId
                text
                timestamp
                senderId
                senderEmail
                senderName

// this node contains information about open chats
// like last message and counter for unread messages
openChatMessages
    $userId
        $chatId
            text
            timestamp
            senderId
            senderEmail
            senderName
            counter

My code:

class ChatViewController: JSQMessagesViewController {

    var user: FIRUser!
    var ref: FIRDatabaseReference!
    var chatRef: FIRDatabaseReference!
    var senderOpenChatRef: FIRDatabaseReference!
    var receiverOpenChatRef: FIRDatabaseReference!

    // the following variables will be set before ChatViewController appears

    var chatId: String?
    var receivId: String?
    var receiverEmail: String?
    var receiverName: String?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.user = FIRAuth.auth()?.currentUser!
        self.ref = FIRDatabase.database().reference()
        self.chatRef = self.ref.child("privateChats").child(self.user.uid).child(self.chatId!)
        self.senderOpenChatRef = self.ref.child("openChatMessages").child(self.user.uid).child(self.chatId!)
        self.receiverOpenChatRef = self.ref.child("openChatMessages").child(self.receiverId!).child(self.chatId!)
    }

    func sendMessage(text: String) {
        var messageObject = [String: AnyObject]()
        messageObject["text"] = text
        messageObject["timestamp"] = FIRServerValue.timestamp()
        messageObject["senderEmail"] = self.user.email
        messageObject["senderName"] = self.user.displayName
        messageObject["senderId"] = self.user.uid

        let messageId = self.ref.child("privateChats").child(self.user.uid).child(self.chatId!).childByAutoId().key

        let childUpdates = [
            "/privateChats/\(self.user.uid)/\(self.chatId!)/\(messageId)": messageObject,
            "/privateChats/\(self.receiverId!)/\(self.chatId!)/\(messageId)": messageObject
        ]

        self.ref.updateChildValues(childUpdates, withCompletionBlock: { (error, ref) -> Void in
            if error != nil {
                print("childUpdates error:\(error)")
                return
            }

            JSQSystemSoundPlayer.jsq_playMessageSentSound()
            self.finishSendingMessage()
            self.updateOpenChats(text)
        })
    }


    func updateOpenChats(text: String) {

        // update the receivers openChatObject with increasing the counter
        self.receiverOpenChatRef.runTransactionBlock({ (currentData: FIRMutableData) -> FIRTransactionResult in

            var openChatObject = [String: AnyObject]()

            // update openChatObject with the latest information from currentData
            if currentData.hasChildren() {
                 openChatObject = currentData.value as! [String: AnyObject]
            }

            openChatObject["text"] = text
            openChatObject["timestamp"] = FIRServerValue.timestamp()
            openChatObject["senderEmail"] = self.user.email
            openChatObject["senderName"] = self.user.displayName
            openChatObject["senderId"] = self.user.uid

            var counter = openChatObject["counter"] as? Int
            if counter == nil {
                counter = 1
            } else {
                counter = counter! + 1
            }
            openChatObject["counter"] = counter

            currentData.value = openChatObject
            return FIRTransactionResult.successWithValue(currentData)
            }) { (error, committed, snapshot) in
                if let error = error {
                    print("updateOpenChats: \(error.localizedDescription)")
                }
        }

        // update your (the sender's) openChatObject with setting the counter to zero
        self.senderOpenChatRef.runTransactionBlock({ (currentData: FIRMutableData) -> FIRTransactionResult in

            var openChatObject = [String: AnyObject]()

            // update openChatObject with the latest information from currentData
            if currentData.hasChildren() {
                 openChatObject = currentData.value as! [String: AnyObject]
            }

            openChatObject["text"] = text
            openChatObject["timestamp"] = FIRServerValue.timestamp()
            openChatObject["senderEmail"] = self.receiverEmail
            openChatObject["senderName"] = self.receiverName
            openChatObject["senderId"] = self.receiverId
            openChatObject["counter"] = 0

            currentData.value = openChatObject
            return FIRTransactionResult.successWithValue(currentData)
            }) { (error, committed, snapshot) in
                if let error = error {
                    print(error.localizedDescription)
                }
        }
    }
}

EDIT:

Contrary to my expectations from my first answer the bug still occurs. I assume it has something to do with the connection? E.g. when there is no good connection it sometimes takes longer to run the transaction? But sometimes it also occurs when I sit right next to the router. The other nodes get written to, but not the ones with the transaction. After restarting the app in those situations it starts to work again. So I guess there is something wrong under the hood.

I would highly appreciate solutions for this problem. A chat app where the receiver sometimes does not get notified about new messages is a no go.

I am also ok with workarounds: Are transactions actually needed when you want to increment a counter? I could update the other data like text, senderId or timestamp with setValue, but that would lead to corrupt data, when both users try to set the value of subnodes at the same time, wouldn't it?

Here's my latest code:

func sendMessage(text: String?, video: NSURL?, image: UIImage?) {

    var messageObject = [String: AnyObject]()
    messageObject["text"] = text
    messageObject["timestamp"] = FIRServerValue.timestamp()
    messageObject["senderEmail"] = self.user.email
    messageObject["senderName"] = self.user.displayName
    messageObject["senderId"] = self.user.uid

    func completeSending() {
        let messagesRef = self.ref.child("messages").child(self.chatId!).childByAutoId()
        messagesRef.setValue(messageObject)

        JSQSystemSoundPlayer.jsq_playMessageSentSound()
        if let _ = image {
            self.updateOpenChats("📷 Photo")
        } else if let text = text {
            self.updateOpenChats(text)
        }

        self.finishSendingMessageAnimated(true)
    }

    if let image = image { // if an image is being sent
        let data: NSData = UIImageJPEGRepresentation(image, 0.37)!
        let fileName = "image_\(NSDate().timeIntervalSince1970).jpg"
        let chatImagesRef = storageRef.child("chatImages/\(self.chatId!)/\(fileName)")
        let uploadTask = chatImagesRef.putData(data, metadata: nil) { metadata, error in
            if (error != nil) {
                print(error)
                return
            }
        }

        uploadTask.observeStatus(.Failure) { snapshot in
            ProgressHUD.showError("Uploading image failed.")
        }

        uploadTask.observeStatus(.Success) { snapshot in
            let imageUrl = snapshot.reference
            messageObject["imageUrl"] = String(imageUrl)
            completeSending()
        }
    } else { // if it's just a text message
        completeSending()
    }

}

func updateOpenChats(text: String) {

        self.receiverChatRef.runTransactionBlock({ (currentData: FIRMutableData) -> FIRTransactionResult in

            var openChatObject = [String: AnyObject]()

            if currentData.hasChildren() {
                openChatObject = currentData.value as! [String: AnyObject]
            }

            openChatObject["text"] = text
            openChatObject["timestamp"] = FIRServerValue.timestamp()
            openChatObject["senderEmail"] = self.user.email
            openChatObject["senderName"] = self.user.displayName
            openChatObject["senderId"] = self.user.uid
            openChatObject["pushId"] = Database.pushId

            var counter = openChatObject["counter"] as? Int
            if counter == nil {
                counter = 1
            } else {
                counter = counter! + 1
            }
            openChatObject["counter"] = counter

            // Set value and report transaction success
            currentData.value = openChatObject
            return FIRTransactionResult.successWithValue(currentData)
        }) { (error, committed, snapshot) in
            if let error = error {
                print("updateOpenChats: \(error.localizedDescription)")
            }
        }

        self.senderChatRef.runTransactionBlock({ (currentData: FIRMutableData) -> FIRTransactionResult in
            var openChatObject = [String: AnyObject]()

            if currentData.hasChildren() {
                openChatObject = currentData.value as! [String: AnyObject]
            }

            openChatObject["text"] = text
            openChatObject["timestamp"] = FIRServerValue.timestamp()
            openChatObject["senderEmail"] = self.receiver.email
            openChatObject["senderName"] = self.receiver.name
            openChatObject["senderId"] = self.receiver.uid
            openChatObject["counter"] = 0

            // Set value and report transaction success
            currentData.value = openChatObject
            return FIRTransactionResult.successWithValue(currentData)
        }) { (error, committed, snapshot) in
            if let error = error {
                print(error.localizedDescription)
            }
    }
}
like image 447
MJQZ1347 Avatar asked Jun 12 '16 13:06

MJQZ1347


1 Answers

Ok, apparently there is a bug in the Firebase SDK. The callback of updateChildValues doesn't get executed sometimes, even though the update was successful. I removed the completionBlock and now it works flawlessly.

  self.ref.updateChildValues(childUpdates) 
  JSQSystemSoundPlayer.jsq_playMessageSentSound()
  self.finishSendingMessage()
  self.updateOpenChats(text)

EDIT: See updated question, the problem still occurs.

like image 171
MJQZ1347 Avatar answered Oct 06 '22 18:10

MJQZ1347