firebase-ios-sdk icon indicating copy to clipboard operation
firebase-ios-sdk copied to clipboard

App crashed on removing firestore listener from event of DispatchSourceTimer

Open sspogra opened this issue 2 years ago • 12 comments

[REQUIRED] Step 1: Describe your environment

  • Xcode version: 13.4.1
  • Firebase SDK version: 9.1.0
  • Installation method: CocoaPods
  • Firebase Component: (Auth, Core, Database, Firestore, Messaging, Storage)
  • Target platform(s): iOS

[REQUIRED] Step 2: Describe the problem

Crashed: com.apple.root.default-qos.overcommit 0 libsystem_pthread.dylib 0x7744 + 102 1 libc++.1.dylib 0x3cc88 std::__1::recursive_mutex::lock() + 16 2 FirebaseFirestore 0x10d8ec firebase::firestore::api::QueryListenerRegistration::Remove() + 90 (__mutex_base:90) 3 Appname 0x73160 DbQuery.dispose() + 4335366496 (:4335366496) 4 Appname 0xb4a80 DataDownloadManager.addDownloadListener() + 4335635072 (:4335635072) 5 Appname 0x1bc3a0 closure #1 in closure #1 in UploadDownloadTimerManager.startTimer() + 4336714656 6 Appname 0x255194 closure #1 in closure #1 in RepeatingTimer.timer.getter + 4337340820 7 Appname 0x13411c thunk for @escaping @callee_guaranteed () -> () + 4336156956 (:4336156956) 8 libdispatch.dylib 0x481c _dispatch_client_callout + 20 9 libdispatch.dylib 0x7cf4 _dispatch_continuation_pop + 448 10 libdispatch.dylib 0x1a4b8 _dispatch_source_invoke + 1284 11 libdispatch.dylib 0x15fe0 _dispatch_root_queue_drain + 388 12 libdispatch.dylib 0x167d8 _dispatch_worker_thread2 + 112 13 libsystem_pthread.dylib 0x3768 _pthread_wqthread + 216 14 libsystem_pthread.dylib 0xa74c start_wqthread + 8

Steps to reproduce:

We have implemented a repeating DispatchSourceTimer which is fire in every one minute to check data on firestore collection (by removing existing listener and adding new one.). You can check added code for better understanding.

Relevant Code:

class RepeatingTimer {

    let timeInterval: TimeInterval
    
    init(timeInterval: TimeInterval) {
        self.timeInterval = timeInterval
    }
    
    private lazy var timer: DispatchSourceTimer = {
        let t = DispatchSource.makeTimerSource()
        t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
        t.setEventHandler(handler: { [weak self] in
            self?.eventHandler?()
        })
        return t
    }()

    var eventHandler: (() -> Void)?

    private enum State {
        case suspended
        case resumed
    }

    private var state: State = .suspended

    deinit {
        timer.setEventHandler {}
        timer.cancel()
        /*
         If the timer is suspended, calling cancel without resuming
         triggers a crash. This is documented here https://forums.developer.apple.com/thread/15902
         */
        resume()
        eventHandler = nil
    }

    func resume() {
        if state == .resumed {
            return
        }
        state = .resumed
        timer.resume()
    }

    func suspend() {
        if state == .suspended {
            return
        }
        state = .suspended
        timer.suspend()
    }
}



class UploadDownloadTimerManager: NSObject {
func startTimer(){
        DispatchQueue.global(qos: .background).async {
            self.timer = RepeatingTimer(timeInterval: TimeInterval(AppConstants.uploadTimerInterval))
            self.timer?.eventHandler = {
                DataDownloadManager.sharedManager().addDownloadListener() // Add download collection listener
            }
            self.timer?.resume()
        }
    }
}


class DataDownloadManager: NSObject {

var downloadListenerQuery:DbQuery? = nil

static var _sharedManager:DataDownloadManager? = nil
    
    static func sharedManager() -> DataDownloadManager {
        if _sharedManager == nil {
            _sharedManager = DataDownloadManager()
        }
        return _sharedManager ?? DataDownloadManager();
    }

 func addDownloadListener(){
        self.removeDownloadListener()
// code for reinitiate listener 
}

 func removeDownloadListener(){
        downloadListenerQuery?.dispose()
    }

}


class DbQuery: NSObject {
    private var firebaseListener: ListenerRegistration?

 init(listener: ListenerRegistration? = nil){
firebaseListener = listener
}

 func dispose() {
        firebaseListener?.remove()
        firebaseListener = nil
}

}

sspogra avatar Jul 19 '22 11:07 sspogra

I found a few problems with this issue:

  • I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.
  • This issue does not seem to follow the issue template. Make sure you provide all the required information.

google-oss-bot avatar Jul 19 '22 11:07 google-oss-bot

Hi @sspogra. This seems to be a duplicate of https://github.com/firebase/firebase-ios-sdk/issues/9302 which you logged earlier this year. Unfortunately, there are no additional insights into the root cause of this issue. Based on your description, however, it sounds like you are able to reproduce this crash. If that is the case, can you provide a reproducible app, in a GitHub repo that I could clone an reproduce myself? Without being able to reproduce I'm not sure how to investigate. Alternately, could you do a thread dump at the time that the crash occurs?

Also, the com.apple.root.default-qos.overcommit error generally means that the dispatch queue in question is overwhelmed with work. Is there anything your app is doing that may be flooding GCD with work, such as enqueuing a bunch of jobs that end up blocking and hogging the threads from GCD's thread pool?

dconeybe avatar Jul 19 '22 20:07 dconeybe

Hi @dconeybe I am not able to reproduce it on local. Crashed only on live app.

sspogra avatar Jul 20 '22 08:07 sspogra

@sspogra Is there a chance that your application is flooding GCD with blocking work?

Also, you mentioned in the original post that every minute your app "checks data on firestore collection (by removing existing listener and adding new one.)". This removing-of-and-re-adding-of the listener seems to be unnecessary. If you simply add a listener and leave it then it will get automatically notified when anything changes. If you do need to poll for some reason, then consider using DocumentReference.getDocument() or Query.getDocuments() to perform a one-time request.

Does any of this help your situation?

dconeybe avatar Jul 20 '22 13:07 dconeybe

@dconeybe Yes I can remove every minute check data on firestore.

sspogra avatar Jul 21 '22 11:07 sspogra

@sspogra Can you clarify what you mean by your last comment? Were you able to solve the problem by performing one-time "gets" instead of repeatedly adding a snapshot listener then removing it? Are you still experiencing this issue?

dconeybe avatar Jul 26 '22 20:07 dconeybe

@dconeybe Problem solved by removing repeatedly adding snapshot listener.

sspogra avatar Jul 27 '22 04:07 sspogra

@dconeybe Again getting this type of crash on removing listener only once.

Crashed: com.apple.root.background-qos 0 libc++.1.dylib 0xe318 std::__1::__shared_weak_count::__release_weak() + 4 1 FirebaseFirestore 0x10d954 firebase::firestore::api::QueryListenerRegistration::Remove() + 39 (swap.h:39) 2 AppName 0x73160 DbQuery.dispose() + 4300616032 (:4300616032) 3 AppName 0x153854 MyInteractor.addCurrentUserListener() + 4301535316 (:4301535316) 4 AppName 0x135a94 closure #1 in ViewController.refresh(_:) + 4301413012 5 AppName 0x140724 partial apply for closure #1 in ViewController.updateView() + 4301457188 (:4301457188) 6 AppName 0x13411c thunk for @escaping @callee_guaranteed () -> () + 4301406492 (:4301406492) 7 libdispatch.dylib 0x63094 _dispatch_call_block_and_release + 24 8 libdispatch.dylib 0x64094 _dispatch_client_callout + 16 9 libdispatch.dylib 0x13cbc _dispatch_root_queue_drain + 636 10 libdispatch.dylib 0x1439c _dispatch_worker_thread2 + 172 11 libsystem_pthread.dylib 0x1dd4 _pthread_wqthread + 224 12 libsystem_pthread.dylib 0x193c start_wqthread + 8

sspogra avatar Aug 03 '22 05:08 sspogra

@sspogra I still don't have too much insight into this crash. It's quite puzzling. It looks like a use-after-free crash with the ListenerRegistration object. Maybe there is some race condition in your DbQuery class where two threads are calling dispose() concurrently. In that case, one thread could set firebaseListener to nil, causing it to be garbage collected, while the other thread calls its remove() method. If this is indeed the case, one thing you could try is to to avoid setting firebaseListener to nil. Actually, this makes me think that there is a concurrency bug in Firestore's ListenerRegistration class where it could crash if invoked concurrently. I'll put out a fix for that and see if it helps your situation.

dconeybe avatar Aug 03 '22 14:08 dconeybe

@dconeybe Thanks for your reply. I will try it.

sspogra avatar Aug 04 '22 05:08 sspogra

I've merged https://github.com/firebase/firebase-ios-sdk/pull/10065 which may fix this crash for you. It will be included in the next release, which is planned for the last week of August 2022. I'll post back here once it's released.

dconeybe avatar Aug 05 '22 16:08 dconeybe

@dconeybe Thanks.

sspogra avatar Aug 06 '22 09:08 sspogra

9.5.0 was released yesterday, Aug 23, 2022, with the potential fix. Please test it out and report back.

dconeybe avatar Aug 24 '22 18:08 dconeybe

@dconeybe Thanks for the update. Issue not found at local.

sspogra avatar Aug 25 '22 05:08 sspogra