как исправить зависание приложения после вызова диспетчера

Я читал этот вопрос о зависаниях приложений и семафорах, и я попытался реализовать ответьте на мой код, но приложение все еще зависает, несмотря на вызов пользовательского интерфейса в основном потоке. Моя цель - предотвратить зависание приложения после вызова всех записей и продолжить работу пользовательского интерфейса, как обычно.

Это предупреждающее действие, которое у меня есть в методе удаления до сих пор:

let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
            let semaphore = DispatchSemaphore(value: 0)
            
            
            self.deleteButton.isHidden = true
            self.loadingToDelete.alpha = 1
            self.loadingToDelete.startAnimating()
            
            DispatchQueue.global(qos: .userInitiated).async {
                self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("The docs couldn't be retrieved for deletion.")
                        return
                    }
                    
                    guard querySnapshot?.isEmpty == false else {
                        print("The user being deleted has no events purchased.")
                        return
                    }
                    
                    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
                            }
                            
                            for doc in querySnap!.documents {
                                let guest = doc.documentID
                                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!")
                                    semaphore.signal()
                                }
                                semaphore.wait()
                            }
                        }
                    }
                }
    
                self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("There was an error retrieving docs for user deletion.")
                        return
                    }
                    guard querySnapshot?.isEmpty == false else {
                        return
                    }
                    for document in querySnapshot!.documents {
                        let docID = document.documentID
                        
                        self.db.document("student_users/\(user.uid)/events_bought/\(docID)").delete { (err) in
                            guard err == nil else {
                                print("There was an error deleting the the purchased events for the user being deleted.")
                                return
                            }
                            print("Purchases have been deleted for deleted user!")
                            semaphore.signal()
                        }
                        semaphore.wait()
                    }
                }

                
                self.db.document("student_users/\(user.uid)").delete(completion: { (error) in
                    
                    guard error == nil else {
                        print("There was an error deleting the user document.")
                        return
                    }
                    print("User doc deleted!")
                    semaphore.signal()
                })
                semaphore.wait()
                
                user.delete(completion: { (error) in
                    guard error == nil else {
                        print("There was an error deleting user from the system.")
                        return
                    }
                    print("User Deleted.")
                    semaphore.signal()
                })
                semaphore.wait()
                
                DispatchQueue.main.async {
                    self.loadingToDelete.stopAnimating()
                    self.performSegue(withIdentifier: Constants.Segues.studentUserDeletedAccount, sender: self)
                }
            }
        }

Таким образом, на самом деле это удаляет все чисто, без остаточных данных в базе данных Firestore, чего я и хотел все это время, единственная проблема заключается в том, что приложение зависает. Я думал, что ответ на вопрос, который я дал выше, сработает в моем случае, но это не так.

Также следует отметить, что у меня были предложения по использованию облачных функций для решения этой проблемы, но в моем приложении есть два типа пользователей с разной логикой и синтаксисом в процессе удаления, поэтому я не мог просто использовать простой auth().onDelete() в облачных функциях и очистить остатки . Даже если бы я мог, это была бы та же проблема, с которой я столкнулся здесь, но только на стороне сервера, пытаясь правильно упорядочить задачи, что, на мой взгляд, является повторяющимся и не самым разумным делом на данный момент.

Есть ли другие предложения по преодолению этой проблемы? Заранее спасибо.

ИЗМЕНИТЬ Поскольку семафоры не подходящие, я прибег к следующему:

let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
            
            
            self.deleteButton.isHidden = true
            self.loadingToDelete.alpha = 1
            self.loadingToDelete.startAnimating()
            
                self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("The docs couldn't be retrieved for deletion.")
                        return
                    }
                    
                    guard querySnapshot?.isEmpty == false else {
                        print("The user being deleted has no events purchased.")
                        return
                    }
                    
                    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()
                                }
                            }
                        }
                    }
                }
    
                self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("There was an error retrieving docs for user deletion.")
                        return
                    }
                    guard querySnapshot?.isEmpty == false else {
                        return
                    }
                    let group = DispatchGroup()
                    for document in querySnapshot!.documents {
                        let docID = document.documentID
                        group.enter()
                        self.db.document("student_users/\(user.uid)/events_bought/\(docID)").delete { (err) in
                            guard err == nil else {
                                print("There was an error deleting the the purchased events for the user being deleted.")
                                return
                            }
                            print("Purchases have been deleted for deleted user!")
                            group.leave()
                        }
                    }
                }
            
            self.db.collection("student_users").whereField("userID", isEqualTo: user.uid).getDocuments { (querySnapshot, error) in
                guard error == nil else {
                    print("There was an error deleting the user document.")
                    return
                }
                guard querySnapshot?.isEmpty == false else {
                    return
                }
                let group = DispatchGroup()
                for document in querySnapshot!.documents {
                    let docID = document.documentID
                    group.enter()
                    self.db.document("student_users/\(docID)").delete { (err) in
                        guard err == nil else {
                            return
                        }
                        print("User doc deleted!")
                        group.leave()
                    }
                }
            }
            let group = DispatchGroup()
            group.enter()
                user.delete(completion: { (error) in
                    guard error == nil else {
                        print("There was an error deleting user from the system.")
                        return
                    }
                    print("User Deleted.")
                    group.leave()
                })
                
            group.notify(queue: .main) {
                
                self.loadingToDelete.stopAnimating()
                self.performSegue(withIdentifier: Constants.Segues.studentUserDeletedAccount, sender: self)
            }
         
        }

Это по-прежнему оставляет остаточные данные и не выполняет задачи по порядку. Есть другие предложения?


comment
Облачные функции не нужно вызывать из триггера. Вы можете явно вызывать их и передавать им информацию. И, как кто-то указал в одном из ваших прошлых вопросов, использование async/await (которого еще нет в Swift) может значительно упростить упорядочение задач.   -  person jnpdx    schedule 27.04.2021
comment
То есть вы говорите мне, что на стороне клиента нет никакого способа решить эту проблему? Я в значительной степени нахожусь на вершине решения этой проблемы, поэтому было бы отстойно начинать все сначала и пытаться изучить некоторый синтаксис на стороне сервера. @jnpdx   -  person dante    schedule 27.04.2021
comment
Я не говорил, что это невозможно решить на стороне клиента. В своем вопросе вы указали причину, по которой вы считали, что это невозможно сделать на стороне сервера, что я считаю неверным, поэтому я пытался объяснить, что есть возможность сделать это на стороне сервера.   -  person jnpdx    schedule 27.04.2021
comment
Если вы настаиваете на выполнении этой задачи на стороне клиента, я бы настоятельно рекомендовал не использовать семафор, потому что в этом нет необходимости, и их следует использовать очень экономно. Если у вас есть набор асинхронных задач, которым вы хотите передать обработчик завершения, используйте группы диспетчеризации. Вы также можете сохранить список ссылок на документы всякий раз, когда пользователь создает новые документы, а затем просто удалить все в этом списке, когда придет время удалять пользователя. Но в этом конкретном случае использования просто используйте вложенные группы отправки и выполняйте одну задачу за другой, а затем выполняйте переход к последней.   -  person liquid    schedule 27.04.2021
comment
Вы можете создать логическую переменную для отслеживания любых ошибок на этом пути и, когда она дойдет до обработчика окончательного завершения, автоматически повторно запустить задачу, если переменная равна true. Но лучшей формой рекурсии было бы завершение задачи, когда запросы к базе данных возвращают безошибочные пустые моментальные снимки. Когда последний запрос ничего не показывает, завершите задачу. Но здесь я бы избегал семафоров.   -  person liquid    schedule 27.04.2021
comment
Я хочу, чтобы задачи выполнялись в определенном порядке, порядок в моем фрагменте кода выше - это не DispatchGroup, когда вам все равно, в каком порядке выполняются задачи? @bxod   -  person dante    schedule 27.04.2021
comment
Вы можете поддерживать порядок, если используете несколько групп отгрузки. Когда вы запрашиваете базу данных и получаете обратно кучу документов, а затем просматриваете эти документы в цикле, группа диспетчеризации может просто предоставить этому циклу свой собственный обработчик завершения. Поэтому, когда этот цикл завершается и завершается последний асинхронный вызов, вы можете перейти к следующей задаче (и повторить, если необходимо, с другой группой диспетчеризации). Пока вы начинаете следующую задачу по завершении предыдущей, порядок никогда не нарушается.   -  person liquid    schedule 27.04.2021
comment
что вы имеете в виду под несколькими? несколько объявлений dispatchgroup в каждом обработчике завершения запроса firestore или несколько блоков group.enter() и group.leave()? Я обновлю свой пост, чтобы показать вам, что у меня есть на данный момент, так что мы оба на одной странице. @bxod   -  person dante    schedule 27.04.2021
comment
Несколько экземпляров группы отправки.   -  person liquid    schedule 27.04.2021
comment
Хорошо, у меня все еще проблемы, и, возможно, я не понял, что вы имели в виду в своем последнем абзаце, у меня тоже будет несколько group.notify() вызовов или только один последний после всех задач? В настоящее время у меня есть один group.notify() в последнем экземпляре группы отправки, но порядок задач по-прежнему неверен, когда срабатывает действие предупреждения. @bxod   -  person dante    schedule 27.04.2021
comment
Что вы имеете в виду под своей ревизией, в которой говорится «оставляет остаточные данные»? Что касается того, что они не в порядке, это хорошо, поскольку это означает, что вы наслаждаетесь параллелизмом (что заставит его работать быстрее). Поэтому соберите результаты в некоторой локальной, независимой от порядка структуре (например, в словаре), а затем измените их порядок перед обновлением объекта модели.   -  person Rob    schedule 27.04.2021


Ответы (1)


Позвольте мне дать вам несколько идей, потому что я считаю, что ваше решение должно включать некоторые или все из них. Во-первых, как работают группы диспетчеризации и как вы можете вложить их для выполнения блоков асинхронных задач по порядку:

func deleteUser(completion: @escaping (_ done: Bool) -> Void) {
    // put UI into loading state
    
    db.collection("someCollection").getDocuments { (snapshot, error) in
        if let snapshot = snapshot {
            if snapshot.isEmpty {
                completion(true) // no errors, nothing to delete
            } else {
                let dispatchGroup = DispatchGroup() // instantiate the group outside the loop
                var hasErrors = false
                
                for doc in snapshot.documents {
                    dispatchGroup.enter() // enter on every iteration
                    
                    db.document("someDocument").delete { (error) in
                        if let error = error {
                            print(error)
                            hasErrors = true
                        }
                        
                        dispatchGroup.leave() // leave on every iteration regardless of outcome
                    }
                }
                
                dispatchGroup.notify(queue: .main) {
                    if hasErrors {
                        completion(false) // failed to delete
                    } else {
                        // execute next task and repeat
                    }
                }
            }
        } else {
            if let error = error {
                print(error)
                completion(false) // failed to delete
            }
        }
    }
}

deleteUser { (done) in
    if done {
        // segue to next view controller
    } else {
        // retry or alert user
    }
}

Приведенный выше пример представляет собой основы того, как диспетчерская группа может работать на вас. Когда вы покидаете группу столько же раз, сколько входили в нее, вызывается обработчик завершения. В этом примере нет рекурсии и не проверяется, действительно ли все было удалено. Вот пример того, как вы могли бы добавить что-нибудь из этого:

func deleteUser(completion: @escaping (_ done: Bool) -> Void) {
    var retries = 0
    
    func task() {
        db.collection("someCollection").getDocuments { (snapshot, error) in
            if let snapshot = snapshot {
                if snapshot.isEmpty {
                    completion(true) // done, nothing left to delete
                } else {
                    // delete the documents using a dispatch group or a Firestore batch delete
                    
                    task() // 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 < 5 else {
            completion(false) // 5 failed attempts, exit function
            return
        }
        if retries == 0 {
            task()
        } else { // the more failures, the longer we wait until retrying
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(retries)) {
                task()
            }
        }
    }
    
    run()
}

Это не дает прямого ответа на ваш вопрос, но должно помочь вам с задачей в целом. Вы также можете отказаться от некоторых циклов и удалений и сделать все это внутри пакетной операции Firestore, которая имеет собственный обработчик завершения. Есть много способов решить эту проблему, но я бы рассмотрел некоторые из них.

person liquid    schedule 27.04.2021
comment
Еще одна вещь, о которой у меня возник вопрос, поскольку в моем фрагменте кода у меня есть несколько запросов, следует ли мне поместить все эти запросы в один большой метод deleteUser, как у вас в вашем ответе, или мне следует сделать более мелкие методы, которые каждый запрос, а затем заказать вызов метода при закрытии действия предупреждения? @bxod - person dante; 27.04.2021
comment
@dante Я бы посоветовал вам выбрать то, что легче всего читать и понять (другими разработчиками), и поместить их в одну большую функцию или выделить их по частям - это полностью предпочтение и то, что вы считаете более легким для чтения. Лично я думаю, что у меня была бы основная функция с именем deleteUser(), которая для ясности может содержать в себе более мелкие функции. И что бы вы ни выбрали, я также предлагаю вам быть последовательными. Однако вы обычно делаете что-то во всем приложении, вероятно, вы должны продолжать делать это здесь. Последовательный код легче читать. - person liquid; 27.04.2021
comment
да, я не буду врать, чувак, я даже сам не могу этого понять, не говоря уже о других разработчиках, я все еще сталкиваюсь с проблемами и получаю бесконечную рекурсивную печать при попытке вызвать функцию при удалении. Вместо этого я сделаю еще один пост, чтобы обсудить эту проблему, потому что это просто самая неприятная вещь, с которой мне приходилось иметь дело. @bxod - person dante; 27.04.2021
comment
@dante Нарисуйте на листе бумаги, что именно вы хотите сделать и в каком порядке вы хотите, чтобы это было сделано. Максимально упростите это. То, что вы пытаетесь сделать, просто и понятно, но это может расстраивать, если вы пытаетесь исправить это с середины. Я предлагаю начать эту задачу с самого начала. Начните с родительской функции deleteUser() и ничего не делайте, кроме кодирования шагов рекурсии. Протестируйте его на игровой площадке и убедитесь, что он работает. Делайте все на детской площадке по частям. - person liquid; 27.04.2021
comment
@dante, тебе нужно думать как пещерный человек прямо сейчас. Что происходит, когда пользователь нажимает кнопку удаления? Пользовательский интерфейс завис? Если он завис, заморозьте пользовательский интерфейс. Тогда что? Затем вам нужно получить покупки пользователя, так что сделайте это. Тогда что? Затем мы просматриваем эти покупки и удаляем их одну за другой. Или, может быть, мы просто собираем эти ссылки на документы в массив, возможно, и в самом конце запускаем пакетную операцию Firestore и удаляем их атомарно (все сразу, все или ни одного). Если пакет не удался, повторите попытку. Прежде чем писать какой-либо код, запишите это на бумаге и сделайте это невероятно простым. - person liquid; 27.04.2021
comment
Проверьте мой последний вопрос, не думайте, что все это нужно. @bxod - person dante; 27.04.2021