Firebase FSnapshotUtilities crashes due to mutating a NSMutableDictionary while enumerating

StackOverflow https://stackoverflow.com/questions/23326161

  •  10-07-2023
  •  | 
  •  

문제

EDIT the solution, based upon the accepted answer, is to use mutableDeepCopy. You need to use this for any value being sent to Firebase's setValue, as well as any value coming back from observing changes. This is a known issue with Firebase's SDK and should be fixed soon.

@interface NSDictionary (DeepCopy) 

- (NSDictionary*)mutableDeepCopy {
  return (NSMutableDictionary *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)self, kCFPropertyListMutableContainers));
}

@end

I am developing an application using Firebase for real-time collaboration. The Firebase library is intermittently crashing due to a race condition where it mutates a NSMutableDictionary while enumerating it. I am posting it here for visibility, as well as the fact that Firebase prefers to use Stack Overflow as the primary method of bug reporting.

*** Collection <__NSDictionaryM: 0xd8198f0> was mutated while being enumerated.
2014-04-27 09:39:45.328 SharedNotesPro[29350:870b] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSDictionaryM: 0xd8198f0> was mutated while being enumerated.'
*** First throw call stack:
(
    0   CoreFoundation                      0x044711e4 __exceptionPreprocess + 180
    1   libobjc.A.dylib                     0x03f3e8e5 objc_exception_throw + 44
    2   CoreFoundation                      0x04500cf5 __NSFastEnumerationMutationHandler + 165
    3   SharedNotesPro                      0x003fe8f5 +[FSnapshotUtilities nodeFrom:withPriority:] + 1405
    4   SharedNotesPro                      0x003fe373 +[FSnapshotUtilities nodeFrom:] + 51
    5   SharedNotesPro                      0x003fe971 +[FSnapshotUtilities nodeFrom:withPriority:] + 1529
    6   SharedNotesPro                      0x003e2504 -[FRepo setInternal:newVal:withPriority:withCallback:andPutId:] + 298
    7   SharedNotesPro                      0x003e23af -[FRepo set:withVal:withPriority:withCallback:] + 165
    8   SharedNotesPro                      0x00402aaf __61-[Firebase setValueInternal:andPriority:withCompletionBlock:]_block_invoke + 174
    9   libdispatch.dylib                   0x047a07b8 _dispatch_call_block_and_release + 15
    10  libdispatch.dylib                   0x047b54d0 _dispatch_client_callout + 14
    11  libdispatch.dylib                   0x047a3047 _dispatch_queue_drain + 452
    12  libdispatch.dylib                   0x047a2e42 _dispatch_queue_invoke + 128
    13  libdispatch.dylib                   0x047a3de2 _dispatch_root_queue_drain + 78
    14  libdispatch.dylib                   0x047a4127 _dispatch_worker_thread2 + 39
    15  libsystem_pthread.dylib             0x04ae4dab _pthread_wqthread + 336
    16  libsystem_pthread.dylib             0x04ae8cce start_wqthread + 30
)
libc++abi.dylib: terminating with uncaught exception of type NSException

Now, I would assume this is my fault... except I've done everything conceivable to prevent it. First every single Firebase object I create is completely transient. That is, it is single-use (allocated for a single read/write operation). Also, when I load data from Firebase I create a mutable copy of the contents.

For reference, here are the save/load methods I have created; this exists in a base class I have created to serve as a thin wrapper around Firebase, which can load and save data. You can find the full .h and .m files in these gists. These are the only ways I interact with the Firebase SDK. Also note that the crash happens on a background thread.

- (void)save:(void (^)(BOOL success))completionHandler {
  Firebase *fb = [[Firebase alloc] initWithUrl:self.firebaseURL];
  [fb setValue:[self.contents copy]  withCompletionBlock:^(NSError *error, Firebase *ref) {
    if(completionHandler) {
      completionHandler(error ? NO : YES);
    }
  }];
}

- (void)save {
  [self save:nil];
}

- (void)load:(void (^)(BOOL success))block {
  Firebase *fb = [[Firebase alloc] initWithUrl:self.firebaseURL];
  [fb observeSingleEventOfType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
    _contents = [[snapshot.value isKindOfClass:[NSDictionary class]]?snapshot.value:@{} mutableCopy];
    block(_contents.allKeys.count > 0);
  }];
}
도움이 되었습니까?

해결책

EDIT: This should no longer be an issue as the latest Firebase SDK will clone your object synchronously within the setValue call. There's no longer any need to manually clone data before passing it to Firebase

Although you're calling "copy", this only does a "shallow" copy of the outermost NSDictionary and so if you have any NSDictionaries inside the outer NSDictionary, and you are modifying those, we can still hit this error when Firebase enumerates those inner NSDictionary objects, and from the callstack, it does look like we're enumerating one of the inner ones.

Firebase should really be doing this copy for you automatically so you don't have to worry about it. We have a bug opened to address that. But for now, you'll want to do a "deep copy" instead of a shallow copy. See here for some possible approaches: deep mutable copy of a NSMutableDictionary (the 2nd or 3rd answer look like decent possibilities).

다른 팁

EDIT: I believe I have found a potential cause of the exception:

I had a hunch that multiple transactions were trying to run locally on the same node and causing contention because of the tall stack trace. I ended up saving the currently running transactions in a set and testing for a running transaction at the node before starting another one. Here is the code:

@interface MyViewController ()

@property (nonatomic, strong) NSMutableSet *transactions;   // holds transactions to prevent contention
@property (nonatomic, strong) NSMutableDictionary *values;  // holds most recent values to avoid callback roundtrip

@end

@implementation MyViewController

-(NSArray*)firebasePathTokens:(Firebase*)firebase
{
    NSMutableArray  *tokens = [NSMutableArray array];

    while(firebase.name)
    {
        [tokens insertObject:firebase.name atIndex:0];

        firebase = firebase.parent;
    }

    return tokens;
}

// workaround for private firebase.path
-(NSString*)firebasePath:(Firebase*)firebase
{
    return firebase ? [@"/" stringByAppendingString:[[self firebasePathTokens:firebase] componentsJoinedByString:@"/"]] : nil;
}

- (void)runTransaction:(Firebase*)firebase
{
    NSString    *firebasePath = [self firebasePath:firebase];

    if([self.transactions containsObject:firebasePath])
    {
        NSLog(@"transaction already in progress: %@", firebasePath);

        return;
    }

    [self.transactions addObject:firebasePath];

    NSNumber    *myValue = @(42);

    [firebase runTransactionBlock:^FTransactionResult *(FMutableData *currentData) {
        currentData.value = myValue;

        return [FTransactionResult successWithValue:currentData];
    } andCompletionBlock:^(NSError *error, BOOL committed, FDataSnapshot *snapshot) {

        values[firebasePath] = snapshot.value;  // short example for brevity, the value should really be merged into a hierarchy of NSMutableDictionary at the appropriate node

        [self.transactions removeObject:firebasePath];
    } withLocalEvents:NO];
}

@end

I am getting this problem as well, here is my stack trace:

2014-05-01 12:18:31.641 MY_APP_NAME______[6076:60b] {
    UncaughtExceptionHandlerAddressesKey = (
    0   CoreFoundation                      0x030131e4 __exceptionPreprocess + 180
    1   libobjc.A.dylib                     0x02d928e5 objc_exception_throw + 44
    2   CoreFoundation                      0x030a2cf5 __NSFastEnumerationMutationHandler + 165
    3   MY_APP_NAME______                   0x000ecf53 -[FTree forEachChild:] + 290
    4   MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    5   MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    6   MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    7   MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    8   MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    9   MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    10  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    11  MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    12  MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    13  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    14  MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    15  MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    16  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    17  MY_APP_NAME______                   0x001127ea -[FRepo(Transaction) rerunTransactionQueue:atPath:] + 2888
    18  MY_APP_NAME______                   0x00111a7f -[FRepo(Transaction) rerunTransactionsAndUpdateVisibleDataForPath:] + 422
    19  MY_APP_NAME______                   0x001114c7 __50-[FRepo(Transaction) sendTransactionQueue:atPath:]_block_invoke + 3092
    20  MY_APP_NAME______                   0x000e61d6 -[FPersistentConnection ackPuts] + 286
    21  MY_APP_NAME______                   0x000e492a __38-[FPersistentConnection sendListen:::]_block_invoke + 778
    22  MY_APP_NAME______                   0x000e268a -[FPersistentConnection onDataMessage:withMessage:] + 465
    23  MY_APP_NAME______                   0x000d733a -[FConnection onDataMessage:] + 106
    24  MY_APP_NAME______                   0x000d7293 -[FConnection onMessage:withMessage:] + 282
    25  MY_APP_NAME______                   0x000d4ba4 -[FWebSocketConnection appendFrame:] + 402
    26  MY_APP_NAME______                   0x000d4c73 -[FWebSocketConnection handleIncomingFrame:] + 161
    27  MY_APP_NAME______                   0x000d4cab -[FWebSocketConnection webSocket:didReceiveMessage:] + 40
    28  MY_APP_NAME______                   0x000cfbe1 __31-[FSRWebSocket _handleMessage:]_block_invoke + 151
    29  libdispatch.dylib                   0x0366f7b8 _dispatch_call_block_and_release + 15
    30  libdispatch.dylib                   0x036844d0 _dispatch_client_callout + 14
    31  libdispatch.dylib                   0x03672047 _dispatch_queue_drain + 452
    32  libdispatch.dylib                   0x03671e42 _dispatch_queue_invoke + 128
    33  libdispatch.dylib                   0x03672de2 _dispatch_root_queue_drain + 78
    34  libdispatch.dylib                   0x03673127 _dispatch_worker_thread2 + 39
    35  libsystem_pthread.dylib             0x039b3dab _pthread_wqthread + 336
    36  libsystem_pthread.dylib             0x039b7cce start_wqthread + 30
);
}
2014-05-01 12:18:35.897 MY_APP_NAME______[6076:3e07] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSDictionaryM: 0x7c93e260> was mutated while being enumerated.'
*** First throw call stack:
(
    0   CoreFoundation                      0x030131e4 __exceptionPreprocess + 180
    1   libobjc.A.dylib                     0x02d928e5 objc_exception_throw + 44
    2   CoreFoundation                      0x030a2cf5 __NSFastEnumerationMutationHandler + 165
    3   MY_APP_NAME______                   0x000ecf53 -[FTree forEachChild:] + 290
    4   MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    5   MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    6   MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    7   MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    8   MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    9   MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    10  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    11  MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    12  MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    13  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    14  MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    15  MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    16  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    17  MY_APP_NAME______                   0x001127ea -[FRepo(Transaction) rerunTransactionQueue:atPath:] + 2888
    18  MY_APP_NAME______                   0x00111a7f -[FRepo(Transaction) rerunTransactionsAndUpdateVisibleDataForPath:] + 422
    19  MY_APP_NAME______                   0x001114c7 __50-[FRepo(Transaction) sendTransactionQueue:atPath:]_block_invoke + 3092
    20  MY_APP_NAME______                   0x000e61d6 -[FPersistentConnection ackPuts] + 286
    21  MY_APP_NAME______                   0x000e492a __38-[FPersistentConnection sendListen:::]_block_invoke + 778
    22  MY_APP_NAME______                   0x000e268a -[FPersistentConnection onDataMessage:withMessage:] + 465
    23  MY_APP_NAME______                   0x000d733a -[FConnection onDataMessage:] + 106
    24  MY_APP_NAME______                   0x000d7293 -[FConnection onMessage:withMessage:] + 282
    25  MY_APP_NAME______                   0x000d4ba4 -[FWebSocketConnection appendFrame:] + 402
    26  MY_APP_NAME______                   0x000d4c73 -[FWebSocketConnection handleIncomingFrame:] + 161
    27  MY_APP_NAME______                   0x000d4cab -[FWebSocketConnection webSocket:didReceiveMessage:] + 40
    28  MY_APP_NAME______                   0x000cfbe1 __31-[FSRWebSocket _handleMessage:]_block_invoke + 151
    29  libdispatch.dylib                   0x0366f7b8 _dispatch_call_block_and_release + 15
    30  libdispatch.dylib                   0x036844d0 _dispatch_client_callout + 14
    31  libdispatch.dylib                   0x03672047 _dispatch_queue_drain + 452
    32  libdispatch.dylib                   0x03671e42 _dispatch_queue_invoke + 128
    33  libdispatch.dylib                   0x03672de2 _dispatch_root_queue_drain + 78
    34  libdispatch.dylib                   0x03673127 _dispatch_worker_thread2 + 39
    35  libsystem_pthread.dylib             0x039b3dab _pthread_wqthread + 336
    36  libsystem_pthread.dylib             0x039b7cce start_wqthread + 30
)
libc++abi.dylib: terminating with uncaught exception of type NSException
2014-05-01 12:18:49.810 MY_APP_NAME______[6076:60b] {
    UncaughtExceptionHandlerSignalKey = 6;
}

It originally started because I was using setValue:withCompletionBlock to attempt to set a node containing a number representing a timestamp. It has various rules to determine whether the timestamp can be updated (if it's < now, etc). Here was my original code:

myValue = @(42);

[myFirebase setValue:myValue withCompletionBlock:^(NSError *error, Firebase *ref) {
    if(!error)
        myMostRecentValue = myValue;
    else
        [myFirebase observeSingleEventOfType:FEventTypeValue withBlock:^(FDataSnapshot *mySnapshot) {
            myMostRecentValue = mySnapshot.value;
        }];
}];

Unfortunately, I think there is an issue with Firebase that would sometimes result in this sequence:

value on server: 41
setValue: 42
    error: permission error
    observeSingleEventOfType: 42    // returns the attempted value 42 instead of the previous value 41
value on server: 41
app proceeds to inappropriate state with wrong value 42

I think that what is happening is that since I never called observeSingleEventOfType before calling setValue, there was no previous value for Firebase to fall back on when the setValue failed the Firebase rules. So it returns the attempted value instead of an "undefined" placeholder like null. I'm not sure if this is a bug or a feature, but it's something to be aware of. So I replaced that code with the following:

[myFirebase runTransactionBlock:^FTransactionResult *(FMutableData *currentData) {
    currentData.value = myValue;

    return [FTransactionResult successWithValue:currentData];
} andCompletionBlock:^(NSError *error, BOOL committed, FDataSnapshot *snapshot) {
    myMostRecentValue = snapshot.value;
} withLocalEvents:NO];

Which resulted in the NSMutableDictionary being mutated while being enumerated exception. The curious thing is that I'm just passing an NSNumber for the value, and that I'm not trying to set an NSMutableDictionary of my own inside the runTransactionBlock. However, myMostRecentValue is inside of an NSMutableDictionary, but I only set that in the andCompletionBlock so it shouldn't matter.

The only thing I can think of is that maybe I sometimes have two or more transactions running on the same node, or maybe one is running on a parent while another is running on a child. This may be happening because I could be installing listeners as I segue between view controllers if the old view controllers are not unloaded. This is hard for me to test though so it's only a theory.

Not sure if it helps but here is a mutableDeepCopy category function I use to copy the values from Firebase into a local NSMutableDictionary that I use to cache the most recently known values (for example in an observeSingleEventOfType callback):

// category to simplify getting a deep mutableCopy
@implementation NSDictionary(mutableDeepCopy)

- (NSMutableDictionary*)mutableDeepCopy
{
    NSMutableDictionary *returnDict = [[NSMutableDictionary alloc] initWithCapacity:self.count];

    for(id key in [self allKeys])
    {
        id oneValue = [self objectForKey:key];

        if([oneValue respondsToSelector:@selector(mutableDeepCopy)])
            oneValue = [oneValue mutableDeepCopy];
        else if([oneValue respondsToSelector:@selector(mutableCopy)] && ![oneValue isKindOfClass:[NSNumber class]]) // workaround for -[__NSCFNumber mutableCopyWithZone:]: unrecognized selector sent to instance
            oneValue = [oneValue mutableCopy];
        else
            oneValue = [oneValue copy];

        [returnDict setValue:oneValue forKey:key];
    }

    return returnDict;
}

Sometimes I need to avoid a roundtrip in viewDidLoad so I put the last known value in a GUI element until I get the callback for the new value. I can't imagine this would affect Firebase, but perhaps something low level is expecting NSDictionary and chokes because it has a reference to a portion of my NSMutableDictionary that I gave it?

I'm kind of stuck until a solution is found, so hope this helps, thanks!

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top