async deletion task won't work if guest count is more than 1

62 views Asked by At

So my goal is to delete all user's guests if the guest count for a purchased ticket is over 1 when a user is deleting their account.

Currently I have this function to try to accomplish this:

func deleteUserGuests(completion: @escaping (_ done: Bool) -> Void) {
    var retries = 0
    guard let user = Auth.auth().currentUser else { return  }
    
    func checkForGuestsAndDeleteIfAny() {
        db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
            if let snapshot = querySnapshot {
                if snapshot.isEmpty {
                    completion(true)
                    // done, nothing left to delete
                } else {
                    // delete the documents using a dispatch group or a Firestore batch delete
                    for document in querySnapshot!.documents {
                        let docID = document.documentID
                        self.db.collection("student_users/\(user.uid)/events_bought/\(docID)/guests").getDocuments { (querySnap, error) in
                            guard querySnap?.isEmpty == false else {
                                print("The user being deleted has no guests with his purchases.")
                                return
                            }
                            let group = DispatchGroup()
                            for doc in querySnap!.documents {
                                let guest = doc.documentID
                                group.enter()
                                self.db.document("student_users/\(user.uid)/events_bought/\(docID)/guests/\(guest)").delete { (error) in
                                    guard error == nil else {
                                        print("Error deleting guests while deleting user.")
                                        return
                                    }
                                    print("Guests deleted while deleting user!")
                                    group.leave()
                                }
                            }
                        }
                    }
                 
                    checkForGuestsAndDeleteIfAny()// call task again when this finishes
                           // because this function only exits when there is nothing left to delete
                           // or there have been too many failed attempts
                }
            } else {
                if let error = error {
                    print(error)
                }
                retries += 1 // increment retries
                run() // retry
            }
        }
    }
    
   
    func run() {
        guard retries < 30 else {
            completion(false) // 5 failed attempts, exit function
            return
        }
        if retries == 0 {
            checkForGuestsAndDeleteIfAny()

        } else { // the more failures, the longer we wait until retrying
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(retries)) {
                checkForGuestsAndDeleteIfAny()
            }
        }
    }
    
    run()
}

I upped the retry limit, to see if that was the issue, but it still doesn't delete the guests if there are more than one.

I call it in an alert action when the user successfully reauthenticates before deleting their account:

let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
            
            
            self.deleteButton.isHidden = true
            self.loadingToDelete.alpha = 1
            self.loadingToDelete.startAnimating()
            
            self.deleteUserGuests { (response) in
                if response == false {
                    return
                }
            }
            
            self.deleteUserPurchases { (purchase) in
                if purchase == false {
                    return
                }
            }
            self.deleteUserOutOfFirestore { (removed) in
                if removed == false {
                    return
                }
            }
            
            user.delete(completion: { (error) in
                guard error == nil else {
                    print("There was an error deleting user from the system.")
                    return
                }
                print("User Deleted.")
                
            })
            
            self.loadingToDelete.stopAnimating()
            self.performSegue(withIdentifier: Constants.Segues.studentUserDeletedAccount, sender: self)
            
            
            
            
        }

This is the result in the database:

residual data

Everything else gets deleted fine in correct order, purchases, the user itself, and then the user out of Firebase auth, but the guests never get deleted if it is over 1 guest. Is there something I did wrong or left out in the deleteUserGuests method that is causing this issue?

1

There are 1 answers

4
trndjc On

As I've said a number of times, I'd approach this entire task differently--I'd do this sort of cleanup on the server side, perform the deletes atomically using a batch or transaction operation, and have robust recursion throughout. However, to fix your immediate problem of why you can't delete the documents in this subcollection, this will do it.

func deleteUserGuests(completion: @escaping (_ done: Bool) -> Void) {
    guard let user = Auth.auth().currentUser else {
        return
    }
    var retries = 0
    
    func task() {
        db.collection("student_users/\(user.uid)/events_bought").getDocuments { (snapshot, error) in
            if let snapshot = snapshot {
                if snapshot.isEmpty {
                    completion(true)
                } else {
                    let dispatchEvents = DispatchGroup()
                    var errors = false
                    
                    for doc in snapshot.documents {
                        dispatchEvents.enter()

                        self.db.collection("student_users/\(user.uid)/events_bought/\(doc.documentID)/guests").getDocuments { (snapshot, error) in
                            if let snapshot = snapshot {
                                if snapshot.isEmpty {
                                    dispatchEvents.leave()
                                } else {
                                    let dispatchGuests = DispatchGroup()
                                    
                                    for doc in snapshot.documents {
                                        dispatchGuests.enter()

                                        doc.reference.delete { (error) in
                                            if let error = error {
                                                print(error)
                                                errors = true
                                            }
                                            
                                            dispatchGuests.leave()
                                        }
                                    }
                                    
                                    dispatchGuests.notify(queue: .main) {
                                        dispatchEvents.leave()
                                    }
                                }
                            } else {
                                if let error = error {
                                    print(error)
                                }
                                errors = true
                                dispatchEvents.leave()
                            }
                        }
                    }
                    
                    dispatchEvents.notify(queue: .main) {
                        if errors {
                            retries += 1
                            run()
                        } else {
                            completion(true)
                        }
                    }
                }
            } else {
                if let error = error {
                    print(error)
                }
                retries += 1
                run()
            }
        }
    }
    
    
    func run() {
        guard retries < 30 else {
            completion(false)
            return
        }
        if retries == 0 {
            task()
        } else {
            let delay = Double(retries)
            
            DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
                task()
            }
        }
    }
    
    run()
}