Search code examples
iosswiftmultithreadingcore-datarx-swift

Error libsystem_platform.dylib`_os_unfair_lock_corruption_abort during saving data with Core Data on iOS with Swift


I'm new to iOS development. I'm using Core Data for saving data in my iOS app. My app is accessing sharing object from App Delegate in multiple View Controllers and is reading and saving data with Core Data via this object. I'm also using RxSwift there and doing other things like sending data to the server. I delete all data after sending it to the server.

From time to time, my app is crashing and I get the following error:

Name of the thread:

Thread 14 Queue : com.apple.runningboardservices.background-workloop (serial)

Error message:

libsystem_platform.dylib`_os_unfair_lock_corruption_abort
"BUG IN CLIENT OF LIBPLATFORM: os_unfair_lock is corrupt, or owner thread exited without unlocking"

It's invoked in the following function:

    func perform(_ function: @escaping (NSManagedObjectContext) -> Void) {
        let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        privateContext.parent = context
        privateContext.automaticallyMergesChangesFromParent = true
        context.perform {
            do {
                function(privateContext)
                guard self.context.hasChanges else { return }
                try self.context.save()
                self.context.reset()
            } catch let error {
                self.logger.error("Error while saving data \(error)")
            }
        }
    }

On the context.perform line debugger says: Enqueued from rx.global_dispatch_queue.serial (Thread 2) Queue : rx.global_dispatch_queue.serial (serial)

Context for this function is cerated as follows:

let bundle = Bundle(for: DatabaseFacade.self)
let modelUrl = bundle.url(forResource: modelName, withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelUrl)!
let persistentContainer = NSPersistentContainer(name: modelName, managedObjectModel: managedObjectModel)
        
persistentContainer.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
  internalLogger.error("Unresolved error: \(error.localizedDescription), \(error.userInfo)")
  }
}
        
context = persistentContainer.newBackgroundContext()

This function is called from another function in another class like that:

    func insertMany(events: [AcquisitionEvent]) {
        if events.isEmpty { return }
        database.perform { context in
            do {
                events.forEach { event in self.create(context: context, event: event as! SensorEvent) }
                if context.hasChanges {
                    try context.save()
                }
            } catch let error {
                self.logger.error("Error while saving entity: \(error)")
            }
        }
    }

and insertMany function is called in another class like that:

    fileprivate func createMyDisposable() -> Disposable {
        return myService
            .observe()
            .buffer(timeSpan: .seconds(1), count: 200, scheduler: Schedulers.serialBackground)
            .subscribe(on: ConcurrentDispatchQueueScheduler.init(qos: .background))
            .observe(on: SerialDispatchQueueScheduler.init(qos: .background))
            .map(dao.insertMany)
            .subscribe()
    }

Created RxSwift disposable is added to DisposeBag and disposable is disposed in view controller's viewWillDisappear function and during willResignActiveNotification of the application in the App Delegate. It is resumed during viewWillAppear function of the view controller and in the willEnterForegroundNotification of the application in App Delegate.

I'm using asynchronous calls for all CPU intensive operations (database read, save, network requests, etc.) via RxSwift. I also use context.perform method, which is asynchronous in contrast to synchronous performAndWait.

This bug does not occur everytime, but randomly from time to time. App is working fine and then it crashes. I observed that switching the view controllers or putting app to background and then to foreground may cause these problems, but I'm stopping database operations before going to background and then I start them again after going to foreground.

Here is the trace from the thread, which crashed the app.

Thread 14 Queue : com.apple.runningboardservices.background-workloop (serial)
#0  0x00000001f2daf118 in _os_unfair_lock_corruption_abort ()
#1  0x00000001f2da9a20 in _os_unfair_lock_lock_slow ()
#2  0x000000019b1046c4 in objc_sync_enter ()
#3  0x000000018d952500 in -[RBSTarget shortDescription] ()
#4  0x000000018d9558cc in -[RBSAssertionDescriptor description] ()
#5  0x00000001842440f8 in _NS_os_log_callback ()
#6  0x000000019d2225d0 in _os_log_fmt_flatten_NSCF ()
#7  0x000000019d221d60 in _os_log_fmt_flatten_object ()
#8  0x000000019d21fb14 in _os_log_impl_flatten_and_send ()
#9  0x000000019d21db74 in _os_log ()
#10 0x000000019d223144 in _os_log_impl ()
#11 0x000000018d957ce0 in -[RBSConnection acquireAssertion:error:] ()
#12 0x000000018d961c90 in -[RBSAssertion acquireWithError:] ()
#13 0x000000019ccef05c in -[BKSAssertion acquire] ()
#14 0x000000019ccf0594 in -[BKSProcessAssertion acquire] ()
#15 0x00000001012006d4 in _dispatch_call_block_and_release ()
#16 0x00000001012023b4 in _dispatch_client_callout ()
#17 0x000000010120cb78 in _dispatch_workloop_invoke ()
#18 0x0000000101217e20 in _dispatch_workloop_worker_thread ()
#19 0x00000001f2db20f4 in _pthread_wqthread ()
Enqueued from com.apple.uikit.backgroundTaskAssertionQueue (Thread 12) Queue : com.apple.uikit.backgroundTaskAssertionQueue (serial)
#0  0x000000010120712c in dispatch_async ()
#1  0x000000018d942f20 in +[RBSWorkloop performBackgroundWork:] ()
#2  0x000000019ccf0f0c in -[BKSAssertion _acquireAsynchronously] ()
#3  0x000000019ccf0db0 in -[BKSProcessAssertion initWithBundleIdentifier:pid:flags:reason:name:withHandler:acquire:] ()
#4  0x000000019ccedc44 in -[BKSProcessAssertion initWithPID:flags:reason:name:withHandler:acquire:] ()
#5  0x00000001850c4efc in ___addBackgroundTask_block_invoke ()
#6  0x00000001012023b4 in _dispatch_client_callout ()
#7  0x00000001012138e4 in _dispatch_lane_barrier_sync_invoke_and_complete ()
#8  0x00000001850b9378 in _addBackgroundTask ()
#9  0x00000001850b5418 in -[UIApplication _beginBackgroundTaskWithName:expirationHandler:] ()
#10 0x000000018a0ab2e0 in +[NSPersistentStoreCoordinator _beginPowerAssertionNamed:withAssert:] ()
#11 0x000000018a067a90 in -[NSPersistentStoreCoordinator executeRequest:withContext:error:] ()
#12 0x000000018a039cd0 in -[NSManagedObjectContext save:] ()
#13 0x0000000102824d28 in closure #1 in DatabaseFacade.perform(_:) at /Users/pw/Development/wrk/dfp/ios/ios-sdk/iOS-SDK/Persistence/DatabaseFacade.swift:60
#14 0x0000000102824fe0 in thunk for @escaping @callee_guaranteed () -> () ()
#15 0x000000018a02b754 in developerSubmittedBlockToNSManagedObjectContextPerform ()
#16 0x00000001012023b4 in _dispatch_client_callout ()
#17 0x000000010120a540 in _dispatch_lane_serial_drain ()
#18 0x000000010120b290 in _dispatch_lane_invoke ()
#19 0x0000000101217e20 in _dispatch_workloop_worker_thread ()
#20 0x00000001f2db20f4 in _pthread_wqthread ()
#21 0x00000001f2db1e94 in start_wqthread ()
Enqueued from rx.global_dispatch_queue.serial (Thread 2) Queue : rx.global_dispatch_queue.serial (serial)
#0  0x0000000101206de0 in dispatch_async_f ()
#1  0x0000000102824b38 in DatabaseFacade.perform(_:) at /Users/pw/Development/wrk/dfp/ios/ios-sdk/iOS-SDK/Persistence/DatabaseFacade.swift:56
#2  0x00000001028d458c in SensorEventDao.insertMany(events:) at /Users/pw/Development/wrk/dfp/ios/ios-sdk/iOS-SDK/Persistence/Dao/SensorEventDao.swift:42
#3  0x00000001028d4e9c in protocol witness for Dao.insertMany(events:) in conformance SensorEventDao ()
#4  0x00000001028e5fc0 in implicit closure #2 in implicit closure #1 in UserDataAcquisition.createAccelerometerDisposable() at /Users/pw/Development/wrk/dfp/ios/ios-sdk/iOS-SDK/Acquisition/UserDataAcquisition.swift:112
#5  0x00000001028e5a4c in thunk for @escaping @callee_guaranteed (@guaranteed [AcquisitionEvent]) -> () ()
#6  0x00000001028e9d1c in partial apply for thunk for @escaping @callee_guaranteed (@guaranteed [AcquisitionEvent]) -> () ()
#7  0x00000001028e5a84 in thunk for @escaping @callee_guaranteed (@guaranteed [SensorEvent]) -> (@error @owned Error) ()
#8  0x00000001028e9d84 in partial apply for thunk for @escaping @callee_guaranteed (@guaranteed [SensorEvent]) -> (@error @owned Error) ()
#9  0x0000000101a92be0 in MapSink.on(_:) at /Users/pw/Development/wrk/dfp/ios/ifrodo/Pods/RxSwift/RxSwift/Observables/Map.swift:43
#10 0x0000000101a93414 in protocol witness for ObserverType.on(_:) in conformance MapSink<τ_0_0, τ_0_1> ()
#11 0x0000000101ab1664 in closure #1 in ObserveOnSerialDispatchQueueSink.init(scheduler:observer:cancel:) at /Users/pw/Development/wrk/dfp/ios/ifrodo/Pods/RxSwift/RxSwift/Observables/ObserveOn.swift:196
#12 0x0000000101ab1c68 in thunk for @escaping @callee_guaranteed (@guaranteed ObserveOnSerialDispatchQueueSink<τ_0_0>, @in_guaranteed Event<τ_0_0.ObserverType.Element>) -> (@out Disposable) ()
#13 0x0000000101a6fad8 in closure #1 in DispatchQueueConfiguration.schedule<τ_0_0>(_:action:) at /Users/pw/Development/wrk/dfp/ios/ifrodo/Pods/RxSwift/RxSwift/Schedulers/Internal/DispatchQueueConfiguration.swift:27
#14 0x0000000101a5d764 in thunk for @escaping @callee_guaranteed () -> () ()
#15 0x00000001012006d4 in _dispatch_call_block_and_release ()
#16 0x00000001012023b4 in _dispatch_client_callout ()
#17 0x000000010120a540 in _dispatch_lane_serial_drain ()
#18 0x000000010120b290 in _dispatch_lane_invoke ()
#19 0x0000000101217e20 in _dispatch_workloop_worker_thread ()
#20 0x00000001f2db20f4 in _pthread_wqthread ()
#21 0x00000001f2db1e94 in start_wqthread ()

I don't know what may be wrong. I read that Core Data is not thread-safe, so this may be causing an errors, but I don't know how can I use it in the multithreaded iOS application. Do you have any suggestions, how can I deal with this issue? Or maybe do you know any thread-safe alternatives to Core Data?

I will appreciate any help or answer.

Regards,

Piotr


Solution

  • As nobody answered my question, I'm going to answer it by myself, to provide you the possible solution if you encounter a similar issue.

    This bug is probably caused by concurrency issues regarding Apple's Core Data library which is part of the iOS SDK. In my project, I'm saving a lot of data concurrently into the database. I researched this topic and found out that Core Data is not thread-safe, so due to this fact, it may not be suitable for my use case and such concurrency errors may occur from time to time in non-deterministic way.

    I solved this problem by abstracting persistence layer in my project and then replacing Core Data with SQLite database via GRDB library. Author of GRDB claims that this database is thread-safe when we use appropriate methods and transactions, so basically switching to another persistence layer and using appropriate library to access it, fixed my issue.

    EDIT:

    Due to further project requirements, I replaced GRDB implementation with native sqlite3 implementation available in the Apple iOS SDK and I wrapped all database queries with RxSwift data types like Observables and Completables. After that, I could use appropriate schedulers to delegate operations to the specific thread with subscribe and observe methods. I also added SQLITE_OPEN_FULLMUTEX flag to the sqlite3_open_v2 method to allow to access database with multiple threads.

    To wrap up, both GRDB and sqlite3 + proper multithreaded implementation solves this issue.