1

I'm getting a "RUNNINGBOARD 0xdead10cc" error intermittently when my app is in the background and I'm using Realm database. I suspect the error is related to file locking during database writes. I have tried adding sleep to the write block, but the error has not occurred during testing.

Here's a code snippet of the function I'm using to update my Realm database:

func update(completion: @escaping () -> ()) {
    let dispatchGroup = DispatchGroup()
    array1 = Array(Set(self.arrayData))
    for element in array1 {
        dispatchGroup.enter()
        element.updateRealmModel {
                dispatchGroup.leave()
        }
    }
    dispatchGroup.notify(queue: .main) {
        completion()
    }
    
}

and the element function:

func updateRealmModel(completion: @escaping(() -> Void)) {
    let primaryKey = self.id
    DispatchQueue.global(qos: .userInitiated).async {
        let realm = try! Realm()
        if let user = realm.object(ofType: Mytype, forPrimaryKey: primaryKey) {
            do {
                try realm.write {
                    user.boolField = false
                }
            } catch let error {
                debugPrint(error)
            }
        }
        completion()

    }

}

Can anyone help me reproduce and resolve this error? Are there any known issues related to Realm database writes when an app is in the background? I would appreciate any insights or suggestions on how to debug and fix this issue.

MSm
  • 125
  • 4
  • The deadlock (0xdead10cc) is explained [here](https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report/#EXCCRASH-SIGKILL). Have you [extended your app’s background execution time](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/extending_your_app_s_background_execution_time/)? – Rob May 10 '23 at 03:45
  • By the way, how long do these writes take? The `beginBackgroundTask` only gives you an extra 30 sec or so… – Rob May 10 '23 at 04:31
  • The writing takes no time, it's just set a boolean variable to false. Is there a way to prevent the deadlock without ensuring the writing will finish? (it's more important to me avoid crash than finish the writing task) – MSm May 10 '23 at 06:52
  • Just use that `beginBackgroundTask` (as outlined in the above link), and your app will continue to execute in the background. By the way, I now notice that `realm.write` appears to be asynchronous, but you are not waiting for realm to finish before calling the completion handler. Don't call the completion handler until the asynchronous task is done. That is the whole purpose of the completion block closure, so the caller knows when the asynchronous work is done. – Rob May 10 '23 at 07:27
  • By the way, if `write` is asynchronous, it begs the question why one would use the global queue, at all. – Rob May 10 '23 at 13:38
  • which part? dispatchGroup.notify(queue: .main) or DispatchQueue.global(qos: .userInitiated).async ? – MSm May 10 '23 at 16:46
  • 1
    Hey, I take that back! The overwhelming vast majority of time, when you see a method with a closure parameter, that is because it is an asynchronous method. Turns out, the Realm `write` method is *not* asynchronous. Wow, that is a non-Swifty pattern!! So please disregard my comment about “if `write` is asynchronous…”, because it ain’t! – Rob May 11 '23 at 04:57

2 Answers2

0

I don't believe the RUNNINGBOARD error is directly related to Realm in this use case.

It doesn't appear those DispatchQueues are needed and in some cases, can be incredibly hard to manage - along with the completion handler, whoaaa.

Since the primary key of each user to be updated is known, there's no reason to query for the user - you already know who they are by their primary key.

It appears to be a small amount of data and if he array is reasonably sized, my first suggestion is to simply update each user object directly within the loop via their primary key.

array1 = Array(Set(self.arrayData))
for element in array1 {
   let _id = element.id
   try! realm.write {
      realm.create(UserClass, value: ["_id": _id, "boolField": false], update: .modified)
   }
}

The above will update the boolField property to false on each user with a primary key found in array1, and not modify any other properties.

That being said, if it's a large loop, that can tie up the UI so you could also dump the whole process onto a single background thread

DispatchQueue(label: "background").async {
    autoreleasepool {
       //write the data

which is a technique we use when the dataset is huge.

Another option is to leverage the writeAsync function to update objects asynchronously

A nice explanation and example code can be found here Update Objects Asynchronously

Jay
  • 34,438
  • 18
  • 52
  • 81
0

Apple’s Understanding the exception types in a crash report describes the 0xdead10cc as follows:

  • 0xdead10cc (pronounced “dead lock”). The operating system terminated the app because it held on to a file lock or SQLite database lock during suspension. Request additional background execution time on the main thread with beginBackgroundTask(withName:expirationHandler:). Make this request well before starting to write to the file in order to complete those operations and relinquish the lock before the app suspends. In an app extension, use beginActivity(options:reason:) to manage this work.

See Extending your app’s background execution time for guidance on how to keep your app running in the background for a short period (30s, last time I checked) after the user leaves the app.

For example, you could call your existing update method, and use its completion handler to determine when to call endBackgroundTask(_:). Perhaps:

func updateAndContinueInBackground() {
    var identifier: UIBackgroundTaskIdentifier = .invalid
    identifier = UIApplication.shared.beginBackgroundTask(withName: Bundle.main.bundleIdentifier! + ".realm.updates") {
        if identifier != .invalid {
            UIApplication.shared.endBackgroundTask(identifier)
            identifier = .invalid
        }
    }

    update {
        if identifier != .invalid {
            UIApplication.shared.endBackgroundTask(identifier)
            identifier = .invalid
        }
    }
}

This is the bare minimum (informing the OS that you want a little more time with beginBackgroundTask(withName:expirationHandler:); telling it when you are done with endBackgroundTask(_:).


Frankly, while it might be a bit of an edge-case scenario, you probably want to handle the timeout scenario, where that expirationHandler might get called. If the OS calls that closure because the app has run out of time, you might want to cancel/rollback updates that are still in progress and did not complete in time. Maybe you should use the id returned by writeAsync to cancel just those which have not yet completed. Or perhaps wrap the updates in a transaction (with beginWrite followed by either commitWrite or cancelWrite). There are a number of cancelation patterns that Realm offers.

Rob
  • 415,655
  • 72
  • 787
  • 1,044