My app crashes on the following line:
sqlite3_prepare_v2(db, [sql UTF8String], -1, &pStmt, 0);
in the method of the FMDB sqlite wrapper:
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args {
if (![self databaseExists]) {
return 0x00;
}
if (inUse) {
[self warnInUse];
return 0x00;
}
[self setInUse:YES];
FMResultSet *rs = nil;
int rc = 0x00;
sqlite3_stmt *pStmt = 0x00;
FMStatement *statement = 0x00;
if (traceExecution && sql) {
NSLog(@"%@ executeQuery: %@", self, sql);
}
if (shouldCacheStatements) {
statement = [self cachedStatementForQuery:sql];
pStmt = statement ? [statement statement] : 0x00;
}
int numberOfRetries = 0;
BOOL retry = NO;
if (!pStmt) {
do {
retry = NO;
const char *sqlStatement = [sql UTF8String];
rc = sqlite3_prepare_v2(db, sqlStatement, -1, &pStmt, 0);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
retry = YES;
usleep(20);
if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) {
NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]);
NSLog(@"Database busy");
sqlite3_finalize(pStmt);
[self setInUse:NO];
return nil;
}
}
else if (SQLITE_OK != rc) {
if (logsErrors) {
NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
NSLog(@"DB Query: %@", sql);
#ifndef NS_BLOCK_ASSERTIONS
if (crashOnErrors) {
NSAssert2(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
}
#endif
}
sqlite3_finalize(pStmt);
[self setInUse:NO];
return nil;
}
}
while (retry);
}
id obj;
int idx = 0;
int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)
while (idx < queryCount) {
if (arrayArgs) {
obj = [arrayArgs objectAtIndex:idx];
}
else {
obj = va_arg(args, id);
}
if (traceExecution) {
NSLog(@"obj: %@", obj);
}
idx++;
[self bindObject:obj toColumn:idx inStatement:pStmt];
}
if (idx != queryCount) {
NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
sqlite3_finalize(pStmt);
[self setInUse:NO];
return nil;
}
[statement retain]; // to balance the release below
if (!statement) {
statement = [[FMStatement alloc] init];
[statement setStatement:pStmt];
if (shouldCacheStatements) {
[self setCachedStatement:statement forQuery:sql];
}
}
// the statement gets closed in rs's dealloc or [rs close];
rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
[rs setQuery:sql];
NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
[openResultSets addObject:openResultSet];
statement.useCount = statement.useCount + 1;
[statement release];
[self setInUse:NO];
return rs;
}
The app crashes with EXC_BAD_ACCESS. I have tried to find out why by debugging with NSZombieEnabled and malloc_history, but it does not give me any answers. Also - the debugger tells me that the sql variable has a retain count of a very large number (which is probably because it is a static NSString) - so the EXC_BAD_ACCESS should not be because of the sql object being over-relesed.
Does anyone have any ideas on how to further debug this to find out what the problem is?
Solution: The problem was that my database was accessed by several threads. And even if all threads had synchronized access to the database handle, for sqlite versions prior to 3.3.1 (iOS uses 3.0) you can not safely use the same database handle across threads.
My solution was to create on-demand handles to the database for each thread that tries to access the database, like this:
- (ADatabaseConnection *)databaseConnection {
NSDictionary *dictionary = [[NSThread currentThread] threadDictionary];
NSString *key = @"aDatabaseConnection";
ADatabaseConnection *connection = [dictionary objectForKey:key];
if (connection == nil) {
connection = [[[ADatabaseConnection alloc] initWithDatabase:self] autorelease];
[dictionary setValue:connection forKey:key];
}
return connection;
}
Note that for sqlite versions >= 3.3.1, this is not needed as the same handle can be used across threads.
Another important thing to remember is that even if you use this approach to safely use the same database across threads, it might be wise to synchronize access to the database so that you do not access it simultaneously anyway to avoid database lock errors. I do both, use one handle for each thread and synchronize on the database.
It's not safe to use FMDatabase from multiple threads at the same time- so I've started work on a new class to help you make queries and updates from multiple threads using a pool. Right now it's on a branch, which you can view here: https://github.com/ccgus/fmdb/tree/threadtests
Read the section titled "Using FMDatabasePool and Thread Safety."
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With