Realm-cocoa: Sequential calls to realm.write() sometimes throw an objc exception

Created on 13 Apr 2017  路  21Comments  路  Source: realm/realm-cocoa

In the context of a TestFlight beta-test, I get a significant number (a few each day) of crash reports similar to this one:

````
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0

Last Exception Backtrace:
0 CoreFoundation 0x2596b916 __exceptionPreprocess + 122 (NSException.m:162)
1 libobjc.A.dylib 0x25106e12 objc_exception_throw + 34 (objc-exception.mm:531)
2 Realm 0x520e68 -[RLMRealm beginWriteTransaction] + 180 (RLMRealm.mm:446)
3 RealmSwift 0x3eb150 _TFC10RealmSwift5Realm5writefzFzT_T_T_ + 44 (Realm.swift:153)
4 LapMonitor 0xe80fc _TTSf4g_n___TFC10LapMonitor16ActiveLapSessionP33_35B0DADE8A11F20A2053872E3981933117updateSessionDatafT20forDeviceSessionInfoVS_21LapMonitorSessionInfo_T_ + 972 (LapSession.swift:639)
5 LapMonitor 0xeafc8 _TTSf4d_g_n___TFC10LapMonitor16ActiveLapSession10lapMonitorfTCS_10LapMonitor20didUpdateSessionInfoVS_21LapMonitorSessionInfo_T_ + 360 (LapSession.swift:0)
6 LapMonitor 0xe13d8 _TTWC10LapMonitor16ActiveLapSessionS_24LapMonitorSessionHandlerS_FS1_10lapMonitorfTCS_10LapMonitor20didUpdateSessionInfoVS_21LapMonitorSessionInfo_T_ + 112 (LapSession.swift:0)
7 LapMonitor 0xd0c60 _TTSf4g_n___TFC10LapMonitor10LapMonitorP33_25576D73CF51C6E590B13A2BA772807319handleStatusMessagefVS0_P33_25576D73CF51C6E590B13A2BA772807323LapMonitorStatusMessageT_ + 564 (LapMonitor.swift:263)
8 LapMonitor 0xd25c0 _TTSf4d_g_d_n___TFC10LapMonitor10LapMonitor10peripheralfTCSo12CBPeripheral17didUpdateValueForCSo16CBCharacteristic5errorGSqPs5Error___T_ + 836 (LapMonitor.swift:0)
9 LapMonitor 0xcea44 _TToFC10LapMonitor10LapMonitor10peripheralfTCSo12CBPeripheral17didUpdateValueForCSo16CBCharacteristic5errorGSqPs5Error___T_ + 68 (LapMonitor.swift:0)
10 CoreBluetooth 0x2aa70cc0 -[CBPeripheral handleAttributeEvent:args:attributeSelector:delegateSelector:delegateFlag:] + 124 (CBPeripheral.m:644)
11 CoreBluetooth 0x2aa70d5c -[CBPeripheral handleCharacteristicEvent:characteristicSelector:delegateSelector:delegateFlag:] + 68 (CBPeripheral.m:666)
12 CoreBluetooth 0x2aa70e96 -[CBPeripheral handleCharacteristicValueUpdated:] + 70 (CBPeripheral.m:703)
13 CoreBluetooth 0x2aa769ee __29-[CBXpcConnection handleMsg:]_block_invoke + 58 (CBXpcConnection.m:227)
14 libdispatch.dylib 0x254d981e _dispatch_call_block_and_release + 6 (init.c:757)
15 libdispatch.dylib 0x254d980a _dispatch_client_callout + 18 (init.c:817)
16 libdispatch.dylib 0x254e7ba4 _dispatch_main_queue_callback_4CF$VARIANT$mp + 1520 (inline_internal.h:1063)
17 CoreFoundation 0x2592db68 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 4 (CFRunLoop.c:1612)
18 CoreFoundation 0x2592c062 __CFRunLoopRun + 1570 (CFRunLoop.c:2718)
19 CoreFoundation 0x2587b224 CFRunLoopRunSpecific + 516 (CFRunLoop.c:2814)
20 CoreFoundation 0x2587b010 CFRunLoopRunInMode + 104 (CFRunLoop.c:2844)
21 GraphicsServices 0x26e6bac4 GSEventRunModal + 156 (GSEvent.c:2245)
22 UIKit 0x29f4f184 UIApplicationMain + 140 (UIApplication.m:3772)
23 LapMonitor 0x6d208 main + 132 (LapMonitorData.swift:12)
24 libdyld.dylib 0x2552386e tlv_get_addr + 42 (threadLocalHelpers.s:311)
````

All Realm write transaction in the app use realm.write and are called from the main thread, so it seems very unlikely that a previous write transaction was left open.

I could not so far reproduce the issue in a debug environment, so any advice about how to add instrumentation to the code so that this issue can be further investigated via crash logs is welcome. 馃榾

Note that this looks very similar to #3831, that was supposed to be fixed in v1.0.2.

Any idea of what in the app could cause this ?

This issue is problematic because -if my understanding is correct - the objc exception thrown by -[RLMRealm beginWriteTransaction] can not be catched in Swift , so the app will inevitably crash when this occurs.

Version of Realm and Tooling

Realm framework version: RealmSwift 2.4.x, RealmSwift 2.5.1

Realm Object Server version: n/a

Xcode version: Xcode 8.2, Xcode 8.3.1

iOS versions / devices:

  • iOS 9.3.5 (13G36) - iPhone4,1
  • iOS 10.2.1 (14D27) - iPhone7,2
  • iOS 10.3 (14E277) - iPhone7,2
  • iOS 10.3.1 (14E304) - iPhone6,2 iPhone7,2 iPhone8,4
O-Community Pipeline-On-Hold Reproduction-Required T-Bug

Most helpful comment

I had the same problem, but found a fairly simple solution that is working very well for me. Basically, check if isInWriteTransaction and, if so, first call realm.refresh(). I have the following write function in my DAO (data access object) base-class:

public func write(_ block: ((Realm) throws -> Swift.Void)) throws {
   let realm = self.realm()
   if realm.isInWriteTransaction {
      try! block(realm)
   } else {
      realm.doWriteTransaction {
         try! block(realm)
      }
   }
}

which relies on the following Realm extension (where the secret sauce is the refresh() call):

extension Realm {
   public func doWriteTransaction(file: String = #file, line: Int = #line, function: String = #function, _ block: (() throws -> Void)) {
      // Work around the crash that occurs when two write transactions are called in the same runloop.
      refresh()
      precondition(!isInWriteTransaction, "realm.write() was called when isInWriteTransaction == true")
      do {
         try self.write(block)
      } catch let error as NSError {
         let errorString = "Realm write error in \(function) (\(file) line \(line)): \(error)"
         ... handleErrorAnyWayYouPrefer(error, ...)
      }
   }
}

All 21 comments

Hi @jlj. I wanted to let you know that we've received your issue and that someone will review what you've shared and follow-up soon.

I finally succeeded in catching this objc exception under debugger (with the exact same callstack as listed above).

This confirms that the (default and only) realm considers to be in a write transaction:

2017-04-14 19:58:02.645282+0200 LapMonitor[1341:277860] *** Terminating app due to uncaught exception 'RLMException', reason: 'Cannot create asynchronous query while in a write transaction'

But:

  • I triple-checked that every write transaction in the app is encapsulated in a call to realm.write {}.

    • The exception callstack does not show any sign of a recursive call to realm.write {}.

How can the program possibly be in a write transaction?

Hi @jlj!

Hmm, that error message would seem to indicate you're trying to set up a notification block inside a write transaction. Can you confirm if that's the case? :)

Thanks!

Hi @TimOliver

No, write transaction blocks are limited to setting fields or adding objects to the database, and I have already checked that all notification blocks in the app are not set up inside a write transactions.

Fact which is btw confirmed by the callstack, since, as mentioned earlier in this thread, all write transaction are encapsulated by passing closures to realm.write() calls, and we would certainly see, if this were the case, more than one call to realm in the callstack. :)

@TimOliver @jpsim Are there cases where realm.write() could leave a write transaction uncommitted after returning?

@jlj Yes, that's possible. We've had issues in the past before where a user somehow ended up with a write transaction that failed on commitWrite() and so it stayed open to the point where it caused an exception when a second write transaction attempt was made at a later point in the app's execution. We weren't ever able to work with them to isolate why the write transaction was failing.

That being said, the exception message was different in that case (that one was failing since it said it's not possible to open 2 write transactions at the same time on the same thread), so I'm not sure if this is actually the same problem.

@TimOliver Thanks for your answer. I agree with you that the past issue that you mention is a different problem, as I have not seen any sign of a failed commitWrite() in my case.

On my side, I am pretty confident that I have identified the sequence in my app that caused the exception: under specific circumstances, two successive realm.write() transactions were executed sequentially in the same run loop occurence (triggered by the same Bluetooth message) and - in rare cases - the second write transaction threw an exception in -[RLMRealm beginWriteTransaction] because isInWriteTransaction was still considered as true.

This is confirmed by the exception callstack, which always corresponds to the second write transaction.

Which means that there are cases in which realm.write() succeeds and returns without throwing any error, but during some time after it returns, isInWriteTranaction can still be tested as true, and starting a new write transaction may crash the program.

An interesting fact is that the crash was reported to be much more frequent (compared to the number of application sessions) on some device types, and especially on the old and slower iPhones 4S.

To confirm this analysis, I have released a new version of my app under TestFlight, that combine the two successive write transactions into one. No crash reported so far, but let's wait a couple of days to be sure. :)

I just renamed the issue with a more specific title. 馃槑

To confirm this analysis, I have released a new version of my app under TestFlight, that combine the two successive write transactions into one. No crash reported so far, but let's wait a couple of days to be sure. :)

This is confirmed: merging the two sequential write transactions removes the crash, as shown by build 25 in this TestFlight screenshot (every crash in the 3 previous builds had a callstack identical to the one in the first message above).

Screen capture 2017-04-19 a 17 01 52

So I suppose that further investigations about this issue should now be on the Realm team side. :)

Yeah, we can't do anything with this until we can reproduce it. It sounds impossible and there's nothing that jumps out as causing this in our code.

I have the same problem. When i try set property in first transaction(by .write) and second transaction add new object with the same runloop app crashes. When i merge transaction everything works fine. Realm refresh helps.

Hi @misha83, we have yet to receive steps to reproduce. Could you please share an Xcode project with numbered steps that we can take that would trigger this? Thanks!

We also see this in our app. The circumstances are very similar the ones @jlj described.

I was able to track this down further to the following lines in our code:

    func delete(object: Object) {
        autoreleasepool {
            if let realm = try? Realm(), !object.isInvalidated {
                realm.beginWrite() // Here we crash, but only some time after try realm.commitWrite() failed for a yet unknown reason
                realm.delete(object)

                do {
                    try realm.commitWrite() // This seems to fail sometimes
                } catch {
                    Log.e("Error while deleting object")
                }
            }
        }
    }

At first, I was curios how a realm which was opened a line before realm.beginWrite() could be in a transaction already. This is until I noticed, if there is something failing when calling realm.commitWrite() one of the NEXT calls to the delete(object: Object) fails with this stacktrace.

Is a aborted transaction not canceled? Do I need to cancel it via realm.cancelWrite() in the catch?

If Realm.commitWrite() throws an Error, the write transaction is automatically canceled and Realm.isInWriteTransaction will return false. We have some unit tests that validate this behavior, such as this one: https://github.com/realm/realm-cocoa/blob/v2.8.3/Realm/Tests/RealmTests.mm#L1353-L1374

I did some field tests (as I can't reproduce this in the testing env). When I add a

if realm.isInWriteTransaction {
    realm.cancelWrite()
}

in front of the realm.beginWrite() these errors seem to be less frequent. Currently we have like 0.5-1% of our sessions crashing with that. So far (after a day of testing) only around 0.05% of all sessions see this.

I will do another test and add a realm.cancelWrite() in the catch block and see if these errors further reduce.

If I call Realm() two times on the same thread - do I get the same realm instance? Or two independent? I guess first.

I can see this problem again, and it still looks related with very close back-to-back write transactions done in the main thread.

All realm write transactions done in the app now use the instrumented method below:

````
extension Realm {

func doWriteTransaction(file : String = #file, line: Int = #line, function: String = #function, _ block: (() throws -> Void)) {

    let logTransactionString = "Realm.doWriteTransaction called from \(function) (\(file) line \(line))"
    NSLogDebug("%@", logTransactionString)

    precondition(!isInWriteTransaction, "realm.write() was called when isInWriteTransaction == true")

    do {
        try self.write (block)
    }
    catch let error as NSError {
        let errorString = "Realm write error in \(function) (\(file) line \(line)): \(error)"
        NSLog("%@", errorString)
        preconditionFailure(errorString)
    }
}

}
````

Note the precondition call that checks that the realm doesn't consider to be in a write transaction. This precondition never triggers.

However, I still sometimes crash on the same objc exception, on the try self.write (block).

Here is the trace when this occurs (with the NSLog and the exception):

2017-07-24 22:46:58.362042+0200 LapMonitor[2642:913141] Realm.doWriteTransaction called from lapMonitor(_:didReceiveLapEvent:) (/Users/jean-luc/Developpement/Xcode apps/iOS apps/LapMonitor/LapMonitor/LapSession.swift line 934) 2017-07-24 22:50:04.049373+0200 LapMonitor[2642:913141] *** Terminating app due to uncaught exception 'RLMException', reason: 'Cannot create asynchronous query while in a write transaction'

And the callstack:

````
(lldb) thread backtrace

  • thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
    frame #0: 0x000000018e811014 libsystem_kernel.dylib__pthread_kill + 8 frame #1: 0x000000018e8db264 libsystem_pthread.dylibpthread_kill + 112
    frame #2: 0x000000018e7859c4 libsystem_c.dylibabort + 140 frame #3: 0x000000018e2511b0 libc++abi.dylibabort_message + 132
    frame #4: 0x000000018e26ac04 libc++abi.dylibdefault_terminate_handler() + 304 frame #5: 0x000000018e278820 libobjc.A.dylib_objc_terminate() + 124
    frame #6: 0x000000018e2675d4 libc++abi.dylibstd::__terminate(void (*)()) + 16 frame #7: 0x000000018e266ef8 libc++abi.dylib__cxa_throw + 136
    frame #8: 0x000000018e27866c libobjc.A.dylibobjc_exception_throw + 364 frame #9: 0x000000010096b6d4 Realm-[RLMRealm beginWriteTransaction] + 80
    frame #10: 0x00000001005ea5e8 RealmSwiftRealmSwift.Realm.write (() throws -> ()) throws -> () + 48 <ul> <li>frame #11: 0x000000010012ac90 LapMonitor<code>md5-8f847c83ce9898ec1ec47441e25e789f</code>partial apply forwarder for LapMonitor.ActiveLapSession.(lapMonitor (LapMonitor.LapMonitor, didReceiveLapEvent : LapMonitor.LapMonitorEventInfo) -> ()).(closure #1) at LapSession.swift, self=0x000000017022c080) throws -> ()) -> () at RealmWrite.<a href="swift:21">swift:21</a><br /> frame #12: 0x000000010011624c LapMonitor<code>md5-65a562a4e1245958e3473e3d0ea1eb57</code>protocol witness for LapMonitorSessionHandler.lapMonitor(LapMonitor, didReceiveLapEvent : LapMonitorEventInfo) -> () in conformance ActiveLapSession at LapSession.<a href="swift:0">swift:0</a><br /> frame #14: 0x00000001000ea154 LapMonitor<code>md5-a3dbc5587accf1b65d205fcebe641f33</code>LapMonitor.peripheral(peripheral=0x00000001702e9a80, characteristic=0x00000001702ad0e0, error=nil, self=0x00000001701b3a20) -> () at LapMonitor.<a href="swift:639">swift:639</a><br /> frame #16: 0x00000001000ef014 LapMonitor<code>md5-8fd4b616874a83bdcd2f6c1bd3393295</code>-[CBPeripheral handleAttri<a href="buteEvent:args">buteEvent:args</a>:attribut<a href="eSelector:delegateSelector">eSelector:delegateSelector</a>:delegateFlag:] + 248<br /> frame #18: 0x00000001966f0444 CoreBluetooth<code>md5-7fc726ea36873f623a9b3e5af4b3ebcc</code>-[CBPeripheral <a href="handleMsg:args">handleMsg:args</a>:] + 320<br /> frame #20: 0x00000001966e7de8 CoreBluetooth<code>md5-af1dcc860b4f1bcdffe1ce4197377713</code>-[CBManager xpcConnectionDidR<a href="eceiveMsg:args">eceiveMsg:args</a>:] + 116<br /> frame #22: 0x00000001966f46a0 CoreBluetooth<code>md5-39d5aa126f991fa4f8005ba7857e8413</code>_dispatch_call_block_and_release + 24<br /> frame #24: 0x00000001028a1a10 libdispatch.dylib<code>md5-49441312d489d7d70c79812e342e663e</code>_dispatch_queue_serial_drain + 1140<br /> frame #26: 0x00000001028a5634 libdispatch.dylib<code>md5-c6f96ae1a01e2c768a8444dc8ff35f2a</code>_dispatch_main_queue_callback_4CF + 784<br /> frame #28: 0x000000018f7c50c8 CoreFoundation<code>md5-bf3f3aadcb814864d3d4caa21dca6c12</code>__CFRunLoopRun + 1572<br /> frame #30: 0x000000018f6f2da4 CoreFoundation<code>md5-f6a9fb31f1df8a30563b5feb73cf8c34</code>GSEventRunModal + 100<br /> frame #32: 0x00000001959ad058 UIKit<code>md5-f619744dcdf11eeeae9e435f63213d7b</code>main at AppDelegate.<a href="swift:12">swift:12</a><br /> frame #34: 0x000000018e70159c libdyld.dylibstart + 4

    ````

Realm framework version: RealmSwift 2.8.3

How can we progress on this?

I'm running into the exact same problem, with RealmSwift 2.8.3. Actions triggered from UI cause sequential (non-overlapping) realm.write { ... } blocks to be executed, and without fail the second realm.write { ... } block will fail with exception.

Interestingly enough, if I call realm.refresh() before starting the second realm.write { ... }, then the crash goes away.

This seems to be related to notifications (and, yes, I'm dead sure that I'm not adding a notification inside of a write transaction), as it only occurs if I have subscriptions (via RxRealm) to property changes on an object being modified.

We faced with the same problem, 100% reproduced in our project. Steps to reproduce:

  1. Create by RXSwift two observables and concat them, perform on MainScheduler
  2. Both observable should be as deferred and should write/delete items in realm
  3. Before this create notification subscription to the fields that modified in step 2
  4. Subscription should check numbers of items in realm as reloadData() does.

Expected result: SIGABRT

Workaround: Combining 2 write transaction to 1 resolves the issue.

Reproduced on Environment 1:
Realm: 3.3.0
RealmSwift: 3.3.0
RxRealm: 0.7.5
RxSwift: 4.2.0

* Reproduced on Environment 2:*
Realm: 3.7.5
RealmSwift: 3.7.5
RxRealm: 0.7.5
RxSwift: 4.2.0

I had the same problem, but found a fairly simple solution that is working very well for me. Basically, check if isInWriteTransaction and, if so, first call realm.refresh(). I have the following write function in my DAO (data access object) base-class:

public func write(_ block: ((Realm) throws -> Swift.Void)) throws {
   let realm = self.realm()
   if realm.isInWriteTransaction {
      try! block(realm)
   } else {
      realm.doWriteTransaction {
         try! block(realm)
      }
   }
}

which relies on the following Realm extension (where the secret sauce is the refresh() call):

extension Realm {
   public func doWriteTransaction(file: String = #file, line: Int = #line, function: String = #function, _ block: (() throws -> Void)) {
      // Work around the crash that occurs when two write transactions are called in the same runloop.
      refresh()
      precondition(!isInWriteTransaction, "realm.write() was called when isInWriteTransaction == true")
      do {
         try self.write(block)
      } catch let error as NSError {
         let errorString = "Realm write error in \(function) (\(file) line \(line)): \(error)"
         ... handleErrorAnyWayYouPrefer(error, ...)
      }
   }
}

Realm, RxSwift, etc. versions I'm using are as follows:

github "realm/realm-cocoa"               == 3.7.5 
github "RxSwiftCommunity/RxRealm"        == 0.7.5
github "ReactiveX/RxSwift"               == 4.2.0
github "RxSwiftCommunity/RxSwiftExt"     == 3.3.0
Was this page helpful?
0 / 5 - 0 ratings