Open ww1234 opened 7 years ago
Thanks for the full stack trace. Can you post your code starting with just before the call to 25 Elephant -[FMDatabaseQueue inDatabase:] (FMDatabaseQueue.m:176) and then through it? It looks like you're calling beginTransaction: inside an an inDatabase: call. Are you using two FMDatabaseQueues?
my code like this:
FMDatabaseQueue *dbQueue1 = [FMDatabase databaseQueueWithPath:path1];
FMDatabaseQueue *dbQueue2 = [FMDatabase databaseQueueWithPath:path2];
[dbQueue1 inDatabase:^(FMDatabase *db) {
[db exeQuery:....];
......
[dbQueue2 inTransaction:^(FMDatabase *db, BOOL *rollback) {
[db exeQuery:....];
....
}];
}];
this is a online crash, and i can't reproduce
Sorry, I can't really help without more information. I'd try my best to see if you can reproduce it.
My app ran into a similar crash. I was able to repro this with something like this:
for (NSInteger j = 10000 - 1; j >= 0; j--) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
[q inTransaction:^(FMDatabase *db, BOOL *rollback) {
NSDictionary *params = @{
@"col2" : [NSString stringWithFormat:@"%@", @(j)],
@"col3" : [NSString stringWithFormat:@"%@", @(j)],
@"col4" : [NSString stringWithFormat:@"%@", @(j)],
@"col5" : [NSString stringWithFormat:@"%@", @(j)],
@"col6" : [NSNull null],
@"col7" : [NSString stringWithFormat:@"%@", @(j)],
@"col8" : [NSString stringWithFormat:@"%@", @(j)],
@"col9" : [NSString stringWithFormat:@"%@", @(j)],
@"col10" : [NSString stringWithFormat:@"%@", @(j)]
};
assert([db executeUpdate:kInsertQuery withParameterDictionary:params]);
NSLog(@"last row id = %@", @(db.lastInsertRowId));
FMResultSet *rs = [db executeQuery:@"SELECT * FROM test_table ORDER BY col1 DESC LIMIT 1"];
assert([rs next]);
NSLog(@"row added: %@ (%@)", [rs stringForColumnIndex:0], [NSThread currentThread]);
}];
usleep(100 * 1000); // 100ms
}
});
}
This usually results in either updates starting to run into errors, or a crash that looks like this:
* thread #6: tid = 0x2d51c, 0x00000001995edf58 libsqlite3.dylib`___lldb_unnamed_symbol153$$libsqlite3.dylib + 96, queue = 'fmdb.<FMDatabaseQueue: 0x125d3d150>', stop reason = EXC_BAD_ACCESS (code=1, address=0x100000001)
frame #0: 0x00000001995edf58 libsqlite3.dylib`___lldb_unnamed_symbol153$$libsqlite3.dylib + 96
frame #1: 0x00000001995c3bb0 libsqlite3.dylib`___lldb_unnamed_symbol79$$libsqlite3.dylib + 116
frame #2: 0x0000000199636fd4 libsqlite3.dylib`___lldb_unnamed_symbol518$$libsqlite3.dylib + 72
frame #3: 0x00000001995ab94c libsqlite3.dylib`___lldb_unnamed_symbol40$$libsqlite3.dylib + 12828
frame #4: 0x00000001995a8634 libsqlite3.dylib`___lldb_unnamed_symbol39$$libsqlite3.dylib + 284
frame #5: 0x00000001995a7734 libsqlite3.dylib`___lldb_unnamed_symbol36$$libsqlite3.dylib + 240
frame #6: 0x00000001995a6fa4 libsqlite3.dylib`___lldb_unnamed_symbol35$$libsqlite3.dylib + 536
frame #7: 0x00000001995a6c5c libsqlite3.dylib`___lldb_unnamed_symbol34$$libsqlite3.dylib + 352
* frame #8: 0x000000010009234c Tester`-[FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:](self=0x0000000125d43a90, _cmd="executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:", sql=@"INSERT INTO test_table ( col2, col3, col4, col5, col6, col7, col8, col9, col10 ) VALUES ( :col2, :col3, :col4, :col5, :col6, :col7, :col8, :col9, :col10 )", outErr=0x0000000000000000, arrayArgs=0x0000000000000000, dictionaryArgs=9 key/value pairs, args=0x0000000000000000) + 716 at FMDatabase.m:968
frame #9: 0x0000000100093738 Tester`-[FMDatabase executeUpdate:withParameterDictionary:](self=0x0000000125d43a90, _cmd="executeUpdate:withParameterDictionary:", sql=@"INSERT INTO test_table ( col2, col3, col4, col5, col6, col7, col8, col9, col10 ) VALUES ( :col2, :col3, :col4, :col5, :col6, :col7, :col8, :col9, :col10 )", arguments=9 key/value pairs) + 128 at FMDatabase.m:1183
frame #10: 0x0000000100088358 Tester`__39-[RCContainedViewController testMethod]_block_invoke_3((null)=0x000000016ee4eda8, db=0x0000000125d43a90, rollback=NO) + 1556 at ViewController.m:262
frame #11: 0x000000010008c8e4 Tester`__46-[FMDatabaseQueue beginTransaction:withBlock:]_block_invoke((null)=<unavailable>) + 288 at FMDatabaseQueue.m:201
frame #12: 0x0000000100121d30 libdispatch.dylib`_dispatch_client_callout + 16
frame #13: 0x000000010012d55c libdispatch.dylib`_dispatch_barrier_sync_f_slow + 908
frame #14: 0x000000010008c77c Tester`-[FMDatabaseQueue beginTransaction:withBlock:](self=0x0000000125d3d150, _cmd="beginTransaction:withBlock:", useDeferred=NO, block=0x0000000100087d44) + 204 at FMDatabaseQueue.m:190
frame #15: 0x000000010008cb20 Tester`-[FMDatabaseQueue inTransaction:](self=0x0000000125d3d150, _cmd="inTransaction:", block=0x0000000100087d44) + 80 at FMDatabaseQueue.m:219
Digging into the code, my hypothesis is that this is caused by us running sqlite3_reset
or a sqlite3_finalize
on a statement in FMStatement
while also touching the database with things like sqlite3_prepare_v2
at the same time. This can happen because -[FMResultSet dealloc]
calls into -[FMResultSet close]
which calls into -[FMResultSet reset]
. Because the autoreleasepool of a block running on FMDatabaseQueue
's dispatch queue is tied to the thread, it is nondeterministic as to when the FMResultSet
gets deallocated (the pool gets drained when the thread is idle, which can happen AFTER the dispatch queue started running a block on a different thread). e.g. screenshot of a dealloc happening while the database queue continues.
I tried to work around this by adding a [rs close]
to manually close the FMResultSet
(which resets the associated statement). This seems to make the repro above no longer trigger, but I am still seeing crashes from users (with a much lower frequency though).
My guess is that somehow the FMStatement
instance itself was also hitting the same issue (its dealloc calls into -[FMStatement close]
). I was able to repro the crash if I modified the repro above to something like this:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
__block FMStatement *statement = nil;
[q inTransaction:^(FMDatabase *db, BOOL *rollback) {
NSDictionary *params = @{
@"col2" : [NSString stringWithFormat:@"%@", @(j)],
@"col3" : [NSString stringWithFormat:@"%@", @(j)],
@"col4" : [NSString stringWithFormat:@"%@", @(j)],
@"col5" : [NSString stringWithFormat:@"%@", @(j)],
@"col6" : [NSNull null],
@"col7" : [NSString stringWithFormat:@"%@", @(j)],
@"col8" : [NSString stringWithFormat:@"%@", @(j)],
@"col9" : [NSString stringWithFormat:@"%@", @(j)],
@"col10" : [NSString stringWithFormat:@"%@", @(j)]
};
assert([db executeUpdate:kInsertQuery withParameterDictionary:params]);
NSLog(@"last row id = %@", @(db.lastInsertRowId));
FMResultSet *rs = [db executeQuery:@"SELECT * FROM test_table ORDER BY col1 DESC LIMIT 1"];
assert([rs next]);
NSLog(@"row added: %@ (%@)", [rs stringForColumnIndex:0], [NSThread currentThread]);
statement = rs.statement;
[rs close];
}];
usleep(100 * 1000); // 100ms
NSLog(@"statement = %@", statement);
}
});
Since I never cache statements on my FMDatabase
instance, I think I can workaround this by calling [statement close]
directly when I close the FMResultSet
, which eliminated the repro. Not sure what is a good general fix though.
Please let me know if you have better suggestions, thanks!
We are using an internal SQLite wrapper that is very similar to FMDB; the memory management is basically the same. We ran into the exact same issue: Many different threads are competing for a single connection—protected by a dispatch queue—and the app would occasionally crash because deallocation of the wrapper object (and calling sqlite3_finalize
on the statement object) is tied to the thread, not the dispatch queue. We fixed this by explicitly closing all statements—as suggested by @richchan, thanks for the investigation.
I would suggest to update the README.markdown
accordingly, especially this paragraph in the Executing Queries section:
Typically, there's no need to
-close
anFMResultSet
yourself, since that happens when either the result set is deallocated, or the parent database is closed.
I'm not entirely sure if the same problem arises when calling sqlite3_reset
at the wrong time. If not, enabling the statement cache could prevent this issue. Otherwise, deallocation of FMResultSet
objects will occasionally lead to the same crash.
stack trace: